1 | var fs = require("fs");
|
2 | var spawn = require("child_process").spawn;
|
3 | var path = require("path");
|
4 | var fileExtensionPattern;
|
5 | var startChildProcess;
|
6 | var noRestartOn = null;
|
7 | var debug = true;
|
8 | var verbose = false;
|
9 | var restartVerbose = false;
|
10 | var ignoredPaths = {};
|
11 | var ignoreSymLinks = false;
|
12 | var forceWatchFlag = false;
|
13 | var instantKillFlag = false;
|
14 | var timestampFlag = false;
|
15 | var interactive = true;
|
16 | var log = console.log;
|
17 | var crash_queued = false;
|
18 | var harmony_default_parameters = false;
|
19 | var harmony_destructuring = false;
|
20 |
|
21 | exports.run = run;
|
22 |
|
23 | function 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 |
|
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 |
|
95 | extensions = "node,js";
|
96 | if (programExt && extensions.indexOf(programExt) == -1) {
|
97 |
|
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 |
|
131 | program.unshift("--nodejs")
|
132 | }
|
133 | if (pidFilePath) {
|
134 | var pid = process.pid;
|
135 |
|
136 |
|
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 |
|
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 |
|
178 |
|
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 |
|
207 |
|
208 | startChildProcess = function() { startProgram(program, executor); };
|
209 |
|
210 |
|
211 |
|
212 | startChildProcess();
|
213 |
|
214 |
|
215 | if(interactive) {
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | var stdin = process.stdin;
|
221 |
|
222 | stdin.setEncoding( 'utf8' );
|
223 | stdin.on('readable', function() {
|
224 | var chunk = process.stdin.read();
|
225 |
|
226 |
|
227 |
|
228 | if (chunk !== null && chunk === "rs\n" || chunk === "rs\r\n") {
|
229 |
|
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 |
|
261 | function print (m) { console.log(m); return print; }
|
262 |
|
263 | function 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 |
|
360 | function 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 |
|
365 |
|
366 |
|
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 |
|
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 |
|
390 | function logTimestamp() {
|
391 | if (timestampFlag) {
|
392 |
|
393 |
|
394 | console.log(Date().toString());
|
395 | }
|
396 | }
|
397 |
|
398 | function 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 |
|
421 | function 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 |
|
442 |
|
443 | function 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 |
|
464 | var nodeVersion = process.version.split(".");
|
465 |
|
466 | var isWindowsWithoutWatchFile = process.platform === 'win32' && parseInt(nodeVersion[1]) <= 6;
|
467 |
|
468 | function 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 |
|
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 |
|
487 | var 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 | };
|