hackcentercontrol/nodeartnet/node_modules/nodemon/nodemon.js

704 lines
23 KiB
JavaScript
Raw Permalink Normal View History

2012-12-28 22:17:47 +00:00
#!/usr/bin/env node
var fs = require('fs'),
util = require('util'),
childProcess = require('child_process'),
dirs = [],
path = require('path'),
exists = fs.exists || path.exists, // yay, exists moved from path to fs in 0.7.x ... :-\
existsSync = fs.existsSync || path.existsSync,
spawn = childProcess.spawn,
meta = JSON.parse(fs.readFileSync(__dirname + '/package.json')),
exec = childProcess.exec,
flag = './.monitor',
child = null,
monitor = null,
ignoreFilePath = './.nodemonignore',
oldIgnoreFilePath = './nodemon-ignore',
ignoreFiles = [],
reIgnoreFiles = null,
timeout = 1000, // check every 1 second
restartDelay = 0, // controlled through arg --delay 10 (for 10 seconds)
restartTimer = null,
lastStarted = +new Date,
statOffset = 0, // stupid fix for https://github.com/joyent/node/issues/2705
platform = process.platform,
isWindows = platform === 'win32',
noWatch = (platform !== 'win32') || !fs.watch, // && platform !== 'linux' - removed linux fs.watch usage #72
watchFile = platform === 'darwin' ? fs.watchFile : fs.watch, // lame :(
watchWorks = true, // whether or not fs.watch actually works on this platform, tested and set later before starting
// create once, reuse as needed
reEscComments = /\\#/g,
reUnescapeComments = /\^\^/g, // note that '^^' is used in place of escaped comments
reComments = /#.*$/,
reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g,
reEscapeChars = /[.|\-[\]()\\]/g,
reAsterisk = /\*/g,
// Flag to distinguish an app crash from intentional killing (used on Windows only for now)
killedAfterChange = false,
// Make this the last call so it can use the variables defined above (specifically isWindows)
program = getNodemonArgs(),
watched = [];
// test to see if the version of find being run supports searching by seconds (-mtime -1s -print)
var testAndStart = function() {
if (noWatch) {
exec('find -L /dev/null -type f -mtime -1s -print', function(error, stdout, stderr) {
if (error) {
if (!fs.watch) {
util.error('\x1B[1;31mThe version of node you are using combined with the version of find being used does not support watching files. Upgrade to a newer version of node, or install a version of find that supports search by seconds.\x1B[0m');
process.exit(1);
} else {
noWatch = false;
watchFileChecker.check(function(success) {
watchWorks = success;
startNode();
});
}
} else {
// Find is compatible with -1s
startNode();
}
});
} else {
watchFileChecker.check(function(success) {
watchWorks = success;
startNode();
});
}
}
// This is a fallback function if fs.watch does not work
function changedSince(time, dir, callback) {
callback || (callback = dir);
var changed = [],
i = 0,
j = 0,
dir = dir && typeof dir !== 'function' ? [dir] : dirs,
dlen = dir.length,
todo = 0,
flen = 0,
done = function () {
todo--;
if (todo === 0) callback(changed);
};
dir.forEach(function (dir) {
todo++;
fs.readdir(dir, function (err, files) {
if (err) return;
files.forEach(function (file) {
if (program.includeHidden == true || !program.includeHidden && file.indexOf('.') !== 0) {
todo++;
file = path.resolve(dir + '/' + file);
var stat = fs.stat(file, function (err, stat) {
if (stat) {
if (stat.isDirectory()) {
todo++;
changedSince(time, file, function (subChanged) {
if (subChanged.length) changed = changed.concat(subChanged);
done();
});
} else if (stat.mtime > time) {
changed.push(file);
}
}
done();
});
}
});
done();
});
});
}
// Attempts to see if fs.watch will work. On some platforms, it doesn't.
// See: http://nodejs.org/api/fs.html#fs_caveats
// Sends the callback true if fs.watch will work, false if it won't
//
// Caveats:
// If there is no writable tmp directory, it will also return true, although
// a warning message will be displayed.
//
var watchFileChecker = {};
watchFileChecker.check = function(cb) {
var tmpdir,
seperator = '/';
this.cb = cb;
this.changeDetected = false;
if (isWindows) {
seperator = '\\';
tmpdir = process.env.TEMP;
} else if (process.env.TMPDIR) {
tmpdir = process.env.TMPDIR
} else {
tmpdir = '/tmp';
}
var watchFileName = tmpdir + seperator + 'nodemonCheckFsWatch'
var watchFile = fs.openSync(watchFileName, 'w');
if (!watchFile) {
util.log('\x1B[32m[nodemon] Unable to write to temp directory. If you experience problems with file reloading, ensure ' + tmpdir + ' is writable.\x1B[0m');
cb(true);
return;
}
fs.watch(watchFileName, function(event, filename) {
if (watchFileChecker.changeDetected) { return; }
watchFileChecker.changeDetected = true;
cb(true);
});
// This should trigger fs.watch, if it works
fs.writeSync(watchFile, '1');
fs.unlinkSync(watchFileName);
setTimeout(function() { watchFileChecker.verify() }, 250);
};
// Verifies that fs.watch was not triggered and sends false to the callback
watchFileChecker.verify = function() {
if (!this.changeDetected) {
this.cb(false);
}
};
function startNode() {
util.log('\x1B[32m[nodemon] starting `' + program.options.exec + ' ' + program.args.join(' ') + '`\x1B[0m');
child = spawn(program.options.exec, program.args);
lastStarted = +new Date;
child.stdout.on('data', function (data) {
util.print(data);
});
child.stderr.on('data', function (data) {
process.stderr.write(data);
});
child.on('exit', function (code, signal) {
// In case we killed the app ourselves, set the signal thusly
if (killedAfterChange) {
killedAfterChange = false;
signal = 'SIGUSR2';
}
// this is nasty, but it gives it windows support
if (isWindows && signal == 'SIGTERM') signal = 'SIGUSR2';
// exit the monitor, but do it gracefully
if (signal == 'SIGUSR2') {
// restart
startNode();
} else if (code === 0) { // clean exit - wait until file change to restart
util.log('\x1B[32m[nodemon] clean exit - waiting for changes before restart\x1B[0m');
child = null;
} else if (program.options.exitcrash) {
util.log('\x1B[1;31m[nodemon] app crashed\x1B[0m');
process.exit(0);
} else {
util.log('\x1B[1;31m[nodemon] app crashed - waiting for file changes before starting...\x1B[0m');
child = null;
}
});
// pinched from https://github.com/DTrejo/run.js - pipes stdin to the child process - cheers DTrejo ;-)
if (program.options.stdin) {
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.pipe(child.stdin);
}
setTimeout(startMonitor, timeout);
}
function startMonitor() {
var changeFunction;
if (noWatch) {
// if native fs.watch doesn't work the way we want, we keep polling find command (mac only oddly)
changeFunction = function (lastStarted, callback) {
var cmds = [],
changed = [];
dirs.forEach(function(dir) {
cmds.push('find -L "' + dir + '" -type f -mtime -' + ((+new Date - lastStarted)/1000|0) + 's -print');
});
exec(cmds.join(';'), function (error, stdout, stderr) {
var files = stdout.split(/\n/);
files.pop(); // remove blank line ending and split
callback(files);
});
}
} else if (watchWorks) {
changeFunction = function (lastStarted, callback) {
// recursive watch - watch each directory and it's subdirectories, etc, etc
var watch = function (err, dir) {
try {
fs.watch(dir, { persistent: false }, function (event, filename) {
var filepath = path.join(dir, filename);
callback([filepath]);
});
fs.readdir(dir, function (err, files) {
if (!err) {
files = files.
map(function (file ) { return path.join(dir, file); }).
filter(ignoredFilter);
files.forEach(function (file) {
if (-1 === watched.indexOf(file)) {
watched.push(file);
fs.stat(file, function (err, stat) {
if (!err && stat) {
if (stat.isDirectory()) {
fs.realpath(file, watch);
}
}
});
}
});
}
});
} catch (e) {
if ('EMFILE' === e.code) {
console.error('EMFILE: Watching too many files.');
}
// ignoring this directory, likely it's "My Music"
// or some such windows fangled stuff
}
}
dirs.forEach(function (dir) {
fs.realpath(dir, watch);
});
}
} else {
// changedSince, the fallback for when both the find method and fs.watch don't work,
// is not compatible with the way changeFunction works. If we have reached this point,
// changeFunction should not be called from herein out.
changeFunction = function() { util.error("Nodemon error: changeFunction called when it shouldn't be.") }
}
// filter ignored files
var ignoredFilter = function (file) {
// If we are in a Windows machine
if (isWindows) {
// Break up the file by slashes
var fileParts = file.split(/\\/g);
// Remove the first piece (C:)
fileParts.shift();
// Join the parts together with Unix slashes
file = '/' + fileParts.join('/');
}
return !reIgnoreFiles.test(file);
};
var isWindows = process.platform === 'win32';
if ((noWatch || watchWorks) && !program.options.forceLegacyWatch) {
changeFunction(lastStarted, function (files) {
if (files.length) {
files = files.filter(ignoredFilter);
if (files.length) {
if (restartTimer !== null) clearTimeout(restartTimer);
restartTimer = setTimeout(function () {
if (program.options.verbose) util.log('[nodemon] restarting due to changes...');
files.forEach(function (file) {
if (program.options.verbose) util.log('[nodemon] ' + file);
});
if (program.options.verbose) util.print('\n\n');
killNode();
}, restartDelay);
return;
}
}
if (noWatch) setTimeout(startMonitor, timeout);
});
} else {
// Fallback for when both find and fs.watch don't work
changedSince(lastStarted, function (files) {
if (files.length) {
// filter ignored files
if (ignoreFiles.length) {
files = files.filter(function(file) {
return !reIgnoreFiles.test(file);
});
}
if (files.length) {
if (restartTimer !== null) clearTimeout(restartTimer);
restartTimer = setTimeout(function () {
if (program.options.verbose) util.log('[nodemon] restarting due to changes...');
files.forEach(function (file) {
if (program.options.verbose) util.log('[nodemon] ' + file);
});
if (program.options.verbose) util.print('\n\n');
killNode();
}, restartDelay);
return;
}
}
setTimeout(startMonitor, timeout);
});
}
}
function killNode() {
if (child !== null) {
// When using CoffeeScript under Windows, child's process is not node.exe
// Instead coffee.cmd is launched, which launches cmd.exe, which starts node.exe as a child process
// child.kill() would only kill cmd.exe, not node.exe
// Therefore we use the Windows taskkill utility to kill the process and all its children (/T for tree)
if (isWindows) {
// For the on('exit', ...) handler above the following looks like a crash, so we set the killedAfterChange flag
killedAfterChange = true;
// Force kill (/F) the whole child tree (/T) by PID (/PID 123)
exec('taskkill /pid '+child.pid+' /T /F');
} else {
child.kill('SIGUSR2');
}
} else {
startNode();
}
}
function addIgnoreRule(line, noEscape) {
// remove comments and trim lines
// this mess of replace methods is escaping "\#" to allow for emacs temp files
if (!noEscape) {
if (line = line.replace(reEscComments, '^^').replace(reComments, '').replace(reUnescapeComments, '#').replace(reTrim, '')) {
ignoreFiles.push(line.replace(reEscapeChars, '\\$&').replace(reAsterisk, '.*'));
}
} else if (line = line.replace(reTrim, '')) {
ignoreFiles.push(line);
}
reIgnoreFiles = new RegExp(ignoreFiles.join('|'));
}
function readIgnoreFile(curr, prev) {
// unless the ignore file was actually modified, do no re-read it
if(curr && prev && curr.mtime.valueOf() === prev.mtime.valueOf()) return;
if (platform === 'darwin') fs.unwatchFile(ignoreFilePath);
// Check if ignore file still exists. Vim tends to delete it before replacing with changed file
exists(ignoreFilePath, function(exists) {
if (program.options.verbose) util.log('[nodemon] reading ignore list');
// ignoreFiles = ignoreFiles.concat([flag, ignoreFilePath]);
// addIgnoreRule(flag);
addIgnoreRule(ignoreFilePath.substring(2)); // ignore the ./ part of the filename
fs.readFileSync(ignoreFilePath).toString().split(/\n/).forEach(function (rule, i) {
addIgnoreRule(rule);
});
watchFile(ignoreFilePath, { persistent: false }, readIgnoreFile);
});
}
// attempt to shutdown the wrapped node instance and remove
// the monitor file as nodemon exists
function cleanup() {
child && child.kill();
// fs.unlink(flag);
}
function getNodemonArgs() {
var args = process.argv,
len = args.length,
i = 2,
dir = process.cwd(),
indexOfApp = -1,
app = null;
for (; i < len; i++) {
if (existsSync(path.resolve(dir, args[i]))) {
// double check we didn't use the --watch or -w opt before this arg
if (args[i-1] && (args[i-1] == '-w' || args[i-1] == '--watch')) {
// ignore
} else {
indexOfApp = i;
break;
}
}
}
if (indexOfApp !== -1) {
// not found, so assume we're reading the package.json and thus swallow up all the args
// indexOfApp = len;
app = process.argv[i];
indexOfApp++;
} else {
indexOfApp = len;
}
var appargs = [], //process.argv.slice(indexOfApp),
// app = appargs[0],
nodemonargs = process.argv.slice(2, indexOfApp - (app ? 1 : 0)),
arg,
options = {
delay: 1,
watch: [],
exec: 'node',
verbose: true,
js: false, // becomes the default anyway...
includeHidden: false,
exitcrash: false,
forceLegacyWatch: false, // forces nodemon to use the slowest but most compatible method for watching for file changes
stdin: true
// args: []
};
// process nodemon args
args.splice(0, 2);
while (arg = args.shift()) {
if (arg === '--help' || arg === '-h' || arg === '-?') {
return help(); // exits program
} else if (arg === '--version' || arg == '-v') {
return version(); // also exits
} else if (arg == '--js') {
options.js = true;
} else if (arg == '--quiet' || arg == '-q') {
options.verbose = false;
} else if (arg == '--hidden') {
options.includeHidden = true;
} else if (arg === '--watch' || arg === '-w') {
options.watch.push(args.shift());
} else if (arg === '--exitcrash') {
options.exitcrash = true;
} else if (arg === '--delay' || arg === '-d') {
options.delay = parseInt(args.shift());
} else if (arg === '--exec' || arg === '-x') {
options.exec = args.shift();
} else if (arg == '--legacy-watch' || arg == '-L') {
options.forceLegacyWatch = true;
} else if (arg === '--no-stdin' || arg === '-I') {
options.stdin = false;
} else { //if (arg === "--") {
// Remaining args are node arguments
appargs.push(arg);
}
}
var program = { options: options, args: appargs, app: app };
getAppScript(program);
return program;
}
function getAppScript(program) {
var hokeycokey = false;
if (!program.args.length || program.app === null) {
// try to get the app from the package.json
// doing a try/catch because we can't use the path.exist callback pattern
// or we could, but the code would get messy, so this will do exactly
// what we're after - if the file doesn't exist, it'll throw.
try {
// note: this isn't nodemon's package, it's the user's cwd package
program.app = JSON.parse(fs.readFileSync('./package.json').toString()).main;
if (program.app === undefined) {
// no app found to run - so give them a tip and get the feck out
help();
}
program.args.unshift(program.app);
hokeycokey = true;
} catch (e) {}
}
if (!program.app) {
program.app = program.args[0];
}
program.app = path.basename(program.app);
program.ext = path.extname(program.app);
if (program.options.exec.indexOf(' ') !== -1) {
var execOptions = program.options.exec.split(' ');
program.options.exec = execOptions.splice(0, 1)[0];
program.args = execOptions.concat(program.args);
}
if (program.options.exec === 'node' && program.ext == '.coffee') {
program.options.exec = 'coffee';
}
if (program.options.exec === 'coffee') {
if (hokeycokey) {
program.args.push(program.args.shift());
}
//coffeescript requires --nodejs --debug
var debugIndex = program.args.indexOf('--debug');
if (debugIndex === -1) debugIndex = program.args.indexOf('--debug-brk');
if (debugIndex !== -1 && program.args.indexOf('--nodejs') === -1) {
program.args.splice(debugIndex, 0, '--nodejs');
}
// monitor both types - TODO possibly make this an option?
program.ext = '.coffee|.js';
if (!program.options.exec || program.options.exec == 'node') program.options.exec = 'coffee';
// because windows can't find 'coffee', it needs the real file 'coffee.cmd'
if (isWindows) program.options.exec += '.cmd';
}
}
function findStatOffset() {
var filename = './.stat-test';
fs.writeFile(filename, function (err) {
if (err) return;
fs.stat(filename, function (err, stat) {
if (err) return;
statOffset = stat.mtime.getTime() - new Date().getTime();
fs.unlink(filename);
});
});
}
function version() {
console.log(meta.version);
process.exit(0);
}
function help() {
util.print([
'',
' Usage: nodemon [options] [script.js] [args]',
'',
' Options:',
'',
' -d, --delay n throttle restart for "n" seconds',
' -w, --watch dir watch directory "dir". use once for each',
' directory to watch',
' -x, --exec app execute script with "app", ie. -x "python -v"',
' -I, --no-stdin don\'t try to read from stdin',
' -q, --quiet minimise nodemon messages to start/stop only',
' --exitcrash exit on crash, allows use of nodemon with',
' daemon tools like forever.js',
' -L, --legacy-watch Forces node to use the most compatible',
' version for watching file changes',
' -v, --version current nodemon version',
' -h, --help you\'re looking at it',
'',
' Note: if the script is omitted, nodemon will try to ',
' read "main" from package.json and without a .nodemonignore,',
' nodemon will monitor .js and .coffee by default.',
'',
' Examples:',
'',
' $ nodemon server.js',
' $ nodemon -w ../foo server.js apparg1 apparg2',
' $ PORT=8000 nodemon --debug-brk server.js',
' $ nodemon --exec python app.py',
'',
' For more details see http://github.com/remy/nodemon/',
''
].join('\n') + '\n');
process.exit(0);
}
// this little bit of hoop jumping is because sometimes the file can't be
// touched properly, and it send nodemon in to a loop of restarting.
// this way, the .monitor file is removed entirely, and recreated with
// permissions that anyone can remove it later (i.e. if you run as root
// by accident and then try again later).
// if (path.existsSync(flag)) fs.unlinkSync(flag);
// fs.writeFileSync(flag, '.'); // requires some content https://github.com/remy/nodemon/issues/36
// fs.chmodSync(flag, '666');
// remove the flag file on exit
process.on('exit', function (code) {
if (program.options.verbose) util.log('[nodemon] exiting');
cleanup();
});
if (!isWindows) { // because windows borks when listening for the SIG* events
// usual suspect: ctrl+c exit
process.on('SIGINT', function () {
child && child.kill('SIGINT');
cleanup();
process.exit(0);
});
process.on('SIGTERM', function () {
cleanup();
process.exit(0);
});
}
// TODO on a clean exit, we could continue to monitor the directory and reboot the service
// on exception *inside* nodemon, shutdown wrapped node app
process.on('uncaughtException', function (err) {
util.log('[nodemon] exception in nodemon killing node');
util.error(err.stack);
cleanup();
});
if (program.options.delay) {
restartDelay = program.options.delay * 1000;
}
// this is the default - why am I making it a cmd line opt?
if (program.options.js) {
addIgnoreRule('^((?!\.js|\.coffee$).)*$', true); // ignores everything except JS
}
if (program.options.watch && program.options.watch.length > 0) {
program.options.watch.forEach(function (dir) {
dirs.push(path.resolve(dir));
});
} else {
dirs.unshift(process.cwd());
}
if (!program.app) {
help();
}
if (program.options.verbose) util.log('[nodemon] v' + meta.version);
// this was causing problems for a lot of people, so now not moving to the subdirectory
// process.chdir(path.dirname(app));
dirs.forEach(function(dir) {
if (program.options.verbose) util.log('\x1B[32m[nodemon] watching: ' + dir + '\x1B[0m');
});
// findStatOffset();
exists(ignoreFilePath, function (exist) {
// watch it: "exist" not to be confused with "exists"....
if (!exist) {
// try the old format
exists(oldIgnoreFilePath, function (exist) {
if (exist) {
if (program.options.verbose) util.log('[nodemon] detected old style .nodemonignore');
ignoreFilePath = oldIgnoreFilePath;
} else {
// don't create the ignorefile, just ignore the flag & JS
// addIgnoreRule(flag);
var ext = program.ext.replace(/\./g, '\\.');
if (ext) {
addIgnoreRule('^((?!' + ext + '$).)*$', true);
} else {
addIgnoreRule('^((?!\.js|\.coffee$).)*$', true); // ignores everything except JS
}
}
});
} else {
readIgnoreFile();
}
});
testAndStart();