UNPKG

bedrock

Version:

A core foundation for rich Web applications.

648 lines (586 loc) 18.3 kB
/* * Copyright (c) 2012-2016 Digital Bazaar, Inc. All rights reserved. */ var async = require('async'); var brUtil = require('./util'); var cc = brUtil.config.main.computer(); var cluster = require('cluster'); var config = require('./config'); var errio = require('errio'); var events = require('./events'); var jsonld = require('./jsonld'); var loggers = require('./loggers'); var path = require('path'); var pkginfo = require('pkginfo'); var program = require('commander'); var test = require('./test'); const BedrockError = brUtil.BedrockError; // core API var api = {}; api.config = config; api.events = events; api.jsonld = jsonld; api.loggers = loggers; api.test = test; // NOTE: api.tools is deprecated api.util = api.tools = brUtil; module.exports = api; // read package.json fields pkginfo(module, 'version'); // register error class errio.register(BedrockError); // config paths // configured here instead of config.js due to util dependency issues // FIXME: v2.0.0: remove when removing warnings below. const _warningShown = { cache: false, log: false }; cc({ 'paths.cache': () => { // FIXME: v2.0.0: remove warning and default and throw exception . //throw new BedrockError( // 'bedrock.config.paths.cache not set.', // 'ConfigError'); const cachePath = path.join(__dirname, '..', '.cache'); if(!_warningShown.cache) { loggers.get('app').error( `"bedrock.config.paths.cache" not set, using default: "${cachePath}"`); _warningShown.cache = true; } return cachePath; }, 'paths.log': () => { // FIXME: v2.0.0: remove warning and default and throw exception . //throw new BedrockError( // 'bedrock.config.paths.log not set.', // 'ConfigError'); const logPath = path.join('/tmp/bedrock-dev'); if(!_warningShown.log) { // Using console since this config value used during logger setup. console.warn('WARNING: ' + `"bedrock.config.paths.log" not set, using default: "${logPath}"`); _warningShown.log = true; } return logPath; } }); // expose bedrock program api.program = program.version(api.version); /** * Starts the bedrock application. * * @param options the options to use: * [script] the script to execute when forking bedrock workers, by * default this will be process.argv[1]. * @param done(err) called by bedrock workers once the bedrock application has * started or once an error has occured. */ api.start = function(options, done) { if(typeof options === 'function') { done = options; options = null; } options = options || {}; var startTime = Date.now(); function collect(val, memo) { memo.push(val); return memo; } // add built-in CLI options program .option('--config <config>', 'Load a config file. (repeatable)', collect, []) .option('--log-level <level>', 'Console log level: ' + 'silly, verbose, debug, info, warning, error, critical.') .option('--log-timestamps <timestamps>', 'Override console log timestamps config. (boolean)', brUtil.boolify) .option('--log-colorize <colorize>', 'Override console log colorization config. (boolean)', brUtil.boolify) .option('--log-exclude <modules>', 'Do not log events from the specified comma separated modules.') .option('--log-only <modules>', 'Only log events from the specified comma separated modules.') .option('--log-transports <spec>', 'Transport spec. Use category=[-|+]transport[;...][, ...] ' + 'eg, access=+console;-access,app=-console') .option('--silent', 'Show no console output.') .option('--workers <num>', 'The number of workers to use (0: # of cpus).', parseInt); async.auto({ beforeInit: function(callback) { events.emit('bedrock-cli.init', callback); }, init: ['beforeInit', function(results, callback) { _parseCommandLine(); _loadConfigs(); _configureLoggers(); _configureWorkers(); _configureProcess(); events.emit('bedrock-loggers.init', callback); }], initLoggers: ['init', function(results, callback) { loggers.init(function(err) { if(err) { // can't log, quit console.error('Failed to initialize logging system:', err); process.exit(1); } callback(); }); }], run: ['initLoggers', function(results, callback) { if(cluster.isMaster) { _runMaster(options); // don't call callback in master process return; } _runWorker(startTime, function(err) { if(err) { return events.emit('bedrock.error', err, function() { callback(err); }); } callback(); }); }] }, function(err) { if(done) { return done(err); } if(err) { throw err; } }); }; /** * Called from workers to set the worker and master process user if it hasn't * already been set. */ api.setProcessUser = function() { if(_switchedProcessUser) { return; } _switchedProcessUser = true; // switch group if(config.core.running.groupId && process.setgid) { process.setgid(config.core.running.groupId); } // switch user if(config.core.running.userId && process.setuid) { process.setuid(config.core.running.userId); } // send message to master process.send({type: 'bedrock.switchProcessUser'}); }; var _switchedProcessUser = false; /** * Called from a worker to executes the given function in only one worker. * * @param id a unique identifier for the function to execute. * @param fn the function to execute; if it takes a parameter, it will be * assumed to be a callback and `fn` will be executed asynchronously. * @param [options] the options to use: * [allowOnRestart] true to allow this function to execute again, * but only once, on worker restart; this option is useful for * behavior that persists but should only run in a single worker. * @param callback(err, ran) called once the operation has completed in * one worker. */ api.runOnce = function(id, fn, options, callback) { if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; // listen to run function once process.on('message', runOnce); var ran = false; function runOnce(msg) { // ignore other messages if(!_isMessageType(msg, 'bedrock.runOnce') || msg.id !== id) { return; } process.removeListener('bedrock.runOnce', runOnce); if(msg.error) { msg.error = errio.fromObject(msg.error, {stack: true}); } if(msg.done) { return callback(msg.error, ran); } var _callback = function(err) { var _msg = { type: 'bedrock.runOnce', id: id, done: true }; if(err) { _msg.error = errio.toObject(err, {stack: true}); } process.send(_msg); }; // run in this worker if(fn.length > 0) { return fn(_callback); } try { fn(); } catch(e) { return _callback(e); } _callback(); } // schedule function to run process.send({type: 'bedrock.runOnce', id: id, options: options}); }; /** * Called from a worker to exit gracefully. Typically used by subcommands. */ api.exit = function() { cluster.worker.kill(); }; function _parseCommandLine() { program.parse(process.argv); if(config.cli.command === null) { // set default command config.cli.command = new program.Command('bedrock'); } } function _loadConfigs() { program.config.forEach(function(cfg) { require(path.resolve(process.cwd(), cfg)); }); } function _configureLoggers() { // set console log flags if('logLevel' in program) { config.loggers.console.level = program.logLevel; } if('logColorize' in program) { config.loggers.console.colorize = program.logColorize; } if('logTimestamps' in program) { config.loggers.console.timestamp = program.logTimestamps; } if('logExclude' in program) { config.loggers.console.bedrock.excludeModules = program.logExclude.split(','); } if('logOnly' in program) { config.loggers.console.bedrock.onlyModules = program.logOnly.split(','); } // adjust transports if('logTransports' in program) { var t = program.logTransports; var cats = t.split(','); cats.forEach(function(cat) { var catName = cat.split('=')[0]; var catTransports; if(catName in config.loggers.categories) { catTransports = config.loggers.categories[catName]; } else { catTransports = config.loggers.categories[catName] = []; } var transports = cat.split('=')[1].split(';'); transports.forEach(function(transport) { if(transport.indexOf('-') === 0) { const tName = transport.slice(1); const tIndex = catTransports.indexOf(tName); if(tIndex !== -1) { catTransports.splice(tIndex, 1); } } else if(transport.indexOf('+') === 0) { const tName = transport.slice(1); const tIndex = catTransports.indexOf(tName); if(tIndex === -1) { catTransports.push(tName); } } else { const tName = transport; const tIndex = catTransports.indexOf(tName); if(tIndex === -1) { catTransports.push(tName); } } }); }); } if(program.silent || program.logLevel === 'none') { config.loggers.console.silent = true; } } function _configureWorkers() { if('workers' in program) { config.core.workers = program.workers; } if(config.core.workers <= 0) { config.core.workers = require('os').cpus().length; } } function _configureProcess() { // set no limit on event listeners process.setMaxListeners(0); // exit on terminate process.on('SIGTERM', function() { process.exit(); }); if(cluster.isMaster) { cluster.setupMaster({ exec: path.join(__dirname, 'worker.js') }); // set group before initializing loggers if(config.core.starting.groupId && process.setgid) { try { process.setgid(config.core.starting.groupId); } catch(ex) { console.warn('Failed to set master starting gid: ' + ex); } } // set user before initializing loggers if(config.core.starting.userId && process.setuid) { try { process.setuid(config.core.starting.userId); } catch(ex) { console.warn('Failed to set master starting uid: ' + ex); } } } } function _setupUncaughtExceptionHandler(mode, logger) { // log uncaught exception and exit, except in test mode if(config.cli.command.name() !== 'test') { process.on('uncaughtException', function(err) { process.removeAllListeners('uncaughtException'); logger.critical(mode + ': uncaught error:', err); process.exit(1); }); } } function _runMaster(options) { // get starting script var script = options.script || process.argv[1]; var logger = loggers.get('app'); logger.info('starting bedrock...'); // setup cluster if running with istanbul coverage if(process.env.running_under_istanbul) { // TODO: does this need adjusting after fixing the worker `cwd` issue? // re-call cover with no reporting and using pid named output cluster.setupMaster({ exec: './node_modules/.bin/istanbul', args: [ 'cover', '--report', 'none', '--print', 'none', '--include-pid', process.argv[1], '--'].concat(process.argv.slice(2)) }); } // set 'ps' title var args = process.argv.slice(2).join(' '); process.title = config.core.master.title + (args ? (' ' + args) : ''); _setupUncaughtExceptionHandler('master', logger); logger.info('running bedrock master process "' + config.core.master.title + '"', {pid: process.pid}); // keep track of master state var masterState = { switchedUser: false, runOnce: {} }; // notify workers to exit if master exits process.on('exit', function() { for(var id in cluster.workers) { cluster.workers[id].send({type: 'bedrock.core', message: 'exit'}); } }); // handle worker exit cluster.on('exit', function(worker, code) { // if the worker called kill() or disconnect(), it was intentional, so exit // the process if(worker.exitedAfterDisconnect) { logger.info( 'worker ' + worker.process.pid + ' exited on purpose with code ' + code + '; exiting master process.'); process.exit(code); } // accidental worker exit (crash) logger.critical( 'worker ' + worker.process.pid + ' exited with code ' + code); // if configured, fork a replacement worker if(config.core.worker.restart) { // clear any runOnce records w/allowOnRestart option set for(var id in masterState.runOnce) { if(masterState.runOnce[id].worker === worker.id && masterState.runOnce[id].options.allowOnRestart) { delete masterState.runOnce[id]; } } _startWorker(masterState, script); } else { process.exit(1); } }); // fork each app process var workers = config.core.workers; for(var i = 0; i < workers; ++i) { _startWorker(masterState, script); } } function _runWorker(startTime, done) { var logger = loggers.get('app'); // set 'ps' title var args = process.argv.slice(2).join(' '); process.title = config.core.worker.title + (args ? (' ' + args) : ''); _setupUncaughtExceptionHandler('worker', logger); logger.info('running bedrock worker process "' + config.core.worker.title + '"'); // listen for master process exit var bedrockStarted = false; process.on('message', function(msg) { if(!_isMessageType(msg, 'bedrock.core') || msg.message !== 'exit') { return; } if(!bedrockStarted) { return events.emit('bedrock-cli.exit', function() { process.exit(); }); } events.emit('bedrock.stop', function(err) { if(err) { throw err; } events.emit('bedrock-cli.exit', function() { process.exit(); }); }); }); async.auto({ cliReady: function(callback) { events.emit('bedrock-cli.ready', callback); }, configure: ['cliReady', function(results, callback) { if(results.cliReady === false) { // skip default behavior (do not emit bedrock core events) return done(); } bedrockStarted = true; events.emit('bedrock.configure', callback); }], adminInit: ['configure', function(results, callback) { events.emit('bedrock.admin.init', callback); }], init: ['adminInit', function(results, callback) { // set process user api.setProcessUser(); events.emit('bedrock.init', callback); }], start: ['init', function(results, callback) { events.emit('bedrock.start', callback); }], ready: ['start', function(results, callback) { var dtTime = Date.now() - startTime; logger.info('startup time: ' + dtTime + 'ms'); events.emit('bedrock.ready', callback); }], started: ['ready', function(results, callback) { events.emit('bedrock.started', callback); }] }, function(err) { done(err); }); } function _startWorker(state, script) { var worker = cluster.fork(); loggers.attach(worker); // listen to start requests from workers worker.on('message', initWorker); // TODO: simplify with cluster.on('online')? function initWorker(msg) { if(!_isMessageType(msg, 'bedrock.worker.started')) { return; } // notify worker to initialize and provide the cwd and script to run worker.removeListener('message', initWorker); worker.send({ type: 'bedrock.worker.init', cwd: process.cwd(), script: script }); } // listen to exit requests from workers worker.on('message', function(msg) { if(_isMessageType(msg, 'bedrock.core') && msg.message === 'exit') { process.exit(msg.status); } }); // if app process user hasn't been switched yet, wait for a message // from a worker indicating to do so if(!state.switchedUser) { worker.on('message', switchProcessUserListener); } function switchProcessUserListener(msg) { if(!_isMessageType(msg, 'bedrock.switchProcessUser')) { return; } worker.removeListener('message', switchProcessUserListener); if(!state.switchedUser) { state.switchedUser = true; // switch group if(config.core.running.groupId && process.setgid) { process.setgid(config.core.running.groupId); } // switch user if(config.core.running.userId && process.setuid) { process.setuid(config.core.running.userId); } } } // listen to schedule run once functions worker.on('message', function(msg) { if(!_isMessageType(msg, 'bedrock.runOnce')) { return; } if(msg.done) { state.runOnce[msg.id].done = true; state.runOnce[msg.id].error = msg.error || null; // notify workers to call callback var notify = state.runOnce[msg.id].notify; while(notify.length > 0) { var id = notify.shift(); if(id in cluster.workers) { cluster.workers[id].send({ type: 'bedrock.runOnce', id: msg.id, done: true, error: msg.error }); } } return; } if(msg.id in state.runOnce) { if(state.runOnce[msg.id].done) { // already ran, notify worker immediately worker.send({ type: 'bedrock.runOnce', id: msg.id, done: true, error: state.runOnce[msg.id].error }); } else { // still running, add worker ID to notify queue for later notification state.runOnce[msg.id].notify.push(worker.id); } return; } // run in this worker state.runOnce[msg.id] = { worker: worker.id, notify: [worker.id], options: msg.options, done: false, error: null }; worker.send({type: 'bedrock.runOnce', id: msg.id, done: false}); }); } function _isMessageType(msg, type) { return (typeof msg === 'object' && msg.type === type); }