diff --git a/lib/blessed-widget/layout.js b/lib/blessed-widget/layout.js index 8253644..21a7746 100644 --- a/lib/blessed-widget/layout.js +++ b/lib/blessed-widget/layout.js @@ -1,14 +1,19 @@ +'use strict' + var blessed = require('blessed') var chalk = require('chalk') var async = require('async') var _ = require('lodash') + var widgets = require('./widgets') var conf = require('../util/conf') var Log = require('../util/log') +var ignoredENVKeys = ['LS_COLORS'] +var regJSON = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g + module.exports = Layout -var exiting = false /** * Create layout. * @param {Object} options @@ -17,6 +22,7 @@ function Layout (options) { if (!(this instanceof Layout)) { return new Layout(options) } + // initialize options options = _.clone(options || {}) if (!options.hostname) { options.hostname = '127.0.0.1' @@ -24,14 +30,20 @@ function Layout (options) { if (!options.port) { throw new Error('Port of socket.io server is required!') } - options.sockets = options.sockets || {} - this.options = options this._eles = {} - this._procCount = 0 + this._data = { + processCount: -1, + sockets: options.sockets || {} + } + delete options.sockets + this.options = options + Object.freeze(this.options) } /** * Render GUI. + * @param {Monitor} monitor + * @return {N/A} */ Layout.prototype.render = function (monitor) { var self = this @@ -39,22 +51,19 @@ Layout.prototype.render = function (monitor) { // Preparing all socket.io clients. async.series(Object.keys(conf.NSP).map(function (ns) { - return function (callback) { - var callbackOnce = _.once(callback) + return function (next) { var nsl = ns.toLowerCase() - if (options.sockets[nsl]) { - return callbackOnce() + if (self._data.sockets[nsl]) { + return next() } - + // connect to monitor monitor.connect(_.extend({ namespace: conf.NSP[ns] - }, options), function (socket) { - console.info('Connected to', socket.nsp) - callbackOnce(null, socket) - }, function (err, socket) { + }, options), function (err, socket) { if (err) { - return callbackOnce(new Error('Failed to connect to [' + ns + '] due to ' + err.message)) + return next(new Error('Fatal to connect to ' + socket.nsp + ' due to ' + err)) } + next(null, socket) }) } }), function (err, res) { @@ -62,6 +71,7 @@ Layout.prototype.render = function (monitor) { console.error(err.message) return process.exit(0) } + // muted logger. Log({ level: 1000 }) @@ -69,14 +79,15 @@ Layout.prototype.render = function (monitor) { res.forEach(function (socket) { connectedSockets[socket.nsp.replace(/^\/+/g, '')] = socket }) - self.sockets = _.extend(connectedSockets, options.sockets) - delete options.sockets + // cache sockets. + _.extend(self._data.sockets, connectedSockets) + // render layout. self._observe() self._draw() - + // refresh processes every 1s setInterval(function () { - self._bindProcesses() + self._processesTable() }, 1000) }) } @@ -87,108 +98,121 @@ Layout.prototype.render = function (monitor) { Layout.prototype._observe = function () { var self = this console.info('Listening socket events...') - var socketSys = this._socket(conf.NSP.SYS) - socketSys.on('procs', function (procs) { - self._procs = { - data: procs, - tick: Date.now() - } - if (typeof self._procs === 'undefined') { - self._bindProcesses() - } - }) - socketSys.emit('procs') + // watch processes + this._socket(conf.NSP.PROCESS) + .on(conf.SOCKET_EVENTS.DATA_PROCESSES, function (procs) { + self._data.processes = { + data: procs, + tick: Date.now() + } + self._processesTable() + }) + .emit(conf.SOCKET_EVENTS.PULL_PROCESSES) + .on(conf.SOCKET_EVENTS.DATA_USAGE, function (proc) { + if (!self._data.usages || proc.pid !== self._data.usages.pid || self._data.usages.time === proc.time) { + return + } + self._data.usages.time = proc.time + self._data.usages.cpu.shift() + self._data.usages.cpu.push(Math.min(100, Math.max(proc.usage.cpu, 1))) + self._data.usages.mem.shift() + self._data.usages.mem.push(Math.min(100, Math.max(proc.usage.memory, 1))) + }) - this._socket(conf.NSP.PROC).on('proc', function (proc) { - if (!self._usages || proc.pid !== self._usages.pid || self._usages.time === proc.time) { + // subscribe logs + this._socket(conf.NSP.LOG).on(conf.SOCKET_EVENTS.DATA, function (log) { + if (!self._eles.logs || self._data.lastLogPMId !== log.id) { return } - self._usages.time = proc.time - self._usages.cpu.shift() - self._usages.cpu.push(Math.min(100, Math.max(proc.usage.cpu, 1))) - self._usages.mem.shift() - self._usages.mem.push(Math.min(100, Math.max(proc.usage.memory, 1))) - }) - - this._socket(conf.NSP.LOG).on('log', function (log) { - if (!self._eles.logs || self._lastLogPMId !== log.pm_id) { - return - } - self._eles.logs.log(log.msg) + self._eles.logs.log(log.text) }) } /** - * Bind processes to table. + * Render processes in a datatable + * @return {N/A} */ -Layout.prototype._bindProcesses = function () { - if (exiting || !this._eles.processes || !this._procs) { +Layout.prototype._processesTable = function () { + if (this._data.exiting || !this._eles.processes || !this._data.processes) { return } - if (this._procs.tick === this._procsLastTick) { + if (this._data.processes.tick === this._data.processesLastTick) { // Update tick only. - return setRows.call(this, true) + return this._processesTableRows(true) } - if (typeof this._procsLastTick === 'undefined') { + if (_.isUndefined(this._data.processesLastTick)) { + // show first process informations. this._describeInfo(0) - this._eles.processes.rows.on('select', onSelect.bind(this)) + // bind `select` event on datatable. + this._eles.processes.rows.on('select', this._onProcessesTableSelect.bind(this)) } + // cache last tick + this._data.processesLastTick = this._data.processes.tick + // render rows of datatable + this._processesTableRows(true) +} - this._procsLastTick = this._procs.tick +/** + * Render processes datatable rows + * @param {Boolean} forceRefresh + * @return {N/A} + */ +Layout.prototype._processesTableRows = function (forceRefresh) { + var rows = [] + var selectedIndex = this._eles.processes.rows.selected + var len = this._data.processes.data.length - setRows.call(this, true) + this._data.processes.data.forEach(function (p, i) { + var pm2 = p.pm2_env + var index = '[' + (i + 1) + '/' + len + ']' + rows.push([ + ' ' + chalk.grey((index + Array(8 - index.length).join(' '))) + ' ' + p.name, + pm2.restart_time, + pm2.status !== 'online' ? '0s' : _fromNow(Math.ceil((Date.now() - pm2.pm_uptime) / 1000), true), + pm2.status === 'online' ? chalk.green('✔') : chalk.red('✘') + ]) + }) + this._eles.processes.setData({ + headers: [' Name', 'Restarts', 'Uptime', ''], + rows: rows + }) - function setRows (forceRefresh) { - var rows = [] - var selectedIndex = this._eles.processes.rows.selected - var len = this._procs.data.length + selectedIndex = !_.isUndefined(selectedIndex) ? selectedIndex : 0 + var maxIndex = this._eles.processes.rows.items.length - 1 + if (selectedIndex > maxIndex) { + selectedIndex = maxIndex + } + this._eles.processes.rows.select(selectedIndex) - this._procs.data.forEach(function (p, i) { - var pm2 = p.pm2_env - var index = '[' + i + '/' + len + ']' - rows.push([ - ' ' + chalk.grey((index + Array(8 - index.length).join(' '))) + ' ' + p.name, - pm2.restart_time, - pm2.status !== 'online' ? '0s' : _fromNow(Math.ceil((Date.now() - pm2.pm_uptime) / 1000), true), - pm2.status === 'online' ? chalk.green('✔') : chalk.red('✘') - ]) - }) - this._eles.processes.setData({ - headers: [' Name', 'Restarts', 'Uptime', ''], - rows: rows - }) + if (forceRefresh) { + this._onProcessesTableSelect() + } +} - selectedIndex = typeof selectedIndex !== 'undefined' ? selectedIndex : 0 - var maxIndex = this._eles.processes.rows.items.length - 1 - if (selectedIndex > maxIndex) { - selectedIndex = maxIndex - } - this._eles.processes.rows.select(selectedIndex) - - if (forceRefresh) { - onSelect.call(this) +/** + * Listening select event on processes datatable. + * @param {Object} item + * @param {Number} selectedIndex + * @return {N/A} + */ +Layout.prototype._onProcessesTableSelect = function (item, selectedIndex) { + if (!!item) { // eslint-disable-line no-extra-boolean-cast + var lastIndex = this._data.lastSelectedIndex + this._data.lastSelectedIndex = selectedIndex + if (selectedIndex !== lastIndex) { + this._describeInfo(selectedIndex) } } - - function onSelect (item, selectedIndex) { - if (!!item) { // eslint-disable-line no-extra-boolean-cast - var lastIndex = this._lastSelectedIndex - - this._lastSelectedIndex = selectedIndex - if (selectedIndex !== lastIndex) { - this._describeInfo(selectedIndex) - } - } - this._cpuAndMemUsage(this._lastSelectedIndex || 0) - this._displayLogs(this._lastSelectedIndex || 0) - this.screen.render() - } + this._cpuAndMemUsage(this._data.lastSelectedIndex || 0) + this._displayLogs(this._data.lastSelectedIndex || 0) + this._eles.screen.render() } /** * Get description of a specified process. * @param {Number} index the selected row index. + * @return {N/A} */ Layout.prototype._describeInfo = function (index) { var pm2 = this._dataOf(index) @@ -199,7 +223,9 @@ Layout.prototype._describeInfo = function (index) { } if (pm2.pm2_env && pm2.pm2_env.env) { // Remove useless large-bytes attributes. - delete pm2.pm2_env.env['LS_COLORS'] + ignoredENVKeys.forEach(function (envKey) { + delete pm2.pm2_env.env[envKey] + }) } delete pm2.monit this._eles.json.setContent(_formatJSON(pm2)) @@ -208,60 +234,63 @@ Layout.prototype._describeInfo = function (index) { /** * CPU and Memory usage of a specific process * @param {Number} index the selected row index. + * @return {N/A} */ Layout.prototype._cpuAndMemUsage = function (index) { var pm2 = this._dataOf(index) if (!pm2) { return } - if (!this._usages) { - this._usages = { + if (!this._data.usages) { + this._data.usages = { mem: [], cpu: [] } var len = this._eles.cpu.width - 4 for (var i = 0; i < len; i++) { - this._usages.cpu.push(1) - this._usages.mem.push(1) + this._data.usages.cpu.push(1) + this._data.usages.mem.push(1) } } - if (pm2.pid !== 0 && this._procCount === 2) { - this._procCount = -1 - this._socket(conf.NSP.PROC).emit('proc', pm2.pid) + // fetch process info every 3 times + if (pm2.pid !== 0 && this._data.processCount === 2) { + this._data.processCount = -1 + this._socket(conf.NSP.PROCESS).emit(conf.SOCKET_EVENTS.PULL_USAGE, pm2.pid) } - this._procCount++ - this._usages.pid = pm2.pid + this._data.processCount++ + this._data.usages.pid = pm2.pid - this._eles.cpu.setData(this._usages.cpu, 0, 100) - this._eles.cpu.setLabel('CPU Usage (' + (this._usages.cpu[this._usages.cpu.length - 1]).toFixed(2) + '%)') + this._eles.cpu.setData(this._data.usages.cpu, 0, 100) + this._eles.cpu.setLabel('CPU Usage (' + (this._data.usages.cpu[this._data.usages.cpu.length - 1]).toFixed(2) + '%)') - this._eles.mem.setData(this._usages.mem, 0, 100) - this._eles.mem.setLabel('Memory Usage (' + (this._usages.mem[this._usages.mem.length - 1]).toFixed(2) + '%)') + this._eles.mem.setData(this._data.usages.mem, 0, 100) + this._eles.mem.setLabel('Memory Usage (' + (this._data.usages.mem[this._data.usages.mem.length - 1]).toFixed(2) + '%)') } /** * Display logs. - * @param {Number} index [description] - * @return {[type]} [description] + * @param {Number} index + * @return {N/A} */ Layout.prototype._displayLogs = function (index) { var pm2 = this._dataOf(index) - if (!pm2 || this._lastLogPMId === pm2.pm_id) { + if (!pm2 || this._data.lastLogPMId === pm2.pm_id) { return } - this._killLogs() - this._socket(conf.NSP.LOG).emit('tail', this._lastLogPMId = pm2.pm_id, true) + this._stopLogging() + this._socket(conf.NSP.LOG).emit(conf.SOCKET_EVENTS.PULL_LOGS, pm2.pm_id, true) + this._data.lastLogPMId = pm2.pm_id } /** - * Kill `tail` process - * @return {[type]} [description] + * Stop logging. + * @return {N/A} */ -Layout.prototype._killLogs = function () { - if (typeof this._lastLogPMId === 'undefined') { +Layout.prototype._stopLogging = function () { + if (_.isUndefined(this._data.lastLogPMId)) { return } - this._socket(conf.NSP.LOG).emit('tail_kill', this._lastLogPMId) + this._socket(conf.NSP.LOG).emit(conf.SOCKET_EVENTS.PULL_LOGS_END, this._data.lastLogPMId) } /** @@ -270,14 +299,15 @@ Layout.prototype._killLogs = function () { * @return {Object} */ Layout.prototype._dataOf = function (index) { - if (!this._procs || !Array.isArray(this._procs.data) || index >= this._procs.data.length) { + if (!this._data.processes || !Array.isArray(this._data.processes.data) || index >= this._data.processes.data.length) { return null } - return this._procs.data[index] + return this._data.processes.data[index] } /** * Draw elements. + * @return {N/A} */ Layout.prototype._draw = function () { console.info('Rendering dashboard...') @@ -289,21 +319,20 @@ Layout.prototype._draw = function () { // Processes. this._eles.processes = grid.get(0, 0) - this._bindProcesses() + this._processesTable() - this._eles.cpu = grid.get(1, 0) - this._eles.mem = grid.get(1, 1) + _.extend(this._eles, { + cpu: grid.get(1, 0), + mem: grid.get(1, 1), + logs: grid.get(2, 0), + json: grid.get(0, 2) + }) - // Logs. - this._eles.logs = grid.get(2, 0) - - // Detail. - this._eles.json = grid.get(0, 2) var offset = Math.round(this._eles.json.height * 100 / this._eles.json.getScrollHeight()) var dir // Key bindings screen.key('s', function (ch, key) { - if (exiting) { + if (self._data.exiting) { return } var perc = Math.min((dir !== 'down' ? offset : 0) + self._eles.json.getScrollPerc() + 5, 100) @@ -311,20 +340,19 @@ Layout.prototype._draw = function () { self._eles.json.setScrollPerc(perc) }) screen.key('w', function (ch, key) { - if (exiting) { + if (self._data.exiting) { return } var perc = Math.max(self._eles.json.getScrollPerc() - 5 - (dir !== 'up' ? offset : 0), 0) dir = 'up' self._eles.json.setScrollPerc(perc) }) - screen.key(['escape', 'q', 'C-c'], function (ch, key) { - if (exiting) { + if (self._data.exiting) { return } - exiting = true - this._killLogs() + self._data.exiting = true + this._stopLogging() screen.title = 'PM2 Monitor (Exiting...)' screen.destroy() screen.title = '' @@ -337,16 +365,17 @@ Layout.prototype._draw = function () { }.bind(this)) screen.render() - this.screen = screen + this._eles.screen = screen } /** * Get socket.io object by namespace * @param {String} ns + * @return {socket.io} */ Layout.prototype._socket = function (ns) { - if (ns && this.sockets) { - return this.sockets[(ns || '').replace(/^\/+/g, '').toLowerCase()] + if (ns && this._data.sockets) { + return this._data.sockets[(ns || '').replace(/^\/+/g, '').toLowerCase()] } return null } @@ -480,9 +509,9 @@ function _grid (screen) { * @private */ function _formatJSON (data) { - data = JSON.stringify(typeof data !== 'string' ? data : JSON.parse(data), null, 2) + data = JSON.stringify(!_.isString(data) ? data : JSON.parse(data), null, 2) - return data.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (m) { + return data.replace(regJSON, function (m) { var color = 'blue' if (/^"/.test(m)) { color = ['magenta', 'green'][/:$/.test(m) ? 0 : 1] diff --git a/lib/blessed-widget/widgets.js b/lib/blessed-widget/widgets.js index 23f3935..3a75bac 100644 --- a/lib/blessed-widget/widgets.js +++ b/lib/blessed-widget/widgets.js @@ -1,9 +1,13 @@ +'use strict' + // Inspired by the blessed-contrib, but more powerful and free. // (c) Tjatse var blessed = require('blessed') +var _ = require('lodash') var util = require('util') -var re_stripANSI = /(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/g + +var regStripANSI = /(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/g exports.Grid = Grid exports.Table = Table @@ -14,13 +18,12 @@ exports.Log = Log * Grid cells. * @param {Object} options * @returns {Grid} - * @constructor */ function Grid (options) { if (!(this instanceof Grid)) { return new Grid(options) } - options = util._extend({ + options = _.extend({ margin: 2 }, options || {}) @@ -55,7 +58,7 @@ Grid.prototype.set = function (ele) { } return } - this.grids[ele.row][ele.col] = util._extend({rowSpan: 1, colSpan: 1}, ele) + this.grids[ele.row][ele.col] = _.extend({rowSpan: 1, colSpan: 1}, ele) } /** * Draw grid. @@ -109,7 +112,7 @@ Grid.prototype.draw = function (screen, rect) { left: left }) } else { - screen.append(ele.instance = ele.element(util._extend(ele.options || {}, { + screen.append(ele.instance = ele.element(_.extend(ele.options || {}, { top: top + '%', left: left + '%', width: width + '%', @@ -136,7 +139,7 @@ function Table (options) { blessed.Box.call(this, this.options) - this.rows = blessed.list(util._extend(this.options.rows || {}, { + this.rows = blessed.list(_.extend(this.options.rows || {}, { height: 0, top: 1, width: 0, @@ -175,7 +178,7 @@ Table.prototype.setData = function (data) { var dataToString = function (d) { return d.map(function (s, i) { s = s.toString() - var s1 = s.replace(re_stripANSI, '') + var s1 = s.replace(regStripANSI, '') var size = !def ? widths : widths[i] var len = size - s1.length @@ -208,7 +211,7 @@ function Sparkline (options) { return new Sparkline(options) } - this.options = util._extend({ + this.options = _.extend({ chars: ['▂', '▃', '▄', '▅', '▆', '▇', '█'], tags: true, padding: {