remoting web

This commit is contained in:
Tjatse 2015-12-28 22:51:32 +08:00
parent 60cfdbf178
commit 5ef85f2e0b
9 changed files with 7273 additions and 528 deletions

View File

@ -20,7 +20,8 @@ An elegant web & terminal interface for Unitech/PM2.
<a name="feats" />
# Features
- Curses-like dashboard
- Curses-like dashboard.
- Remoting monitor / web control.
- All the heartbeats (no matter **monitor** or **tail (logs)**) are automatic destroyed.
- The `PM2` processes are watched by a subscribed emitter.
- Communicated with `PM2` through **RPC** socket directly.
@ -28,10 +29,10 @@ An elegant web & terminal interface for Unitech/PM2.
- Monitor CPU and Memory usage of server in a real-time.
- Monitor `PM2` processes in a real-time.
- PM2 *restart/stop/delete*.
- *stopWatch* files before *restart/stop/delete*
- *restartWatch* files before *restart*
- *stopWatch* files before *restart/stop/delete*.
- *restartWatch* files before *restart*.
- Supports [ANSI color codes](#ss_logs) by [ansi-html](https://github.com/Tjatse/ansi-html).
- High performance. In my case, there are near one hundred processes, but `pm2-gui` works fine.
- High performance. In my case, there are near one hundred processes, but `pm2-gui` works without any suck.
<a name="cauts" />
# Cautions

View File

@ -29,10 +29,6 @@ function Layout(options) {
this.options = options;
this._eles = {};
this._procCount = 0;
/*
Log({
level: 1000
});*/
};
/**
@ -54,13 +50,22 @@ Layout.prototype.render = function (monitor) {
namespace: conf.NSP[ns]
}, options);
monitor.connect(opts, function (err, socket) {
if (err) {
console.error('Failed due to', err.message, 'when connecting to', socket.nsp);
monitor.connect(opts, function (socket) {
console.info('Connected to', socket.nsp);
!next._called && next(null, socket);
next._called = true;
}, function (err, socket) {
console.log(err);
if (!next._called) {
next(err, socket);
next._called = true;
} else {
console.info('Connected to', socket.nsp);
//Log(options.log);
}
console.error('Failed due to', err.message, 'when connecting to', socket.nsp);
if (next._called) {
process.exit(0);
}
next(err, socket);
});
}
});
@ -69,6 +74,9 @@ Layout.prototype.render = function (monitor) {
if (err) {
return process.exit(0);
}
Log({
level: 1000
});
self.sockets = _.extend(res, options.sockets);
delete options.sockets;

View File

@ -5,6 +5,7 @@ var fs = require('fs'),
ansiHTML = require('ansi-html'),
totalmem = require('os').totalmem(),
pidusage = require('pidusage'),
url = require('url'),
socketIOClient = require('socket.io-client'),
pm = require('./pm'),
stat = require('./stat'),
@ -33,25 +34,6 @@ Monitor.ACCEPT_KEYS = ['pm2', 'refresh', 'daemonize', 'max_restarts', 'port', 'l
Monitor.DEF_CONF_FILE = 'pm2-gui.ini';
Monitor.PM2_DAEMON_PROPS = ['DAEMON_RPC_PORT', 'DAEMON_PUB_PORT', 'PM2_LOG_FILE_PATH'];
/**
* Resolve home path.
* @param {String} pm2Home
* @returns {*}
* @private
*/
Monitor.prototype._resolveHome = function (pm2Home) {
if (pm2Home && pm2Home.indexOf('~/') == 0) {
// Get root directory of PM2.
pm2Home = process.env.PM2_HOME || path.resolve(process.env.HOME || process.env.HOMEPATH, pm2Home.substr(2));
// 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 environment variable vi `export PM2_HOME=[ROOT]`.');
}
}
return pm2Home;
}
/**
* Run socket.io server.
*/
@ -88,25 +70,47 @@ Monitor.prototype.quit = function () {
/**
* Connect to socket.io server.
* @param {String} ns the namespace.
* @param {Function} callback
* @param {Function} success
* @param {Function} failure
*/
Monitor.prototype.connect = function (options, callback) {
if (!options.port || !options.namespace) {
throw new Error('Port and namespace are both required!');
Monitor.prototype.connect = function (options, success, failure) {
if (!options.port) {
throw new Error('Port is required!');
}
var serverUri = (options.protocol || 'http:') + '//' + (options.hostname || '127.0.0.1') + ':' + options.port + (options.path || '') + options.namespace;
var auth,
serverUri = Monitor.toConnectionString(options);
console.info('Connecting to', serverUri);
var socket = socketIOClient(serverUri);
socket.on('connect', function () {
!callback.__called && callback(null, socket);
callback.__called = true;
!success._called && success(socket);
success._called = true;
});
socket.on('connect_error', function (err) {
!callback.__called && callback(err, socket);
callback.__called = true;
socket.on('error', function (err) {
!failure._called && failure(err, socket);
failure._called = true;
});
};
/**
* Resolve home path.
* @param {String} pm2Home
* @returns {*}
* @private
*/
Monitor.prototype._resolveHome = function (pm2Home) {
if (pm2Home && pm2Home.indexOf('~/') == 0) {
// Get root directory of PM2.
pm2Home = process.env.PM2_HOME || path.resolve(process.env.HOME || process.env.HOMEPATH, pm2Home.substr(2));
// 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 environment variable vi `export PM2_HOME=[ROOT]`.');
}
}
return pm2Home;
};
/**
* Initialize options and configurations.
@ -177,13 +181,13 @@ Monitor.prototype._connectSysSock = function (socket) {
// Trigger actions of process.
socket.on('action', function (action, id) {
console.info('[' + id + ']', action, 'sending to pm2 daemon...');
console.debug('[pm2:' + id + ']', action, 'sending to pm2 daemon...');
pm.action(self.options.pm2Conf.DAEMON_RPC_PORT, action, id, function (err, forceRefresh) {
if (err) {
console.error(action, err.message);
return socket.emit('action', id, err.message);
}
console.info('[' + id + ']', action, 'completed!');
console.debug('[pm2:' + id + ']', action, 'completed!');
forceRefresh && self._throttleRefresh();
});
});
@ -252,7 +256,7 @@ Monitor.prototype._connectLogSock = function (socket) {
return emitError(err, pm_id, keepANSI);
}
console.info('[' + pm_id + ']', 'tail starting...');
console.info('[pm2:' + pm_id + ']', 'tail starting...');
self._tails[pm_id] = tails;
});
}
@ -282,18 +286,19 @@ Monitor.prototype._connectProcSock = function (socket) {
function killObserver() {
var socks = self._sockio.of(conf.NSP.PROC).sockets,
canNotBeDeleted = {};
if (socks && socks.length > 0) {
if (Array.isArray(socks) && socks.length > 0) {
socks.forEach(function (sock) {
canNotBeDeleted[sock.pid.toString()] = 1;
});
}
for (var pid in this._usages) {
for (var pid in self._usages) {
var timer;
if (!canNotBeDeleted[pid] && (timer = this._usages[pid])) {
if (!canNotBeDeleted[pid] && (timer = self._usages[pid])) {
clearInterval(timer);
delete this._usages[pid];
console.info('[' + pid + ']', 'cpu and memory observer destroyed!');
delete self._usages[pid];
console.debug('[pid:' + pid + ']', 'cpu and memory observer destroyed!');
}
}
}
@ -306,7 +311,7 @@ Monitor.prototype._connectProcSock = function (socket) {
return;
}
console.info('[' + pidStr + ']', 'cpu and memory observer is running...');
console.debug('[pid:' + pidStr + ']', 'cpu and memory observer is running...');
function runTimer() {
pidusage.stat(pid, function (err, stat) {
@ -493,7 +498,7 @@ Monitor.prototype._killTailProcess = function (pm_id) {
} catch (err) {}
});
delete self._tails[id];
console.info('[' + id + ']', 'tail destroyed!');
console.info('[pm2:' + id + ']', 'tail destroyed!');
}
if (!isNaN(pm_id)) {
return killTail(pm_id);
@ -529,10 +534,113 @@ Monitor.prototype._listeningSocketIO = function () {
console.info('Listening connection event on', nsp.toLowerCase());
}
var auth;
if (!(this.options.agent && (auth = this.options.agent.authorization))) {
return;
}
this._sockio.use(function (socket, next) {
// console.log(socket.handshake);
if (auth !== socket.handshake.query.auth) {
return next(new Error('unauthorized'));
}
next();
})
});
};
/**
* List all available monitors.
* @param {Object} options
* @return {Object}
*/
Monitor.available = function (options) {
options.agent = options.agent || {};
var remotable = options.remotes && _.keys(options.remotes).length > 0;
if (options.agent.offline && !remotable) {
return null;
}
options.port = options.port || 8088;
if (!remotable) {
return options;
}
var q = {
name: 'socket_server',
message: 'Which socket server would you wanna connect to',
type: 'list',
choices: []
},
maxShortLength = 0;
for (var remote in options.remotes) {
var connectionString = options.remotes[remote];
q.choices.push({
value: connectionString,
short: remote
});
maxShortLength = Math.max(maxShortLength, remote.length);
}
if (!options.agent.offline) {
var short = 'localhost',
connectionString = (options.agent && options.agent.authorization ? options.agent.authorization + '@' : '') + '127.0.0.1:' + options.port;
q.choices.push({
value: connectionString,
short: short
});
maxShortLength = Math.max(maxShortLength, short.length);
}
if (q.choices.length > 1) {
q.choices.forEach(function (c) {
c.name = '[' + c.short + Array(maxShortLength - c.short.length + 1).join(' ') + '] ' + c.value;
});
}
return q;
};
/**
* Convert connection object to string.
* @param {Object} connection
* @return {String}
*/
Monitor.toConnectionString = function (connection) {
var uri = (connection.protocol || 'http:') + '//' + (connection.hostname || '127.0.0.1') + ':' + connection.port +
(connection.path || '') + (connection.namespace || '');
if (connection.authorization) {
uri += (uri.indexOf('?') > 0 ? '&' : '?') + 'auth=' + connection.authorization;
}
return uri;
};
/**
* Parse connection string to an uri object.
* @param {String} connectionString
* @return {Object}
*/
Monitor.parseConnectionString = function (connectionString) {
var connection = {
port: 8088,
hostname: '127.0.0.1',
authorization: ''
};
var lastAt = connectionString.lastIndexOf('@');
if (lastAt >= 0) {
connection.authorization = connectionString.slice(0, lastAt);
connectionString = connectionString.slice(lastAt + 1);
}
if (!/^http(s)?:\/\//i.test(connectionString)) {
connectionString = 'http://' + connectionString;
}
if (connectionString) {
connectionString = url.parse(connectionString);
connection.hostname = connectionString.hostname;
connection.port = connectionString.port;
connection.path = _.trimLeft(connectionString.path, '/');
connection.protocol = connectionString.protocol;
}
return connection;
};
Object.defineProperty(Monitor.prototype, 'sockio', {

View File

@ -31,17 +31,17 @@ date = false
;
; Log level, one of debug, log, info, warn, error.
;
level = log
level = debug
[agent]
;
; This authorization will be used to authorize socket / web connections if it's set.
;
; authorization = AuTh
authorization = AuTh
;
; A value indicates whether agent offline or not.
;
; offline = false
; offline = true
[remotes]
;
; the dashboard and web server will use this section to connect remoting socket server
@ -51,3 +51,4 @@ level = log
; pm2@172 = 192.168.1.172:9001
; pm2@173 = 192.168.1.173:9000
;
pm2@138 = AuTh107@192.168.100.138:8088

View File

@ -2,7 +2,6 @@ var chalk = require('chalk'),
path = require('path'),
fs = require('fs'),
_ = require('lodash'),
url = require('url'),
socketIO = require('socket.io'),
inquirer = require("inquirer"),
conf = require('./lib/util/conf'),
@ -93,59 +92,28 @@ function dashboard(confFile) {
var monitor = slave({
confFile: confFile
}),
options = _.clone(monitor.options);
options = _.clone(monitor.options),
q = Monitor.available(options);
options.agent = options.agent || {};
var remotable = options.remotes && _.keys(options.remotes).length > 0;
if (options.agent.offline && remotable) {
if (!q) {
console.error('No agent is online, can not start it.');
return process.exit(0);
}
options.port = options.port || 8088;
if (!remotable) {
return _connectToDashboard(monitor, options);
var ql = q.choices.length;
if (ql == 1) {
console.info('There is just one remoting server online, try to connect it.')
return _connectToDashboard(monitor, options, Monitor.parseConnectionString(q.choices[0].value));
}
q.choices.splice(ql - 1, 0, new inquirer.Separator());
console.info('Remoting servers are online, choose one you are intrested in.')
var q = {
name: 'socket_server',
message: 'Which socket server would you wanna connect to',
type: 'list',
choices: []
},
maxShortLength = 0;
for (var remote in options.remotes) {
var connectionString = options.remotes[remote];
q.choices.push({
value: connectionString,
short: remote
});
maxShortLength = Math.max(maxShortLength, remote.length);
}
if (!options.agent.offline) {
q.choices.push(new inquirer.Separator());
var short = 'localhost',
connectionString = (options.agent && options.agent.authorization ? options.agent.authorization + '@' : '') + '127.0.0.1:' + options.port;
q.choices.push({
value: connectionString,
short: short
});
maxShortLength = Math.max(maxShortLength, short.length);
}
q.choices.forEach(function (c) {
if (c.type != 'separator') {
c.name = '[' + c.short + Array(maxShortLength - c.short.length + 1).join(' ') + '] ' + c.value;
}
});
console.log('');
inquirer.prompt(q, function (answers) {
console.log('');
_connectToDashboard(monitor, options, _parseConnectionString(answers.socket_server));
_connectToDashboard(monitor, options, Monitor.parseConnectionString(answers.socket_server));
});
}
@ -233,51 +201,24 @@ function slave(options) {
}
function _connectToDashboard(monitor, options, connection) {
if (!connection || !!~['127.0.0.1', '0.0.0.0', 'localhost'].indexOf(connection.hostname)) {
return monitor.connect(_.extend({
namespace: conf.NSP.SYS
}, options), function (err, socket) {
if (err || !socket) {
console.warn('Agent is offline, try to start it.');
var sockio = socketIO();
sockio.listen(options.port);
monitor.sockio = sockio;
monitor.run();
} else {
console.info('Agent is online, try to connect it in dashboard directly.');
options.sockets = {};
var ns = _.trimLeft(conf.NSP.SYS, '/');
options.sockets[ns] = socket;
connection = _.extend({}, options, connection);
if (!!~['127.0.0.1', '0.0.0.0', 'localhost'].indexOf(connection.hostname)) {
return monitor.connect(connection, function (socket) {
console.info('Agent is online, try to connect it in dashboard directly.');
layout(connection).render(monitor);
}, function (err, socket) {
if (err == 'unauthorized') {
console.error('There was an error with the authentication:', err);
return process.exit(0);
}
layout(options).render(monitor);
console.warn('Agent is offline, try to start it.');
var sockio = socketIO();
sockio.listen(connection.port);
monitor.sockio = sockio;
monitor.run();
layout(connection).render(monitor);
});
}
layout(connection).render(monitor);
}
function _parseConnectionString(connectionString) {
var connection = {
port: 8088,
hostname: '127.0.0.1',
authorization: ''
};
var lastAt = connectionString.lastIndexOf('@');
if (lastAt >= 0) {
connection.authorization = connectionString.slice(0, lastAt);
connectionString = connectionString.slice(lastAt + 1);
}
if (!/^http(s)?:\/\//i.test(connectionString)) {
connectionString = 'http://' + connectionString;
}
if (connectionString) {
connectionString = url.parse(connectionString);
connection.hostname = connectionString.hostname;
connection.port = connectionString.port;
connection.path = _.trimLeft(connectionString.path, '/');
connection.protocol = connectionString.protocol;
}
return connection;
}
}

View File

@ -19,21 +19,6 @@ var sysStat,
popupProc,
scrolled;
/**
* Initialization.
*/
$(window).ready(function () {
prepareDOM();
initFullPage();
listenSocket();
renderFanavi();
});
/**
* Prepare DOM, cache elements, templates...
*/
@ -98,11 +83,11 @@ function initFullPage() {
/**
* Set fullPage enable or disable.
* @param {Boolean} enable
* @param {Boolean} exceptScroll
* @param {Boolean} unscrollable
*/
function setFPEnable(enable, exceptScroll) {
function setFPEnable(enable, unscrollable) {
$.fn.fullpage.setAllowScrolling(enable);
if (!exceptScroll) {
if (!unscrollable) {
$.fn.fullpage.setKeyboardScrolling(enable);
eles.fpNav[enable ? 'fadeIn' : 'fadeOut']();
}
@ -112,15 +97,40 @@ function setFPEnable(enable, exceptScroll) {
* Connect to socket server.
*/
function connectSocketServer(ns) {
var socket = io('127.0.0.1:8088' + ns + '?token=abc');
socket.on('error', info);
var uri = GUI.connection.value,
index = uri.indexOf('?'),
query = '';
if (index > 0) {
query = uri.slice(index);
uri = uri.slice(0, index);
}
console.log('before', uri, query, ns);
uri = _.trimRight(uri, '/') + (ns || '') + query;
if (!ns) {
return io.connect(uri).on('error', onError);
}
var socket = io.connect(uri);
socket.on('error', onError);
return socket;
}
/**
* Fires on error.
* @param {String} err
*/
function onError(err) {
if (err == 'unauthorized') {
err = 'There was an error with the authentication: ' + err;
}
info(err);
}
/**
* Initialize socket.io client and add listeners.
*/
function listenSocket() {
connectSocketServer();
sockets.sys = connectSocketServer(NSP.SYS);
// information from server.
sockets.sys.on('info', info);

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,54 @@
var Monitor = require('../../lib/monitor');
// Authorization
action(function auth(req, res){/*
if (!req._config.password || (req._config.password === req.session['password'])) {
action(function auth(req, res) {
if (!req._config.agent || (req._config.agent.authorization === req.session['authorization'])) {
return res.redirect('/');
}*/
res.render('auth', {title: 'Authorization'});
}
res.render('auth', {
title: 'Authorization'
});
});
// Index
action(function(req, res){
if (req._config.agent && (req._config.agent.authorization !== req.session['authorization'])) {
action(function (req, res) {
var auth;
if (req._config.agent && ((auth = req._config.agent.authorization) !== req.session['authorization'])) {
return res.redirect('/auth');
}
res.render('index', {title: 'Monitor'});
var q = Monitor.available(req._config),
connections = [];
q.choices.forEach(function (c) {
c.value = Monitor.toConnectionString(Monitor.parseConnectionString(c.value));
connections.push(c);
});
res.render('index', {
title: 'Monitor',
connections: connections
});
});
// API
action(function auth_api(req, res){
if(!req._config.agent || !req._config.agent.authorization){
return res.json({error: 'Can not found agent[.authorization] config, no need to authorize!'});
action(function auth_api(req, res) {
if (!req._config.agent || !req._config.agent.authorization) {
return res.json({
error: 'Can not found agent[.authorization] config, no need to authorize!'
});
}
if (!req.query || !req.query.authorization) {
return res.json({error: 'Authorization is required!'});
return res.json({
error: 'Authorization is required!'
});
}
if (req._config.agent && req.query.authorization === req._config.agent.authorization) {
req.session['authorization'] = req.query.authorization;
return res.json({status: 200});
return res.json({
status: 200
});
}
return res.json({error: 'Failed, authorization is incorrect.'});
});
return res.json({
error: 'Failed, authorization is incorrect.'
});
});

View File

@ -27,7 +27,7 @@ block content
div
div
div
input#authorization(type="hidden")= authorization
include ../partials/tmpls
block js
@ -36,3 +36,18 @@ block js
script(src='js/jquery.fullPage.min.js')
script(src='js/fanavi.min.js')
script(src='js/index.html.js')
script.
var GUI = {};
GUI.connections = !{JSON.stringify(connections)};
$(window).ready(function () {
if (!Array.isArray(GUI.connections) || GUI.connections.length == 0) {
info('No agent is online, can not start it.');
} else {
GUI.connection = GUI.connections[GUI.connections.length - 1];
}
prepareDOM();
initFullPage();
listenSocket();
renderFanavi();
});