scalra
Version:
node.js framework to prototype and scale rapidly
401 lines (332 loc) • 9.78 kB
JavaScript
//
// frontier.js
//
// main lobby server
//
require('scalra')('dev');
const ansi_up = require('ansi_up'); // converts console message to colorful HTML
const humanize = require('humanize'); // better human readability
const pm2 = require('pm2');
// show debug / warning / error messages
LOG.setLevel(2);
LOG.show('all');
var l_name = 'Monitor';
//-----------------------------------------
// define local variables
//
//-----------------------------------------
// a list of names for all collections to be created
var collections = ['log'];
var config = {
path: __dirname,
handlers: [
{file: 'log.js', owner: 'SR'}
],
components: [
//SR.Component.REST('HTTP', ['REST_handle.js']), // start a HTTP server
//SR.Component.REST('HTTPS', ['REST_handle.js']), // start a HTTPs server
],
modules: {
'reporting': {},
'swagger': {}
}
};
// create frontier
var l_frontier = new SR.Frontier(config);
// set custom REST handlers
SR.REST.addHandler('REST_handle.js');
SR.REST.addHandler('REST_execute.js');
//
// define API
//
// API to get system paths
SR.API.add('_SYS_PATH', {
type: 'string'
}, function (args, onDone) {
switch (args.type) {
case 'demo': {
var path = SR.path.resolve(SR.Settings.SR_PATH, 'demo');
return onDone(null, path);
}
default: {
// check if the requested path exists in settings
if (SR.Settings.hasOwnProperty(args.type)) {
return onDone(null, SR.Settings[args.type]);
}
return onDone('unknown type [' + args.type + ']');
}
}
});
// API to set system paths
SR.API.add('_SET_SYS_PATH', {
type: 'string',
path: 'string'
}, function (args, onDone) {
if (SR.Settings.hasOwnProperty(args.type) === true) {
return onDone('[' + args.type + '] already set');
}
SR.Settings[args.type] = args.path;
l_buildPaths();
onDone(null);
});
// server mapping from id -> info
var l_servers = {};
// API to subscribe for monitor alerts
SR.API.add('_MONITOR_ALERT', {
id: 'string'
}, function (args, onDone, extra) {
LOG.warn('server [' + args.id + '] subscribing alert:', l_name);
LOG.warn(extra.conn, l_name);
l_servers[args.id] = extra.conn;
onDone(null);
});
// remove disconnected servers from list
SR.Callback.onDisconnect(function (conn) {
for (var id in l_servers) {
if (l_servers[id] === conn) {
LOG.warn('server [' + id + '] disconnected', l_name);
delete l_servers[id];
return;
}
}
});
// list of subscribers to screen (server id -> conn object list)
var l_subscribers = {};
const spawn = require('child_process').spawn;
var l_tailOutput = function (owner, project, name, subscriber) {
project = project.replace(/\[.*\]$/, '');
var serverID = owner + '-' + project + '-' + name;
// start tailing the project's output
try {
new Promise((resolve, reject) => {
if (SR.Settings.PM2_ENABLE) {
pm2.connect((err) => {
if (err) {
LOG.error(err);
reject(err);
return;
}
pm2.describe(`${owner}-${project}-${name}`, (err, data) => {
if (err) {
LOG.error(err);
reject(err);
return;
}
if (!data[0]) {
LOG.error(`Cannot get pm2 info of server "${owner}-${project}-${name}"`);
reject(`Cannot get pm2 info of server "${owner}-${project}-${name}"`);
return;
}
if (!SR.Settings.PATH_USERBASE) {
LOG.error(`Cannot get settings of PATH_USERBASE`);
reject(`Cannot get settings of PATH_USERBASE`);
return;
}
const pmID = data[0].pm_id;
resolve(SR.path.resolve(
SR.Settings.PATH_USERBASE,
owner,
project,
'log',
`output-${pmID}.log`
));
})
})
} else {
resolve(SR.path.resolve(
SR.Settings.PATH_USERBASE,
owner,
project,
'log',
'output.log'
));
}
}).then((log_file) => {
LOG.warn('tailing [' + log_file + ']...', l_name);
subscriber.proc = spawn('tail', ['-n', '1000', '-f', log_file]);
var onData = function (d) {
// NOTE: strings like '[monitor]' will get replaced as well (incorrectly)
var logData = d.toString().replace(/\[m/g, '[');
SR.EventManager.send('_SUBSCRIBE_LOG',
{ err: null, result: { subID: subscriber.subID, data: logData } },
subscriber.conns);
}
subscriber.proc.stdout.on('data', onData);
subscriber.proc.stderr.on('data', onData);
subscriber.proc.on('exit', function (code) {
LOG.warn('screen spawn exit: ' + code, l_name);
subscriber.proc = undefined;
delete l_subscribers[serverID];
});
}).catch((err) => {
LOG.error(err);
});
}
catch (e) {
LOG.error(e, l_name);
}
}
// API to subscribe for the continous output of a log file
SR.API.add('_SUBSCRIBE_LOG', {
owner: 'string',
project: 'string',
name: 'string',
subID: 'string'
}, function (args, onDone, extra) {
var owner = args.owner;
var project = args.project;
var name = args.name;
var serverID = owner + '-' + project + '-' + name;
// check if already subscribed
if (l_subscribers.hasOwnProperty(serverID) === false) {
l_subscribers[serverID] = {
proc: null,
subID: args.subID,
conns: []
}
}
var subscriber = l_subscribers[serverID];
// add a new subscriber connection (only if new)
if (subscriber.conns.indexOf(extra.conn) === (-1)) {
subscriber.conns.push(extra.conn);
}
// return subID to indicate subscription success
onDone(null, {subID: args.subID, data: 'showing screen for [' + serverID + ']...\n'});
if (subscriber.proc !== null) {
LOG.warn('screen output for [' + serverID + '] already exists, show existing tailing process', l_name);
return;
}
// use another function to make sure 'subscriber' is still valid in callback
l_tailOutput(owner, project, name, subscriber);
});
// API to subscribe for the continous output of a log file
SR.API.add('_UNSUBSCRIBE_LOG', {
subID: 'string',
connID: '+string'
}, function (args, onDone, extra) {
var connID = args.connID || extra.conn.connID;
var subID = args.subID;
var subscriber = undefined;
for (var serverID in l_subscribers) {
if (l_subscribers[serverID].subID === subID) {
subscriber = l_subscribers[serverID];
break;
}
}
if (!subscriber) {
return onDone('subID [' + args.subID + '] not found');
}
// find & remove the connection object
for (var i=0; i < subscriber.conns.length; i++) {
if (subscriber.conns[i].connID === connID) {
subscriber.conns.splice(i, 1);
// check if nobody's subscribing, then should remove tailing process
if (subscriber.conns.length === 0) {
LOG.warn('no subscriber for [' + serverID + '] screen, stop tailing...', l_name);
subscriber.proc.kill('SIGHUP');
delete l_subscribers[serverID];
}
return onDone(null);
}
}
return onDone('cannot find connection to remove');
});
// remove subscription or stop tailing if nobody's interested in viewing
// TODO: should have system-mechanism for auto-unsubscribe
SR.Callback.onDisconnect(function (conn) {
for (var serverID in l_subscribers) {
for (var i=0; i < l_subscribers[serverID].conns.length; i++) {
var c = l_subscribers[serverID].conns[i];
if (c.connID !== conn.connID)
continue;
// remove the connection
SR.API._UNSUBSCRIBE_LOG({
serverID: serverID,
connID: c.connID
});
// NOTE: we just break because it's possible the same connection (from a single icpm instance)
// may subscribe for multiple serverID's output
break;
}
}
});
var l_buildPaths = function () {
// (re-)build path settings
if (SR.Settings.PATH_USERBASE) {
SR.Settings.PATH_LIB = SR.path.join(SR.Settings.PATH_USERBASE, 'lib');
}
}
// execute all the steps for running a server
l_frontier.init(function () {
// callback when lobby is started
LOG.warn('monitor started successfully', l_name);
});
// restart any existing, started servers
SR.Callback.onStart(function () {
SR.startedServers = {};
// determine where the log file should be
var logfile = SR.path.resolve(SR.Settings.LOG_PATH, SR.Settings.Project.serverList);
// start server
if (SR.fs.existsSync(logfile) === false) {
return;
}
LOG.warn('Found previously started servers in: ' + logfile, l_name);
SR.fs.readFile(logfile, 'utf8', function (err, data) {
if (err) {
LOG.error(err, l_name);
return;
}
if (data == '') {
return;
}
try {
var servers = JSON.parse(data);
} catch (e) {
return LOG.error(e, l_name);
}
if (typeof servers != 'object') {
return LOG.error('invalid started server log');
}
// NOTE: we don't store this list by default, as if the server is not started successfully,
// it should be removed from the list
//SR.startedServers = servers;
if (SR.Settings.PM2_ENABLE) {
return;
}
setTimeout(function () {
for (let serverID in servers) {
SR.API._START_SERVER({
owner: servers[serverID].owner,
project: servers[serverID].project,
name: servers[serverID].name,
size: servers[serverID].size,
onOutput: function (output) {
// make output HTML displable
// TODO: do this at the client to save bandwidth?
if (SR.Settings.REFORMAT_HTML_TEXT === true)
output.data = humanize.nl2br(ansi_up.ansi_to_html(output.data));
// record to file
SR.StreamManager.publish(output.id, output);
}
}, function (err, list) {
if (err) {
LOG.error(err, l_name);
return;
}
LOG.warn('execute success:', l_name);
LOG.warn(list, l_name);
// SR.REST.reply(res, list);
});
}
}, 3000);
});
//} else {
// SR.fs.writeFile(logfile, '', function(err) {
// if (err) {
// return console.log(err);
// }
// SR.startedServers = {};
// LOG.warn("initialize started server log: " + logfile, l_name);
// });
//}
})