conquer
Version:
Restarts NodeJS or Coffee (or any other program) on file changes or crashes
345 lines (306 loc) • 11.6 kB
JavaScript
var watchr = require('watchr'),
program = require('commander'),
clc = require('cli-color'),
wsock = require('wsock'),
spawn = require('child_process').spawn,
path = require('path'),
fs = require('fs'),
logger = require('./logger');
var extensions = ['.js', '.json', '.coffee'], // All file extensions to watch.
watchPaths = ['./'], // Paths to watch for changes.
script = 'script.js', // The script to run.
scriptName = 'script', // Name of the script it run, without extension.
scriptParams= [], // Parameters that will be sent to the parser.
parser = 'node', // The parser that should run the script.
parserParams= [], // Parameters sent to the parser.
instance = null, // Instance of the parser process.
restartOnCleanExit = false, // Indicates if the parser should be restarted
// on clean exits (error code 0).
keepAlive = false, // Restart on exit, error and change.
webSocketServer = null // A WebSocket server that will notify any
// connected client of changes made to files.
// This will allow browsers to refresh their
// page. The WebSocket client will be sent
// 'restart' when the script is restarted and
// 'exit' when the script exists.
/**
* Parses the supplied string of comma seperated file extensions and returns an
* array of its values.
* @param {String} str - a string on the format ".ext1,.ext2,.ext3".
* @retruns {String[]} - a list of all the found extensions in @a str.
*/
function extensionsParser(str) {
// Convert the file extensions string to a list.
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
// Make sure the file extension has the correct format: '.ext'
var ext = '.' + list[i].replace(/(^\s?\.?)|(\s?$)/g, '');
list[i] = ext.toLowerCase();
}
return list;
}
/**
* Parses the supplied string of comma seperated list and returns an array of
* its values.
* @param {String} str - a string on the format "value1, value2, value2".
* @retruns {String[]} - a list of all the found extensions in @a str.
*/
function listParser(str) {
var list = str.split(',');
for (var i = 0; i < list.length; i++)
list[i] = list[i].replace(/(^\s?)|(\s?$)/g, '');
return list;
}
/**
* Kills the parser.
* @param {Boolean} [noMsg] - indicates if no message should be written to the
* log. Defaults to false.
* @param {String} [signal] - indicates which kill signal that sould be sent
* toLowerCase the child process (only applicable on Linux). Defaults to null.
* @return {Bool} - true if the process was killed; otherwise false.
*/
function kill(noMsg, signal) {
if (!instance)
return false;
try {
if (signal)
instance.kill(signal);
else
process.kill(instance.pid);
if ((noMsg || false) !== true)
logger.log('Killed', clc.green(script));
} catch (ex) {
// Process was already dead.
return false;
}
return true;
}
/** Restarts the parser. */
function restart() {
logger.log('Restarting', clc.green(script));
notifyWebSocket('restart');
if (!kill(true)) {
// The process wasn't running, start it now.
start(true);
} /*else {
// The process will restart when its 'exit' event is emitted.
}*/
}
/**
* Notifies all connection WebSocket clients by sending them the supplied
* message.
* @param message {String} - a message that will be sent to all WebSocket
* clients currently connected.
*/
function notifyWebSocket(message) {
if (!webSocketServer || !message)
return;
// Send the message to all connection in the WebSocket server.
for (var value in webSocketServer.conn) {
var connection = webSocketServer.conn[value];
if (connection)
connection.send(message)
}
}
/**
* Starts and instance of the parser if none is running.
* @param {Boolean} [noMsg] - indicates if no message should be written to the
* log. Defaults to false.
*/
function start(noMsg) {
if ((noMsg || false) !== true)
logger.log('Starting', clc.green(script), 'with', clc.magenta(parser));
if (instance)
return;
// Spawn an instance of the parser that will run the script.
instance = spawn(parser, parserParams);
// Redirect the parser/script's output to the console.
instance.stdout.on('data', function (data) {
logger.scriptLog(scriptName, data.toString());
});
instance.stderr.on('data', function (data) {
logger.scriptLog(scriptName, data.toString(), true);
});
instance.stderr.on('data', function (data) {
if (/^execvp\(\)/.test(data.toString())) {
logger.error('Failed to restart child process.');
process.exit(0);
}
});
instance.on('exit', function (code, signal) {
instance = null;
if (signal == 'SIGUSR2') {
logger.error('Signal interuption');
start();
return;
}
logger.log(clc.green(script), 'exited with code', clc.yellow(code));
notifyWebSocket('exit');
if (keepAlive || (restartOnCleanExit && code == 0)) {
start();
return;
}
});
}
// Listen for uncaught exceptions that might be thrown by this script.
process.on('uncaughtException', function(error){
logger.error(error.stack.toString());
restart();
});
// Make sure to kill the parser process when this process is killed.
process.on('exit', function(code) {
kill();
});
// Propage signals to child process.
['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 'SIGBUS',
'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGPIPE', 'SIGTERM'
].forEach(function(signal) {
try {
process.on(signal, (function(signal) {
logger.log('Sending signal', clc.yellow(signal), 'to',
clc.green(script));
kill(true, signal);
process.exit(0);
}).bind(this, signal));
} catch (ex) {
// Ignore those that does not exist on Windows.
}
});
// Configure commander for the commands it should accept from the user.
program
.version('1.1.5')
.usage('[-ewras] [-x|-c] <script> [script args ...]')
.option('-e, --extensions <list>', 'a list of extensions to watch for changes', extensionsParser)
.option('-w, --watch <list>', 'a list of folders to watch for changes', listParser)
.option('-r, --restart-on-exit', 'restart on clean exit (exit status 0)')
.option('-a, --keep-alive', 'restart on exit, error or chanage')
.option('-x, --exec <executable>', 'the executable that runs the script')
.option('-c, --sys-command', 'executes the script as a system command')
.option('-s, --websocket <port>', 'start a WebSocket server to notify browsers', parseInt)
program.on('--help', function() {
console.log(' Required:');
console.log('');
console.log(' <script> the script to run, eg. "server.js"');
console.log('');
console.log(' More Info:');
console.log('');
console.log(' <list> a comma-delimited list, eg. "coffee,jade" or "./, bin/"');
console.log('');
console.log('');
console.log(' The default extensions are "js, json, coffee". Override using -e.');
console.log('');
console.log(' By default the directory containing the script is watched. Override');
console.log(' using -w.');
console.log('');
console.log(' The executable that will run the script is automatically choosen based on');
console.log(' file extension. Coffee is used for ".coffee"; otherwise Node is used.');
console.log(' Override using -x.');
console.log('');
console.log(' Any program can be executed when a file changes by using the -c option.');
console.log(' It can for example be used to watch and compile Stylus files.');
console.log('');
console.log(' A WebSocket server can be started using the -s options. The WebSocket');
console.log(' server can be used to automatically reload browsers when a file changes.');
console.log(' See ./test/websocket/ for an example.');
console.log('');
console.log(' Example:');
console.log('');
console.log(' $ conquer server.js');
console.log(' $ conquer -w templates -e .jade server.coffee');
console.log(' $ conquer -w styles -e .styl -c stylus.cmd styles -o css');
console.log(' $ conquer -e ".js, .jade" server.js --port 80');
console.log('');
console.log(' The last example will start server.js on port 80 using Node. It will');
console.log(' monitor all .js and .jade files in the same directory (and subdirectories)');
console.log(' as server.js for changes.');
console.log('');
});
program.parse(process.argv);
// The input file should be the first argument.
script = program.args[0];
scriptName = path.basename(script, path.extname(script));
if (!script) {
logger.warn('No input file!');
process.exit(0);
return;
}
if (!program.sysCommand && !fs.existsSync(script)) {
logger.warn('Input file not found!');
process.exit(0);
return;
}
if (program.watch) {
// Watch the paths supplied by the user.
watchPaths = program.watch;
//logger.log('Watching paths: ' + clc.green(watchPaths.join(', ')));
} else {
// Watch the path containing the script.
watchPaths = [path.dirname(script)];
}
// All arguments after the input file (script) are script parameters that will
// be passed to the parser.
scriptParams = process.argv.slice(process.argv.indexOf(script) + 1);
if (program.sysCommand) {
// No parser will be used, since no script will be ran. Instead, the script
// variable holds the name of the executable to run.
parser = script;
parserParams = scriptParams;
} else {
if (program.exec) {
// Use the user supplied parser.
parser = program.exec;
} else {
// Select parser based on file extension.
if (path.extname(script) == '.coffee') {
parser = 'coffee';
if (process.platform.substr(0, 3) == 'win')
parser = 'coffee.cmd';
}
}
// The first parameters give to Node should be the name of the script to run
// followed by the parameters to that script.
parserParams = [script].concat(scriptParams);
}
if (program.extensions) {
// Watch the user supplied file extensions.
extensions = program.extensions;
//logger.log('Watching extensions: ' + clc.green(extensions.join(', ')));
}
restartOnCleanExit = program.restartOnExit || false;
keepAlive = program.keepAlive || false;
if (program.websocket) {
webSocketServer = wsock.createServer();
// Store all new connections in a list of the server.
webSocketServer.conn = {};
webSocketServer.on('connect', function(connection) {
webSocketServer.conn[connection] = connection;
connection.on('close', function() {
delete webSocketServer.conn[connection];
})
});
webSocketServer.listen(program.websocket || 8083, function() {
logger.log('WebSocket server running at',
clc.green('ws://localhost:' + program.websocket));
});
}
// Watch the directory supplied by the user.
logger.log('Watching', clc.green(watchPaths.join(', ')), 'for changes to', clc.green(extensions.join(', ')));
watchr.watch({
paths: watchPaths,
listener: function(eventName, filePath, fileCurrentStat, filePreviousStat) {
// We are only interesed in files with extensions that are part of the
// extensions array.
var ext = path.extname(filePath).toLowerCase();
if (extensions.indexOf(ext) != -1) {
logger.log(clc.green(path.basename(filePath)), 'changed');
restart();
}
},
next: function(err, watcher) {
if (err)
throw err;
//logger.log('Watcher setup successfully');
start();
}
});