pm2/lib/God/ActionMethods.js
2017-07-04 20:13:15 +02:00

791 lines
22 KiB
JavaScript

/**
* Copyright 2013 the PM2 project authors. All rights reserved.
* Use of this source code is governed by a license that
* can be found in the LICENSE file.
*/
'use strict';
/**
* @file ActionMethod like restart, stop, monitor... are here
* @author Alexandre Strzelewicz <as@unitech.io>
* @project PM2
*/
var fs = require('fs');
var path = require('path');
var async = require('async');
var os = require('os');
var p = path;
var cst = require('../../constants.js');
var pkg = require('../../package.json');
var pidusage = require('pidusage');
var util = require('util');
var debug = require('debug')('pm2:ActionMethod');
var Utility = require('../Utility');
var semver = require('semver');
/**
* Description
* @method exports
* @param {} God
* @return
*/
module.exports = function(God) {
/**
* Description
* @method getMonitorData
* @param {} env
* @param {} cb
* @return
*/
God.getMonitorData = function getMonitorData(env, cb) {
var processes = God.getFormatedProcesses();
async.eachSeries(processes, function computeMonitor(pro, next) {
if (pro.pm2_env.status == cst.ONLINE_STATUS) {
var pid = pro.pid;
if (pro.pm2_env.axm_options && pro.pm2_env.axm_options.pid) {
if (isNaN(pro.pm2_env.axm_options.pid)) {
pro['monit'] = {
memory : 0,
cpu : 0
};
return process.nextTick(next);
}
pid = pro.pm2_env.axm_options.pid;
}
pidusage.stat(pid, function retPidUsage(err, res) {
if (err) {
// Do not log, some time modules does not retrieve PID
// console.error('Error caught while calling pidusage');
// console.error(err);
pro['monit'] = {
memory : 0,
cpu : 0
};
return next();
}
pro['monit'] = {
memory : Math.floor(res.memory),
cpu : Math.floor(res.cpu)
};
res = null;
pid = null;
return next();
});
}
else {
pro['monit'] = {
memory : 0,
cpu : 0
};
return next();
}
}, function retMonitor(err, res) {
if (err) return cb(God.logAndGenerateError(err), null);
return cb(null, processes);
});
};
/**
* Description
* @method getSystemData
* @param {} env
* @param {} cb
* @return
*/
God.getSystemData = function getSystemData(env, cb) {
God.getMonitorData(env, function(err, processes) {
cb(err, {
system: {
hostname: os.hostname(),
uptime: os.uptime(),
cpus: os.cpus(),
load: os.loadavg(),
memory: {
free: os.freemem(),
total: os.totalmem()
},
time: Utility.getDate()
},
processes: processes
});
});
};
/**
* Description
* @method dumpProcessList
* @param {} cb
* @return
*/
God.dumpProcessList = function(cb) {
var process_list = [];
var apps = Utility.clone(God.getFormatedProcesses());
// Don't override the actual dump file if process list is empty
// unless user explicitely did `pm2 dump`.
// This often happens when PM2 crashed, we don't want to override
// the dump file with an empty list of process.
if (!apps[0]) {
debug('[PM2] Did not override dump file because list of processes is empty');
return cb(null, {success:true, process_list: process_list});
}
function fin(err) {
try {
fs.writeFileSync(cst.DUMP_FILE_PATH, JSON.stringify(process_list));
} catch (e) {
console.error(e.stack || e);
}
return cb(null, {success:true, process_list: process_list});
}
function saveProc(apps) {
if (!apps[0])
return fin(null);
delete apps[0].pm2_env.instances;
delete apps[0].pm2_env.pm_id;
// Do not dump modules
if (!apps[0].pm2_env.pmx_module)
process_list.push(apps[0].pm2_env);
apps.shift();
return saveProc(apps);
}
saveProc(apps);
};
/**
* Description
* @method ping
* @param {} env
* @param {} cb
* @return CallExpression
*/
God.ping = function(env, cb) {
return cb(null, {msg : 'pong'});
};
/**
* Description
* @method notifyKillPM2
*/
God.notifyKillPM2 = function() {
God.pm2_being_killed = true;
};
/**
* Duplicate a process
* @method duplicateProcessId
* @param {} id
* @param {} cb
* @return CallExpression
*/
God.duplicateProcessId = function(id, cb) {
if (!(id in God.clusters_db))
return cb(God.logAndGenerateError(id + ' id unknown'), {});
if (!God.clusters_db[id] || !God.clusters_db[id].pm2_env)
return cb(God.logAndGenerateError('Error when getting proc || proc.pm2_env'), {});
var proc = Utility.clone(God.clusters_db[id].pm2_env);
delete proc.created_at;
delete proc.pm_id;
God.injectVariables(proc, function inject (_err, proc) {
return God.executeApp(Utility.clone(proc), function (err, clu) {
if (err) return cb(err);
God.notify('start', clu, true);
return cb(err, Utility.clone(clu));
});
});
};
/**
* Start a stopped process by ID
* @method startProcessId
* @param {} id
* @param {} cb
* @return CallExpression
*/
God.startProcessId = function(id, cb) {
if (!(id in God.clusters_db))
return cb(God.logAndGenerateError(id + ' id unknown'), {});
var proc = God.clusters_db[id];
if (proc.pm2_env.status == cst.ONLINE_STATUS)
return cb(God.logAndGenerateError('process already online'), {});
if (proc.pm2_env.status == cst.LAUNCHING_STATUS)
return cb(God.logAndGenerateError('process already started'), {});
if (proc.process && proc.process.pid)
return cb(God.logAndGenerateError('Process with pid ' + proc.process.pid + ' already exists'), {});
return God.executeApp(God.clusters_db[id].pm2_env, function(err, proc) {
return cb(err, Utility.clone(proc));
});
};
/**
* Stop a process and set it on state 'stopped'
* @method stopProcessId
* @param {} id
* @param {} cb
* @return Literal
*/
God.stopProcessId = function(id, cb) {
if (typeof id == 'object' && 'id' in id)
id = id.id;
if (!(id in God.clusters_db))
return cb(God.logAndGenerateError(id + ' : id unknown'), {});
//clear time-out restart task
clearTimeout(God.clusters_db[id].pm2_env.restart_task);
if (God.clusters_db[id].pm2_env.status == cst.STOPPED_STATUS)
return cb(null, God.getFormatedProcess(id));
// state == 'none' means that the process is not online yet
if (God.clusters_db[id].state && God.clusters_db[id].state === 'none')
return setTimeout(function() { God.stopProcessId(id, cb); }, 250);
var proc = God.clusters_db[id];
console.log('Stopping app:%s id:%s', proc.pm2_env.name, proc.pm2_env.pm_id);
proc.pm2_env.status = cst.STOPPING_STATUS;
if (!proc.process.pid) {
console.error('app=%s id=%d does not have a pid', proc.pm2_env.name, proc.pm2_env.pm_id);
proc.pm2_env.status = cst.STOPPED_STATUS;
return cb(null, { error : true, message : 'could not kill process w/o pid'});
}
God.killProcess(proc.process.pid, proc.pm2_env, function(err) {
proc.pm2_env.status = cst.STOPPED_STATUS;
pidusage.unmonitor(proc.process.pid);
God.notify('exit', proc);
if (err && err.type && err.type === 'timeout') {
console.error('app=%s id=%d pid=%s could not be stopped',
proc.pm2_env.name,
proc.pm2_env.pm_id,
proc.process.pid);
proc.pm2_env.status = cst.ERRORED_STATUS;
return cb(null, God.getFormatedProcess(id));
}
if (proc.pm2_env.pm_id.toString().indexOf('_old_') !== 0) {
try {
fs.unlinkSync(proc.pm2_env.pm_pid_path);
} catch (e) {}
}
if (proc.pm2_env.axm_actions) proc.pm2_env.axm_actions = [];
if (proc.pm2_env.axm_monitor) proc.pm2_env.axm_monitor = {};
proc.process.pid = 0;
return cb(null, God.getFormatedProcess(id));
});
};
God.resetMetaProcessId = function(id, cb) {
if (!(id in God.clusters_db))
return cb(God.logAndGenerateError(id + ' id unknown'), {});
if (!God.clusters_db[id] || !God.clusters_db[id].pm2_env)
return cb(God.logAndGenerateError('Error when getting proc || proc.pm2_env'), {});
God.clusters_db[id].pm2_env.created_at = Utility.getDate();
God.clusters_db[id].pm2_env.unstable_restarts = 0;
God.clusters_db[id].pm2_env.restart_time = 0;
return cb(null, God.getFormatedProcesses());
};
/**
* Delete a process by id
* It will stop it and remove it from the database
* @method deleteProcessId
* @param {} id
* @param {} cb
* @return Literal
*/
God.deleteProcessId = function(id, cb) {
God.stopProcessId(id, function(err, proc) {
if (err) return cb(God.logAndGenerateError(err), {});
// ! transform to slow object
delete God.clusters_db[id];
if (Object.keys(God.clusters_db).length == 0)
God.next_id = 0;
return cb(null, proc);
});
return false;
};
/**
* Restart a process ID
* If the process is online it will not put it on state stopped
* but directly kill it and let God restart it
* @method restartProcessId
* @param {} id
* @param {} cb
* @return Literal
*/
God.restartProcessId = function(opts, cb) {
var id = opts.id;
var env = opts.env || {};
if (typeof(id) === 'undefined')
return cb(God.logAndGenerateError('opts.id not passed to restartProcessId', opts));
if (!(id in God.clusters_db))
return cb(God.logAndGenerateError('God db process id unknown'), {});
var proc = God.clusters_db[id];
God.resetState(proc.pm2_env);
/**
* Merge new application configuration on restart
* Same system in reloadProcessId and softReloadProcessId
*/
Utility.extendExtraConfig(proc, opts);
Utility.extend(proc.pm2_env.env, env);
if (God.pm2_being_killed) {
return cb(God.logAndGenerateError('[RestartProcessId] PM2 is being killed, stopping restart procedure...'));
}
if (proc.pm2_env.status === cst.ONLINE_STATUS || proc.pm2_env.status === cst.LAUNCHING_STATUS) {
God.stopProcessId(id, function(err) {
if (God.pm2_being_killed)
return cb(God.logAndGenerateError('[RestartProcessId] PM2 is being killed, stopping restart procedure...'));
proc.pm2_env.restart_time += 1;
return God.startProcessId(id, cb);
});
return false;
}
else {
debug('[restart] process not online, starting it');
return God.startProcessId(id, cb);
}
};
/**
* Restart all process by name
* @method restartProcessName
* @param {} name
* @param {} cb
* @return Literal
*/
God.restartProcessName = function(name, cb) {
var processes = God.findByName(name);
if (processes && processes.length === 0)
return cb(God.logAndGenerateError('Unknown process'), {});
async.eachLimit(processes, cst.CONCURRENT_ACTIONS, function(proc, next) {
if (God.pm2_being_killed)
return next('[Watch] PM2 is being killed, stopping restart procedure...');
if (proc.pm2_env.status === cst.ONLINE_STATUS)
return God.restartProcessId({id:proc.pm2_env.pm_id}, next);
else if (proc.pm2_env.status !== cst.STOPPING_STATUS
&& proc.pm2_env.status !== cst.LAUNCHING_STATUS)
return God.startProcessId(proc.pm2_env.pm_id, next);
else
return next("[Watch] Process name %s is being stopped so I won't restart it", name);
}, function(err) {
if (err) return cb(God.logAndGenerateError(err));
return cb(null, God.getFormatedProcesses());
});
return false;
};
/**
* Send system signal to process id
* @method sendSignalToProcessId
* @param {} opts
* @param {} cb
* @return CallExpression
*/
God.sendSignalToProcessId = function(opts, cb) {
var id = opts.process_id;
var signal = opts.signal;
if (!(id in God.clusters_db))
return cb(God.logAndGenerateError(id + ' id unknown'), {});
var proc = God.clusters_db[id];
//God.notify('send signal ' + signal, proc, true);
try {
process.kill(God.clusters_db[id].process.pid, signal);
} catch(e) {
return cb(God.logAndGenerateError('Error when sending signal (signal unknown)'), {});
}
return cb(null, God.getFormatedProcesses());
};
/**
* Send system signal to all processes by name
* @method sendSignalToProcessName
* @param {} opts
* @param {} cb
* @return
*/
God.sendSignalToProcessName = function(opts, cb) {
var processes = God.findByName(opts.process_name);
var signal = opts.signal;
if (processes && processes.length === 0)
return cb(God.logAndGenerateError('Unknown process name'), {});
async.eachLimit(processes, cst.CONCURRENT_ACTIONS, function(proc, next) {
if (proc.pm2_env.status == cst.ONLINE_STATUS || proc.pm2_env.status == cst.LAUNCHING_STATUS) {
try {
process.kill(proc.process.pid, signal);
} catch(e) {
return next(e);
}
}
return setTimeout(next, 200);
}, function(err) {
if (err) return cb(God.logAndGenerateError(err), {});
return cb(null, God.getFormatedProcesses());
});
};
/**
* Stop watching daemon
* @method stopWatch
* @param {} method
* @param {} value
* @param {} fn
* @return
*/
God.stopWatch = function(method, value, fn) {
var env = null;
if (method == 'stopAll' || method == 'deleteAll') {
var processes = God.getFormatedProcesses();
processes.forEach(function(proc) {
God.clusters_db[proc.pm_id].pm2_env.watch = false;
God.watch.disable(proc.pm2_env);
});
} else {
if (method.indexOf('ProcessId') !== -1) {
env = God.clusters_db[value];
} else if (method.indexOf('ProcessName') !== -1) {
env = God.clusters_db[God.findByName(value)];
}
if (env) {
God.watch.disable(env.pm2_env);
env.pm2_env.watch = false;
}
}
return fn(null, {success:true});
};
/**
* Toggle watching daemon
* @method toggleWatch
* @param {String} method
* @param {Object} application environment, should include id
* @param {Function} callback
*/
God.toggleWatch = function(method, value, fn) {
var env = null;
if (method == 'restartProcessId') {
env = God.clusters_db[value.id];
} else if(method == 'restartProcessName') {
env = God.clusters_db[God.findByName(value)];
}
if (env) {
env.pm2_env.watch = !env.pm2_env.watch;
if (env.pm2_env.watch)
God.watch.enable(env.pm2_env);
else
God.watch.disable(env.pm2_env);
}
return fn(null, {success:true});
};
/**
* Start Watch
* @method startWatch
* @param {String} method
* @param {Object} application environment, should include id
* @param {Function} callback
*/
God.startWatch = function(method, value, fn) {
var env = null;
if (method == 'restartProcessId') {
env = God.clusters_db[value.id];
} else if(method == 'restartProcessName') {
env = God.clusters_db[God.findByName(value)];
}
if (env) {
if (env.pm2_env.watch)
return fn(null, {success:true, notrestarted:true});
God.watch.enable(env.pm2_env);
//env.pm2_env.env.watch = true;
env.pm2_env.watch = true;
}
return fn(null, {success:true});
};
/**
* Description
* @method reloadLogs
* @param {} opts
* @param {} cb
* @return CallExpression
*/
God.reloadLogs = function(opts, cb) {
console.log('Reloading logs...');
var processIds = Object.keys(God.clusters_db);
processIds.forEach(function (id) {
var cluster = God.clusters_db[id];
console.log('Reloading logs for process id %d', id);
if (cluster && cluster.pm2_env) {
// Cluster mode
if (cluster.send && cluster.pm2_env.exec_mode == 'cluster_mode') {
try {
cluster.send({
type:'log:reload'
});
} catch(e) {
console.error(e.message || e);
}
}
// Fork mode
else if (cluster._reloadLogs) {
cluster._reloadLogs(function(err) {
if (err) God.logAndGenerateError(err);
});
}
}
});
return cb(null, {});
};
/**
* Send Line To Stdin
* @method sendLineToStdin
* @param Object packet
* @param String pm_id Process ID
* @param String line Line to send to process stdin
*/
God.sendLineToStdin = function(packet, cb) {
if (typeof(packet.pm_id) == undefined || !packet.line)
return cb(God.logAndGenerateError('pm_id or line field missing'), {});
var pm_id = packet.pm_id;
var line = packet.line;
var proc = God.clusters_db[pm_id];
if (!proc)
return cb(God.logAndGenerateError('Process with ID <' + pm_id + '> unknown.'), {});
if (proc.pm2_env.exec_mode == 'cluster_mode')
return cb(God.logAndGenerateError('Cannot send line to processes in cluster mode'), {});
if (proc.pm2_env.status != cst.ONLINE_STATUS && proc.pm2_env.status != cst.LAUNCHING_STATUS)
return cb(God.logAndGenerateError('Process with ID <' + pm_id + '> offline.'), {});
try {
proc.stdin.write(line, function() {
return cb(null, {
pm_id : pm_id,
line : line
});
});
} catch(e) {
return cb(God.logAndGenerateError(e), {});
}
}
God.sendDataToProcessId = function(packet, cb) {
if (typeof(packet.id) == undefined ||
!packet.data ||
!packet.topic)
return cb(God.logAndGenerateError('ID, DATA or TOPIC field is missing'), {});
var pm_id = packet.id;
var data = packet.data;
var proc = God.clusters_db[pm_id];
if (!proc)
return cb(God.logAndGenerateError('Process with ID <' + pm_id + '> unknown.'), {});
if (proc.pm2_env.status != cst.ONLINE_STATUS && proc.pm2_env.status != cst.LAUNCHING_STATUS)
return cb(God.logAndGenerateError('Process with ID <' + pm_id + '> offline.'), {});
try {
proc.send(packet);
}
catch(e) {
return cb(God.logAndGenerateError(e), {});
}
return cb(null, {
success: true,
data : packet
});
};
/**
* Send Message to Process by id or name
* @method msgProcess
* @param {} cmd
* @param {} cb
* @return Literal
*/
God.msgProcess = function(cmd, cb) {
if ('id' in cmd) {
var id = cmd.id;
if (!(id in God.clusters_db))
return cb(God.logAndGenerateError(id + ' id unknown'), {});
var proc = God.clusters_db[id];
var action_exist = false;
proc.pm2_env.axm_actions.forEach(function(action) {
if (action.action_name == cmd.msg) {
action_exist = true;
// Reset output buffer
action.output = [];
}
});
if (action_exist == false) {
return cb(God.logAndGenerateError('Action doesn\'t exist ' + cmd.msg + ' for ' + proc.pm2_env.name), {});
}
if (proc.pm2_env.status == cst.ONLINE_STATUS || proc.pm2_env.status == cst.LAUNCHING_STATUS) {
/*
* Send message
*/
if (cmd.opts == null)
proc.send(cmd.msg);
else
proc.send(cmd);
return cb(null, { process_count : 1, success : true });
}
else
return cb(God.logAndGenerateError(id + ' : id offline'), {});
}
else if ('name' in cmd) {
/*
* As names are not unique in case of cluster, this
* will send msg to all process matching 'name'
*/
var name = cmd.name;
var arr = Object.keys(God.clusters_db);
var sent = 0;
(function ex(arr) {
if (arr[0] == null) return cb(null, { process_count : sent, success : true });
var id = arr[0];
if (!God.clusters_db[id] || !God.clusters_db[id].pm2_env) {
arr.shift();
return ex(arr);
}
var proc_env = God.clusters_db[id].pm2_env;
if (p.basename(proc_env.pm_exec_path) == name || proc_env.name == name) {
if (proc_env.status == cst.ONLINE_STATUS || proc.pm2_env.status == cst.LAUNCHING_STATUS) {
if (cmd.opts == null)
God.clusters_db[id].send(cmd.msg);
else
God.clusters_db[id].send(cmd);
sent++;
arr.shift();
return ex(arr);
}
}
else {
arr.shift();
return ex(arr);
}
return false;
})(arr);
}
else return cb(God.logAndGenerateError('method requires name or id field'), {});
return false;
};
/**
* Description
* @method getVersion
* @param {} env
* @param {} cb
* @return CallExpression
*/
God.getVersion = function(env, cb) {
process.nextTick(function() {
return cb(null, pkg.version);
});
};
God.getReport = function(arg, cb) {
var report = {
pm2_version : pkg.version,
node_version : 'N/A',
node_path : process.env['_'],
argv0 : process.argv0,
argv : process.argv,
user : process.env.USER,
uid : process.geteuid ? process.geteuid() : 'N/A',
gid : process.getegid ? process.getegid() : 'N/A',
env : process.env,
managed_apps : Object.keys(God.clusters_db).length,
started_at : God.started_at
};
if (process.versions && process.versions.node) {
report.node_version = process.versions.node;
}
process.nextTick(function() {
return cb(null, report);
});
};
};