// Cronicle JobDetails Page Class.subclass( Page.Base, "Page.JobDetails", { pie_colors: { cool: 'green', warm: 'rgb(240,240,0)', hot: '#F7464A', progress: '#3f7ed5', empty: 'rgba(0, 0, 0, 0.05)' }, onInit: function() { // called once at page load // var html = ''; // this.div.html( html ); this.charts = {}; }, onActivate: function(args) { // page activation if (!this.requireLogin(args)) return true; if (!args) args = {}; this.args = args; if (!args.id) { app.doError("The Job Details page requires a Job ID."); return true; } app.setWindowTitle( "Job Details: #" + args.id ); app.showTabBar(true); this.tab.show(); this.tab[0]._page_id = Nav.currentAnchor(); this.retry_count = 3; this.go_when_ready(); return true; }, go_when_ready: function() { // make sure we're not in the limbo state between starting a manual job, // and waiting for activeJobs to be updated var self = this; var args = this.args; if (this.find_job(args.id)) { // job is currently active -- jump to real-time view args.sub = 'live'; this.gosub_live(args); } else { // job must be completed -- jump to archive view args.sub = 'archive'; this.gosub_archive(args); } }, gosub_archive: function(args) { // show job archive var self = this; Debug.trace("Showing archived job: " + args.id); this.div.addClass('loading'); app.api.post( 'app/get_job_details', { id: args.id }, this.receive_details.bind(this), function(resp) { // error capture if (self.retry_count >= 0) { Debug.trace("Failed to get_job_details, trying again in 1s..."); self.retry_count--; setTimeout( function() { self.go_when_ready(); }, 1000 ); } else { // show error app.doError("Error: " + resp.description); self.div.removeClass('loading'); } } ); }, get_job_result_banner: function(job) { // render banner based on job result var icon = ''; var type = ''; if (job.abort_reason || job.unknown) { type = 'warning'; icon = 'exclamation-circle'; } else if (job.code) { type = 'error'; icon = 'exclamation-triangle'; } else { type = 'success'; icon = 'check-circle'; } if (!job.description && job.code) { job.description = "Job failed with code: " + job.code; } if (!job.code && (!job.description || job.description.replace(/\W+/, '').match(/^success(ful)?$/i))) { job.description = "Job completed successfully at " + get_nice_date_time(job.time_end, false, true); // add timezone abbreviation job.description += " " + moment.tz(job.time_end * 1000, app.tz).format('z'); } if (job.code && !job.description.match(/^\s*error/i)) { var desc = job.description; job.description = "Error"; if (job.code != 1) job.description += " " + job.code; job.description += ": " + desc; } var job_desc_html = trim( job.description.replace(/\r\n/g, "\n") ); var multiline = !!job.description.match(/\n/); job_desc_html = encode_entities( job_desc_html ).replace(/\n/g, "
\n"); var html = ''; html += '
'; if (multiline) { html += job_desc_html; } else { html += '   ' + job_desc_html; } html += '
'; return html; }, delete_job: function() { // delete job, after confirmation var self = this; var job = this.job; app.confirm( 'Delete Job', "Are you sure you want to delete the current job log and history?", "Delete", function(result) { if (result) { app.showProgress( 1.0, "Deleting job..." ); app.api.post( 'app/delete_job', job, function(resp) { app.hideProgress(); app.showMessage('success', "Job ID '"+job.id+"' was deleted successfully."); $('#tab_History').trigger('click'); self.tab.hide(); } ); } } ); }, run_again: function() { // run job again var self = this; var event = find_object( app.schedule, { id: this.job.event } ) || null; if (!event) return app.doError("Could not locate event in schedule: " + this.job.event_title + " (" + this.job.event + ")"); var job = deep_copy_object( event ); job.now = this.job.now; job.params = this.job.params; app.showProgress( 1.0, "Starting job..." ); app.api.post( 'app/run_event', job, function(resp) { // app.showMessage('success', "Event '"+event.title+"' has been started."); self.jump_live_job_id = resp.ids[0]; self.jump_live_time_start = hires_time_now(); self.jump_to_live_when_ready(); } ); }, jump_to_live_when_ready: function() { // make sure live view is ready (job may still be starting) var self = this; if (!this.active) return; // user navigated away from page if (app.activeJobs[this.jump_live_job_id] || ((hires_time_now() - this.jump_live_time_start) >= 3.0)) { app.hideProgress(); Nav.go( 'JobDetails?id=' + this.jump_live_job_id ); delete this.jump_live_job_id; delete this.jump_live_time_start; } else { setTimeout( self.jump_to_live_when_ready.bind(self), 250 ); } }, receive_details: function(resp) { // receive job details from server, render them var html = ''; var job = this.job = resp.job; this.div.removeClass('loading'); var size = get_inner_window_size(); var col_width = Math.floor( ((size.width * 0.9) - 300) / 4 ); // locate objects var event = find_object( app.schedule, { id: job.event } ) || {}; var cat = job.category ? find_object( app.categories, { id: job.category } ) : null; var group = event.target ? find_object( app.server_groups, { id: event.target } ) : null; var plugin = job.plugin ? find_object( app.plugins, { id: job.plugin } ) : null; if (group && event.multiplex) { group = copy_object(group); group.multiplex = 1; } html += '
'; html += 'Completed Job'; if (event.id && !event.multiplex) html += '
 Run Again
'; if (app.isAdmin()) html += '
 Delete Job
'; html += '
'; html += '
'; // result banner html += this.get_job_result_banner(job); // fieldset header html += '
Job Details'; // if (event.id && !event.multiplex) html += '
Run Again
'; html += '
'; html += '
JOB ID
'; html += '
' + job.id + '
'; html += '
EVENT NAME
'; html += '
'; if (event.id) html += '' + this.getNiceEvent(job.event_title, col_width) + ''; else if (job.event_title) html += this.getNiceEvent(job.event_title, col_width); else html += '(None)'; html += '
'; html += '
EVENT TIMING
'; html += '
' + (event.enabled ? summarize_event_timing(event.timing, event.timezone) : '(Disabled)') + '
'; html += '
'; html += '
'; html += '
CATEGORY NAME
'; html += '
'; if (cat) html += this.getNiceCategory(cat, col_width); else if (job.category_title) html += this.getNiceCategory({ title: job.category_title }, col_width); else html += '(None)'; html += '
'; html += '
PLUGIN NAME
'; html += '
'; if (plugin) html += this.getNicePlugin(plugin, col_width); else if (job.plugin_title) html += this.getNicePlugin({ title: job.plugin_title }, col_width); else html += '(None)'; html += '
'; html += '
EVENT TARGET
'; html += '
'; if (group || event.target) html += this.getNiceGroup(group, event.target, col_width); else if (job.nice_target) html += '
' + job.nice_target + '
'; else html += '(None)'; html += '
'; html += '
'; html += '
'; html += '
JOB SOURCE
'; html += '
' + (job.source || 'Scheduler') + '
'; html += '
SERVER HOSTNAME
'; html += '
' + this.getNiceGroup( null, job.hostname, col_width ) + '
'; html += '
PROCESS ID
'; html += '
' + (job.detached_pid || job.pid || '(Unknown)') + '
'; html += '
'; html += '
'; html += '
JOB STARTED
'; html += '
'; if ((job.time_start - job.now >= 60) && !event.multiplex && !job.source) { html += ''; html += get_nice_date_time(job.time_start, false, true); html += ''; } else html += get_nice_date_time(job.time_start, false, true); html += '
'; html += '
JOB COMPLETED
'; html += '
' + get_nice_date_time(job.time_end, false, true) + '
'; html += '
ELAPSED TIME
'; html += '
' + get_text_from_seconds(job.elapsed, false, false) + '
'; html += '
'; html += '
'; html += '
'; // pies html += '
'; html += '
'; html += '
Performance Metrics
'; html += '
'; // html += ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
Memory Usage
'; html += '
'; // html += ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
CPU Usage
'; html += '
'; // html += ''; html += '
'; html += '
'; html += '
'; // custom data table if (job.table && job.table.rows && job.table.rows.length) { var table = job.table; html += '
' + (table.title || 'Job Stats') + '
'; html += ''; if (table.header && table.header.length) { html += ''; for (var idx = 0, len = table.header.length; idx < len; idx++) { html += ''; } html += ''; } var filters = table.filters || []; for (var idx = 0, len = table.rows.length; idx < len; idx++) { var row = table.rows[idx]; if (row && row.length) { html += ''; for (var idy = 0, ley = row.length; idy < ley; idy++) { var col = row[idy]; html += ''; } // foreach col html += ''; } // good row } // foreach row html += '
' + table.header[idx] + '
'; if (typeof(col) != 'undefined') { if (filters[idy] && window[filters[idy]]) html += window[filters[idy]](col); else if ((typeof(col) == 'string') && col.match(/^filter\:(\w+)\((.+)\)$/)) { var filter = RegExp.$1; var value = RegExp.$2; if (window[filter]) html += window[filter](value); else html += value; } else html += col; } html += '
'; if (table.caption) html += '
' + table.caption + '
'; } // custom data table // custom html table if (job.html) { html += '
' + (job.html.title || 'Job Report') + '
'; html += '
' + job.html.content + '
'; if (job.html.caption) html += '
' + job.html.caption + '
'; } // job log (IFRAME) html += '
'; html += 'Job Event Log'; if (job.log_file_size) html += ' (' + get_text_from_bytes(job.log_file_size, 1) + ')'; html += '
 View Full Log
'; html += '
 Download Log
'; html += '
'; html += '
'; var max_log_file_size = config.max_log_file_size || 10485760; if (job.log_file_size && (job.log_file_size >= max_log_file_size)) { // too big to show? ask user html += '
'; html += '
'; html += '
Warning: Job event log file is ' + get_text_from_bytes(job.log_file_size, 1) + '. Please consider downloading instead of viewing in browser.
'; html += '
Download Log
'; html += '
View Log
'; html += '
'; html += '
'; html += '
'; } else { var size = get_inner_window_size(); var iheight = size.height - 100; html += ''; } this.div.html( html ); // arch perf chart var suffix = ' sec'; var pscale = 1; if (!job.perf) job.perf = { total: job.elapsed }; if (!isa_hash(job.perf)) job.perf = parse_query_string( job.perf.replace(/\;/g, '&') ); if (job.perf.scale) { pscale = job.perf.scale; delete job.perf.scale; } var perf = job.perf.perf ? job.perf.perf : job.perf; // remove counters from pie for (var key in perf) { if (key.match(/^c_/)) delete perf[key]; } // clean up total, add other if (perf.t) { perf.total = perf.t; delete perf.t; } if ((num_keys(perf) > 1) && perf.total) { if (!perf.other) { var totes = 0; for (var key in perf) { if (key != 'total') totes += perf[key]; } if (totes < perf.total) { perf.other = perf.total - totes; } } delete perf.total; // only show total if by itself } // remove outer 'umbrella' perf keys if inner ones are more specific // (i.e. remove "db" if we have "db_query" and/or "db_connect") for (var key in perf) { for (var subkey in perf) { if ((subkey.indexOf(key + '_') == 0) && (subkey.length > key.length + 1)) { delete perf[key]; break; } } } // divide everything by scale, so we get seconds for (var key in perf) { perf[key] /= pscale; } var colors = this.graph_colors; var color_idx = 0; var p_data = []; var p_colors = []; var p_labels = []; var perf_keys = hash_keys_to_array(perf).sort(); for (var idx = 0, len = perf_keys.length; idx < len; idx++) { var key = perf_keys[idx]; var value = perf[key]; p_data.push( short_float(value) ); p_colors.push( 'rgb(' + colors[color_idx] + ')' ); p_labels.push( key ); color_idx = (color_idx + 1) % colors.length; } var ctx = $("#c_arch_perf").get(0).getContext("2d"); var perf_chart = new Chart(ctx, { type: 'pie', data: { datasets: [{ data: p_data, backgroundColor: p_colors, label: '' }], labels: p_labels }, options: { responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: false, position: 'right', }, title: { display: false, text: '' }, animation: { animateScale: true, animateRotate: true } } }); var legend_html = ''; legend_html += '
'; for (var idx = 0, len = perf_keys.length; idx < len; idx++) { legend_html += '
' + p_labels[idx] + '
'; } legend_html += '
'; var perf_legend = $('#d_arch_perf_legend'); perf_legend.html( legend_html ); this.charts.perf = perf_chart; // arch cpu pie var cpu_avg = 0; if (!job.cpu) job.cpu = {}; if (job.cpu.total && job.cpu.count) { cpu_avg = short_float( job.cpu.total / job.cpu.count ); } var jcm = 100; var ctx = $("#c_arch_cpu").get(0).getContext("2d"); var cpu_chart = new Chart(ctx, { type: 'doughnut', data: { datasets: [{ data: [ Math.min(cpu_avg, jcm), jcm - Math.min(cpu_avg, jcm), ], backgroundColor: [ (cpu_avg < jcm*0.5) ? this.pie_colors.cool : ((cpu_avg < jcm*0.75) ? this.pie_colors.warm : this.pie_colors.hot), this.pie_colors.empty ], label: '' }], labels: [] }, options: { events: [], responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: false, position: 'right', }, title: { display: false, text: '' }, animation: { animateScale: true, animateRotate: true } } }); // arch cpu overlay var html = ''; html += '
'+cpu_avg+'%
'; html += '
Average
'; $('#d_arch_cpu_overlay').html( html ); // arch cpu legend var html = ''; html += '
MIN
'; html += '
' + short_float(job.cpu.min || 0) + '%
'; html += '
PEAK
'; html += '
' + short_float(job.cpu.max || 0) + '%
'; $('#d_arch_cpu_legend').html( html ); this.charts.cpu = cpu_chart; // arch mem pie var mem_avg = 0; if (!job.mem) job.mem = {}; if (job.mem.total && job.mem.count) { mem_avg = Math.floor( job.mem.total / job.mem.count ); } var jmm = config.job_memory_max || 1073741824; var ctx = $("#c_arch_mem").get(0).getContext("2d"); var mem_chart = new Chart(ctx, { type: 'doughnut', data: { datasets: [{ data: [ Math.min(mem_avg, jmm), jmm - Math.min(mem_avg, jmm), ], backgroundColor: [ (mem_avg < jmm*0.5) ? this.pie_colors.cool : ((mem_avg < jmm*0.75) ? this.pie_colors.warm : this.pie_colors.hot), this.pie_colors.empty ], label: '' }], labels: [] }, options: { events: [], responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: false, position: 'right', }, title: { display: false, text: '' }, animation: { animateScale: true, animateRotate: true } } }); // arch mem overlay var html = ''; html += '
'+get_text_from_bytes(mem_avg, 10)+'
'; html += '
Average
'; $('#d_arch_mem_overlay').html( html ); // arch mem legend var html = ''; html += '
MIN
'; html += '
' + get_text_from_bytes(job.mem.min || 0, 1) + '
'; html += '
PEAK
'; html += '
' + get_text_from_bytes(job.mem.max || 0, 1) + '
'; $('#d_arch_mem_legend').html( html ); this.charts.mem = mem_chart; }, do_download_log: function() { // download job log file var job = this.job; window.location = '/api/app/get_job_log?id=' + job.id + '&download=1'; }, do_view_inline_log: function() { // swap out job log size warning with IFRAME containing inline log var job = this.job; var html = ''; var size = get_inner_window_size(); var iheight = size.height - 100; html += ''; $('#d_job_log_warning').html( html ); }, abort_job: function() { // abort job, after confirmation var job = this.find_job(this.args.id); app.confirm( 'Abort Job', "Are you sure you want to abort the current job?", "Abort", function(result) { if (result) { app.showProgress( 1.0, "Aborting job..." ); app.api.post( 'app/abort_job', job, function(resp) { app.hideProgress(); app.showMessage('success', "Job '"+job.event_title+"' was aborted successfully."); } ); } } ); }, check_watch_enabled: function(job) { // check if watch is enabled on current live job var watch_enabled = 0; var email = app.user.email.toLowerCase(); if (email && job.notify_success && (job.notify_success.toLowerCase().indexOf(email) > -1)) watch_enabled++; if (email && job.notify_fail && (job.notify_fail.toLowerCase().indexOf(email) > -1)) watch_enabled++; return (watch_enabled == 2); }, watch_add_me: function(job, key) { // add current user's e-mail to job property if (!job[key]) job[key] = ''; var value = trim( job[key].replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '') ); var email = app.user.email.toLowerCase(); var regexp = new RegExp( "\\b" + escape_regexp(email) + "\\b", "i" ); if (!value.match(regexp)) { if (value) value += ', '; job[key] = value + app.user.email; } }, watch_remove_me: function(job, key) { // remove current user's email from job property if (!job[key]) job[key] = ''; var value = trim( job[key].replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '') ); var email = app.user.email.toLowerCase(); var regexp = new RegExp( "\\b" + escape_regexp(email) + "\\b", "i" ); value = value.replace( regexp, '' ).replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, ''); job[key] = trim(value); }, toggle_watch: function() { // toggle watch on/off on current live job var job = this.find_job(this.args.id); var watch_enabled = this.check_watch_enabled(job); if (!watch_enabled) { this.watch_add_me( job, 'notify_success' ); this.watch_add_me( job, 'notify_fail' ); } else { this.watch_remove_me( job, 'notify_success' ); this.watch_remove_me( job, 'notify_fail' ); } // update on server $('#s_watch_job > i').removeClass().addClass('fa fa-spin fa-spinner'); app.api.post( 'app/update_job', { id: job.id, notify_success: job.notify_success, notify_fail: job.notify_fail }, function(resp) { watch_enabled = !watch_enabled; if (watch_enabled) { app.showMessage('success', "You will now be notified via e-mail when the job completes (success or fail)."); $('#s_watch_job').css('color', '#3f7ed5'); $('#s_watch_job > i').removeClass().addClass('fa fa-check-square-o'); } else { app.showMessage('success', "You will no longer be notified about this job."); $('#s_watch_job').css('color', '#777'); $('#s_watch_job > i').removeClass().addClass('fa fa-square-o'); } } ); }, gosub_live: function(args) { // show live job status Debug.trace("Showing live job: " + args.id); var job = this.find_job(args.id); var html = ''; this.div.removeClass('loading'); var size = get_inner_window_size(); var col_width = Math.floor( ((size.width * 0.9) - 300) / 4 ); // locate objects var event = find_object( app.schedule, { id: job.event } ) || {}; var cat = job.category ? find_object( app.categories, { id: job.category } ) : { title: 'n/a' }; var group = event.target ? find_object( app.server_groups, { id: event.target } ) : null; var plugin = job.plugin ? find_object( app.plugins, { id: job.plugin } ) : { title: 'n/a' }; if (group && event.multiplex) { group = copy_object(group); group.multiplex = 1; } // new header with watch & abort var watch_enabled = this.check_watch_enabled(job); html += '
'; html += 'Live Job Progress'; html += '
 Abort Job
'; html += '
 Watch Job
'; html += '
'; html += '
'; // fieldset header html += '
Job Details'; // html += '
Abort Job...
'; html += '
'; html += '
JOB ID
'; html += '
' + job.id + '
'; html += '
EVENT NAME
'; html += ''; html += '
EVENT TIMING
'; html += '
' + (event.enabled ? summarize_event_timing(event.timing, event.timezone) : '(Disabled)') + '
'; html += '
'; html += '
'; html += '
CATEGORY NAME
'; html += '
' + this.getNiceCategory(cat, col_width) + '
'; html += '
PLUGIN NAME
'; html += '
' + this.getNicePlugin(plugin, col_width) + '
'; html += '
EVENT TARGET
'; html += '
' + this.getNiceGroup(group, event.target, col_width) + '
'; html += '
'; html += '
'; html += '
JOB SOURCE
'; html += '
' + (job.source || 'Scheduler') + '
'; html += '
SERVER HOSTNAME
'; html += '
' + this.getNiceGroup( null, job.hostname, col_width ) + '
'; html += '
PROCESS ID
'; html += '
' + job.pid + '
'; html += '
'; html += '
'; html += '
JOB STARTED
'; html += '
' + get_nice_date_time(job.time_start, false, true) + '
'; html += '
ELAPSED TIME
'; var elapsed = Math.floor( Math.max( 0, app.epoch - job.time_start ) ); html += '
' + get_text_from_seconds(elapsed, false, false) + '
'; var progress = job.progress || 0; var nice_remain = 'n/a'; if (job.pending && job.when) { nice_remain = 'Retry in '+get_text_from_seconds( Math.max(0, job.when - app.epoch), true, true )+''; } else if ((elapsed >= 10) && (progress > 0) && (progress < 1.0)) { var sec_remain = Math.floor(((1.0 - progress) * elapsed) / progress); nice_remain = get_text_from_seconds( sec_remain, false, true ); } html += '
REMAINING TIME
'; html += '
' + nice_remain + '
'; html += '
'; html += '
'; html += '
'; // pies html += '
'; html += '
'; html += '
'; html += '
Job Progress
'; html += '
'; // html += ''; // html += '
'; html += '
'; html += '
'; html += '
'; html += '
Memory Usage
'; html += '
'; // html += ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
CPU Usage
'; html += '
'; // html += ''; html += '
'; html += '
'; html += '
'; // live job log tail var remote_api_url = app.proto + job.hostname + ':' + app.port + config.base_api_uri; if (config.custom_live_log_socket_url) { // custom websocket URL for single-master systems behind an LB remote_api_url = config.custom_live_log_socket_url + config.base_api_uri; } else if (!config.web_socket_use_hostnames && app.servers && app.servers[job.hostname] && app.servers[job.hostname].ip) { // use ip if available, may work better in some setups remote_api_url = app.proto + app.servers[job.hostname].ip + ':' + app.port + config.base_api_uri; } html += '
'; html += 'Live Job Event Log'; html += '
 View Full Log
'; html += '
 Download Log
'; html += '
'; html += '
'; var size = get_inner_window_size(); var iheight = size.height - 100; html += '
'; this.div.html( html ); // open websocket for log tail stream this.start_live_log_watcher(job); // live progress pie if (!job.progress) job.progress = 0; var progress = Math.min(1, Math.max(0, job.progress)); var prog_pct = short_float( progress * 100 ); var ctx = $("#c_live_progress").get(0).getContext("2d"); var progress_chart = new Chart(ctx, { type: 'doughnut', data: { datasets: [{ data: [ prog_pct, 100 - prog_pct ], backgroundColor: [ this.pie_colors.progress, this.pie_colors.empty ], label: '' }], labels: [] }, options: { events: [], responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: false, position: 'right', }, title: { display: false, text: '' }, animation: { animateScale: true, animateRotate: true } } }); this.charts.progress = progress_chart; // live cpu pie if (!job.cpu) job.cpu = {}; if (!job.cpu.current) job.cpu.current = 0; var cpu_cur = job.cpu.current; var cpu_avg = 0; if (job.cpu.total && job.cpu.count) { cpu_avg = short_float( job.cpu.total / job.cpu.count ); } var jcm = 100; var ctx = $("#c_live_cpu").get(0).getContext("2d"); var cpu_chart = new Chart(ctx, { type: 'doughnut', data: { datasets: [{ data: [ Math.min(cpu_cur, jcm), jcm - Math.min(cpu_cur, jcm), ], backgroundColor: [ (cpu_cur < jcm*0.5) ? this.pie_colors.cool : ((cpu_cur < jcm*0.75) ? this.pie_colors.warm : this.pie_colors.hot), this.pie_colors.empty ], label: '' }], labels: [] }, options: { events: [], responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: false, position: 'right', }, title: { display: false, text: '' }, animation: { animateScale: true, animateRotate: true } } }); this.charts.cpu = cpu_chart; // live mem pie if (!job.mem) job.mem = {}; if (!job.mem.current) job.mem.current = 0; var mem_cur = job.mem.current; var mem_avg = 0; if (job.mem.total && job.mem.count) { mem_avg = short_float( job.mem.total / job.mem.count ); } var jmm = config.job_memory_max || 1073741824; var ctx = $("#c_live_mem").get(0).getContext("2d"); var mem_chart = new Chart(ctx, { type: 'doughnut', data: { datasets: [{ data: [ Math.min(mem_cur, jmm), jmm - Math.min(mem_cur, jmm), ], backgroundColor: [ (mem_cur < jmm*0.5) ? this.pie_colors.cool : ((mem_cur < jmm*0.75) ? this.pie_colors.warm : this.pie_colors.hot), this.pie_colors.empty ], label: '' }], labels: [] }, options: { events: [], responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: false, position: 'right', }, title: { display: false, text: '' }, animation: { animateScale: true, animateRotate: true } } }); this.charts.mem = mem_chart; // update dynamic data this.update_live_progress(job); }, start_live_log_watcher: function(job) { // open special websocket to target server for live log feed var self = this; var $cont = null; var chunk_count = 0; var error_shown = false; var url = app.proto + job.hostname + ':' + app.port; if (config.custom_live_log_socket_url) { // custom websocket URL for single-master systems behind an LB url = config.custom_live_log_socket_url; } else if (!config.web_socket_use_hostnames && app.servers && app.servers[job.hostname] && app.servers[job.hostname].ip) { // use ip if available, may work better in some setups url = app.proto + app.servers[job.hostname].ip + ':' + app.port; } $('#d_live_job_log').append( '
Log Watcher: Connecting to server: ' + url + '...
' ); this.socket = io( url, { forceNew: true, transports: config.socket_io_transports || ['websocket'], reconnection: true, reconnectionDelay: 1000, reconnectionDelayMax: 5000, reconnectionAttempts: 9999, timeout: 5000 } ); this.socket.on('connect', function() { Debug.trace("JobDetails socket.io connected successfully: " + url); // cache this for later $cont = $('#d_live_job_log'); $cont.append( '
Log Watcher: Connected successfully!
' ); // get auth token from master server (uses session) app.api.post( 'app/get_log_watch_auth', { id: job.id }, function(resp) { // now request log watch stream on target server self.socket.emit( 'watch_job_log', { token: resp.token, id: job.id } ); }); // api.post } ); this.socket.on('connect_error', function(err) { Debug.trace("JobDetails socket.io connect error: " + err); $('#d_live_job_log').append( '
Log Watcher: Server Connect Error: ' + err + ' (' + url + ')
' ); error_shown = true; } ); this.socket.on('connect_timeout', function(err) { Debug.trace("JobDetails socket.io connect timeout"); if (!error_shown) $('#d_live_job_log').append( '
Log Watcher: Server Connect Timeout: ' + err + ' (' + url + ')
' ); } ); this.socket.on('reconnect', function() { Debug.trace("JobDetails socket.io reconnected successfully"); } ); this.socket.on('log_data', function(lines) { // received log data, as array of lines var scroll_y = $cont.scrollTop(); var scroll_max = Math.max(0, $cont.prop('scrollHeight') - $cont.height()); var need_scroll = ((scroll_max - scroll_y) <= 10); $cont.append( '
' + 
					lines.map( function(line) { return line.replace(/' 
			);
			
			// only show newest 1K chunks
			chunk_count++;
			if (chunk_count >= 1000) {
				$cont.children().first().remove();
				chunk_count--;
			}
			
			if (need_scroll) $cont.scrollTop( $cont.prop('scrollHeight') );
		} );
	},
	
	update_live_progress: function(job) {
		// update job progress, elapsed time, time remaining, cpu pie, mem pie
		if (job.complete && !app.progress) app.showProgress( 1.0, "Job is finishing..." );
		
		// pid may have changed (retry)
		$('#d_live_pid').html( job.pid || 'n/a' );
		
		// elapsed time
		var elapsed = Math.floor( Math.max( 0, app.epoch - job.time_start ) );
		$('#d_live_elapsed').html( get_text_from_seconds(elapsed, false, false) );
		
		// remaining time
		var progress = job.progress || 0;
		var nice_remain = 'n/a';
		if (job.pending && job.when) {
			nice_remain = 'Retry in '+get_text_from_seconds( Math.max(0, job.when - app.epoch), true, true )+'';
		}
		else if ((elapsed >= 10) && (progress > 0) && (progress < 1.0)) {
			var sec_remain = Math.floor(((1.0 - progress) * elapsed) / progress);
			nice_remain = get_text_from_seconds( sec_remain, false, true );
		}
		$('#d_live_remain').html( nice_remain );
		
		// progress pie
		if (!job.progress) job.progress = 0;
		var progress = Math.min(1, Math.max(0, job.progress));
		var prog_pct = short_float( progress * 100 );
		
		if (prog_pct != this.charts.progress.__cronicle_prog_pct) {
			this.charts.progress.__cronicle_prog_pct = prog_pct;
			this.charts.progress.config.data.datasets[0].data[0] = prog_pct;
			this.charts.progress.config.data.datasets[0].data[1] = 100 - prog_pct;
			this.charts.progress.update();
		}
		
		// progress overlay
		var html = '';
		html += '
'+Math.floor(prog_pct)+'%
'; html += '
Current
'; $('#d_live_progress_overlay').html( html ); // cpu pie if (!job.cpu) job.cpu = {}; if (!job.cpu.current) job.cpu.current = 0; var cpu_cur = job.cpu.current; var cpu_avg = 0; if (job.cpu.total && job.cpu.count) { cpu_avg = short_float( job.cpu.total / job.cpu.count ); } var jcm = 100; if (cpu_cur != this.charts.cpu.__cronicle_cpu_cur) { this.charts.cpu.__cronicle_cpu_cur = cpu_cur; this.charts.cpu.config.data.datasets[0].data[0] = Math.min(cpu_cur, jcm); this.charts.cpu.config.data.datasets[0].data[1] = jcm - Math.min(cpu_cur, jcm); this.charts.cpu.config.data.datasets[0].backgroundColor[0] = (cpu_cur < jcm*0.5) ? this.pie_colors.cool : ((cpu_cur < jcm*0.75) ? this.pie_colors.warm : this.pie_colors.hot); this.charts.cpu.update(); } // live cpu overlay var html = ''; html += '
' + short_float(cpu_cur) + '%
'; html += '
Current
'; $('#d_live_cpu_overlay').html( html ); // live cpu legend var html = ''; html += '
MIN
'; html += '
' + short_float(job.cpu.min || 0) + '%
'; html += '
AVERAGE
'; html += '
' + (cpu_avg || 0) + '%
'; html += '
PEAK
'; html += '
' + short_float(job.cpu.max || 0) + '%
'; $('#d_live_cpu_legend').html( html ); // mem pie if (!job.mem) job.mem = {}; if (!job.mem.current) job.mem.current = 0; var mem_cur = job.mem.current; var mem_avg = 0; if (job.mem.total && job.mem.count) { mem_avg = short_float( job.mem.total / job.mem.count ); } var jmm = config.job_memory_max || 1073741824; if (mem_cur != this.charts.mem.__cronicle_mem_cur) { this.charts.mem.__cronicle_mem_cur = mem_cur; this.charts.mem.config.data.datasets[0].data[0] = Math.min(mem_cur, jmm); this.charts.mem.config.data.datasets[0].data[1] = jmm - Math.min(mem_cur, jmm); this.charts.mem.config.data.datasets[0].backgroundColor[0] = (mem_cur < jmm*0.5) ? this.pie_colors.cool : ((mem_cur < jmm*0.75) ? this.pie_colors.warm : this.pie_colors.hot); this.charts.mem.update(); } // live mem overlay var html = ''; html += '
'+get_text_from_bytes(mem_cur, 10)+'
'; html += '
Current
'; $('#d_live_mem_overlay').html( html ); // live mem legend var html = ''; html += '
MIN
'; html += '
' + get_text_from_bytes(job.mem.min || 0, 1) + '
'; html += '
AVERAGE
'; html += '
' + get_text_from_bytes(mem_avg || 0, 1) + '
'; html += '
PEAK
'; html += '
' + get_text_from_bytes(job.mem.max || 0, 1) + '
'; $('#d_live_mem_legend').html( html ); }, jump_to_archive_when_ready: function() { // make sure archive view is ready (job log may still be uploading) var self = this; if (!this.active) return; // user navigated away from page app.api.post( 'app/get_job_details', { id: this.args.id, need_log: 1 }, function(resp) { // got it, ready to switch app.hideProgress(); Nav.refresh(); }, function(err) { // job not complete yet if (!app.progress) app.showProgress( 1.0, "Job is finishing..." ); // self.jump_timer = setTimeout( self.jump_to_archive_when_ready.bind(self), 1000 ); } ); }, find_job: function(id) { // locate active or pending (retry delay) job if (!id) id = this.args.id; var job = app.activeJobs[id]; if (!job) { for (var key in app.activeJobs) { var temp_job = app.activeJobs[key]; if (temp_job.pending && (temp_job.id == id)) { job = temp_job; break; } } } return job; }, onStatusUpdate: function(data) { // received status update (websocket), update sub-page if needed if (this.args && (this.args.sub == 'live')) { if (!app.activeJobs[this.args.id]) { // check for pending job (retry delay) var pending_job = null; for (var key in app.activeJobs) { var job = app.activeJobs[key]; if (job.pending && (job.id == this.args.id)) { pending_job = job; break; } } if (pending_job) { // job switched to pending (retry delay) if (app.progress) app.hideProgress(); this.update_live_progress( pending_job ); } else { // the live job we were watching just completed, jump to archive view this.jump_to_archive_when_ready(); } } else { // job is still active this.update_live_progress(app.activeJobs[this.args.id]); } } }, onResize: function(size) { // window was resized var iheight = size.height - 110; if (this.args.sub == 'live') { $('#d_live_job_log').css( 'height', '' + iheight + 'px' ); } else { $('#i_arch_job_log').css( 'height', '' + iheight + 'px' ); } }, onResizeDelay: function(size) { // called 250ms after latest window resize // so we can run more expensive redraw operations }, onDeactivate: function() { // called when page is deactivated for (var key in this.charts) { this.charts[key].destroy(); } if (this.jump_timer) { clearTimeout( this.jump_timer ); delete this.jump_timer; } if (this.socket) { this.socket.disconnect(); delete this.socket; } this.charts = {}; this.div.html( '' ); // this.tab.hide(); return true; } });