#!/usr/bin/env node // Shell Script Runner for Cronicle // Invoked via the 'Shell Script' Plugin // Copyright (c) 2015 Joseph Huckaby // Released under the MIT License var fs = require('fs'); var os = require('os'); var cp = require('child_process'); var path = require('path'); var JSONStream = require('pixl-json-stream'); var Tools = require('pixl-tools'); // setup stdin / stdout streams process.stdin.setEncoding('utf8'); process.stdout.setEncoding('utf8'); var stream = new JSONStream( process.stdin, process.stdout ); stream.on('json', function(job) { // got job from parent var script_file = path.join( os.tmpdir(), 'cronicle-script-temp-' + job.id + '.sh' ); fs.writeFileSync( script_file, job.params.script, { mode: "775" } ); var child = cp.spawn( script_file, [], { stdio: ['pipe', 'pipe', 'pipe'] } ); var kill_timer = null; var stderr_buffer = ''; var cstream = new JSONStream( child.stdout, child.stdin ); cstream.recordRegExp = /^\s*\{.+\}\s*$/; cstream.on('json', function(data) { // received JSON data from child, pass along to Cronicle or log if (job.params.json) stream.write(data); else cstream.emit('text', JSON.stringify(data) + "\n"); } ); cstream.on('text', function(line) { // received non-json text from child // look for plain number from 0 to 100, treat as progress update if (line.match(/^\s*(\d+)\%\s*$/)) { var progress = Math.max( 0, Math.min( 100, parseInt( RegExp.$1 ) ) ) / 100; stream.write({ progress: progress }); } else { // otherwise just log it if (job.params.annotate) { var dargs = Tools.getDateArgs( new Date() ); line = '[' + dargs.yyyy_mm_dd + ' ' + dargs.hh_mi_ss + '] ' + line; } fs.appendFileSync(job.log_file, line); } } ); cstream.on('error', function(err, text) { // Probably a JSON parse error (child emitting garbage) if (text) fs.appendFileSync(job.log_file, text + "\n"); } ); child.on('error', function (err) { // child error stream.write({ complete: 1, code: 1, description: "Script failed: " + Tools.getErrorDescription(err) }); fs.unlink( script_file, function(err) {;} ); } ); child.on('exit', function (code, signal) { // child exited if (kill_timer) clearTimeout(kill_timer); code = (code || signal || 0); var data = { complete: 1, code: code, description: code ? ("Script exited with code: " + code) : "" }; if (stderr_buffer.length && stderr_buffer.match(/\S/)) { data.html = { title: "Error Output", content: "
" + stderr_buffer.replace(/" }; if (code) { // possibly augment description with first line of stderr, if not too insane var stderr_line = stderr_buffer.trim().split(/\n/).shift(); if (stderr_line.length < 256) data.description += ": " + stderr_line; } } stream.write(data); fs.unlink( script_file, function(err) {;} ); } ); // exit // silence EPIPE errors on child STDIN child.stdin.on('error', function(err) { // ignore } ); // track stderr separately for display purposes child.stderr.setEncoding('utf8'); child.stderr.on('data', function(data) { // keep first 32K in RAM, but log everything if (stderr_buffer.length < 32768) stderr_buffer += data; else if (!stderr_buffer.match(/\.\.\.$/)) stderr_buffer += '...'; fs.appendFileSync(job.log_file, data); }); // pass job down to child process (harmless for shell, useful for php/perl/node) cstream.write( job ); child.stdin.end(); // Handle shutdown process.on('SIGTERM', function() { console.log("Caught SIGTERM, killing child: " + child.pid); kill_timer = setTimeout( function() { // child didn't die, kill with prejudice console.log("Child did not exit, killing harder: " + child.pid); child.kill('SIGKILL'); }, 9 * 1000 ); // try killing nicely first child.kill('SIGTERM'); } ); } ); // stream