/** * 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 fs = require('fs'); var ipm2 = require('./pm2-interface.js'); var rpc = require('pm2-axon-rpc'); var axon = require('pm2-axon'); var debug = require('debug')('interface:driver'); // Interface var chalk = require('chalk'); var Url = require('url'); var os = require('os'); var domain = require('domain'); var fmt = require('../tools/fmt.js'); var pkg = require('../../package.json'); var PM2 = require('../..'); var cst = require('../../constants.js'); var Cipher = require('./Cipher.js'); var ReverseInteractor = require('./ReverseInteractor.js'); var PushInteractor = require('./PushInteractor.js'); var Utility = require('../Utility.js'); var WatchDog = require('./WatchDog.js'); var Conf = require('../Configuration.js'); var HttpRequest = require('./HttpRequest.js'); var InternalIP = require('./internal-ip.js'); global._pm2_password_protected = false; // Flag for log streaming status global._logs = false; var Daemon = module.exports = { connectToPM2 : function() { return ipm2(); }, exit : function() { var self = this; this.opts.pm2_instance.disconnect(function() { console.log('Connection to PM2 via CLI closed'); }); process.nextTick(function() { try { fs.unlinkSync(cst.INTERACTOR_RPC_PORT); fs.unlinkSync(cst.INTERACTOR_PID_PATH); } catch(e) {} if (self.opts.ipm2) self.opts.ipm2.disconnect(); console.log('Exiting Interactor'); if (!this._rpc || !this._rpc.sock) return process.exit(cst.ERROR_EXIT); this._rpc.sock.close(function() { console.log('RPC closed - Interactor killed'); process.exit(cst.SUCCESS_EXIT); }); }); }, activateRPC : function() { console.log('Launching Interactor exposure'); var self = this; var rep = axon.socket('rep'); var daemon_server = new rpc.Server(rep); var sock = rep.bind(cst.INTERACTOR_RPC_PORT); daemon_server.expose({ kill : function(cb) { console.log('Killing interactor'); cb(null); return Daemon.exit(); }, passwordSet : function(cb) { global._pm2_password_protected = true; return cb(null); }, getInfos : function(cb) { if (self.opts && self.opts.DAEMON_ACTIVE == true) return cb(null, { machine_name : self.opts.MACHINE_NAME, public_key : self.opts.PUBLIC_KEY, secret_key : self.opts.SECRET_KEY, remote_host : cst.REMOTE_HOST, remote_port : cst.REMOTE_PORT, reverse_interaction : self.opts.REVERSE_INTERACT, socket_path : cst.INTERACTOR_RPC_PORT, pm2_home_monitored : cst.PM2_HOME }); else { return cb(null); } } }); return daemon_server; }, formatMetada : function() { var cpu, memory; var self = this; try { cpu = os.cpus(); memory = Math.floor(os.totalmem() / 1024 / 1024); } catch(e) { cpu = 0; memory = 0; }; var ciphered_data = Cipher.cipherMessage(JSON.stringify({ MACHINE_NAME : this.opts.MACHINE_NAME, PUBLIC_KEY : this.opts.PUBLIC_KEY, PM2_VERSION : this.opts.PM2_VERSION, RECYCLE : this.opts.RECYCLE || false, MEMORY : memory, HOSTNAME : os.hostname(), CPUS : cpu.length }), this.opts.SECRET_KEY); return ciphered_data; }, pingKeepAlive : function() { var self = this; (function checkInternet() { require('dns').lookup('google.com',function(err) { if (err && (err.code == 'ENOTFOUND' || err.code == 'EAI_AGAIN')) { if (self.opts._connection_is_up == true) console.error('[CRITICAL] Internet is unreachable (via DNS lookup strategy)'); self.opts._connection_is_up = false; } else { if (self.opts._connection_is_up == false) { console.log('[TENTATIVE] Reactivating connection'); PushInteractor.connectRemote(); } self.opts._connection_is_up = true; } setTimeout(checkInternet, 15000); }); })(); }, changeUrls : function(push_url, reverse) { if (push_url) PushInteractor.connectRemote(push_url); if (reverse) ReverseInteractor.changeUrl(reverse); }, refreshWorker : function() { var self = this; function refreshMetadata() { var ciphered_data = Daemon.formatMetada(); HttpRequest.post({ url : self.opts.ROOT_URL, port : self.opts.ROOT_PORT, data : { public_id : self.opts.PUBLIC_KEY, data : ciphered_data } }, function(err, km_data) { if (err) return console.error(err); /** protect against malformated data **/ if (!km_data || !km_data.endpoints || !km_data.endpoints.push || !km_data.endpoints.reverse) { console.error('[CRITICAL] Malformated data received, skipping...'); return false; } if (km_data.disabled == true) { console.error('Server DISABLED BY ADMINISTRATION contact support contact@keymetrics.io with reference to your public and secret keys)'); return Daemon.exit(); } /************************************** * Urls has changed = update workers * **************************************/ if ((Daemon.current_km_data.endpoints.push != km_data.endpoints.push) || (Daemon.current_km_data.endpoints.reverse != km_data.endpoints.reverse)) { self.changeUrls(km_data.endpoints.push, km_data.endpoints.reverse); Daemon.current_km_data = km_data; } else { debug('[REFRESH META] No need to update URL (same)', km_data); } return false; }); }; // Refresh metadata every minutes setInterval(function() { refreshMetadata(); }, 60000); }, validateData : function() { var opts = {}; opts.MACHINE_NAME = process.env.PM2_MACHINE_NAME; opts.PUBLIC_KEY = process.env.PM2_PUBLIC_KEY; opts.SECRET_KEY = process.env.PM2_SECRET_KEY; opts.RECYCLE = process.env.KM_RECYCLE ? JSON.parse(process.env.KM_RECYCLE) : false; opts.REVERSE_INTERACT = JSON.parse(process.env.PM2_REVERSE_INTERACT); opts.PM2_VERSION = pkg.version; if (!opts.MACHINE_NAME) { console.error('You must provide a PM2_MACHINE_NAME environment variable'); process.exit(cst.ERROR_EXIT); } else if (!opts.PUBLIC_KEY) { console.error('You must provide a PM2_PUBLIC_KEY environment variable'); process.exit(cst.ERROR_EXIT); } else if (!opts.SECRET_KEY) { console.error('You must provide a PM2_SECRET_KEY environment variable'); process.exit(cst.ERROR_EXIT); } return opts; }, welcome : function(cb) { var self = this; var ciphered_data = Daemon.formatMetada(); if (!ciphered_data) { process.send({ msg : 'Error while ciphering data', error : true }); return process.exit(1); } var retries = 0; function doWelcomeQuery(cb) { HttpRequest.post({ url : self.opts.ROOT_URL, port : self.opts.ROOT_PORT, data : { public_id : self.opts.PUBLIC_KEY, data : ciphered_data } }, function(err, km_data) { self.current_km_data = km_data; if (err) { console.error('Got error while connecting: %s', err.message || err); if (retries < 5) { retries++; setTimeout(function() { doWelcomeQuery(cb); }, 600); return false; } return cb(err); } if (self.opts.RECYCLE) { if (!km_data.name) { console.error('Error no previous machine name for recycle option returned!'); } self.opts.MACHINE_NAME = km_data.name; }; // For Human feedback if (process.send) process.send({ error : false, km_data : km_data, online : true, pid : process.pid, machine_name : self.opts.MACHINE_NAME, public_key : self.opts.PUBLIC_KEY, secret_key : self.opts.SECRET_KEY, reverse_interaction : self.opts.REVERSE_INTERACT }); // Return get data return cb(null, km_data); }) } doWelcomeQuery(function(err, meta) { return cb(err, meta); }); }, protectedStart : function() { var self = this; var d = domain.create(); d.once('error', function(err) { fmt.sep(); fmt.title('Agent global error caught'); fmt.field('Time', new Date()); console.error(err.message); console.error(err.stack); fmt.sep(); console.error('[Agent] Resurrecting'); var KMDaemon = require('../Interactor/InteractorDaemonizer'); KMDaemon.rescueStart(cst, function(err, dt) { if (err) { console.error('[Agent] Failed to rescue agent, error:'); console.error(err.message || err); process.exit(1); } console.log('[Agent] Rescued.'); process.exit(0); }); }); d.run(function() { self.start(); }); }, start : function() { var self = this; self.opts = self.validateData(); self.opts.ipm2 = null; self.opts.internal_ip = InternalIP(); self.opts.pm2_instance = PM2; self.opts._connection_is_up = true; self.current_km_data = null; self.opts.pm2_instance.connect(function() { console.log('Connected to PM2'); }); self._rpc = self.activateRPC(); // Test mode #1 if (cst.DEBUG) { self.opts.ROOT_URL = '127.0.0.1'; if (process.env.NODE_ENV == 'test') self.opts.ROOT_PORT = 3400; else self.opts.ROOT_PORT = 3000; } else { self.opts.ROOT_URL = cst.KEYMETRICS_ROOT_URL; self.opts.ROOT_PORT = 443; } if (Conf.getSync('pm2:passwd')) global._pm2_password_protected = true; // Test mode #2 if (process.env.NODE_ENV == 'local_test') { self.opts.DAEMON_ACTIVE = true; self.opts.ipm2 = self.connectToPM2(); PushInteractor.start({ url : 'http://127.0.0.1:4321', conf : self.opts }); ReverseInteractor.start({ url : 'http://127.0.0.1:4322', conf : self.opts }); if (process.send) process.send({ success : true, debug : true }); return false; } Daemon.welcome(function(err, km_data) { if (err) { if (process.send) process.send({ error : true, msg : err.stack || err }); console.log(err.stack || err); return Daemon.exit(); } if (km_data.disabled == true) { console.error('Interactor disabled'); return Daemon.exit(); } if (km_data.pending == true) { console.error('Interactor pending'); return Daemon.exit(); } if (km_data.active == true) { self.opts.DAEMON_ACTIVE = true; self.opts.ipm2 = self.connectToPM2(); WatchDog.start({ conf : self.opts }); PushInteractor.start({ url : km_data.endpoints.push, conf : self.opts }); if (self.opts.REVERSE_INTERACT == true) { ReverseInteractor.start({ url : km_data.endpoints.reverse, conf : self.opts }); } Daemon.refreshWorker(); Daemon.pingKeepAlive(); } else { console.log('Nothing to do, exiting'); Daemon.exit(); } return false; }); } }; /** * MAIN */ if (require.main === module) { console.log(chalk.cyan.bold('[Keymetrics.io]') + ' Launching agent'); process.title = 'PM2: KM Agent (' + process.env.PM2_HOME + ')'; Utility.overrideConsole(); Daemon.protectedStart(); }