var fs = require('fs'),
path = require('path'),
events = require('events'),
exec = require('child_process').exec,
spawn = require('child_process').spawn,
cliff = require('cliff'),
nconf = require('nconf'),
nssocket = require('nssocket'),
timespan = require('timespan'),
utile = require('utile'),
winston = require('winston'),
mkdirp = utile.mkdirp,
async = utile.async;
var forever = exports;
forever.log = new (winston.Logger)({
transports: [
new (winston.transports.Console)()
]
});
forever.log.cli();
forever.initialized = false;
forever.kill = require('forever-monitor').kill;
forever.checkProcess = require('forever-monitor').checkProcess;
forever.root = path.join(process.env.HOME || '/root', '.forever');
forever.config = new nconf.File({ file: path.join(forever.root, 'config.json') });
forever.Forever = forever.Monitor = require('forever-monitor').Monitor;
forever.Worker = require('./forever/worker').Worker;
forever.cli = require('./forever/cli');
require('pkginfo')(module, 'version');
forever.__defineGetter__('service', function () {
return require('./forever/service');
});
function getSockets(sockPath, callback) {
var sockets;
try {
sockets = fs.readdirSync(sockPath);
}
catch (ex) {
if (ex.code !== 'ENOENT') {
return callback(ex);
}
return mkdirp(sockPath, '0755', function (err) {
return err ? callback(err) : callback(null, []);
});
}
callback(null, sockets);
}
function getAllProcesses(callback) {
var sockPath = forever.config.get('sockPath');
function getProcess(name, next) {
var fullPath = path.join(sockPath, name),
socket = new nssocket.NsSocket();
socket.connect(fullPath, function (err) {
if (err) {
next(err);
}
socket.dataOnce(['data'], function (data) {
data.socket = fullPath;
next(null, data);
socket.end();
});
socket.send(['data']);
});
socket.on('error', function (err) {
if (err.code === 'ECONNREFUSED') {
fs.unlink(fullPath, function () {
next();
});
}
else {
next();
}
});
}
getSockets(sockPath, function (err, sockets) {
if (err || (sockets && sockets.length === 0)) {
return callback(err);
}
async.map(sockets, getProcess, function (err, processes) {
callback(processes.filter(Boolean));
});
});
}
function getAllPids(processes) {
return !processes ? null : processes.map(function (proc) {
return {
pid: proc.pid,
foreverPid: proc.foreverPid
};
});
}
function stopOrRestart(action, event, format, target) {
var emitter = new events.EventEmitter(),
results = [],
pids;
function sendAction(proc, next) {
var socket = new nssocket.NsSocket();
socket.connect(proc.socket, function (err) {
if (err) {
next(err);
}
socket.dataOnce([action, 'ok'], function (data) {
next();
socket.end();
});
socket.send([action]);
});
socket.on('error', function (err) {
next(err);
});
}
getAllProcesses(function (processes) {
var procs = processes;
if (target !== undefined && target !== null) {
procs = forever.findByIndex(target, processes)
|| forever.findByScript(target, processes)
|| forever.findByUid(target, processes);
}
if (procs && procs.length > 0) {
async.map(procs, sendAction, function (err, results) {
if (err) {
emitter.emit('error', err);
}
emitter.emit(event, forever.format(format, procs));
});
}
else {
process.nextTick(function () {
emitter.emit('error', new Error('Cannot find forever process: ' + target));
});
}
});
return emitter;
}
forever.load = function (options) {
options = options || {};
options.loglength = options.loglength || 100;
options.root = options.root || forever.root;
options.pidPath = options.pidPath || path.join(options.root, 'pids');
options.sockPath = options.sockPath || path.join(options.root, 'sock');
if (forever.initialized && forever.config.get('root') === options.root &&
forever.config.get('pidPath') === options.pidPath) {
return;
}
forever.config = new nconf.File({ file: path.join(options.root, 'config.json') });
try {
forever.config.loadSync();
}
catch (ex) { }
options.columns = options.columns || forever.config.get('columns');
if (!options.columns) {
options.columns = [
'uid', 'command', 'script', 'forever', 'pid', 'logfile', 'uptime'
];
}
forever.config.set('root', options.root);
forever.config.set('pidPath', options.pidPath);
forever.config.set('sockPath', options.sockPath);
forever.config.set('loglength', options.loglength);
forever.config.set('columns', options.columns);
options.debug = options.debug || forever.config.get('debug') || false;
if (options.debug) {
forever._debug();
}
function tryCreate(dir) {
try {
fs.mkdirSync(dir, '0755');
}
catch (ex) { }
}
tryCreate(forever.config.get('root'));
tryCreate(forever.config.get('pidPath'));
tryCreate(forever.config.get('sockPath'));
try {
forever.config.saveSync();
}
catch (ex) { }
forever.initialized = true;
};
forever._debug = function () {
var debug = forever.config.get('debug');
if (!debug) {
forever.config.set('debug', true);
forever.log.add(winston.transports.File, {
level: 'silly',
filename: path.join(forever.config.get('root'), 'forever.debug.log')
});
}
};
forever.load();
forever.stat = function (logFile, script, callback) {
var logAppend;
if (arguments.length === 4) {
logAppend = callback;
callback = arguments[3];
}
fs.stat(script, function (err, stats) {
if (err) {
return callback(new Error('script ' + script + ' does not exist.'));
}
return logAppend ? callback(null) : fs.stat(logFile, function (err, stats) {
return !err
? callback(new Error('log file ' + logFile + ' exists. Use the -a or --append option to append log.'))
: callback(null);
});
});
};
forever.start = function (script, options) {
if (!options.uid) {
options.uid = options.uid || utile.randomString(4).replace(/^\-/, '_');
}
if (!options.logFile) {
options.logFile = forever.logFilePath(options.uid + '.log');
}
var monitor = new forever.Monitor(script, options);
forever.logEvents(monitor);
return monitor.start();
};
forever.startDaemon = function (script, options) {
options = options || {};
options.uid = options.uid || utile.randomString(4).replace(/^\-/, '_');
options.logFile = forever.logFilePath(options.logFile || options.uid + '.log');
options.pidFile = forever.pidFilePath(options.pidFile || options.uid + '.pid');
var monitor, outFD, errFD, workerPath;
outFD = fs.openSync(options.logFile, 'a');
errFD = fs.openSync(options.logFile, 'a');
monitorPath = path.resolve(__dirname, '..', 'bin', 'monitor');
monitor = spawn(process.execPath, [monitorPath, script], {
stdio: ['ipc', outFD, errFD],
detached: true
});
monitor.on('exit', function (code) {
console.error('Monitor died unexpectedly with exit code %d', code);
});
monitor.send(JSON.stringify(options));
monitor.unref();
};
forever.startServer = function () {
var args = Array.prototype.slice.call(arguments),
monitors = [],
callback;
args.forEach(function (a) {
if (Array.isArray(a)) {
monitors = monitors.concat(a.filter(function (m) {
return m instanceof forever.Monitor;
}));
}
else if (a instanceof forever.Monitor) {
monitors.push(a);
}
else if (typeof a === 'function') {
callback = a;
}
});
async.map(monitors, function (monitor, next) {
var worker = new forever.Worker({
monitor: monitor,
sockPath: forever.config.get('sockPath'),
exitOnStop: true
});
worker.start(function (err) {
return err ? next(err) : next(null, worker);
});
}, callback || function () {});
};
forever.stop = function (target, format) {
return stopOrRestart('stop', 'stop', format, target);
};
forever.restart = function (target, format) {
return stopOrRestart('restart', 'restart', format, target);
};
forever.restartAll = function (format) {
return stopOrRestart('restart', 'restartAll', format);
};
forever.stopAll = function (format) {
return stopOrRestart('stop', 'stopAll', format);
};
forever.list = function (format, callback) {
getAllProcesses(function (processes) {
callback(null, forever.format(format, processes));
});
};
forever.tail = function (target, length, callback) {
if (!callback && typeof length === 'function') {
callback = length;
length = 0;
}
length = length || forever.config.get('loglength');
if (!length) {
return callback(new Error('Cannot tail logs without a specified length'));
}
function tailProcess(proc, next) {
exec('tail -n ' + [length, proc.logFile].join(' '), function (err, stdout) {
if (err) {
return next(err);
}
proc.logs = stdout.split('\n');
proc.logs.pop();
return err ? next(err) : next(null, proc);
});
}
getAllProcesses(function (processes) {
if (!processes) {
return callback(new Error('Cannot find forever process: ' + target));
}
var procs = forever.findByIndex(target, processes)
|| forever.findByScript(target, processes);
async.mapSeries(procs, tailProcess, function (err, procs) {
return err
? callback(err)
: callback(null, procs);
});
});
};
forever.findByIndex = function (index, processes) {
var proc = processes && processes[parseInt(index, 10)];
return proc ? [proc] : null;
};
forever.findByScript = function (script, processes) {
if (!processes) return null;
var procs = processes.filter(function (p) {
return p.file === script;
});
if (procs.length === 0) procs = null;
return procs;
};
forever.findByUid = function (script, processes) {
return !processes
? null
: processes.filter(function (p) {
return p.uid === script;
});
};
forever.format = function (format, procs) {
if (!procs || procs.length === 0) {
return null;
}
var index = 0,
columns = forever.config.get('columns'),
rows = [[' '].concat(columns)],
formatted;
function mapColumns(prefix, mapFn) {
return [prefix].concat(columns.map(mapFn));
}
if (format) {
procs.forEach(function (proc) {
rows.push(mapColumns('[' + index + ']', function (column) {
return forever.columns[column]
? forever.columns[column].get(proc)
: 'MISSING';
}));
index++;
});
formatted = cliff.stringifyRows(rows, mapColumns('white', function (column) {
return forever.columns[column]
? forever.columns[column].color
: 'white';
}));
}
return format ? formatted : procs;
};
forever.cleanUp = function (cleanLogs, allowManager) {
var emitter = new events.EventEmitter(),
pidPath = forever.config.get('pidPath');
getAllProcesses(function (processes) {
if (cleanLogs) {
forever.cleanLogsSync(processes);
}
function unlinkProcess(proc, done) {
fs.unlink(path.join(pidPath, proc.uid + '.pid'), function () {
if (cleanLogs && proc.logFile) {
return fs.unlink(proc.logFile, function () {
done();
});
}
done();
});
}
function cleanProcess(proc, done) {
if (proc.child && proc.manager) {
return done();
}
else if (!proc.child && !proc.manager
|| (!proc.child && proc.manager && allowManager)
|| proc.dead) {
return unlinkProcess(proc, done);
}
if (!proc.waited) {
proc.waited = true;
return setTimeout(function () {
checkProcess(proc, done);
}, 500);
}
done();
}
function checkProcess(proc, next) {
proc.child = forever.checkProcess(proc.pid);
proc.manager = forever.checkProcess(proc.foreverPid);
cleanProcess(proc, next);
}
if (processes && processes.length > 0) {
(function cleanBatch(batch) {
async.forEach(batch, checkProcess, function () {
return processes.length > 0
? cleanBatch(processes.splice(0, 10))
: emitter.emit('cleanUp');
});
})(processes.splice(0, 10));
}
else {
process.nextTick(function () {
emitter.emit('cleanUp');
});
}
});
return emitter;
};
forever.cleanLogsSync = function (processes) {
var root = forever.config.get('root'),
files = fs.readdirSync(root),
running,
runningLogs;
running = processes && processes.filter(function (p) {
return p && p.logFile;
});
runningLogs = running && running.map(function (p) {
return p.logFile.split('/').pop();
});
files.forEach(function (file) {
if (/\.log$/.test(file) && (!runningLogs || runningLogs.indexOf(file) === -1)) {
fs.unlinkSync(path.join(root, file));
}
});
};
forever.logFilePath = function (logFile, uid) {
return logFile && logFile[0] === '/'
? logFile
: path.join(forever.config.get('root'), logFile || (uid || 'forever') + '.log');
};
forever.pidFilePath = function (pidFile) {
return pidFile && pidFile[0] === '/'
? pidFile
: path.join(forever.config.get('pidPath'), pidFile);
};
forever.logEvents = function (monitor) {
monitor.on('watch:error', function (info) {
console.error(info.message);
console.error(info.error);
});
monitor.on('watch:restart', function (info) {
console.error('restaring script because ' + info.file + ' changed')
});
monitor.on('restart', function () {
console.error('Forever restarting script for ' + monitor.times + ' time')
});
monitor.on('exit:code', function (code) {
console.error('Forever detected script exited with code: ' + code);
});
};
forever.columns = {
uid: {
color: 'white',
get: function (proc) {
return proc.uid;
}
},
command: {
color: 'grey',
get: function (proc) {
return (proc.command || 'node').grey;
}
},
script: {
color: 'grey',
get: function (proc) {
return [proc.file].concat(proc.options).join(' ').grey;
}
},
forever: {
color: 'white',
get: function (proc) {
return proc.foreverPid;
}
},
pid: {
color: 'white',
get: function (proc) {
return proc.pid;
}
},
logfile: {
color: 'magenta',
get: function (proc) {
return proc.logFile ? proc.logFile.magenta : '';
}
},
uptime: {
color: 'yellow',
get: function (proc) {
return timespan.fromDates(new Date(proc.ctime), new Date()).toString().yellow;
}
}
};