var events = require('events'),
fs = require('fs'),
path = require('path'),
child_process = require('child_process'),
spawn = child_process.spawn,
broadway = require('broadway'),
psTree = require('ps-tree'),
utile = require('utile'),
common = require('./common'),
plugins = require('./plugins');
var Monitor = exports.Monitor = function (script, options) {
function bootstrap(monitor) {
plugins.logger.attach.call(monitor, options);
if (options.watch) {
plugins.watch.attach.call(monitor, options);
}
}
var self = this;
options = options || {};
this.silent = options.silent || false;
this.killTree = options.killTree !== false;
this.uid = options.uid || utile.randomString(4);
this.pidFile = options.pidFile;
this.max = options.max;
this.killTTL = options.killTTL;
this.childExists = false;
this.checkFile = options.checkFile !== false;
this.times = 0;
this.warn = console.error;
this.logFile = options.logFile;
this.outFile = options.outFile;
this.errFile = options.errFile;
this.append = options.append;
this.minUptime = typeof options.minUptime !== 'number' ? 0 : options.minUptime;
this.spinSleepTime = options.spinSleepTime || null;
this.command = options.command || process.execPath;
this.args = options.options || [];
this.spawnWith = options.spawnWith || {};
this.sourceDir = options.sourceDir;
this.fork = options.fork || false;
this.cwd = options.cwd || null;
this.hideEnv = options.hideEnv || [];
this._env = options.env || {};
this._hideEnv = {};
this.watchIgnoreDotFiles = options.watchIgnoreDotFiles || true;
this.watchIgnorePatterns = options.watchIgnorePatterns || [];
this.watchDirectory = options.watchDirectory || this.sourceDir;
this.hideEnv.forEach(function (key) {
self._hideEnv[key] = true;
});
if (Array.isArray(script)) {
this.command = script[0];
this.args = script.slice(1);
}
else {
this.args.unshift(script);
}
if (this.sourceDir) {
this.args[0] = path.join(this.sourceDir, this.args[0]);
}
broadway.App.call(this, { bootstrapper: { bootstrap: bootstrap } });
};
utile.inherits(Monitor, broadway.App);
Monitor.prototype.start = function (restart) {
var self = this,
child;
if (this.running && !restart) {
process.nextTick(function () {
self.emit('error', new Error('Cannot start process that is already running.'));
});
return this;
}
child = this.trySpawn();
if (!child) {
process.nextTick(function () {
self.emit('error', new Error('Target script does not exist: ' + self.args[0]));
});
return this;
}
this.ctime = Date.now();
this.child = child;
this.running = true;
process.nextTick(function () {
self.emit(restart ? 'restart' : 'start', self, self.data);
});
function onMessage(msg) {
self.emit('message', msg);
}
this.child.on('message', onMessage);
child.on('exit', function (code) {
var spinning = Date.now() - self.ctime < self.minUptime;
child.removeListener('message', onMessage);
self.emit('exit:code', code);
function letChildDie() {
self.running = false;
self.forceStop = false;
self.emit('exit', self, spinning);
}
function restartChild() {
self.forceRestart = false;
process.nextTick(function () {
self.start(true);
});
}
self.times++;
if (self.forceStop || (self.times >= self.max)
|| (spinning && typeof self.spinSleepTime !== 'number') && !self.forceRestart) {
letChildDie();
}
else if (spinning) {
setTimeout(restartChild, self.spinSleepTime);
}
else {
restartChild();
}
});
return this;
};
Monitor.prototype.trySpawn = function () {
if (/node/.test(this.command) && this.checkFile && !this.childExists) {
try {
var stats = fs.statSync(this.args[0]);
this.childExists = true;
}
catch (ex) {
return false;
}
}
this.spawnWith.cwd = this.cwd || this.spawnWith.cwd;
this.spawnWith.env = this._getEnv();
if (this.fork) {
this.spawnWith.stdio = [ 'pipe', 'pipe', 'pipe', 'ipc' ];
return spawn(this.command, this.args, this.spawnWith);
}
return spawn(this.command, this.args, this.spawnWith);
};
Monitor.prototype.__defineGetter__('data', function () {
var self = this,
childData;
if (!this.running) {
return {};
}
childData = {
ctime: this.ctime,
command: this.command,
file: this.args[0],
foreverPid: process.pid,
logFile: this.logFile,
options: this.args.slice(1),
pid: this.child.pid,
silent: this.silent,
uid: this.uid,
spawnWith: this.spawnWith
};
['pidFile', 'outFile', 'errFile', 'env', 'cwd'].forEach(function (key) {
if (self[key]) {
childData[key] = self[key];
}
});
if (this.sourceDir) {
childData.sourceDir = this.sourceDir;
childData.file = childData.file.replace(this.sourceDir + '/', '');
}
this.childData = childData;
return this.childData;
});
Monitor.prototype.restart = function () {
this.forceRestart = true;
return this.kill(false);
};
Monitor.prototype.stop = function () {
return this.kill(true);
};
Monitor.prototype.kill = function (forceStop) {
var self = this,
child = this.child;
if (!child || !this.running) {
process.nextTick(function () {
self.emit('error', new Error('Cannot stop process that is not running.'));
});
}
else {
var toKill = [this.child.pid];
if (forceStop) {
this.forceStop = true;
if (this.killTTL) {
var timer = setTimeout(function () {
toKill.forEach(function (pid) {
try {
process.kill(pid, 'SIGKILL');
}
catch (e) {
}
});
self.emit('stop', this.childData);
}, this.killTTL);
child.on('exit', function () {
clearTimeout(timer);
});
}
}
common.kill(this.child.pid, this.killTree, function () {
self.emit('stop', self.childData);
});
}
return this;
};
Monitor.prototype._getEnv = function () {
var self = this,
merged = {};
function addKey(key, source) {
merged[key] = source[key];
}
Object.keys(process.env).forEach(function (key) {
if (!self._hideEnv[key]) {
addKey(key, process.env);
}
});
Object.keys(this._env).forEach(function (key) {
addKey(key, self._env);
});
return merged;
};