/** * 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; }); };