From 31e65020b579e32a0db32e23dbf3dc8f083f3fd2 Mon Sep 17 00:00:00 2001 From: Tjatse Date: Fri, 25 Dec 2015 16:01:50 +0800 Subject: [PATCH] clean house & refactor --- .gitignore | 4 +- .npmignore | 4 +- .travis.yml | 1 + README.md | 81 +----- bin/pm2-gui | 165 ------------ lib/daemon.js | 144 ++++++++++ lib/{mon.js => monitor.js} | 268 ++++++++----------- lib/pm.js | 134 +++++----- lib/stat.js | 36 +-- lib/util/conf.js | 15 +- lib/util/debug.js | 65 ----- lib/util/log.js | 39 +++ package.json | 30 +-- pm2-gui | 123 +++++++++ pm2-gui.ini | 13 +- pm2-gui.js | 96 +++++++ test/fixtures/conf.js | 43 --- test/{fixtures/startup.json => process.json} | 0 web/index.js | 2 +- 19 files changed, 660 insertions(+), 603 deletions(-) delete mode 100755 bin/pm2-gui create mode 100644 lib/daemon.js rename lib/{mon.js => monitor.js} (64%) delete mode 100644 lib/util/debug.js create mode 100644 lib/util/log.js create mode 100755 pm2-gui create mode 100644 pm2-gui.js delete mode 100644 test/fixtures/conf.js rename test/{fixtures/startup.json => process.json} (100%) diff --git a/.gitignore b/.gitignore index b512c09..78f8ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -node_modules \ No newline at end of file +node_modules +logs +*.pid \ No newline at end of file diff --git a/.npmignore b/.npmignore index a267a83..58a7bcb 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,5 @@ node_modules .gitignore -screenshots \ No newline at end of file +screenshots +logs +*.pid \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index be4d592..ec191e6 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,4 @@ branches: node_js: - 0.10 - 0.11 + - 0.12 diff --git a/README.md b/README.md index 3c19e91..6d20617 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,8 @@ An elegant web & terminal interface for Unitech/PM2. - [Cautions](#cauts) - [Installation](#ins) - [CLI](#cli) - - [Curses-like dashboard](#dashboard) - [Run Web Interface](#cli_web) - - [Daemonic](#daemonic) - - [Configs](#cli_confs) + - [Curses-like dashboard](#dashboard) - [Authorization](#auth) - [UI/UX](#ui) - [Serving apps locally with nginx and custom domain](#serv) @@ -103,8 +101,8 @@ $ npm install -g pm2-gui --no-debug hide stdout / stderr information ``` - -## Daemonic + +## daemonize ```bash # start $ nohup pm2-gui start > /dev/null 2>&1 & echo $! > /path/to/pm2-gui.pid @@ -121,79 +119,6 @@ debug = false port = 8088 ``` -- **refresh** The heartbeat duration of monitor (backend), `5000` by default. -- **pm2** Root directory of Unitech/PM2, `~/.pm2` by default. -- **port** Port of web interface. -- **debug** A value indicates whether show the debug information, `true` by default. -- **password** The encrypted authentication code, if this config is set, users need to be authorized before accessing the index page, `password` could only be set by `pm2-gui set password [password]` ([authorization](#authorization)). - -### File -You can quick set configurations by `pm2-gui start --config [file]`, the `[file]` must be a valid **ini** file, and can include all the above keys. - -Example -```bash -# Load the configuration file which is named as `pm2-gui.ini` in current directory. -$ pm2-gui start --config - -# Load the specific configuration file under current directory, `.ini` postfix is optional. -$ pm2-gui start --config conf -$ pm2-gui start --config conf.ini -``` - -### Set -Usage -```bash -$ pm2-gui set -``` - -Example -```bash -$ pm2-gui set refresh 2000 -``` - -Above command will set `refresh` to 2 seconds. - -### Remove -Usage -```bash -$ pm2-gui rm -``` - -Example -```bash -$ pm2-gui rm refresh -``` - -Above command will remove `refresh` config and it will be set to `5000` (milliseconds) by default. - -### Update via `vi` -```bash -$ vi $PM2_ROOT/.pm2/pm2-gui.ini -``` - -### Cleanup -```bash -$ rm $PM2_ROOT/.pm2/pm2-gui.ini -``` - -> The value of `$PM2_ROOT` is `~/` by default. - - -# Authorization -Run the following commands: -```bash -$ pm2-gui set password 1234 -$ pm2-gui start -``` - -When you visiting `http://[domain]:8088` in your browser, it will be redirected to `http://[domain]:8088/auth`, and you need to typo the password (`1234`) to login. - -Otherwise, if you do not want to deny anybody, just simply remove it: -```bash -$ pm2-gui rm password -$ pm2-gui start -``` - # UI/UX - Amazing and smooth animations. diff --git a/bin/pm2-gui b/bin/pm2-gui deleted file mode 100755 index 07b1006..0000000 --- a/bin/pm2-gui +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env node - -var commander = require('commander'), - path = p = require('path'), - fs = require('fs'), - chalk = require('chalk'), - _ = require('lodash'), - pkg = require('../package.json'), - Monitor = require('../lib/mon'), - crypto = require('crypto'), - conf = require('../lib/util/conf'), - interface = require('../web/index'); - -commander.version(pkg.version, '-v, --version') - .usage('[cmd] [options]'); - -commander.on('--help', function(){ - console.log(' Basic Examples:\n\n' + - ' Start the web server, by default port (8088):\n' + - chalk.grey(' $ pm2-gui start\n') + - '\n' + - ' Start the web server, by specific port (8090):\n' + - chalk.grey(' $ pm2-gui start 8090\n') + - '\n' + - ' Start the web server, by specific configuration file (pm2-gui.ini):\n' + - chalk.grey(' $ pm2-gui start --config\n') + - '\n' + - ' Start the web server, by specific configuration file:\n' + - chalk.grey(' $ pm2-gui start --config my-config.ini\n') - ); -}); - -/** - * Run web interface. - */ -commander.command('start [port]') - .option('--config [file]', 'pass ".ini" configuration file (with options)') - .option('--no-debug', 'hide stdout / stderr information') - .description('Launch the web server, port default by 8088') - .action(function(port, cmd){ - if (cmd.config) { - var jsonFile; - if (typeof cmd.config != 'string') { - jsonFile = 'pm2-gui.ini'; - } else { - jsonFile = cmd.config; - if (jsonFile.indexOf('.') < 0) { - jsonFile += '.ini'; - } - } - if (!fs.existsSync(jsonFile)) { - console.log(chalk.red('✘ .ini configuration file does not exist!\n')); - process.exit(); - } - - try { - var config = conf.File(path.resolve(process.cwd(), jsonFile)).loadSync().valueOf(); - setConfig(config); - } catch (err) { - console.log(chalk.red('✘ .ini configuration file is invalid!\n')); - process.exit(); - } - } - port && setConfig('port', port); - interface(cmd.debug); - }); - -commander.command('mon') - .description('curses-like dashboard') - .action(function(){ - var mon = Monitor({ - debug: false - }); - mon.dashboard(); - }); - -/** - * Show configurations. - */ -commander.command('config') - .description('show all configs') - .action(showConfigs); - -/** - * Set config by key-value pairs. - */ -commander.command('set ') - .description('set config by key-value pairs') - .action(function(key, value, cmd){ - var mon = setConfig(key, value); - mon && showConfigs(cmd, mon); - }); - -/** - * Unset config by key. - */ -commander.command('rm ') - .description('remove config by key') - .action(function(key, cmd){ - var mon = Monitor(); - mon.config(key, null); - showConfigs(cmd, mon); - }); - -commander.parse(process.argv); - -if (process.argv.length == 2) { - commander.outputHelp(); - process.exit(0); -} - -/** - * Set configuration. - * @param key - * @param value - * @returns {*} - */ -function setConfig(key, value){ - var mon = Monitor(), - acceptKeys = Object.keys(mon.options).filter(function(key){ - return !~['socketio', 'pm2Conf'].indexOf(key); - }); - - acceptKeys.push('password'); - - (function config(pairs){ - if (pairs.length == 0) { - return; - } - var pair = pairs.shift(); - if (!~acceptKeys.indexOf(pair[0])) { - console.log(chalk.bold.yellow('[WARN]'), chalk.cyan(pair[0]), 'is not an acceptable configuration.'); - return config(pairs); - } - if (pair[0] == 'password') { - var md5 = crypto.createHash('md5'); - md5.update(pair[1]); - pair[1] = md5.digest('hex'); - } - mon.config(pair[0], pair[1]); - config(pairs); - })(typeof key == 'object' ? _.pairs(key) : [[key, value]]); - - return mon; -} -/** - * Show all configurations. - * @param cmd - * @param mon - */ -function showConfigs(cmd, mon){ - if (!mon) { - mon = Monitor(); - } - var storage = mon._config.valueOf(), prints = ''; - var maxLen = 0; - for (var k in storage) { - maxLen = Math.max(k.length, maxLen); - } - maxLen += 1; - for (var k in storage) { - prints += chalk.bold(Array(maxLen - k.length).join(' ') + k + ': ') + ' ' + chalk.blue(storage[k] + '\n'); - } - console.log(prints); -} \ No newline at end of file diff --git a/lib/daemon.js b/lib/daemon.js new file mode 100644 index 0000000..75cb3cf --- /dev/null +++ b/lib/daemon.js @@ -0,0 +1,144 @@ +var chalk = require('chalk'), + path = require('path'), + fs = require('fs'), + async = require('async'), + cp = require('child_process'), + fork = cp.fork, + spawn = cp.spawn, + Monitor = require('./monitor'), + Log = require('./util/log'); + +var processDirname = process.cwd(), + confFile = './pm2-gui.ini'; + +if (process.argv.length > 3) { + var confFile = process.argv[3]; +} + +confFile = path.resolve(processDirname, confFile); + +if (!fs.existsSync(confFile)) { + console.error(chalk.bold(confFile), chalk.red('does not exist!')); + return process.exit(0); +} + +var monitor = Monitor({ + confFile: confFile +}); + +Log(monitor.options.log); + +var pidfile = path.resolve(processDirname, './pm2-gui.pid'); + +var Daemon = { + restarts: 0, + init: function (next) { + process.on('SIGTERM', Daemon.stop); + process.on('SIGINT', Daemon.stop); + process.on('SIGHUP', Daemon.restart); + next && next(); + }, + start: function (next) { + Daemon.worker = Daemon.fork(); + next && next(); + }, + restart: function () { + console.info('Restarting...'); + Daemon.kill(); + Daemon.start(); + }, + stop: function () { + console.info('Stopping...'); + Daemon.kill(); + fs.unlinkSync(pidfile); + }, + kill: function () { + if (Daemon.worker) { + Daemon.worker.suicide = true; + Daemon.worker.kill(); + } + }, + fork: function () { + console.info('Forking slave...'); + var worker = fork(path.resolve(processDirname, 'pm2-gui.js'), [confFile, '--color'], { + silent: monitor.options.daemonize, + env: process.env + }); + worker.on('exit', function (code, signal) { + if (code != 0) { + if (Daemon.restarts < 10) { + Daemon.restarts++; + setTimeout(function () { + Daemon.restarts--; + }, 20000); + } else { + console.error(Daemon.restarts + ' restarts in 20 seconds, view the logs to investigate the crash problem.'); + return process.exit(0); + } + } + if (!(worker.suicide || code === 0)) { + setTimeout(Daemon.fork, 3000); + } + }); + + worker.on('message', function (message) { + if (typeof message == 'object' && message.action) + if (message.action == 'restart') { + Daemon.restart(); + } + }); + + var logDir = monitor.options.log.dir, + stdout = 'pm2-gui.out', + stderr = 'pm2-gui.err'; + + if (!logDir) { + logDir = './logs'; + } + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir); + } + + if (monitor.options.daemonize) { + stdout = fs.createWriteStream(path.resolve(processDirname, logDir, stdout)); + stderr = fs.createWriteStream(path.resolve(processDirname, logDir, stderr)); + worker.stdout.pipe(stdout); + worker.stderr.pipe(stderr); + } + + fs.writeFile(pidfile, worker.pid); + return worker; + }, + daemonize: function () { + if (process.env.daemonized) { + console.info('Daemonized with pid [' + process.pid + '].'); + return; + } + console.info('Spawning daemon...'); + var args = [].concat(process.argv); + args.shift(); + var env = process.env; + env.daemonized = true; + var child = spawn(process.execPath, args, { + env: env, + detached: false, + cwd: processDirname, + stdio: ['ignore', process.stdout, process.stderr] + }); + child.unref(); + process.exit(); + } +}; +if (monitor.options.daemonize) { + Daemon.daemonize(); +} + +process.title = 'pm2-gui daemon'; +async.series([ + Daemon.init, + Daemon.start +], function (err) { + if (err) { + console.error(err.stack); + } +}); \ No newline at end of file diff --git a/lib/mon.js b/lib/monitor.js similarity index 64% rename from lib/mon.js rename to lib/monitor.js index 71c58fe..c87c311 100644 --- a/lib/mon.js +++ b/lib/monitor.js @@ -1,15 +1,15 @@ var fs = require('fs'), path = require('path'), - Debug = require('./util/debug'), - stat = require('./stat'), _ = require('lodash'), chalk = require('chalk'), ansiHTML = require('ansi-html'), - pm = require('./pm'), totalmem = require('os').totalmem(), pidusage = require('pidusage'), + pm = require('./pm'), + stat = require('./stat'), conf = require('./util/conf'), layout = require('./blessed-widget/layout'), + Log = require('./util/log'), defConf; module.exports = Monitor; @@ -29,6 +29,9 @@ function Monitor(options) { this._init(options); }; +Monitor.ACCEPT_KEYS = ['pm2', 'refresh', 'statsd', 'node', 'log', 'daemonize', 'max_restarts']; +Monitor.DEF_CONF_FILE = 'pm2-gui.ini'; + /** * Resolve home path. * @param {String} pm2Home @@ -42,7 +45,7 @@ Monitor.prototype._resolveHome = function (pm2Home) { // Make sure exist. if (!pm2Home || !fs.existsSync(pm2Home)) { - throw new Error('PM2 root can not be located, try to initialize PM2 by executing `pm2 ls` or set env by `export PM2_HOME=[ROOT]`.'); + throw new Error('PM2 root can not be located, try to initialize PM2 by executing `pm2 ls` or set environment variable vi `export PM2_HOME=[ROOT]`.'); } } return pm2Home; @@ -55,11 +58,14 @@ Monitor.prototype._resolveHome = function (pm2Home) { Monitor.prototype._init = function (options) { options = options || {}; - // bind default options. - defConf = conf.File(path.resolve(__dirname, '..', 'pm2-gui.ini')).loadSync().valueOf(); - options = _.defaults(defConf, options); + defConf = conf.File(options.confFile || path.resolve(__dirname, '..', Monitor.DEF_CONF_FILE)).loadSync().valueOf(); + defConf = _.pick.call(null, defConf, Monitor.ACCEPT_KEYS); - options.pm2 = this._resolveHome(options.pm2.home); + options = _.pick.apply(options, Monitor.ACCEPT_KEYS).valueOf(); + options = _.defaults(options, defConf); + + options.pm2 = this._resolveHome(options.pm2); + Log(options.log); // Load PM2 config. var pm2ConfPath = path.join(options.pm2, 'conf.js'); @@ -69,21 +75,17 @@ Monitor.prototype._init = function (options) { throw new Error(404); } } catch (err) { - console.error('Can not load PM2 config, the file "' + pm2ConfPath + '" does not exist.'); + var fbMsg = 'Can not load PM2 config, the file "' + pm2ConfPath + '" does not exist or empty, fallback to auto-load by pm2 home.'; + console.warn(fbMsg); options.pm2Conf = { DAEMON_RPC_PORT: path.resolve(options.pm2, 'rpc.sock'), DAEMON_PUB_PORT: path.resolve(options.pm2, 'pub.sock'), PM2_LOG_FILE_PATH: path.resolve(options.pm2, 'pm2.log') }; - - if (!fs.existsSync(options.pm2Conf.DAEMON_RPC_PORT)) { - throw new Error(fbMsg + ' But file ' + options.pm2Conf.DAEMON_RPC_PORT + ' can not found.'); - } - if (!fs.existsSync(options.pm2Conf.DAEMON_PUB_PORT)) { - throw new Error(fbMsg + ' But file ' + options.pm2Conf.DAEMON_PUB_PORT + ' can not found.'); - } - if (!fs.existsSync(options.pm2Conf.PM2_LOG_FILE_PATH)) { - throw new Error(fbMsg + ' But file ' + options.pm2Conf.PM2_LOG_FILE_PATH + ' can not found.'); + for (var pm2ConfProp in options.pm2Conf) { + if (!fs.existsSync(options.pm2Conf[pm2ConfProp])) { + throw new Error(fbMsg + ' But file ' + options.pm2Conf[pm2ConfProp] + ' can not found.'); + } } } @@ -96,40 +98,6 @@ Monitor.prototype._init = function (options) { // Bind to context. this.options = options; Object.freeze(this.options); - - // Initialize configurations. - this._config = new conf.File(path.resolve(this.options.pm2, 'pm2-gui.ini')); - - // Set configurations. - this.config('pm2', this._resolveHome(this.config('pm2')) || this.options.pm2); - this.config('refresh', this.config('refresh') || this.options.refresh); - this.config('port', this.config('port') || this.options.port || 8088); - var debug = this.config('debug'); - this.config('debug', typeof debug == 'undefined' ? (debug = !!this.options.debug) : !!debug); - - // Logger. - this._log = Debug({ - namespace: 'monitor', - debug: debug - }); -}; - -/** - * Operations of configuration. - * @example: - * set config : mon.config('key', 'value'); - * clear config : mon.config('key', null); - * get config : mon.config('key'); - * @param {String} key - * @param {Mixed} value - * @returns {*} - */ -Monitor.prototype.config = function (key, value) { - var def; - if (value == null) { - def = defConf[key]; - } - return this._config.val(key, value, def); }; /** @@ -145,36 +113,38 @@ Monitor.prototype.run = function () { this._tails = {}; this._usages = {}; - // Watching PM2 - this._startWatching(); + // Observe PM2 + this._observePM2(); // Listen connection event. this._sockio.of(conf.NSP.SYS).on('connection', this._connectSysSock.bind(this)); this._sockio.of(conf.NSP.LOG).on('connection', this._connectLogSock.bind(this)); this._sockio.of(conf.NSP.PROC).on('connection', this._connectProcSock.bind(this)); -} +}; + +/** + * Quit monitor. + * @return {[type]} [description] + */ +Monitor.prototype.quit = function () { + console.debug('Closing pm2 pub emitter socket.'); + this.pm2Sock && this.pm2Sock.close(); +}; /** * Monitor dashboard. */ Monitor.prototype.dashboard = function () { - // No logger anymore. - this._log = Debug({ - namespace: 'monitor', - debug: false - }); - // Socket.io server. - var port = this.config('port'); + var port = this.options.port; this._sockio = require('socket.io')(); this._sockio.listen(port); this.run(); // Render screen. - var lyt = layout({ + layout({ port: port - }); - lyt.render(); + }).render(); }; /** @@ -193,13 +163,13 @@ Monitor.prototype._connectSysSock = function (socket) { // Trigger actions of process. socket.on('action', function (action, id) { - this._log.i('action', chalk.magenta(action), ' ', id); + console.info('[' + id + ']', action, 'sending to pm2 daemon...'); pm.action(this.options.pm2Conf.DAEMON_RPC_PORT, action, id, function (err, forceRefresh) { if (err) { - this._log.e(action, err.message); + console.error(action, err.message); return socket.emit('action', id, err.message); } - this._log.i('action', chalk.magenta(action), 'finished', id); + console.info('[' + id + ']', action, 'completed!'); forceRefresh && this._throttleRefresh(); }.bind(this)); }.bind(this)); @@ -213,8 +183,8 @@ Monitor.prototype._connectSysSock = function (socket) { this._sysStat && this._broadcast('system_stat', this._sysStat); // Grep system states once and again. - (this._status != 'R') && this._nextTick(this.config('refresh') || 5000); -} + (this._status != 'R') && this._nextTick(this.options.refresh || 5000); +}; /** * Connection event of `log` namespace. @@ -222,44 +192,33 @@ Monitor.prototype._connectSysSock = function (socket) { * @private */ Monitor.prototype._connectLogSock = function (socket) { - // Broadcast tailed logs. - function broadcast(data) { - var socks = this._sockio.of(conf.NSP.LOG).sockets; - if (!socks || socks.length == 0) { - return; - } - socks.forEach(function (sock) { - if (sock._pm_id == data.pm_id) { - sock.emit('log', data) - } - }); - } + var self = this; // Emit error. function emitError(err, pm_id, keepANSI) { - broadcast.call(this, { + var data = { pm_id: pm_id, msg: keepANSI ? chalk.red(err.message) : 'Error: ' + err.message + '' - }); + }; + self._broadcast.call(self, 'log', data, conf.NSP.LOG); } // Clean up `tail` after socket disconnected. function killTail(pm_id) { - this._tails[pm_id].forEach(function (tail) { + self._tails[pm_id].forEach(function (tail) { try { tail.kill('SIGTERM'); } catch (err) {} }); - delete this._tails[pm_id]; - this._log.i('tail', chalk.magenta('destroy'), pm_id); + delete self._tails[pm_id]; + console.info('[' + pm_id + ']', 'tail destroyed!'); } function killTailProcess(pm_id) { if (!isNaN(pm_id)) { - killTail.call(this, pm_id); - return; + return killTail(pm_id); } - var socks = this._sockio.of(conf.NSP.LOG).sockets, + var socks = self._sockio.of(conf.NSP.LOG).sockets, canNotBeDeleted = {}; if (socks && socks.length > 0) { socks.forEach(function (sock) { @@ -267,32 +226,31 @@ Monitor.prototype._connectLogSock = function (socket) { }); } - for (var pm_id in this._tails) { + for (var pm_id in self._tails) { if (!canNotBeDeleted[pm_id]) { - killTail.call(this, pm_id); + killTail(pm_id); } } } - socket.on('disconnect', killTailProcess.bind(this)); - socket.on('tail_kill', killTailProcess.bind(this)); - socket.on('tail', function (pm_id, keepANSI) { + + function startTailProcess(pm_id, keepANSI) { socket._pm_id = pm_id; - if (this._tails[pm_id]) { + if (self._tails[pm_id]) { return; } // Tail logs. pm.tail({ - sockPath: this.options.pm2Conf.DAEMON_RPC_PORT, - logPath: this.options.pm2Conf.PM2_LOG_FILE_PATH, + sockPath: self.options.pm2Conf.DAEMON_RPC_PORT, + logPath: self.options.pm2Conf.PM2_LOG_FILE_PATH, pm_id: pm_id }, function (err, lines) { if (err) { - return emitError.call(this, err, pm_id, keepANSI); + return emitError(err, pm_id, keepANSI); } // Emit logs to clients. - broadcast.call(this, { + var data = { pm_id: pm_id, msg: lines.map(function (line) { if (!keepANSI) { @@ -302,16 +260,21 @@ Monitor.prototype._connectLogSock = function (socket) { return line; } }).join(keepANSI ? '\n' : '') - }); - }.bind(this), function (err, tails) { + }; + self._broadcast.call(self, 'log', data, conf.NSP.LOG); + }, function (err, tails) { if (err) { - return emitError.call(this, err, pm_id, keepANSI); + return emitError(err, pm_id, keepANSI); } - this._log.i('tail', chalk.magenta('start'), pm_id); - this._tails[pm_id] = tails; - }.bind(this)); - }.bind(this)); + console.info('[' + pm_id + ']', 'tail starting...'); + self._tails[pm_id] = tails; + }); + } + + socket.on('disconnect', killTailProcess); + socket.on('tail_kill', killTailProcess); + socket.on('tail', startTailProcess); }; /** @@ -320,30 +283,18 @@ Monitor.prototype._connectLogSock = function (socket) { * @private */ Monitor.prototype._connectProcSock = function (socket) { - // Broadcast memory/CPU usage of process. - function broadcast(data) { - var socks = this._sockio.of(conf.NSP.PROC).sockets; - if (!socks || socks.length == 0) { - return; - } - socks.forEach(function (sock) { - if (sock._pid == data.pid) { - sock.emit('proc', data) - } - }); - } - + var self = this; // Emit error. function emitError(err, pid) { - broadcast.call(this, { + var data = { pid: pid, msg: 'Error: ' + err.message + '' - }); + }; + self._broadcast.call(self, 'proc', data, conf.NSP.PROC); } - // Clean up `proc` timer after socket disconnected. - socket.on('disconnect', function () { - var socks = this._sockio.of(conf.NSP.PROC).sockets, + function killObserver() { + var socks = self._sockio.of(conf.NSP.PROC).sockets, canNotBeDeleted = {}; if (socks && socks.length > 0) { socks.forEach(function (sock) { @@ -356,20 +307,20 @@ Monitor.prototype._connectProcSock = function (socket) { if (!canNotBeDeleted[pid] && (timer = this._usages[pid])) { clearInterval(timer); delete this._usages[pid]; - this._log.i('usage', chalk.magenta('destroy'), pid); + console.info('[' + pid + ']', 'cpu and memory observer destroyed!'); } } - }.bind(this)); + } - socket.on('proc', function (pid) { + function runObserver(pid) { socket._pid = pid; var pidStr = pid.toString(); - if (this._usages[pidStr]) { + if (self._usages[pidStr]) { return; } - this._log.i('usage', chalk.magenta('start'), pidStr); + console.info('[' + pidStr + ']', 'cpu and memory observer is running...'); function runTimer(ctx) { pidusage.stat(pid, function (err, stat) { @@ -379,18 +330,22 @@ Monitor.prototype._connectProcSock = function (socket) { return emitError.call(ctx, err, pid); } stat.memory = stat.memory * 100 / totalmem; - // Emit memory/CPU usage to clients. - broadcast.call(ctx, { + + var data = { pid: pid, time: Date.now(), usage: stat - }); + }; + self._broadcast.call(ctx, 'proc', data, conf.NSP.PROC); }); } - this._usages[pidStr] = setInterval(runTimer, 3000, this); + self._usages[pidStr] = setInterval(runTimer, 3000, self); runTimer(this); - }.bind(this)); + } + + socket.on('disconnect', killObserver); + socket.on('proc', runObserver); }; /** @@ -405,7 +360,7 @@ Monitor.prototype._nextTick = function (tick, continuously) { } // Running this._status = 'R'; - this._log.d(chalk.magenta('monitor'), tick); + console.debug('monitor heartbeat', tick); // Grep system state this._systemStat(function () { // If there still has any client, grep again after `tick` ms. @@ -414,9 +369,9 @@ Monitor.prototype._nextTick = function (tick, continuously) { } // Stop delete this._status; - this._log.d(chalk.magenta('monitor'), chalk.red('destroy')); - }.bind(this)); -} + console.debug('monitor heartbeat destroyed!'); + }); +}; /** * Grep system states. @@ -427,7 +382,7 @@ Monitor.prototype._systemStat = function (cb) { stat.cpuUsage(function (err, cpu_usage) { if (err) { // Log only. - this._log.e('sockio', 'Can not load system/cpu/memory information: ' + err.message); + console.error('Can not load system/cpu/memory information: ', err.message); } else { // System states. this._sysStat = _.defaults(_(stat).pick('cpus', 'arch', 'hostname', 'platform', 'release', 'uptime', 'memory').clone(), { @@ -435,20 +390,20 @@ Monitor.prototype._systemStat = function (cb) { }); this._broadcast('system_stat', this._sysStat); } - cb(); - }.bind(this)); -} + cb.call(this); + }, this); +}; /** - * Watching PM2 + * Observe PM2 * @private */ -Monitor.prototype._startWatching = function () { - // Watching sub-emitter. - pm.sub(this.options.pm2Conf.DAEMON_PUB_PORT, function (data) { - this._log.i('sub-emitter', chalk.magenta(data.event), data.process.name + '-' + data.process.pm_id); +Monitor.prototype._observePM2 = function () { + console.info('Connecting to pm2 daemon:', pm2Daemon); + this.pm2Sock = pm.sub(this.options.pm2Conf.DAEMON_PUB_PORT, function (data) { + console.info('sub-emitter', chalk.magenta(data.event), data.process.name + '-' + data.process.pm_id); this._throttleRefresh(); - }.bind(this)); + }, this); // Enforce a refresh operation if RPC is not online. this._throttleRefresh(); @@ -467,6 +422,7 @@ Monitor.prototype._throttleRefresh = function () { ctx._refreshProcs(); }, 500, this); }; + /** * Refresh processes * @private @@ -474,7 +430,7 @@ Monitor.prototype._throttleRefresh = function () { Monitor.prototype._refreshProcs = function () { pm.list(this.options.pm2Conf.DAEMON_RPC_PORT, function (err, procs) { if (err) { - return this._broadcast('info', 'Error: ' + err.message); + return this._broadcast('info', 'Can not connect to pm2 daemon, ' + err.message); } // Wrap processes and cache them. this._procs = procs.map(function (proc) { @@ -499,7 +455,7 @@ Monitor.prototype._refreshProcs = function () { }); // Emit to client. this._broadcast('procs', this._procs); - }.bind(this)) + }, this) }; /** @@ -521,4 +477,14 @@ Monitor.prototype._pm2Ver = function (socket) { */ Monitor.prototype._broadcast = function (event, data, nsp) { this._sockio.of(nsp || conf.NSP.SYS).emit(event, data); + /* + var socks = this._sockio.of(nsp || conf.NSP.SYS).sockets; + if (!socks || socks.length == 0) { + return; + } + socks.forEach(function (sock) { + if (sock._pm_id == data.pm_id) { + sock.emit(event, data) + } + });*/ }; diff --git a/lib/pm.js b/lib/pm.js index bf3292e..04ea1d0 100644 --- a/lib/pm.js +++ b/lib/pm.js @@ -1,12 +1,12 @@ var spawn = require('child_process').spawn, - fs = require('fs'), - path = require('path'), - _ = require('lodash'), - async = require('async'), - chalk = require('chalk'), - stat = require('./stat'), - rpc = require('pm2-axon-rpc'), - axon = require('pm2-axon'); + fs = require('fs'), + path = require('path'), + _ = require('lodash'), + async = require('async'), + chalk = require('chalk'), + stat = require('./stat'), + rpc = require('pm2-axon-rpc'), + axon = require('pm2-axon'); /** * Forever lib. @@ -14,32 +14,33 @@ var spawn = require('child_process').spawn, */ var pm = module.exports = {}; -var re_blank = /^[\s\r\t]*$/, - allowedEvents = ['start', 'restart', 'exit', 'online']; +var re_blank = /^[\s\r\t]*$/, + allowedEvents = ['start', 'restart', 'exit', 'online']; /** * Subscribe event BUS. * @param {String} sockPath * @param {Function} cb + * @param {Object} context */ -pm.sub = function(sockPath, cb){ +pm.sub = function (sockPath, cb, context) { var sub = axon.socket('sub-emitter'); - sub.connect(sockPath); - // Once awake from sleeping. - sub.on('log:*', function(e, d){ + sub.on('log:*', function (e, d) { // Do not subscribe it. sub.off('log:*'); d.event = 'awake'; - cb(d); + cb.call(context, d); }); // Process events. - sub.on('process:*', function(e, d){ + sub.on('process:*', function (e, d) { if (d && !!~allowedEvents.indexOf(d.event)) { - cb(d); + cb.call(context, d); } }); + sub.connect(sockPath); + return sub; }; /** @@ -47,10 +48,10 @@ pm.sub = function(sockPath, cb){ * @param {String} sockPath * @param {Function} cb */ -pm.version = function(sockPath, cb){ +pm.version = function (sockPath, cb) { pm._rpc({ sockPath: sockPath, - events : [ + events: [ ['getVersion', {}, cb] ] }); @@ -60,16 +61,18 @@ pm.version = function(sockPath, cb){ * List available processes. * @param {String} sockPath * @param {Function} cb + * @param {Object} context */ -pm.list = function(sockPath, cb){ +pm.list = function (sockPath, cb, context) { if (!fs.existsSync(sockPath)) { - return cb(null, []); + return cb.call(context, []); } pm._rpc({ sockPath: sockPath, - events : [ + events: [ ['getMonitorData', {}, cb] - ] + ], + context: context || this }); }; @@ -77,42 +80,43 @@ pm.list = function(sockPath, cb){ * Execute remote RPC events. * @param {Object} opts including: * {String} sockPath + * {Object} context * {Object} args * {Object} events * key: event name * value: callback function * @private */ -pm._rpc = function(opts){ +pm._rpc = function (opts) { var req = axon.socket("req"), - rpc_sock = req.connect(opts.sockPath), - rpc_client = new rpc.Client(req); + rpcSock = req.connect(opts.sockPath), + rpcClient = new rpc.Client(req); // Connect RPC server. - rpc_sock.on('connect', function(){ + rpcSock.on('connect', function () { // Execute request. - var waterfalls = opts.events.map(function(event){ - return function(next){ + var waterfalls = opts.events.map(function (event) { + return function (next) { var cb = typeof event[event.length - 1] == 'function' ? event.pop() : null; if (cb) { - event.push(function(){ + event.push(function () { // Wrap arguments, no [].slice (avoid leak)!!! var args = new Array(arguments.length); for (var i = 0; i < args; i++) { args[i] = arguments[i]; } - cb.apply(null, arguments); + cb.apply(opts.context, arguments); next(); }); } - rpc_client.call.apply(rpc_client, event); + rpcClient.call.apply(rpcClient, event); if (!cb) { next(); } }; }); - async.waterfall(waterfalls, function(err, res){ - rpc_sock.close(); + async.waterfall(waterfalls, function (err, res) { + rpcSock.close(); }); }); }; @@ -124,8 +128,8 @@ pm._rpc = function(opts){ * @param {Function} cb * @private */ -pm._findById = function(sockPath, id, cb){ - pm.list(sockPath, function(err, procs){ +pm._findById = function (sockPath, id, cb) { + pm.list(sockPath, function (err, procs) { if (err) { return cb(err); } @@ -133,7 +137,7 @@ pm._findById = function(sockPath, id, cb){ return cb(new Error('No PM2 process running, the sockPath is "' + sockPath + '", please make sure it is existing!')); } - var proc = _.find(procs, function(p){ + var proc = _.find(procs, function (p) { return p && p.pm_id == id; }); @@ -151,9 +155,9 @@ pm._findById = function(sockPath, id, cb){ * @param {String} id * @param {Function} cb */ -pm.action = function(sockPath, action, id, cb){ +pm.action = function (sockPath, action, id, cb) { if (id == 'all') { - pm.list(sockPath, function(err, procs){ + pm.list(sockPath, function (err, procs) { if (err) { return cb(err); } @@ -161,12 +165,12 @@ pm.action = function(sockPath, action, id, cb){ return cb(new Error('No PM2 process running, the sockPath is "' + sockPath + '", please make sure it is existing!')); } - async.map(procs, function(proc, next){ + async.map(procs, function (proc, next) { pm._actionByPMId(sockPath, proc, action, next.bind(null, null)); }, cb); }); } else { - pm._findById(sockPath, id, function(err, proc){ + pm._findById(sockPath, id, function (err, proc) { if (err) { return cb(err); } @@ -183,30 +187,33 @@ pm.action = function(sockPath, action, id, cb){ * @param {Function} cb * @private */ -pm._actionByPMId = function(sockPath, proc, action, cb){ +pm._actionByPMId = function (sockPath, proc, action, cb) { var noBusEvent = action == 'delete' && proc.pm2_env.status != 'online', - pm_id = proc.pm_id; + pm_id = proc.pm_id; action += 'ProcessId'; - var watchEvent = ['stopWatch', action, {id: pm_id}, function(err, success){ - }]; + var watchEvent = ['stopWatch', action, { + id: pm_id + }, function (err, success) {}]; if (!!~['restart'].indexOf(action)) { watchEvent.splice(0, 1, 'restartWatch'); watchEvent.pop(); } - var actionEvent = [action, pm_id, function(err, sock){ + var actionEvent = [action, pm_id, function (err, sock) { cb(err, noBusEvent); }]; if (action == 'restartProcessId') { - actionEvent.splice(1, 1, {id: pm_id}); + actionEvent.splice(1, 1, { + id: pm_id + }); } pm._rpc({ sockPath: sockPath, - events : [ + events: [ watchEvent, actionEvent ] @@ -220,9 +227,9 @@ pm._actionByPMId = function(sockPath, proc, action, cb){ * @param {Function} cb * @returns {*} */ -pm.tail = function(opts, each, cb){ +pm.tail = function (opts, each, cb) { // Fetch the proccess that we need. - pm._findById(opts.sockPath, opts.pm_id, function(err, proc){ + pm._findById(opts.sockPath, opts.pm_id, function (err, proc) { if (err) { return cb(err); } @@ -239,8 +246,10 @@ pm.tail = function(opts, each, cb){ * @returns {*} * @private */ -pm._tailLogs = function(proc, cb){ - var logs = [['PM2', proc.pm2_log]]; +pm._tailLogs = function (proc, cb) { + var logs = [ + ['PM2', proc.pm2_log] + ]; if (proc.pm_log_path) { logs.push(['entire', proc.pm2_env.pm_log_path]); } else { @@ -252,7 +261,7 @@ pm._tailLogs = function(proc, cb){ paths.push(['err', proc.pm2_env.pm_err_log_path]); } - paths = paths.sort(function(a, b) { + paths = paths.sort(function (a, b) { return (fs.existsSync(a[1]) ? fs.statSync(a[1]).mtime.valueOf() : 0) - (fs.existsSync(b[1]) ? fs.statSync(b[1]).mtime.valueOf() : 0); }); @@ -260,7 +269,7 @@ pm._tailLogs = function(proc, cb){ } var tails = []; - (function tailLog(ls){ + (function tailLog(ls) { var log = ls.shift(); if (!log) { return; @@ -270,22 +279,23 @@ pm._tailLogs = function(proc, cb){ return; } var key = log[0], - prefix = chalk[key == 'err' ? 'red' : 'green'].bold('[' + key + ']'); + prefix = chalk[key == 'err' ? 'red' : 'green'].bold('[' + key + ']'); var tail = spawn('tail', ['-f', '-n', 10, logPath], { killSignal: 'SIGTERM', - stdio : [null, 'pipe', 'pipe'] + stdio: [null, 'pipe', 'pipe'] }); // Use utf8 encoding. - tail.stdio.forEach(function(stdio){ + tail.stdio.forEach(function (stdio) { stdio.setEncoding('utf8'); }); // stdout. - tail.stdout.on('data', function(data){ - var lines = [], _lines = data.split(/\n/); - _lines.forEach(function(line){ + tail.stdout.on('data', function (data) { + var lines = [], + _lines = data.split(/\n/); + _lines.forEach(function (line) { if (!re_blank.test(line)) { lines.push(prefix + ' ' + line); } @@ -296,7 +306,7 @@ pm._tailLogs = function(proc, cb){ }); // handle error. - tail.stderr.on('data', function(data){ + tail.stderr.on('data', function (data) { tail.disconnect(); cb(new Error(data.toString().replace(/\n/, ''))); }); @@ -304,4 +314,4 @@ pm._tailLogs = function(proc, cb){ tailLog(ls); })(logs); return tails; -}; \ No newline at end of file +}; diff --git a/lib/stat.js b/lib/stat.js index aef4453..3ff572a 100644 --- a/lib/stat.js +++ b/lib/stat.js @@ -1,4 +1,4 @@ -var os = require('os'); +var os = require('os'); /** * System states @@ -16,33 +16,33 @@ var stat = module.exports = { /** * Architecture, e.g.: 64, 32... */ - arch : os.arch(), + arch: os.arch(), /** * Ver number of system. */ - release : os.release(), + release: os.release(), /** * List all CPUs. * @returns {*} */ - get cpus(){ + get cpus() { return os.cpus(); }, /** * Uptime. * @returns {*} */ - get uptime(){ + get uptime() { return os.uptime(); }, /** * System memory usage. * @returns {{free: *, total: *, percentage: number}} */ - get memory(){ + get memory() { return { - free : os.freemem(), - total : os.totalmem(), + free: os.freemem(), + total: os.totalmem(), percentage: Math.round(100 * (1 - os.freemem() / os.totalmem())) } } @@ -51,22 +51,24 @@ var stat = module.exports = { /** * System CPU usage percentage (total). * @param fn + * @param context */ -stat.cpuUsage = function(fn){ - setTimeout(function(ctx, stat1){ +stat.cpuUsage = function (fn, context) { + setTimeout(function (ctx, stat1) { var stat2 = ctx.cpuInfo(), - perc = 100 * (1 - (stat2.idle - stat1.idle) / (stat2.total - stat1.total)); - fn(null, perc.toFixed(2)); + perc = 100 * (1 - (stat2.idle - stat1.idle) / (stat2.total - stat1.total)); + fn.call(context, perc.toFixed(2)); }, 1000, this, this.cpuInfo()); }; /** * System CPU usage detail information. - * @param fn * @returns {{idle: number, total: number}} */ -stat.cpuInfo = function(fn){ - var cpus = this.cpus, idle = 0, total = 0; +stat.cpuInfo = function () { + var cpus = this.cpus, + idle = 0, + total = 0; for (var i in cpus) { idle += cpus[i].times.idle; for (var k in cpus[i].times) { @@ -74,7 +76,7 @@ stat.cpuInfo = function(fn){ } } return { - 'idle' : idle, + 'idle': idle, 'total': total }; -}; \ No newline at end of file +}; diff --git a/lib/util/conf.js b/lib/util/conf.js index 091d7d4..b7c72c2 100644 --- a/lib/util/conf.js +++ b/lib/util/conf.js @@ -1,6 +1,20 @@ var fs = require('fs'), _ = require('lodash'); +/** + * Namespaces of socket.io + * @type {{SYS: string, LOG: string, PROC: string}} + */ +exports.NSP = { + SYS: '/sys', + LOG: '/log', + PROC: '/proc' +}; + +/** + * Configurations + * @type {[type]} + */ exports.File = File; /** @@ -54,7 +68,6 @@ File.prototype.loadSync = function () { if (re_comment.test(line)) { return; } - var ms; // Sections. if ((ms = line.match(re_setion)) && ms.length == 2) { diff --git a/lib/util/debug.js b/lib/util/debug.js deleted file mode 100644 index 7356505..0000000 --- a/lib/util/debug.js +++ /dev/null @@ -1,65 +0,0 @@ -var chalk = require('chalk'), - _ = require('lodash'); - -module.exports = Debug; - -/** - * Simple debug tool. - * @param {Object} options - * @returns {Debug} - * @constructor - */ -function Debug(options) { - if (!(this instanceof Debug)) { - return new Debug(options); - } - if (typeof options == 'string') { - options = { - namespace: options - }; - } - this.options = _.defaults(options || {}, { - namespace: 'pm2-gui', - timestamp: true, - debug: false - }); -} -Debug.prototype._l = function (level, args) { - if(!this.options.debug){ - return; - } - args = _.values(args); - - var prints = [chalk.bgBlack.grey(this.options.namespace)]; - var prefix, color; - switch (level) { - case 'e': - prefix = 'ERR!', color = 'red'; - break; - case 'w': - prefix = 'warn', color = 'yellow'; - break; - case 'd': - if(this.options.timestamp){ - prints.push(chalk.underline.dim((new Date()).toISOString())) - } - break; - default : - prefix = args.splice(0, 1), color = 'green'; - break; - } - if(prefix && color){ - prints.splice(2, 0, chalk.bgBlack[color](prefix)); - } - prints.push(args.join(' ')); - console.log.apply(null, prints); -}; - -/** - * Loggers: info, error, debug, log, warn. - */ -['i', 'e', 'd', 'l', 'w'].forEach(function(s){ - Debug.prototype[s] = function(){ - this._l.call(this, s, arguments); - }; -}); \ No newline at end of file diff --git a/lib/util/log.js b/lib/util/log.js new file mode 100644 index 0000000..1b00ec0 --- /dev/null +++ b/lib/util/log.js @@ -0,0 +1,39 @@ +var chalk = require('chalk'); + +module.exports = function (options) { + if (console.__hacked) { + return; + } + options = options || {}; + var hacks = ['debug', 'log', 'info', 'warn', 'error'], + colors = ['grey', '', 'green', 'yellow', 'red'], + consoled = {}, + lev = options.level; + + if ((typeof lev == 'string' && !(lev = hacks[lev])) || (isFinite(lev) && (lev < 0 || lev > hacks.length))) { + options.level = 0; + } + + hacks.forEach(function (method) { + if (method == 'debug') { + consoled.debug = console.log; + return; + } + consoled[method] = console[method]; + }); + + hacks.forEach(function (method, index) { + console[method] = function () { + if (index < options.level) { + return; + } + if (method != 'log' && arguments.length > 0) { + arguments[0] = (options.prefix ? chalk.bold[colors[index]]('[' + method.toUpperCase() + '] ') : '') + + (options.date ? (new Date()).toLocaleString() + ' ' : '') + arguments[0]; + } + consoled[method].apply(console, arguments); + }; + }); + + console.__hacked = true; +}; diff --git a/package.json b/package.json index 2769bf3..cd5093d 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,12 @@ "name": "pm2-gui", "version": "0.1.1", "description": "An elegant web & terminal interface for Unitech/PM2.", + "main": "./pm2-gui.js", "scripts": { "test": "NODE_ENV=test bash test/index.sh" }, "bin": { - "pm2-gui": "bin/pm2-gui" + "pm2-gui": "./pm2-gui" }, "repository": { "type": "git", @@ -34,24 +35,23 @@ "node >= 0.8.0" ], "dependencies": { - "async": "~0.9.0", - "lodash": "~2.4.1", - "commander": "~2.5.0", - "chalk": "~1.0.0", - "express": "~4.10.1", + "async": "~1.4.2", + "lodash": "~3.10.1", + "commander": "~2.9.0", + "chalk": "~1.1.0", + "express": "~4.13.3", + "express-session": "~1.12.1", "swig": "~1.4.2", - "windows-cpu": "~0.1.1", - "socket.io": "~1.2.0", + "windows-cpu": "~0.1.4", + "socket.io": "~1.3.7", "pm2-axon": "~2.0.7", "pm2-axon-rpc": "~0.3.6", - "ansi-html": "~0.0.4", - "express-session": "~1.9.3", - "pidusage": "~0.1.0", - "blessed": "^0.1.60", - "socket.io-client": "^1.3.5" - }, - "devDependencies": { + "ansi-html": "~0.0.5", + "pidusage": "~1.0.0", + "blessed": "^0.1.81", + "socket.io-client": "^1.3.7" }, + "devDependencies": {}, "readmeFilename": "README.md", "homepage": "https://github.com/Tjatse/pm2-gui" } diff --git a/pm2-gui b/pm2-gui new file mode 100755 index 0000000..8612148 --- /dev/null +++ b/pm2-gui @@ -0,0 +1,123 @@ +#!/bin/bash +node="$(which node)" +pidfile="pm2-gui.pid" +prefixINFO="\033[1;32m[INFO]\033[0m" +prefixWARN="\033[1;33m[WARNING]\033[0m" + +function isRunning() { + if [ ! -f $pidfile ]; + then + return 0; + fi + if [ ! -s $pidfile ]; + then + rm $pidfile; + return 0; + fi + if test $(ps -p $(cat $pidfile) | wc -l) -gt 1; + then + return 1; + fi + rm $pidfile; + return 0; +} + +function status() { + isRunning; + if [ 0 -eq $? ]; + then + echo -e "$prefixWARN \033[31m✘ stopped\033[0m"; + else + echo -e "$prefixINFO \033[32m✔ running\033[0m"; + fi +} + +function usage () { + cat <<-EOF + + Usage: $0 [options] + + Commands: + start [config_file] start the service + stop stop the running service + restart restart the service + mon run the curses-like dashboard in terminal + logs [log_directory] view the logs + + Examples: + $0 start + $0 start /path/to/my-pm2-gui.ini + $0 mon + $0 logs + $0 logs /path/to/logs + +EOF +} + +case "$1" in + start) + isRunning; + if [ 0 -eq $? ]; + then + echo -e "$prefixINFO Starting..." + $node ./lib/daemon "$@" + sleep 1; + fi + status; + ;; + + stop) + isRunning; + if [ 0 -eq $? ]; + then + echo -e "$prefixWARN Already stopped."; + else + echo -e "$prefixINFO Stopping..." + kill $(cat $pidfile); + sleep 1; + status; + fi + ;; + + restart) + isRunning; + if [ 0 -eq $? ]; + then + echo -e "$prefixWARN PID not found, no need to stop anything."; + $node ./lib/daemon "$@" + else + kill -SIGHUP $(cat $pidfile); + fi + sleep 1; + status; + ;; + + logs) + out="pm2-gui.out"; + err="pm2-gui.err"; + if [ ! -n "$2" ]; + then + out="./logs/$out"; + err="./logs/$err"; + else + out="$2/$out"; + err="$2/$err"; + fi + if [ ! -f $out ] && [ ! -f $err ]; + then + echo -e "$prefixWARN Logs can not be found in directory \033[1m$2\033[0m."; + else + echo -e "$prefixINFO Logs from \033[1m$out\033[0m and \033[1m$err\033[0m:"; + tail -n 20 -F $out $err; + fi + ;; + + status) + status; + ;; + + *) + usage "$0"; + exit 1 + ;; +esac \ No newline at end of file diff --git a/pm2-gui.ini b/pm2-gui.ini index 642d6cd..1a372d8 100644 --- a/pm2-gui.ini +++ b/pm2-gui.ini @@ -1,4 +1,11 @@ -refresh = 5000 +node = LocalPM2 pm2 = ~/.pm2 -debug = true -port = 8088 \ No newline at end of file +refresh = 5000 +port = 8088 +daemonize = false + +[log] +dir = ./logs +prefix = true +date = false +level = debug \ No newline at end of file diff --git a/pm2-gui.js b/pm2-gui.js new file mode 100644 index 0000000..7df1bbb --- /dev/null +++ b/pm2-gui.js @@ -0,0 +1,96 @@ +var Monitor = require('./lib/monitor'), + chalk = require('chalk'), + path = require('path'), + fs = require('fs'), + _ = require('lodash'), + Log = require('./lib/util/log'); + +exports.start = function (options) { + process.title = 'pm2-gui slave'; + options = options || {}; + var confFile = options.confFile; + if (!confFile) { + confFile = './pm2-gui.ini'; + if (process.argv.length > 2) { + confFile = process.argv[2]; + } + confFile = path.resolve(__dirname, confFile); + + if (!fs.existsSync(confFile)) { + console.error(chalk.bold(confFile), 'does not exist!'); + return process.exit(0); + } + } + var monitor = Monitor({ + confFile: confFile + }); + + Log(monitor.options.log); + + console.log(chalk.cyan( + '\n' + + '█▀▀█ █▀▄▀█ █▀█ ░░ █▀▀▀ █░░█ ░▀░\n' + + '█░░█ █░▀░█ ░▄▀ ▀▀ █░▀█ █░░█ ▀█▀\n' + + '█▀▀▀ ▀░░░▀ █▄▄ ░░ ▀▀▀▀ ░▀▀▀ ▀▀▀\n')); + monitor.run(); + + process.on('SIGTERM', shutdown); + process.on('SIGINT', shutdown); + process.on('SIGHUP', restart); + process.on('uncaughtException', caughtException); + process.on('exit', exports.exitGraceful) + + function shutdown(code, signal) { + console.info('Shutting down....'); + monitor.quit(); + console.info('Both', chalk.bold('pm2-emitter'), 'and', chalk.bold('statsd dgram'), 'sockets are closed.'); + console.info('Shutdown complete!'); + exports.exitGraceful(code, '-f'); + } + + function restart() { + if (process.send) { + process.send({ + action: 'restart' + }); + } else { + console.error('No IPC found, could not restart monitor, shutting down.'); + shutdown(1); + } + } + + function caughtException(err) { + console.error(err.stack); + shutdown(1); + } +}; + +exports.exitGraceful = function exit(code, signal) { + code = code || 0; + if (signal != '-f') { + console.debug('Slave has exited, code: ' + code + ', signal: ' + (signal || 'NULL')); + } + var fds = 0; + + function tryToExit() { + if ((fds & 1) && (fds & 2)) { + process.exit(code); + } + } + + [process.stdout, process.stderr].forEach(function (std) { + var fd = std.fd; + if (!std.bufferSize) { + fds = fds | fd; + } else { + std.write && std.write('', function () { + fds = fds | fd; + tryToExit(); + }); + } + }); + tryToExit(); +}; +if (path.basename(process.mainModule.filename, '.js') == 'pm2-ant') { + exports.start(); +} diff --git a/test/fixtures/conf.js b/test/fixtures/conf.js deleted file mode 100644 index 023ae62..0000000 --- a/test/fixtures/conf.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Overidde PM2 configuration - */ - -var p = require('path'); - -module.exports = function(DEFAULT_HOME) { - - if (!DEFAULT_HOME) - return false; - - var PM2_HOME = DEFAULT_HOME; - - var pm2_conf = { - PM2_HOME : PM2_HOME, - - PM2_LOG_FILE_PATH : p.join(PM2_HOME, 'pm2.log'), - PM2_PID_FILE_PATH : p.join(PM2_HOME, 'pm2.pid'), - - DEFAULT_PID_PATH : p.join(PM2_HOME, 'pids'), - DEFAULT_LOG_PATH : p.join(PM2_HOME, 'logs'), - DUMP_FILE_PATH : p.join(PM2_HOME, 'dump.pm2'), - - DAEMON_RPC_PORT : p.join(PM2_HOME, 'rpc.sock'), - DAEMON_PUB_PORT : p.join(PM2_HOME, 'pub.sock'), - INTERACTOR_RPC_PORT : p.join(PM2_HOME, 'interactor.sock'), - - GRACEFUL_TIMEOUT : parseInt(process.env.PM2_GRACEFUL_TIMEOUT) || 8000, - GRACEFUL_LISTEN_TIMEOUT : parseInt(process.env.PM2_GRACEFUL_LISTEN_TIMEOUT) || 4000, - - DEBUG : process.env.PM2_DEBUG || false, - WEB_INTERFACE : parseInt(process.env.PM2_API_PORT) || 9615, - MODIFY_REQUIRE : process.env.PM2_MODIFY_REQUIRE || false, - - PM2_LOG_DATE_FORMAT : process.env.PM2_LOG_DATE_FORMAT !== undefined ? process.env.PM2_LOG_DATE_FORMAT : 'YYYY-MM-DD HH:mm:ss', - - INTERACTOR_LOG_FILE_PATH : p.join(PM2_HOME, 'agent.log'), - INTERACTOR_PID_PATH : p.join(PM2_HOME, 'agent.pid'), - INTERACTION_CONF : p.join(PM2_HOME, 'agent.json5') - }; - - return pm2_conf || null; -}; diff --git a/test/fixtures/startup.json b/test/process.json similarity index 100% rename from test/fixtures/startup.json rename to test/process.json diff --git a/web/index.js b/web/index.js index bfaf571..cbaa22b 100644 --- a/web/index.js +++ b/web/index.js @@ -2,7 +2,7 @@ var express = require('express'), swig = require('swig'), path = require('path'), chalk = require('chalk'), - Monitor = require('../lib/mon'), + Monitor = require('../lib/monitor'), Debug = require('../lib/util/debug'), session = require('express-session');