/** * 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. */ var debug = require('debug')('interface:driver'); var Url = require('url'); var Cipher = require('../Cipher.js'); var PushInteractor = require('../PushInteractor'); var Conf = require('../../Configuration.js'); var Password = require('../Password.js'); /** * Allowed remote PM2 methods * with options * - password_required : force to pass a password in parameter * - password_optional : if a password is set, force it * - lock : enable the locking system (block parallel commands) */ var PM2_REMOTE_METHOD_ALLOWED = { 'restart' : {}, 'reload' : {}, 'gracefulReload' : {}, 'reset' : {}, 'scale' : {}, 'install' : { password_required : true }, 'uninstall' : { password_required : true }, 'stop' : { password_required : true }, 'delete' : { password_required : true }, 'set' : {}, 'multiset' : {}, 'deepUpdate' : { password_required : true }, 'pullAndRestart' : { password_optional : true }, 'forward' : { password_optional : true }, 'backward' : { password_optional : true }, 'startLogging' : {}, 'stopLogging' : {}, 'resetTransactionCache': {}, 'resetFileCache': {}, // This is just for testing purproses 'ping' : { password_required : true } }; var Pm2Actions = module.exports = { /** * Methods to trigger PM2 actions from remote */ pm2Actions : function() { var self = this; function executionBox(msg, cb) { /** * Exemple * msg = { * method_name : 'restart', * parameters : {} * } */ console.log('PM2 action from remote triggered "pm2 %s %j"', msg.method_name, msg.parameters); var method_name = JSON.parse(JSON.stringify(msg.method_name)); var parameters = ''; try { parameters = JSON.parse(JSON.stringify(msg.parameters)); } catch(e) { console.error(e.stack || e); parameters = msg.parameters; } if (!method_name) { console.error('no method name'); return cb(new Error('no method name defined')); } if (!PM2_REMOTE_METHOD_ALLOWED[method_name]) { console.error('method %s not allowed', method_name); return cb(new Error('method ' + method_name + ' not allowed')); } if (method_name === 'startLogging') { global._logs = true; // Stop streaming logs automatically after timeout setTimeout(function() { global._logs = false; }, 120000); return cb(null, 'Log streaming enabled'); } else if (method_name === 'stopLogging') { global._logs = false; return cb(null, 'Log streaming disabled'); } else if (method_name === 'resetTransactionCache') { PushInteractor.aggregator.clearData(); return cb(null, 'Transaction cache has beem reset'); } else if (method_name === 'resetFileCache') { PushInteractor.cache.reset(); return cb(null, 'File cache has beem reset'); } self.pm2_instance.remote(method_name, parameters, cb); return false; } function sendBackResult(data) { self.socket.send('trigger:pm2:result', data); }; this.socket.data('trigger:pm2:action', function(raw_msg) { var d = require('domain').create(); var msg = {}; /** * Uncipher Data */ if (process.env.NODE_ENV && (process.env.NODE_ENV == 'test' || process.env.NODE_ENV == 'local_test')) msg = raw_msg; else msg = Cipher.decipherMessage(raw_msg, self.conf.SECRET_KEY); d.on('error', function(e) { console.error('Error caught in domain'); console.error(e.stack || e); /** * Send error back to */ sendBackResult({ ret : { err : e, data : null }, meta : { method_name : msg.method_name, app_name : msg.parameters.name, machine_name : self.conf.MACHINE_NAME, public_key : self.conf.PUBLIC_KEY } }); }); d.run(function() { if (!msg) throw new Error('Wrong SECRET KEY to uncipher package'); /** * Execute command */ executionBox(msg, function(err, data) { if (err) console.error(err.stack || JSON.stringify(err)); /** * Send back the result */ sendBackResult({ ret : { err : err, data : data || null }, meta : { method_name : msg.method_name, app_name : msg.parameters.name, machine_name : self.conf.MACHINE_NAME, public_key : self.conf.PUBLIC_KEY } }); }); }); }); }, /**************************************************** * * * Scoped PM2 Actions with streaming and multi args * * ****************************************************/ pm2ScopedActions : function() { var self = this; this.socket.data('trigger:pm2:scoped:action', function(raw_msg) { var msg = {}; if (process.env.NODE_ENV && (process.env.NODE_ENV == 'test' || process.env.NODE_ENV == 'local_test')) msg = raw_msg; else { /** * Uncipher Data */ msg = Cipher.decipherMessage(raw_msg, self.conf.SECRET_KEY); } if (!msg.uuid || !msg.action_name) { console.error('PM2 Scoped: Parameter missing!'); return sendEvent('pm2:scoped:error', { at : Date.now(), out : 'Parameter missing', msg : msg.uuid || null }); } sendEvent('pm2:scoped:stream', { at : Date.now(), out : 'Action ' + msg.action_name + ' received', uuid : msg.uuid }); executionBox(msg, function(err, data) { if (err) { console.error(err.stack || err); return sendEvent('pm2:scoped:error', { at : Date.now(), out : err.stack || err, uuid : msg.uuid }); } return sendEvent('pm2:scoped:end', { at : Date.now(), out : data, uuid : msg.uuid }); }); }); /** * Compact event in Push Interactor *pipe* */ function sendEvent(event, data) { var packet = { at : Date.now(), data : { data : data.out, uuid : data.uuid } }; if (!PushInteractor._packet[event]) PushInteractor._packet[event] = []; PushInteractor._packet[event].push(packet); if (process.env.NODE_ENV == 'local_test') process.send({event : event, data : data}); }; /** * Processing */ function executionBox(msg, cb) { var action_name = msg.action_name; var opts = msg.options; if (!PM2_REMOTE_METHOD_ALLOWED[action_name]) { console.error('method %s not allowed', action_name); return cb(new Error('method ' + action_name + ' not allowed')); } var action_conf = PM2_REMOTE_METHOD_ALLOWED[action_name]; /** * Password checking */ if (action_conf.password_required === true) { if (!msg.password) { console.error('Missing password in query'); return cb('Missing password in query'); } var passwd = Conf.getSync('pm2:passwd'); if (passwd === null) { console.error('Password at PM2 level is missing'); return cb('Password at PM2 level is missing please set password via pm2 set pm2:passwd '); } if (Password.verify(msg.password, passwd) != true) { console.error('Password does not match'); return cb('Password does not match'); } } if (action_conf.lock === false) opts.lock = false; /** * Fork the remote action in another process * so we can catch the stdout/stderr and emit it */ var fork = require('child_process').fork; process.env.fork_params = JSON.stringify({ action : action_name, opts : opts}); console.log('Executing: pm2 %s %s', action_name, opts.args ? opts.args.join(' ') : ''); var app = fork(__dirname + '/ScopedExecution.js', [], { silent : true }); app.stdout.on('data', function(dt) { console.log(dt.toString()); sendEvent('pm2:scoped:stream', { at : Date.now(), out : dt.toString(), uuid : msg.uuid }); }); app.once('error', function(dt) { console.error('Error got?', dt); sendEvent('pm2:scoped:error', { at : Date.now(), out : 'Shit happening ' + JSON.stringify(dt), msg : msg.uuid }); }); app.on('message', function(dt) { var ret = JSON.parse(dt); if (ret.isFinished != true) return false; console.log('Action %s finished (err= %s)', action_name, ret.err); return cb(ret.err, ret.dt); }); return false; } } };