var fs = require("fs"); var spawn = require("child_process").spawn; var path = require("path"); var fileExtensionPattern; var startChildProcess; var noRestartOn = null; var debug = true; var verbose = false; var restartVerbose = false; var ignoredPaths = {}; var ignoreSymLinks = false; var forceWatchFlag = false; var instantKillFlag = false; var timestampFlag = false; var interactive = true; var log = console.log; var crash_queued = false; var harmony_default_parameters = false; var harmony_destructuring = false; exports.run = run; function run (args) { var arg, next, watch, ignore, pidFilePath, program, extensions, executor, poll_interval, debugFlag, debugBrkFlag, debugBrkFlagArg, harmony, inspect; while (arg = args.shift()) { if (arg === "--help" || arg === "-h" || arg === "-?") { return help(); } else if (arg === "--quiet" || arg === "-q") { debug = false; log = function(){}; } else if (arg === "--harmony") { harmony = true; } else if (arg === "--inspect") { inspect = true; } else if (arg === "--harmony_default_parameters") { harmony_default_parameters = true; } else if (arg === "--harmony_destructuring") { harmony_destructuring = true; } else if (arg === "--verbose" || arg === "-V") { verbose = true; } else if (arg === "--restart-verbose" || arg === "-RV") { restartVerbose = true; } else if (arg === "--watch" || arg === "-w") { watch = args.shift(); } else if (arg == "--non-interactive" || arg === "-t") { interactive = false; } else if (arg === "--ignore" || arg === "-i") { ignore = args.shift(); } else if (arg === "--save-pid" || arg === "-pid") { pidFilePath = args.shift(); } else if (arg === "--ignore-symlinks") { ignoreSymLinks = true; } else if (arg === "--poll-interval" || arg === "-p") { poll_interval = parseInt(args.shift()); } else if (arg === "--extensions" || arg === "-e") { extensions = args.shift(); } else if (arg === "--exec" || arg === "-x") { executor = args.shift(); } else if (arg === "--no-restart-on" || arg === "-n") { noRestartOn = args.shift(); } else if (arg.indexOf("--debug") > -1 && arg.indexOf('--debug-brk') === -1) { debugFlag = arg; } else if (arg.indexOf('--debug-brk')>=0) { debugBrkFlag = true; debugBrkFlagArg = arg; } else if (arg === "--force-watch") { forceWatchFlag = true; } else if (arg === "--instant-kill" || arg === "-k") { instantKillFlag = true; } else if (arg === "--timestamp" || arg === "-s") { timestampFlag = true; } else if (arg === "--") { program = args; break; } else if (arg[0] != "-" && !args.length) { // Assume last arg is the program program = [arg]; } } if (!program) { return help(); } if (!watch) { watch = "."; } if (!poll_interval) { poll_interval = 1000; } var programExt = program.join(" ").match(/.*\.(\S*)/); programExt = programExt && programExt[1]; if (!extensions) { // If no extensions passed try to guess from the program extensions = "node,js"; if (programExt && extensions.indexOf(programExt) == -1) { // Support coffee and litcoffee extensions if(programExt === "coffee" || programExt === "litcoffee") { extensions += ",coffee,litcoffee"; } else { extensions += "," + programExt; } } } fileExtensionPattern = new RegExp("^.*\.(" + extensions.replace(/,/g, "|") + ")$"); if (!executor) { executor = (programExt === "coffee" || programExt === "litcoffee") ? "coffee" : "node"; } if (debugFlag) { program.unshift(debugFlag); } if (debugBrkFlag) { program.unshift(debugBrkFlagArg); } if (harmony) { program.unshift("--harmony"); } if (inspect) { program.unshift("--inspect"); } if (harmony_default_parameters) { program.unshift("--harmony_default_parameters"); } if (harmony_destructuring) { program.unshift("--harmony_destructuring"); } if (executor === "coffee" && (debugFlag || debugBrkFlag)) { // coffee does not understand debug or debug-brk, make coffee pass options to node program.unshift("--nodejs") } if (pidFilePath) { var pid = process.pid; // // verify if we have write access // canWrite(pidFilePath, function(err) { if ( err ) { log("Continuing..."); } else { fs.writeFileSync(pidFilePath, pid + '\n'); } }); } var deletePidFile = function(){ fs.exists(pidFilePath, function (exists) { if ( exists ) { log("Removing pid file"); fs.unlinkSync(pidFilePath); } else { log("No pid file to remove..."); } process.exit(); }); }; try { // Pass kill signals through to child [ "SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT" ].forEach( function(signal) { process.on(signal, function () { var child = exports.child; if (child) { log("Received "+signal+", killing child process..."); child.kill(signal); } if (pidFilePath){ deletePidFile(); } else { process.exit(); } }); }); } catch(e) { // Windows doesn't support signals yet, so they simply don't get this handling. // https://github.com/joyent/node/issues/1553 } process.on('exit', function () { var child = exports.child; if (child) { log("Parent process exiting, terminating child..."); child.kill("SIGTERM"); } }); log(""); log("Running node-supervisor with"); log(" program '" + program.join(" ") + "'"); log(" --watch '" + watch + "'"); if (!interactive) { log(" --non-interactive"); } if (ignore) { log(" --ignore '" + ignore + "'"); } if (pidFilePath){ log(" --save-pid '" + pidFilePath + "'"); } log(" --extensions '" + extensions + "'"); log(" --exec '" + executor + "'"); log(""); // store the call to startProgramm in startChildProcess // in order to call it later startChildProcess = function() { startProgram(program, executor); }; // if we have a program, then run it, and restart when it crashes. // if we have a watch folder, then watch the folder for changes and restart the prog startChildProcess(); // If interaction has not been disabled, start the CLI if(interactive) { // // Read input from stdin // var stdin = process.stdin; stdin.setEncoding( 'utf8' ); stdin.on('readable', function() { var chunk = process.stdin.read(); // // Restart process when user inputs rs // if (chunk !== null && chunk === "rs\n" || chunk === "rs\r\n") { // process.stdout.write('data: ' + chunk); crash(); } }); } if (ignore) { var ignoreItems = ignore.split(','); ignoreItems.forEach(function (ignoreItem) { ignoreItem = path.resolve(ignoreItem); ignoredPaths[ignoreItem] = true; log("Ignoring directory '" + ignoreItem + "'."); }); } var watchItems = watch.split(','); watchItems.forEach(function (watchItem) { watchItem = path.resolve(watchItem); if ( ! ignoredPaths[watchItem] ) { log("Watching directory '" + watchItem + "' for changes."); if(interactive) { log("Press rs for restarting the process."); } findAllWatchFiles(watchItem, function(f) { watchGivenFile( f, poll_interval ); }); } }); } // function print (m, n) { console.log(m+(!n?"\n":"")); return print; } function print (m) { console.log(m); return print; } function help () { print ("") ("Node Supervisor is used to restart programs when they crash.") ("It can also be used to restart programs when a *.js file changes.") ("") ("Usage:") (" supervisor [options] ") (" supervisor [options] -- [args ...]") ("") ("Required:") (" ") (" The program to run.") ("") ("Options:") (" -w|--watch ") (" A comma-delimited list of folders or js files to watch for changes.") (" When a change to a js file occurs, reload the program") (" Default is '.'") ("") (" -i|--ignore ") (" A comma-delimited list of folders to ignore for changes.") (" No default") ("") (" --ignore-symlinks") (" Enable symbolic links ignoring when looking for files to watch.") ("") (" -p|--poll-interval ") (" How often to poll watched files for changes.") (" Defaults to Node default.") ("") (" -e|--extensions ") (" Specific file extensions to watch in addition to defaults.") (" Used when --watch option includes folders") (" Default is 'node,js'") ("") (" -x|--exec ") (" The executable that runs the specified program.") (" Default is 'node'") ("") (" --debug[=port]") (" Start node with --debug flag. ") ("") (" --debug-brk[=port]") (" Start node with --debug-brk[=port] flag.") ("") (" --harmony") (" Start node with --harmony flag.") (" --inspect") (" Start node with --inspect flag.") ("") (" --harmony_default_parameters") (" Start node with --harmony_default_parameters flag.") ("") (" -n|--no-restart-on error|exit") (" Don't automatically restart the supervised program if it ends.") (" Supervisor will wait for a change in the source files.") (" If \"error\", an exit code of 0 will still restart.") (" If \"exit\", no restart regardless of exit code.") (" If \"success\", no restart only if exit code is 0.") ("") (" -t|--non-interactive") (" Disable interactive capacity.") (" With this option, supervisor won't listen to stdin.") ("") (" -k|--instant-kill") (" use SIGKILL (-9) to terminate child instead of the more gentle SIGTERM.") ("") (" --force-watch") (" Use fs.watch instead of fs.watchFile.") (" This may be useful if you see a high cpu load on a windows machine.") ("") (" -s|--timestamp") (" Log timestamp after each run.") (" Make it easy to tell when the task last ran.") ("") (" -h|--help|-?") (" Display these usage instructions.") ("") (" -q|--quiet") (" Suppress DEBUG messages") ("") (" -V|--verbose") (" Show extra DEBUG messages") ("") ("Options available after start:") ("rs - restart process.") (" Useful for restarting supervisor eaven if no file has changed.") ("") ("Examples:") (" supervisor myapp.js") (" supervisor myapp.coffee") (" supervisor -w scripts -e myext -x myrunner myapp") (" supervisor -- server.js -h host -p port") (""); } function startProgram (prog, exec) { log("Starting child process with '" + exec + " " + prog.join(" ") + "'"); crash_queued = false; var child = exports.child = spawn(exec, prog, {stdio: 'inherit'}); // Support for Windows ".cmd" files // On Windows 8.1, spawn can't launch apps without the .cmd extention // If already retried, let the app crash ... :'( if (process.platform === "win32" && exec.indexOf('.cmd') == -1) { child.on('error', function (err) { if (err.code === "ENOENT") return startProgram(prog, exec + ".cmd"); }); } if (child.stdout) { // node < 0.8 doesn't understand the 'inherit' option, so pass through manually child.stdout.addListener("data", function (chunk) { chunk && console.log(chunk); }); child.stderr.addListener("data", function (chunk) { chunk && console.error(chunk); }); } child.addListener("exit", function (code) { logTimestamp(); if (!crash_queued) { log("Program " + exec + " " + prog.join(" ") + " exited with code " + code + "\n"); exports.child = null; if (noRestartOn == "exit" || noRestartOn == "error" && code !== 0 || noRestartOn == "success" && code === 0) return; } startProgram(prog, exec); }); } function logTimestamp() { if (timestampFlag) { // use console.log() directly rather than log() so that -q/--quiet // does not override/silence it console.log(Date().toString()); } } function crash () { if (crash_queued) return; crash_queued = true; var child = exports.child; setTimeout(function() { if (child) { if (instantKillFlag) { log("crashing child with SIGKILL"); process.kill(child.pid, "SIGKILL"); } else { log("crashing child"); process.kill(child.pid, "SIGTERM"); } } else { log("restarting child"); startChildProcess(); } }, 50); } function crashWin (event, filename) { var shouldCrash = true; if( event === 'change' ) { if (filename) { filename = path.resolve(filename); Object.keys(ignoredPaths).forEach(function (ignorePath) { if ( filename.indexOf(ignorePath + '\\') === 0 || filename === ignorePath) { shouldCrash = false; } }); } if (shouldCrash) { if (verbose || restartVerbose) { log("Changes detected" + (filename ? ": " + filename : "")); } crash(); } } } /** * Determine if a file can be written */ function canWrite(path, callback) { fs.open(path, "w", function (err, fd) { if (err) { if (err.code === "EISDIR") { log("Can't open " + path + ". It's a directory."); } if (err.code === "EACCESS") { log("Can't open " + path + ". No access."); } else { log("Can't open " + path + "."); } return callback(err); } fs.close(fd, function (err) { if (err) return callback(err); callback(null, true); }); }); } var nodeVersion = process.version.split("."); var isWindowsWithoutWatchFile = process.platform === 'win32' && parseInt(nodeVersion[1]) <= 6; function watchGivenFile (watch, poll_interval) { if (isWindowsWithoutWatchFile || forceWatchFlag) { fs.watch(watch, { persistent: true, interval: poll_interval }, crashWin); } else { fs.watchFile(watch, { persistent: true, interval: poll_interval }, function(oldStat, newStat) { // we only care about modification time, not access time. if ( newStat.mtime.getTime() !== oldStat.mtime.getTime() ) { if (verbose) { log("file changed: " + watch); } } crash(); }); } if (verbose) { log("watching file '" + watch + "'"); } } var findAllWatchFiles = function(dir, callback) { dir = path.resolve(dir); if (ignoredPaths[dir]) return; fs[ignoreSymLinks ? 'lstat' : 'stat'](dir, function(err, stats) { if (err) { console.error('Error retrieving stats for file: ' + dir); } else { if (ignoreSymLinks && stats.isSymbolicLink()) { log("Ignoring symbolic link '" + dir + "'."); return; } if (stats.isDirectory()) { if (isWindowsWithoutWatchFile || forceWatchFlag) callback(dir); fs.readdir(dir, function(err, fileNames) { if(err) { console.error('Error reading path: ' + dir); } else { fileNames.forEach(function (fileName) { findAllWatchFiles(path.join(dir, fileName), callback); }); } }); } else { if ((!isWindowsWithoutWatchFile || !forceWatchFlag) && dir.match(fileExtensionPattern)) { callback(dir); } } } }); };