pm2-gui-fr
Version:
Une interface web et terminal élégante pour Unitech / PM2.
548 lines (508 loc) • 13.2 kB
JavaScript
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
/**
* Create layout.
* @param {Object} options
*/
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'
}
if (!options.port) {
throw new Error('Port of socket.io server is required!')
}
this._eles = {}
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
var options = this.options
// Preparing all socket.io clients.
async.series(Object.keys(conf.NSP).map(function (ns) {
return function (next) {
var nsl = ns.toLowerCase()
if (self._data.sockets[nsl]) {
return next()
}
// connect to monitor
monitor.connect(_.extend({
namespace: conf.NSP[ns]
}, options), function (err, socket) {
if (err) {
return next(new Error('Fatal to connect to ' + socket.nsp + ' due to ' + err))
}
next(null, socket)
})
}
}), function (err, res) {
if (err) {
console.error(err.message)
return process.exit(0)
}
// muted logger.
Log({
level: 1000
})
var connectedSockets = {}
res.forEach(function (socket) {
connectedSockets[socket.nsp.replace(/^\/+/g, '')] = socket
})
// cache sockets.
_.extend(self._data.sockets, connectedSockets)
// render layout.
self._observe()
self._draw()
// refresh processes every 1s
setInterval(function () {
self._processesTable()
}, 1000)
})
}
/**
* Observe socket.io events.
*/
Layout.prototype._observe = function () {
var self = this
console.info('Listening socket events...')
// 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)))
})
// 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._eles.logs.log(log.text)
})
}
/**
* Render processes in a datatable
* @return {N/A}
*/
Layout.prototype._processesTable = function () {
if (this._data.exiting || !this._eles.processes || !this._data.processes) {
return
}
if (this._data.processes.tick === this._data.processesLastTick) {
// Update tick only.
return this._processesTableRows(true)
}
if (_.isUndefined(this._data.processesLastTick)) {
// show first process informations.
this._describeInfo(0)
// 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)
}
/**
* 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
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
})
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)
if (forceRefresh) {
this._onProcessesTableSelect()
}
}
/**
* 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)
}
}
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)
if (!pm2) {
return this._eles.json.setContent(_formatJSON({
message: 'There is no process running!'
}))
}
if (pm2.pm2_env && pm2.pm2_env.env) {
// Remove useless large-bytes attributes.
ignoredENVKeys.forEach(function (envKey) {
delete pm2.pm2_env.env[envKey]
})
}
delete pm2.monit
this._eles.json.setContent(_formatJSON(pm2))
}
/**
* 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._data.usages) {
this._data.usages = {
mem: [],
cpu: []
}
var len = this._eles.cpu.width - 4
for (var i = 0; i < len; i++) {
this._data.usages.cpu.push(1)
this._data.usages.mem.push(1)
}
}
// 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._data.processCount++
this._data.usages.pid = pm2.pid
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._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
* @return {N/A}
*/
Layout.prototype._displayLogs = function (index) {
var pm2 = this._dataOf(index)
if (!pm2 || this._data.lastLogPMId === pm2.pm_id) {
return
}
this._stopLogging()
this._socket(conf.NSP.LOG).emit(conf.SOCKET_EVENTS.PULL_LOGS, pm2.pm_id, true)
this._data.lastLogPMId = pm2.pm_id
}
/**
* Stop logging.
* @return {N/A}
*/
Layout.prototype._stopLogging = function () {
if (_.isUndefined(this._data.lastLogPMId)) {
return
}
this._socket(conf.NSP.LOG).emit(conf.SOCKET_EVENTS.PULL_LOGS_END, this._data.lastLogPMId)
}
/**
* Get data by index.
* @param {Number} index
* @return {Object}
*/
Layout.prototype._dataOf = function (index) {
if (!this._data.processes || !Array.isArray(this._data.processes.data) || index >= this._data.processes.data.length) {
return null
}
return this._data.processes.data[index]
}
/**
* Draw elements.
* @return {N/A}
*/
Layout.prototype._draw = function () {
console.info('Rendering dashboard...')
var self = this
var screen = blessed.Screen()
screen.title = 'PM2 Monitor'
var grid = _grid(screen)
// Processes.
this._eles.processes = grid.get(0, 0)
this._processesTable()
_.extend(this._eles, {
cpu: grid.get(1, 0),
mem: grid.get(1, 1),
logs: grid.get(2, 0),
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 (self._data.exiting) {
return
}
var perc = Math.min((dir !== 'down' ? offset : 0) + self._eles.json.getScrollPerc() + 5, 100)
dir = 'down'
self._eles.json.setScrollPerc(perc)
})
screen.key('w', function (ch, key) {
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 (self._data.exiting) {
return
}
self._data.exiting = true
this._stopLogging()
screen.title = 'PM2 Monitor (Exiting...)'
screen.destroy()
screen.title = ''
screen.cursorReset()
setTimeout(function () {
// clear screen.
// process.stdout.write('\u001B[2J\u001B[0;0f')
process.exit(0)
}, 1000)
}.bind(this))
screen.render()
this._eles.screen = screen
}
/**
* Get socket.io object by namespace
* @param {String} ns
* @return {socket.io}
*/
Layout.prototype._socket = function (ns) {
if (ns && this._data.sockets) {
return this._data.sockets[(ns || '').replace(/^\/+/g, '').toLowerCase()]
}
return null
}
/**
* Grid of screen elements.
* @param {blessed.Screen} screen
* @returns {*}
* @private
*/
function _grid (screen) {
var style = {
fg: '#013409',
label: {
bold: true,
fg: '#00500d'
},
border: {
fg: '#5e9166'
}
}
// Layout.
var grid = widgets.Grid({
rows: 3,
cols: 3,
margin: 0,
widths: [25, 25, 50],
heights: [35, 10, 55]
})
// Table of processes
grid.set({
row: 0,
col: 0,
colSpan: 2,
element: widgets.Table,
options: {
keys: true,
border: {
type: 'line'
},
style: style,
label: 'Processes (↑/↓ to move up/down, enter to select)',
widths: [35, 15, 20, 15]
}
})
// Sparkline of CPU
grid.set({
row: 1,
col: 0,
element: widgets.Sparkline,
options: {
border: {
type: 'line'
},
style: {
fg: '#bc6f0a',
label: {
bold: true,
fg: '#00500d'
},
border: {
fg: '#5e9166'
}
},
label: 'CPU Usage(%)'
}
})
// Sparkline of Memory
grid.set({
row: 1,
col: 1,
element: widgets.Sparkline,
options: {
border: {
type: 'line'
},
style: {
fg: '#6a00bb',
label: {
bold: true,
fg: '#00500d'
},
border: {
fg: '#5e9166'
}
},
label: 'Memory Usage(%)'
}
})
// Logs
grid.set({
row: 2,
col: 0,
colSpan: 2,
element: widgets.Log,
options: {
border: {
type: 'line'
},
style: style,
label: 'Logs'
}
})
// JSON data.
grid.set({
row: 0,
col: 2,
rowSpan: 3,
element: blessed.ScrollableBox,
options: {
label: 'Describe Info (w/s to move up/down)',
border: {
type: 'line'
},
style: style,
keys: true
}
})
grid.draw(screen)
return grid
}
/**
* Pretty json data.
* @param {Object} data
* @returns {XML|*|string|void}
* @private
*/
function _formatJSON (data) {
data = JSON.stringify(!_.isString(data) ? data : JSON.parse(data), null, 2)
return data.replace(regJSON, function (m) {
var color = 'blue'
if (/^"/.test(m)) {
color = ['magenta', 'green'][/:$/.test(m) ? 0 : 1]
} else if (/true|false/.test(m)) {
color = 'blue'
} else if (/null|undefined/.test(m)) {
color = 'blue'
}
return chalk[color](m)
})
}
/**
* Wrap tick from now.
* @param {Float} tick
* @param {Boolean} tiny show all of it.
* @returns {string}
*/
function _fromNow (tick, tiny) {
if (tick < 60) {
return tick + 's'
}
var s = tick % 60 + 's'
if (tick < 3600) {
return parseInt(tick / 60) + 'm ' + s
}
var m = parseInt((tick % 3600) / 60) + 'm '
if (tick < 86400) {
return parseInt(tick / 3600) + 'h ' + m + (!tiny ? '' : s)
}
var h = parseInt((tick % 86400) / 3600) + 'h '
return parseInt(tick / 86400) + 'd ' + h + (!tiny ? '' : m + s)
}