UNPKG

17.3 kBJavaScriptView Raw
1var fs = require("fs");
2var spawn = require("child_process").spawn;
3var path = require("path");
4var fileExtensionPattern;
5var startChildProcess;
6var noRestartOn = null;
7var debug = true;
8var verbose = false;
9var restartVerbose = false;
10var ignoredPaths = {};
11var ignoreSymLinks = false;
12var forceWatchFlag = false;
13var instantKillFlag = false;
14var timestampFlag = false;
15var interactive = true;
16var log = console.log;
17var crash_queued = false;
18var harmony_default_parameters = false;
19var harmony_destructuring = false;
20
21exports.run = run;
22
23function run (args) {
24 var arg, next, watch, ignore, pidFilePath, program, extensions, executor, poll_interval, debugFlag, debugBrkFlag, debugBrkFlagArg, harmony, inspect;
25 while (arg = args.shift()) {
26 if (arg === "--help" || arg === "-h" || arg === "-?") {
27 return help();
28 } else if (arg === "--quiet" || arg === "-q") {
29 debug = false;
30 log = function(){};
31 } else if (arg === "--harmony") {
32 harmony = true;
33 } else if (arg === "--inspect") {
34 inspect = true;
35 } else if (arg === "--harmony_default_parameters") {
36 harmony_default_parameters = true;
37 } else if (arg === "--harmony_destructuring") {
38 harmony_destructuring = true;
39 } else if (arg === "--verbose" || arg === "-V") {
40 verbose = true;
41 } else if (arg === "--restart-verbose" || arg === "-RV") {
42 restartVerbose = true;
43 } else if (arg === "--watch" || arg === "-w") {
44 watch = args.shift();
45 } else if (arg == "--non-interactive" || arg === "-t") {
46 interactive = false;
47 } else if (arg === "--ignore" || arg === "-i") {
48 ignore = args.shift();
49 } else if (arg === "--save-pid" || arg === "-pid") {
50 pidFilePath = args.shift();
51 } else if (arg === "--ignore-symlinks") {
52 ignoreSymLinks = true;
53 } else if (arg === "--poll-interval" || arg === "-p") {
54 poll_interval = parseInt(args.shift());
55 } else if (arg === "--extensions" || arg === "-e") {
56 extensions = args.shift();
57 } else if (arg === "--exec" || arg === "-x") {
58 executor = args.shift();
59 } else if (arg === "--no-restart-on" || arg === "-n") {
60 noRestartOn = args.shift();
61 } else if (arg.indexOf("--debug") > -1 && arg.indexOf('--debug-brk') === -1) {
62 debugFlag = arg;
63 } else if (arg.indexOf('--debug-brk')>=0) {
64 debugBrkFlag = true;
65 debugBrkFlagArg = arg;
66 } else if (arg === "--force-watch") {
67 forceWatchFlag = true;
68 } else if (arg === "--instant-kill" || arg === "-k") {
69 instantKillFlag = true;
70 } else if (arg === "--timestamp" || arg === "-s") {
71 timestampFlag = true;
72 } else if (arg === "--") {
73 program = args;
74 break;
75 } else if (arg[0] != "-" && !args.length) {
76 // Assume last arg is the program
77 program = [arg];
78 }
79 }
80 if (!program) {
81 return help();
82 }
83 if (!watch) {
84 watch = ".";
85 }
86 if (!poll_interval) {
87 poll_interval = 1000;
88 }
89
90 var programExt = program.join(" ").match(/.*\.(\S*)/);
91 programExt = programExt && programExt[1];
92
93 if (!extensions) {
94 // If no extensions passed try to guess from the program
95 extensions = "node,js";
96 if (programExt && extensions.indexOf(programExt) == -1) {
97 // Support coffee and litcoffee extensions
98 if(programExt === "coffee" || programExt === "litcoffee") {
99 extensions += ",coffee,litcoffee";
100 } else {
101 extensions += "," + programExt;
102 }
103 }
104 }
105 fileExtensionPattern = new RegExp("^.*\.(" + extensions.replace(/,/g, "|") + ")$");
106
107 if (!executor) {
108 executor = (programExt === "coffee" || programExt === "litcoffee") ? "coffee" : "node";
109 }
110
111 if (debugFlag) {
112 program.unshift(debugFlag);
113 }
114 if (debugBrkFlag) {
115 program.unshift(debugBrkFlagArg);
116 }
117 if (harmony) {
118 program.unshift("--harmony");
119 }
120 if (inspect) {
121 program.unshift("--inspect");
122 }
123 if (harmony_default_parameters) {
124 program.unshift("--harmony_default_parameters");
125 }
126 if (harmony_destructuring) {
127 program.unshift("--harmony_destructuring");
128 }
129 if (executor === "coffee" && (debugFlag || debugBrkFlag)) {
130 // coffee does not understand debug or debug-brk, make coffee pass options to node
131 program.unshift("--nodejs")
132 }
133 if (pidFilePath) {
134 var pid = process.pid;
135 //
136 // verify if we have write access
137 //
138 canWrite(pidFilePath, function(err) {
139 if ( err ) {
140 log("Continuing...");
141 } else {
142 fs.writeFileSync(pidFilePath, pid + '\n');
143 }
144 });
145 }
146
147 var deletePidFile = function(){
148 fs.exists(pidFilePath, function (exists) {
149 if ( exists ) {
150 log("Removing pid file");
151 fs.unlinkSync(pidFilePath);
152 } else {
153 log("No pid file to remove...");
154 }
155 process.exit();
156 });
157 };
158
159 try {
160 // Pass kill signals through to child
161 [ "SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT" ].forEach( function(signal) {
162 process.on(signal, function () {
163 var child = exports.child;
164 if (child) {
165 log("Received "+signal+", killing child process...");
166 child.kill(signal);
167 }
168 if (pidFilePath){
169 deletePidFile();
170 }
171 else {
172 process.exit();
173 }
174 });
175 });
176 } catch(e) {
177 // Windows doesn't support signals yet, so they simply don't get this handling.
178 // https://github.com/joyent/node/issues/1553
179 }
180
181 process.on('exit', function () {
182 var child = exports.child;
183 if (child) {
184 log("Parent process exiting, terminating child...");
185 child.kill("SIGTERM");
186 }
187 });
188
189 log("");
190 log("Running node-supervisor with");
191 log(" program '" + program.join(" ") + "'");
192 log(" --watch '" + watch + "'");
193 if (!interactive) {
194 log(" --non-interactive");
195 }
196 if (ignore) {
197 log(" --ignore '" + ignore + "'");
198 }
199 if (pidFilePath){
200 log(" --save-pid '" + pidFilePath + "'");
201 }
202 log(" --extensions '" + extensions + "'");
203 log(" --exec '" + executor + "'");
204 log("");
205
206 // store the call to startProgramm in startChildProcess
207 // in order to call it later
208 startChildProcess = function() { startProgram(program, executor); };
209
210 // if we have a program, then run it, and restart when it crashes.
211 // if we have a watch folder, then watch the folder for changes and restart the prog
212 startChildProcess();
213
214 // If interaction has not been disabled, start the CLI
215 if(interactive) {
216
217 //
218 // Read input from stdin
219 //
220 var stdin = process.stdin;
221
222 stdin.setEncoding( 'utf8' );
223 stdin.on('readable', function() {
224 var chunk = process.stdin.read();
225 //
226 // Restart process when user inputs rs
227 //
228 if (chunk !== null && chunk === "rs\n" || chunk === "rs\r\n") {
229 // process.stdout.write('data: ' + chunk);
230 crash();
231 }
232 });
233 }
234
235 if (ignore) {
236 var ignoreItems = ignore.split(',');
237 ignoreItems.forEach(function (ignoreItem) {
238 ignoreItem = path.resolve(ignoreItem);
239 ignoredPaths[ignoreItem] = true;
240 log("Ignoring directory '" + ignoreItem + "'.");
241 });
242 }
243
244 var watchItems = watch.split(',');
245 watchItems.forEach(function (watchItem) {
246 watchItem = path.resolve(watchItem);
247
248 if ( ! ignoredPaths[watchItem] ) {
249 log("Watching directory '" + watchItem + "' for changes.");
250 if(interactive) {
251 log("Press rs for restarting the process.");
252 }
253 findAllWatchFiles(watchItem, function(f) {
254 watchGivenFile( f, poll_interval );
255 });
256 }
257 });
258}
259
260// function print (m, n) { console.log(m+(!n?"\n":"")); return print; }
261function print (m) { console.log(m); return print; }
262
263function help () {
264 print
265 ("")
266 ("Node Supervisor is used to restart programs when they crash.")
267 ("It can also be used to restart programs when a *.js file changes.")
268 ("")
269 ("Usage:")
270 (" supervisor [options] <program>")
271 (" supervisor [options] -- <program> [args ...]")
272 ("")
273 ("Required:")
274 (" <program>")
275 (" The program to run.")
276 ("")
277 ("Options:")
278 (" -w|--watch <watchItems>")
279 (" A comma-delimited list of folders or js files to watch for changes.")
280 (" When a change to a js file occurs, reload the program")
281 (" Default is '.'")
282 ("")
283 (" -i|--ignore <ignoreItems>")
284 (" A comma-delimited list of folders to ignore for changes.")
285 (" No default")
286 ("")
287 (" --ignore-symlinks")
288 (" Enable symbolic links ignoring when looking for files to watch.")
289 ("")
290 (" -p|--poll-interval <milliseconds>")
291 (" How often to poll watched files for changes.")
292 (" Defaults to Node default.")
293 ("")
294 (" -e|--extensions <extensions>")
295 (" Specific file extensions to watch in addition to defaults.")
296 (" Used when --watch option includes folders")
297 (" Default is 'node,js'")
298 ("")
299 (" -x|--exec <executable>")
300 (" The executable that runs the specified program.")
301 (" Default is 'node'")
302 ("")
303 (" --debug[=port]")
304 (" Start node with --debug flag. ")
305 ("")
306 (" --debug-brk[=port]")
307 (" Start node with --debug-brk[=port] flag.")
308 ("")
309 (" --harmony")
310 (" Start node with --harmony flag.")
311 (" --inspect")
312 (" Start node with --inspect flag.")
313 ("")
314 (" --harmony_default_parameters")
315 (" Start node with --harmony_default_parameters flag.")
316 ("")
317 (" -n|--no-restart-on error|exit")
318 (" Don't automatically restart the supervised program if it ends.")
319 (" Supervisor will wait for a change in the source files.")
320 (" If \"error\", an exit code of 0 will still restart.")
321 (" If \"exit\", no restart regardless of exit code.")
322 (" If \"success\", no restart only if exit code is 0.")
323 ("")
324 (" -t|--non-interactive")
325 (" Disable interactive capacity.")
326 (" With this option, supervisor won't listen to stdin.")
327 ("")
328 (" -k|--instant-kill")
329 (" use SIGKILL (-9) to terminate child instead of the more gentle SIGTERM.")
330 ("")
331 (" --force-watch")
332 (" Use fs.watch instead of fs.watchFile.")
333 (" This may be useful if you see a high cpu load on a windows machine.")
334 ("")
335 (" -s|--timestamp")
336 (" Log timestamp after each run.")
337 (" Make it easy to tell when the task last ran.")
338 ("")
339 (" -h|--help|-?")
340 (" Display these usage instructions.")
341 ("")
342 (" -q|--quiet")
343 (" Suppress DEBUG messages")
344 ("")
345 (" -V|--verbose")
346 (" Show extra DEBUG messages")
347 ("")
348 ("Options available after start:")
349 ("rs - restart process.")
350 (" Useful for restarting supervisor eaven if no file has changed.")
351 ("")
352 ("Examples:")
353 (" supervisor myapp.js")
354 (" supervisor myapp.coffee")
355 (" supervisor -w scripts -e myext -x myrunner myapp")
356 (" supervisor -- server.js -h host -p port")
357 ("");
358}
359
360function startProgram (prog, exec) {
361 log("Starting child process with '" + exec + " " + prog.join(" ") + "'");
362 crash_queued = false;
363 var child = exports.child = spawn(exec, prog, {stdio: 'inherit'});
364 // Support for Windows ".cmd" files
365 // On Windows 8.1, spawn can't launch apps without the .cmd extention
366 // If already retried, let the app crash ... :'(
367 if (process.platform === "win32" && exec.indexOf('.cmd') == -1) {
368 child.on('error', function (err) {
369 if (err.code === "ENOENT")
370 return startProgram(prog, exec + ".cmd");
371 });
372 }
373 if (child.stdout) {
374 // node < 0.8 doesn't understand the 'inherit' option, so pass through manually
375 child.stdout.addListener("data", function (chunk) { chunk && console.log(chunk); });
376 child.stderr.addListener("data", function (chunk) { chunk && console.error(chunk); });
377 }
378 child.addListener("exit", function (code) {
379 logTimestamp();
380
381 if (!crash_queued) {
382 log("Program " + exec + " " + prog.join(" ") + " exited with code " + code + "\n");
383 exports.child = null;
384 if (noRestartOn == "exit" || noRestartOn == "error" && code !== 0 || noRestartOn == "success" && code === 0) return;
385 }
386 startProgram(prog, exec);
387 });
388}
389
390function logTimestamp() {
391 if (timestampFlag) {
392 // use console.log() directly rather than log() so that -q/--quiet
393 // does not override/silence it
394 console.log(Date().toString());
395 }
396}
397
398function crash () {
399
400 if (crash_queued)
401 return;
402
403 crash_queued = true;
404 var child = exports.child;
405 setTimeout(function() {
406 if (child) {
407 if (instantKillFlag) {
408 log("crashing child with SIGKILL");
409 process.kill(child.pid, "SIGKILL");
410 } else {
411 log("crashing child");
412 process.kill(child.pid, "SIGTERM");
413 }
414 } else {
415 log("restarting child");
416 startChildProcess();
417 }
418 }, 50);
419}
420
421function crashWin (event, filename) {
422 var shouldCrash = true;
423 if( event === 'change' ) {
424 if (filename) {
425 filename = path.resolve(filename);
426 Object.keys(ignoredPaths).forEach(function (ignorePath) {
427 if ( filename.indexOf(ignorePath + '\\') === 0 || filename === ignorePath) {
428 shouldCrash = false;
429 }
430 });
431 }
432 if (shouldCrash) {
433 if (verbose || restartVerbose) {
434 log("Changes detected" + (filename ? ": " + filename : ""));
435 }
436 crash();
437 }
438 }
439}
440/**
441 * Determine if a file can be written
442 */
443function canWrite(path, callback) {
444 fs.open(path, "w", function (err, fd) {
445 if (err) {
446 if (err.code === "EISDIR") {
447 log("Can't open " + path + ". It's a directory.");
448 }
449 if (err.code === "EACCESS") {
450 log("Can't open " + path + ". No access.");
451 } else {
452 log("Can't open " + path + ".");
453 }
454 return callback(err);
455 }
456 fs.close(fd, function (err) {
457 if (err) return callback(err);
458 callback(null, true);
459 });
460 });
461}
462
463
464var nodeVersion = process.version.split(".");
465
466var isWindowsWithoutWatchFile = process.platform === 'win32' && parseInt(nodeVersion[1]) <= 6;
467
468function watchGivenFile (watch, poll_interval) {
469 if (isWindowsWithoutWatchFile || forceWatchFlag) {
470 fs.watch(watch, { persistent: true, interval: poll_interval }, crashWin);
471 } else {
472 fs.watchFile(watch, { persistent: true, interval: poll_interval }, function(oldStat, newStat) {
473 // we only care about modification time, not access time.
474 if ( newStat.mtime.getTime() !== oldStat.mtime.getTime() ) {
475 if (verbose) {
476 log("file changed: " + watch);
477 }
478 }
479 crash();
480 });
481 }
482 if (verbose) {
483 log("watching file '" + watch + "'");
484 }
485}
486
487var findAllWatchFiles = function(dir, callback) {
488 dir = path.resolve(dir);
489 if (ignoredPaths[dir])
490 return;
491 fs[ignoreSymLinks ? 'lstat' : 'stat'](dir, function(err, stats) {
492 if (err) {
493 console.error('Error retrieving stats for file: ' + dir);
494 } else {
495 if (ignoreSymLinks && stats.isSymbolicLink()) {
496 log("Ignoring symbolic link '" + dir + "'.");
497 return;
498 }
499
500 if (stats.isDirectory()) {
501 if (isWindowsWithoutWatchFile || forceWatchFlag) callback(dir);
502 fs.readdir(dir, function(err, fileNames) {
503 if(err) {
504 console.error('Error reading path: ' + dir);
505 }
506 else {
507 fileNames.forEach(function (fileName) {
508 findAllWatchFiles(path.join(dir, fileName), callback);
509 });
510 }
511 });
512 } else {
513 if ((!isWindowsWithoutWatchFile || !forceWatchFlag) && dir.match(fileExtensionPattern)) {
514 callback(dir);
515 }
516 }
517 }
518 });
519};