diff --git a/bin/script-plugin.js b/bin/script-plugin.js new file mode 100644 index 00000000..548eec6f --- /dev/null +++ b/bin/script-plugin.js @@ -0,0 +1,135 @@ +#!/usr/bin/env node + +// Script File Runner for Cronicle +// Modified from 'Shell Script' Plugin +// Modified by Jon Seale +// 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 = job.params.script; + var args = JSON.parse("[" + job.params.args + "]"); + + var child = cp.spawn( script_file, args, { + 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) + }); + + } ); + + 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); + } ); // 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 ); + + // 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