340 lines
9.3 KiB
JavaScript
340 lines
9.3 KiB
JavaScript
/* jslint node: true */
|
|
/* eslint-disable no-console */
|
|
'use strict';
|
|
|
|
//var SegfaultHandler = require('segfault-handler');
|
|
//SegfaultHandler.registerHandler('enigma-bbs-segfault.log');
|
|
|
|
// ENiGMA½
|
|
const conf = require('./config.js');
|
|
const logger = require('./logger.js');
|
|
const database = require('./database.js');
|
|
const clientConns = require('./client_connections.js');
|
|
|
|
// deps
|
|
const async = require('async');
|
|
const util = require('util');
|
|
const _ = require('lodash');
|
|
const mkdirs = require('fs-extra').mkdirs;
|
|
|
|
// our main entry point
|
|
exports.bbsMain = bbsMain;
|
|
|
|
// object with various services we want to de-init/shutdown cleanly if possible
|
|
const initServices = {};
|
|
|
|
function bbsMain() {
|
|
async.waterfall(
|
|
[
|
|
function processArgs(callback) {
|
|
const args = process.argv.slice(2);
|
|
|
|
var configPath;
|
|
|
|
if(args.indexOf('--help') > 0) {
|
|
// :TODO: display help
|
|
} else {
|
|
let argCount = args.length;
|
|
for(let i = 0; i < argCount; ++i) {
|
|
const arg = args[i];
|
|
if('--config' === arg) {
|
|
configPath = args[i + 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
callback(null, configPath || conf.getDefaultPath(), _.isString(configPath));
|
|
},
|
|
function initConfig(configPath, configPathSupplied, callback) {
|
|
conf.init(configPath, function configInit(err) {
|
|
|
|
//
|
|
// If the user supplied a path and we can't read/parse it
|
|
// then it's a fatal error
|
|
//
|
|
if(err) {
|
|
if('ENOENT' === err.code) {
|
|
if(configPathSupplied) {
|
|
console.error('Configuration file does not exist: ' + configPath);
|
|
} else {
|
|
configPathSupplied = null; // make non-fatal; we'll go with defaults
|
|
}
|
|
} else {
|
|
console.error(err.toString());
|
|
}
|
|
}
|
|
callback(err);
|
|
});
|
|
},
|
|
function initSystem(callback) {
|
|
initialize(function init(err) {
|
|
if(err) {
|
|
console.error('Error initializing: ' + util.inspect(err));
|
|
}
|
|
callback(err);
|
|
});
|
|
},
|
|
function listenConnections(callback) {
|
|
startListening(callback);
|
|
}
|
|
],
|
|
function complete(err) {
|
|
if(err) {
|
|
console.error('Error initializing: ' + util.inspect(err));
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
function shutdownSystem() {
|
|
logger.log.info('Process interrupted, shutting down...');
|
|
|
|
async.series(
|
|
[
|
|
function closeConnections(callback) {
|
|
const activeConnections = clientConns.getActiveConnections();
|
|
let i = activeConnections.length;
|
|
while(i--) {
|
|
activeConnections[i].term.write('\n\nServer is shutting down NOW! Disconnecting...\n\n');
|
|
clientConns.removeClient(activeConnections[i]);
|
|
}
|
|
callback(null);
|
|
},
|
|
function stopEventScheduler(callback) {
|
|
if(initServices.eventScheduler) {
|
|
return initServices.eventScheduler.shutdown( () => {
|
|
callback(null); // ignore err
|
|
});
|
|
} else {
|
|
return callback(null);
|
|
}
|
|
},
|
|
function stopMsgNetwork(callback) {
|
|
require('./msg_network.js').shutdown(callback);
|
|
}
|
|
],
|
|
() => {
|
|
process.exit();
|
|
}
|
|
);
|
|
}
|
|
|
|
function initialize(cb) {
|
|
async.series(
|
|
[
|
|
function createMissingDirectories(callback) {
|
|
async.each(Object.keys(conf.config.paths), function entry(pathKey, next) {
|
|
mkdirs(conf.config.paths[pathKey], function dirCreated(err) {
|
|
if(err) {
|
|
console.error('Could not create path: ' + conf.config.paths[pathKey] + ': ' + err.toString());
|
|
}
|
|
next(err);
|
|
});
|
|
}, function dirCreationComplete(err) {
|
|
callback(err);
|
|
});
|
|
},
|
|
function basicInit(callback) {
|
|
logger.init();
|
|
logger.log.info(
|
|
{ version : require('../package.json').version },
|
|
'**** ENiGMA½ Bulletin Board System Starting Up! ****');
|
|
|
|
process.on('SIGINT', shutdownSystem);
|
|
|
|
// Init some extensions
|
|
require('string-format').extend(String.prototype, require('./string_util.js').stringFormatExtensions);
|
|
|
|
callback(null);
|
|
},
|
|
function initDatabases(callback) {
|
|
database.initializeDatabases(callback);
|
|
},
|
|
function initStatLog(callback) {
|
|
require('./stat_log.js').init(callback);
|
|
},
|
|
function initThemes(callback) {
|
|
// Have to pull in here so it's after Config init
|
|
require('./theme.js').initAvailableThemes(function onThemesInit(err, themeCount) {
|
|
logger.log.info({ themeCount : themeCount }, 'Themes initialized');
|
|
callback(err);
|
|
});
|
|
},
|
|
function loadSysOpInformation(callback) {
|
|
//
|
|
// Copy over some +op information from the user DB -> system propertys.
|
|
// * Makes this accessible for MCI codes, easy non-blocking access, etc.
|
|
// * We do this every time as the op is free to change this information just
|
|
// like any other user
|
|
//
|
|
const user = require('./user.js');
|
|
|
|
async.waterfall(
|
|
[
|
|
function getOpUserName(next) {
|
|
return user.getUserName(1, next);
|
|
},
|
|
function getOpProps(opUserName, next) {
|
|
const propLoadOpts = {
|
|
userId : 1,
|
|
names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ],
|
|
};
|
|
user.loadProperties(propLoadOpts, (err, opProps) => {
|
|
return next(err, opUserName, opProps);
|
|
});
|
|
}
|
|
],
|
|
(err, opUserName, opProps) => {
|
|
const StatLog = require('./stat_log.js');
|
|
|
|
if(err) {
|
|
[ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => {
|
|
StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A');
|
|
});
|
|
} else {
|
|
opProps.username = opUserName;
|
|
|
|
_.each(opProps, (v, k) => {
|
|
StatLog.setNonPeristentSystemStat(`sysop_${k}`, v);
|
|
});
|
|
}
|
|
|
|
return callback(null);
|
|
}
|
|
);
|
|
},
|
|
function initMCI(callback) {
|
|
require('./predefined_mci.js').init(callback);
|
|
},
|
|
function readyMessageNetworkSupport(callback) {
|
|
require('./msg_network.js').startup(callback);
|
|
},
|
|
function readyEventScheduler(callback) {
|
|
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
|
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
|
initServices.eventScheduler = modInst;
|
|
return callback(err);
|
|
});
|
|
}
|
|
],
|
|
function onComplete(err) {
|
|
cb(err);
|
|
}
|
|
);
|
|
}
|
|
|
|
function startListening(cb) {
|
|
if(!conf.config.servers) {
|
|
// :TODO: Log error ... output to stderr as well. We can do it all with the logger
|
|
//logger.log.error('No servers configured');
|
|
return cb(new Error('No servers configured'));
|
|
}
|
|
|
|
const moduleUtil = require('./module_util.js'); // late load so we get Config
|
|
|
|
moduleUtil.loadModulesForCategory('servers', (err, module) => {
|
|
if(err) {
|
|
if('EENIGMODDISABLED' === err.code) {
|
|
logger.log.debug(err.message);
|
|
} else {
|
|
logger.log.info( { err : err }, 'Failed loading module');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const port = parseInt(module.runtime.config.port);
|
|
if(isNaN(port)) {
|
|
logger.log.error( { port : module.runtime.config.port, server : module.moduleInfo.name }, 'Cannot load server (Invalid port)');
|
|
return;
|
|
}
|
|
|
|
const moduleInst = new module.getModule();
|
|
let server;
|
|
try {
|
|
server = moduleInst.createServer();
|
|
} catch(e) {
|
|
logger.log.warn(e, 'Exception caught creating server!');
|
|
return;
|
|
}
|
|
|
|
// :TODO: handle maxConnections, e.g. conf.maxConnections
|
|
|
|
server.on('client', function newClient(client, clientSock) {
|
|
//
|
|
// Start tracking the client. We'll assign it an ID which is
|
|
// just the index in our connections array.
|
|
//
|
|
if(_.isUndefined(client.session)) {
|
|
client.session = {};
|
|
}
|
|
|
|
client.session.serverName = module.moduleInfo.name;
|
|
client.session.isSecure = module.moduleInfo.isSecure || false;
|
|
|
|
clientConns.addNewClient(client, clientSock);
|
|
|
|
client.on('ready', function clientReady(readyOptions) {
|
|
|
|
client.startIdleMonitor();
|
|
|
|
// Go to module -- use default error handler
|
|
prepareClient(client, function clientPrepared() {
|
|
require('./connect.js').connectEntry(client, readyOptions.firstMenu);
|
|
});
|
|
});
|
|
|
|
client.on('end', function onClientEnd() {
|
|
clientConns.removeClient(client);
|
|
});
|
|
|
|
client.on('error', function onClientError(err) {
|
|
logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message);
|
|
});
|
|
|
|
client.on('close', function onClientClose(hadError) {
|
|
const logFunc = hadError ? logger.log.info : logger.log.debug;
|
|
logFunc( { clientId : client.session.id }, 'Connection closed');
|
|
|
|
clientConns.removeClient(client);
|
|
});
|
|
|
|
client.on('idle timeout', function idleTimeout() {
|
|
client.log.info('User idle timeout expired');
|
|
|
|
client.menuStack.goto('idleLogoff', function goMenuRes(err) {
|
|
if(err) {
|
|
// likely just doesn't exist
|
|
client.term.write('\nIdle timeout expired. Goodbye!\n');
|
|
client.end();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
server.on('error', function serverErr(err) {
|
|
logger.log.info(err); // 'close' should be handled after
|
|
});
|
|
|
|
server.listen(port);
|
|
|
|
logger.log.info(
|
|
{ server : module.moduleInfo.name, port : port }, 'Listening for connections');
|
|
}, err => {
|
|
cb(err);
|
|
});
|
|
}
|
|
|
|
function prepareClient(client, cb) {
|
|
const theme = require('./theme.js');
|
|
|
|
// :TODO: it feels like this should go somewhere else... and be a bit more elegant.
|
|
|
|
if('*' === conf.config.preLoginTheme) {
|
|
client.user.properties.theme_id = theme.getRandomTheme() || '';
|
|
} else {
|
|
client.user.properties.theme_id = conf.config.preLoginTheme;
|
|
}
|
|
|
|
theme.setClientTheme(client, client.user.properties.theme_id);
|
|
return cb(null); // note: currently useless to use cb here - but this may change...again...
|
|
} |