pm2/lib/Interactor/InteractorDaemonizer.js
2017-06-29 11:14:10 +02:00

518 lines
14 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';
var debug = require('debug')('pm2:interface:daemon');
var fs = require('fs');
var path = require('path');
var util = require('util');
var rpc = require('pm2-axon-rpc');
var axon = require('pm2-axon');
var chalk = require('chalk');
var os = require('os');
var cst = require('../../constants.js');
var Common = require('../Common');
var json5 = require('../tools/json5.js');
var UX = require('../API/CliUx.js');
var crypto = require('crypto');
var InteractorDaemonizer = module.exports = {};
InteractorDaemonizer.rpc = {};
/**
* Description
* @method ping
* @param {} cb
* @return
*/
InteractorDaemonizer.ping = function(conf, cb) {
var req = axon.socket('req');
var client = new rpc.Client(req);
debug('[PING INTERACTOR] Trying to connect to Interactor daemon');
client.sock.once('reconnect attempt', function() {
client.sock.close();
debug('Interactor Daemon not launched');
return cb(false);
});
client.sock.once('connect', function() {
client.sock.once('close', function() {
return cb(true);
});
client.sock.close();
debug('Interactor Daemon alive');
});
req.connect(conf.INTERACTOR_RPC_PORT);
};
InteractorDaemonizer.killInteractorDaemon = function(conf, cb) {
process.env.PM2_INTERACTOR_PROCESSING = true;
debug('Killing interactor #1 ping');
InteractorDaemonizer.ping(conf, function(online) {
debug('Interactor online', online);
if (!online) {
if (!cb) Common.printError('Interactor not launched');
return cb(new Error('Interactor not launched'));
}
InteractorDaemonizer.launchRPC(conf, function(err, data) {
if (err) {
setTimeout(function() {
InteractorDaemonizer.disconnectRPC(cb);
}, 100);
return false;
}
InteractorDaemonizer.rpc.kill(function(err) {
if (err) Common.printError(err);
setTimeout(function() {
InteractorDaemonizer.disconnectRPC(cb);
}, 100);
});
return false;
});
return false;
});
};
/**
* Description
* @method launchRPC
* @param {} cb
* @return
*/
InteractorDaemonizer.launchRPC = function(conf, cb) {
var self = this;
var req = axon.socket('req');
this.client = new rpc.Client(req);
debug('Generating methods');
/**
* Description
* @method generateMethods
* @param {} cb
* @return
*/
var generateMethods = function(cb) {
self.client.methods(function(err, methods) {
Object.keys(methods).forEach(function(key) {
var method_signature = methods[key];
debug('+-- Creating %s method', method_signature.name);
(function(name) {
/**
* Description
* @method name
* @return
*/
self.rpc[name] = function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(name);
self.client.call.apply(self.client, args);
};
})(method_signature.name);
});
return cb();
});
};
this.client.sock.once('reconnect attempt', function(e) {
self.client.sock.removeAllListeners();
return cb({success:false, msg:'reconnect attempt'});
});
this.client.sock.once('error', function(e) {
console.error('Error in error catch all on Interactor');
console.error(e.stack || e);
});
this.client.sock.once('connect', function() {
self.client.sock.removeAllListeners();
generateMethods(function() {
debug('Methods generated');
cb(null, {success:true});
});
});
this.client_sock = req.connect(conf.INTERACTOR_RPC_PORT);
};
/**
* Description
* @method launchOrAttach
* @param {} secret_key
* @param {} public_key
* @param {} machine_name
* @param {} cb
* @return
*/
function launchOrAttach(conf, infos, cb) {
InteractorDaemonizer.ping(conf, function(online) {
if (online) {
debug('Interactor online, restarting it...');
InteractorDaemonizer.launchRPC(conf, function() {
InteractorDaemonizer.rpc.kill(function(err) {
daemonize(conf, infos, function(err, msg, proc) {
return cb(err, msg, proc);
});
});
});
}
else {
debug('Interactor offline, launching it...');
daemonize(conf, infos, function(err, msg, proc) {
return cb(err, msg, proc);
});
}
return false;
});
};
/**
* Description
* @method daemonize
* @param {} secret_key
* @param {} public_key
* @param {} machine_name
* @param {} cb
* @return
*/
var daemonize = function(conf, infos, cb) {
var InteractorJS = path.resolve(path.dirname(module.filename), 'Daemon.js');
var out = null;
var err = null;
if (process.env.TRAVIS || process.env.NODE_ENV == 'local_test') {
// Redirect PM2 internal err and out
// to STDERR STDOUT when running with Travis
out = 1;
err = 2;
}
else {
out = fs.openSync(conf.INTERACTOR_LOG_FILE_PATH, 'a');
err = fs.openSync(conf.INTERACTOR_LOG_FILE_PATH, 'a');
}
var child = require('child_process').spawn('node', [InteractorJS], {
silent : false,
detached : true,
cwd : process.cwd(),
env : util._extend({
PM2_HOME : conf.PM2_HOME,
PM2_MACHINE_NAME : infos.machine_name,
PM2_SECRET_KEY : infos.secret_key,
PM2_PUBLIC_KEY : infos.public_key,
PM2_REVERSE_INTERACT : infos.reverse_interact,
KEYMETRICS_NODE : infos.info_node
}, process.env),
stdio : ['ipc', out, err]
});
UX.processing.start();
fs.writeFileSync(conf.INTERACTOR_PID_PATH, child.pid);
function onError(msg) {
debug('Error when launching Interactor, please check the agent logs');
return cb(msg);
}
child.once('error', onError);
child.unref();
child.once('message', function(msg) {
debug('Interactor daemon launched', msg);
UX.processing.stop();
if (msg.debug) {
return cb(null, msg, child);
}
child.removeAllListeners('error');
child.disconnect();
/*****************
* Error messages
*/
if (msg.error == true) {
console.log(chalk.red('[Keymetrics.io][ERROR]'), msg.msg);
console.log(chalk.cyan('[Keymetrics.io]') + ' Contact support contact@keymetrics.io and send us the error message');
return cb(msg);
}
if (msg.km_data.disabled == true) {
console.log(chalk.cyan('[Keymetrics.io]') + ' Server DISABLED BY ADMINISTRATION contact support contact@keymetrics.io with reference to your public and secret keys)');
return cb(msg);
}
if (msg.km_data.error == true) {
console.log(chalk.red('[Keymetrics.io][ERROR]') + ' ' + msg.km_data.msg + ' (Public: %s) (Secret: %s) (Machine name: %s)', msg.public_key, msg.secret_key, msg.machine_name);
return cb(msg);
}
else if (msg.km_data.active == false && msg.km_data.pending == true) {
console.log(chalk.red('[Keymetrics.io]') + ' ' + chalk.bold.red('Agent PENDING') + ' - Web Access: https://app.keymetrics.io/');
console.log(chalk.red('[Keymetrics.io]') + ' You must upgrade your bucket in order to monitor more servers.');
return cb(msg);
}
if (msg.km_data.active == true) {
console.log(chalk.green.bold('[Monitoring Enabled]') + ' Dashboard access: https://app.keymetrics.io/#/r/%s', msg.public_key);
return cb(null, msg, child);
}
return cb(null, msg, child);
});
};
InteractorDaemonizer.update = function(conf, cb) {
InteractorDaemonizer.ping(conf, function(online) {
if (!online) {
Common.printError('Interactor not launched');
return cb(new Error('Interactor not launched'));
}
InteractorDaemonizer.launchRPC(conf, function() {
InteractorDaemonizer.rpc.kill(function(err) {
if (err) {
Common.printError(err);
return cb(new Error(err));
}
Common.printOut('Interactor successfully killed');
setTimeout(function() {
InteractorDaemonizer.launchAndInteract(conf, {}, function() {
return cb(null, {msg : 'Daemon launched'});
});
}, 500);
});
});
return false;
});
};
/**
* Get/Update/Merge agent configuration
* @param {object} _infos
*/
InteractorDaemonizer.getOrSetConf = function(conf, infos, cb) {
var reverse_interact = true;
var version_management_active = true;
var version_management_password = null;
var secret_key;
var public_key;
var machine_name;
var info_node;
var new_connection = false;
// 1# Load configuration file
try {
var interaction_conf = json5.parse(fs.readFileSync(conf.INTERACTION_CONF));
public_key = interaction_conf.public_key;
machine_name = interaction_conf.machine_name;
secret_key = interaction_conf.secret_key;
info_node = interaction_conf.info_node;
reverse_interact = interaction_conf.reverse_interact || true;
if (interaction_conf.version_management) {
version_management_password = interaction_conf.version_management.password || version_management_password;
version_management_active = interaction_conf.version_management.active || version_management_active;
}
} catch (e) {
debug('Interaction file does not exists');
}
// 2# Override with passed informations
if (infos) {
if (infos.secret_key)
secret_key = infos.secret_key;
if (infos.public_key)
public_key = infos.public_key;
if (infos.machine_name)
machine_name = infos.machine_name;
if (infos.info_node)
info_node = infos.info_node;
new_connection = true;
}
// 3# Override with environment variables (highest-priority conf)
if (process.env.PM2_SECRET_KEY || process.env.KEYMETRICS_SECRET)
secret_key = process.env.PM2_SECRET_KEY || process.env.KEYMETRICS_SECRET;
if (process.env.PM2_PUBLIC_KEY || process.env.KEYMETRICS_PUBLIC)
public_key = process.env.PM2_PUBLIC_KEY || process.env.KEYMETRICS_PUBLIC;
if (new_connection && info_node == null)
info_node = process.env.KEYMETRICS_NODE || cst.KEYMETRICS_ROOT_URL;
if (!info_node)
info_node = cst.KEYMETRICS_ROOT_URL;
if (!secret_key)
return cb(new Error('secret key is not defined'));
if (!public_key)
return cb(new Error('public key is not defined'));
if (!machine_name)
machine_name = os.hostname() + '-' + crypto.randomBytes(4).toString('hex');
/**
* Write new data to configuration file
*/
try {
var new_interaction_conf = {
secret_key : secret_key,
public_key : public_key,
machine_name : machine_name,
reverse_interact : reverse_interact,
info_node : info_node,
version_management : {
active : version_management_active,
password : version_management_password
}
};
fs.writeFileSync(conf.INTERACTION_CONF, json5.stringify(new_interaction_conf, null, 4));
} catch(e) {
console.error('Error when writting configuration file %s', conf.INTERACTION_CONF);
return cb(e);
}
// Don't block the event loop
process.nextTick(function() {
cb(null, new_interaction_conf);
});
};
InteractorDaemonizer.disconnectRPC = function(cb) {
if (!InteractorDaemonizer.client_sock ||
!InteractorDaemonizer.client_sock.close)
return cb(null, {
success : false,
msg : 'RPC connection to Interactor Daemon is not launched'
});
if (InteractorDaemonizer.client_sock.connected === false ||
InteractorDaemonizer.client_sock.closing === true) {
return cb(null, {
success : false,
msg : 'RPC closed'
});
}
try {
var timer;
debug('Closing RPC INTERACTOR');
InteractorDaemonizer.client_sock.once('close', function() {
debug('RPC INTERACTOR cleanly closed');
clearTimeout(timer);
return cb ? cb(null, {success:true}) : false;
});
timer = setTimeout(function() {
if (InteractorDaemonizer.client_sock.destroy)
InteractorDaemonizer.client_sock.destroy();
return cb ? cb(null, {success:true}) : false;
}, 200);
InteractorDaemonizer.client_sock.close();
} catch(e) {
debug('Error while closing RPC INTERACTOR', e.stack || e);
return cb ? cb(e.stack || e) : false;
}
return false;
};
InteractorDaemonizer.rescueStart = function(conf, cb) {
InteractorDaemonizer.getOrSetConf(conf, null, function(err, infos) {
if (err || !infos) {
return cb(err);
}
console.log(chalk.cyan('[Keymetrics.io]') + ' Using (Public key: %s) (Private key: %s)', infos.public_key, infos.secret_key);
daemonize(conf, infos, function(err, msg, proc) {
return cb(err, msg, proc);
});
});
};
InteractorDaemonizer.launchAndInteract = function(conf, opts, cb) {
// For Watchdog
if (process.env.PM2_AGENT_ONLINE) {
return process.nextTick(cb);
}
process.env.PM2_INTERACTOR_PROCESSING = true;
this.getOrSetConf(conf, opts, function(err, data) {
if (err || !data) {
return cb(err);
}
//console.log(chalk.cyan('[Keymetrics.io]') + ' Using (Public key: %s) (Private key: %s)', data.public_key, data.secret_key);
launchOrAttach(conf, data, function(err, msg, proc) {
if (err)
return cb(err);
return cb(null, msg, proc);
});
return false;
});
};
/**
* Description
* @method getInteractInfo
* @param {} cb
* @return
*/
InteractorDaemonizer.getInteractInfo = function(conf, cb) {
debug('Getting interaction info');
if (process.env.PM2_NO_INTERACTION) return;
InteractorDaemonizer.ping(conf, function(online) {
if (!online) {
return cb(new Error('Interactor is offline'));
}
InteractorDaemonizer.launchRPC(conf, function() {
InteractorDaemonizer.rpc.getInfos(function(err, infos) {
if (err)
return cb(err);
// Avoid general CLI to interfere with Keymetrics CLI commands
if (process.env.PM2_INTERACTOR_PROCESSING)
return cb(null, infos);
InteractorDaemonizer.disconnectRPC(function() {
return cb(null, infos);
});
return false;
});
});
return false;
});
};