Created
November 20, 2015 09:13
-
-
Save vymarkov/aa79c734a5e7cf6ff41a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
var os = require('os'); | |
var fs = require('fs'); | |
var path = require('path'); | |
var domain = require('domain'); | |
var util = require('util'); | |
var http = require('http'); | |
var express = require('express'); | |
var session = require('express-session'); | |
var cors = require('cors'); | |
var tv4 = require('tv4'); | |
var passport = require('passport'); | |
var winston = require('winston'); | |
var exception = require('winston/lib/winston/exception'); | |
var async = require('async'); | |
var inflection = require('inflection'); | |
var glob = require('glob'); | |
var moment = require('moment'); | |
var utils = require('momonga-utils'); | |
var env = require('momonga-utils/env'); | |
var _ = require('lodash'); | |
var accepts = require('accepts'); | |
var debug = require('debug')('momonga'); | |
var app = {}; | |
var normalizeConf = function () { | |
var argv = require('minimist')(process.argv.slice(2)); | |
var conf = { | |
log: { | |
colorize: true, | |
silent: argv.silent || false, | |
transports: { | |
console: { | |
colorize: true, | |
level: 'debug', | |
silent: argv.silent || false | |
} | |
} | |
} | |
}; | |
switch (argv.prod || argv.env || env()) { | |
case 'production': | |
conf.log.level = 'info'; | |
break; | |
default: | |
conf.log.level = 'debug'; | |
break; | |
} | |
conf.log.level = argv.level ? argv.level : conf.log.level; | |
conf.log.level = argv.verbose ? 'verbose' : conf.log.level; | |
conf.log.level = argv.debug ? 'debug' : conf.log.level; | |
conf.log.transports.console.colorize = conf.log.colorize; | |
conf.log.transports.console.silent = conf.log.silent; | |
conf.log.transports.console.level = conf.log.level; | |
return conf; | |
}; | |
module.exports = function (conf, cb) { | |
app = this; | |
app.passport = passport; | |
if (process.mainModule.filename === path.resolve(process.cwd(), 'app.js')) { | |
conf = normalizeConf(); | |
} | |
var initialize = function initialize() { | |
return new Promise(function (resolve, reject) { | |
['SIGTERM', 'SIGUSR2'].forEach(function (eventname) { | |
process.on(eventname, function () { | |
app.log.verbose('Got %s signal', eventname); | |
app.log.verbose('Suspending all proceses...'); | |
app.log.verbose('Server closes the connection...'); | |
app.log.verbose('Closes all client connections inside application...'); | |
app.shutdown(); | |
}); | |
}); | |
process.once('SIGINT', function () { | |
app.log.verbose('Got SIGINT signal.'); | |
app.log.verbose('Finishing all proceses...'); | |
app.log.verbose('Server closes the connection...'); | |
app.log.verbose('Closes all client connections inside application...'); | |
app.log.verbose('Exiting...'); | |
app.shutdown(function () { | |
setTimeout(function () { | |
process.exit(0); | |
}, 100); | |
}); | |
}); | |
process.once('uncaughtException', function (err) { | |
app.shutdown(function () { | |
process.exit(0); | |
}); | |
}); | |
process.once('exit', function onExit() { | |
app.shutdown(function () { | |
process.exit(0); | |
}); | |
}); | |
app.config.bootstrap(app, function done() { | |
resolve(); | |
}); | |
}); | |
}; | |
var freezeconfig = function freezeconfig() { | |
return new Promise(function (resolve, reject) { | |
_.each(_.without(_.keys(app.config), ['cwd']), function (configKey) { | |
var oldVal = app.config[configKey]; | |
delete app.config[configKey]; | |
Object.defineProp(app.config, configKey, { | |
value: oldVal, | |
enumerable: true, | |
writable: ['dev', 'test'], | |
configurable: ['dev', 'test'] | |
}); | |
}); | |
resolve(); | |
}); | |
}; | |
var loadBootScripts = function loadbootscripts() { | |
return new Promise(function (resolve, reject) { | |
var dependencies = fs.readdirSync(path.resolve(app.config.cwd, 'node_modules')) | |
.filter(function (modulename) { | |
return modulename !== '.bin' && !_.has(app.config.pkg.devDependencies, modulename); | |
}); | |
try { | |
async.eachSeries(dependencies, function (pkgname, callback) { | |
try { | |
var pkg = require(path.resolve(process.cwd(), 'node_modules', pkgname, 'package.json')); | |
app.log.silly('Loading a package.json for module "%s"', pkgname, JSON.stringify(pkg, null, 2)); | |
if (_.has(pkg, 'momonga.boot')) { | |
var pathtoboot = path.resolve(process.cwd(), 'node_modules', pkg.name, pkg.momonga.boot); | |
// app.log.verbose('Loading a boot script from %s', pathtoboot); | |
var boot = require(pathtoboot); | |
boot(app, function (err) { | |
if (err) { | |
app.log.warn('An error occurred while trying to load a boot script "%s"', pathtoboot); | |
} | |
app.log.debug('Boot script successfully loaded (%s)', pathtoboot); | |
callback(err); | |
}); | |
} else { | |
callback(); | |
} | |
} catch (e1) { | |
app.log.warn(e1.message, { | |
cwd: process.cwd(), | |
trace: e1.stack | |
}); | |
callback(e1); | |
} | |
}, function done(err) { | |
if (err) { | |
reject(err); | |
} | |
resolve(); | |
}); | |
} catch (e0) { | |
console.log(e0); | |
} | |
}); | |
}; | |
var prepareHooks = function prepareHooks() { | |
var hooks = {}; | |
app.controllers = app.controllers || {}; | |
app.controller = function controller(name, controller) { | |
name = utils.normalizeId(name); | |
if (arguments.length === 1) { | |
return app.controllers[name]; | |
} else if (arguments.length === 2) { | |
if (!_.has(app.controllers, name)) { | |
app.controllers[name] = controller; | |
} | |
} | |
}; | |
app.models = app.models || {}; | |
app.model = function model(name, model) { | |
name = utils.normalizeId(name); | |
if (arguments.length === 1) { | |
if (_.has(app.models, name)) { | |
return app.models[name]; | |
} | |
name = name.toLowerCase(); | |
if (_.has(app.models, name)) { | |
return app.models[name]; | |
} | |
name = inflection.camelize(name); | |
if (_.has(app.models, name)) { | |
return app.models[name]; | |
} | |
} else if (arguments.length === 2) { | |
if (!_.has(app.models, name)) { | |
app.models[name] = model; | |
} | |
} | |
}; | |
app.configure = function configure(envs, handler) { | |
if (arguments.length === 1 && _.isFunction(envs)) { | |
handler = envs; | |
switch (app.config.env) { | |
case 'development': | |
case 'testing': | |
handler(); | |
break; | |
} | |
} else if (_.isArray(envs) && _.isFunction(handler)) { | |
if (_.contains(envs, app.config.env)) { | |
handler(); | |
} | |
} else if (_.isString(envs) && _.isFunction(handler)) { | |
if (app.config.env === envs) { | |
handler(); | |
} | |
} | |
}; | |
app.hook = function hook(hookname, hook) { | |
if (arguments.length === 1 && _.isString(hookname)) { | |
return hooks[hookname] || []; | |
} | |
if (_.isString(hookname) && _.isFunction(hook)) { | |
hookname = hookname.toLowerCase(); | |
if (!_.has(hooks, hookname)) { | |
Object.defineProp(hooks, hookname, { | |
value: [], | |
writable: ['dev', 'test'], | |
configurable: ['dev', 'test'], | |
enumerable: true | |
}); | |
} | |
hooks[hookname].push(hook); | |
} | |
}; | |
return new Promise(function (resolve, reject) { | |
app.hook('beforetart', function storePidfile(app, cb) { | |
try { | |
if (!fs.existsSync('.momonga')) { | |
fs.mkdirSync('.momonga'); | |
} | |
fs.writeFileSync('.momonga/momonga.pid', process.pid + '\n'); | |
cb(); | |
} catch (e1) { | |
app.log.warn('Something went wrong...'); | |
app.log.warn(e1.message); | |
cb(e1); | |
} | |
}); | |
app.hook('beforeshutdown', function clearPidfile(app, cb) { | |
try { | |
fs.truncateSync('.momonga/momonga.pid', 0); | |
cb(); | |
} catch (e1) { | |
app.log.warn('Something went wrong...'); | |
app.log.warn(e1.message); | |
cb(e1); | |
} | |
}); | |
['beforeStart', | |
'afterStart', | |
'beforeRestart', | |
'beforeShutdown' | |
].map(function (hookname) { | |
return app.config.hooks[hookname]; | |
}).filter(function (hooks) { | |
return !_.isEmpty(hooks); | |
}).forEach(function (hooks) { | |
hooks = hooks.map(function (hook) { | |
return hook.bind(app); | |
}) | |
}); | |
['beforeStart', | |
'afterStart', | |
'beforeRestart', | |
'beforeShutdown' | |
].forEach(function (hookname) { | |
app.config.hooks[hookname].forEach(function (hook) { | |
app.hook(hookname, hook); | |
}); | |
}); | |
Object.defineProp(app, 'hooks', { | |
value: hooks, | |
writable: true, | |
configurable: ['dev', 'test'], | |
enumerable: true | |
}); | |
app.emit('config:ready'); | |
resolve(); | |
}); | |
}; | |
var beforeStartApplication = function beforeStartApplication() { | |
return new Promise(function (resolve, reject) { | |
app.emit('start:before'); | |
app.config.hooks.hookTimeout = app.config.hooks.hookTimeout || 2000; | |
async.eachSeries(app.hook('beforetart'), function runHook(hook, cb) { | |
try { | |
var timer = setTimeout(function () { | |
app.log.warn('Hook is taking unusually long to execute its callback (%d milliseconds).', | |
app.config.hooks.hookTimeout); | |
}, app.config.hooks.hookTimeout); | |
hook(app, function onComplete() { | |
clearTimeout(timer); | |
app.log.debug('Hook \'%s\' successfully completed (beforetart)', _.isEmpty(hook.name) ? 'anonymous' : hook.name); | |
cb(); | |
}); | |
} catch (e1) { | |
cb(e1); | |
} | |
}, | |
function (err) { | |
if (err) { | |
reject(err); | |
} | |
resolve(); | |
}); | |
}); | |
}; | |
var setupLogger = function setupLogger() { | |
return new Promise(function (resolve, reject) { | |
app.log = new winston.Logger({ | |
transports: [ | |
new winston.transports.Console(app.config.log.transports.console) | |
] | |
}); | |
resolve(); | |
}) | |
}; | |
var runApplication = function runApplication() { | |
return new Promise(function (resolve, reject) { | |
var server = app.server = http.createServer(app); | |
server.listen(app.config.port); | |
server.once('error', function onError(error) { | |
if (error.syscall !== 'listen') { | |
throw error; | |
} | |
var bind = typeof app.config.port === 'string' ? 'Pipe ' + app.config.port : 'Port ' + app.config.port; | |
// handle specific listen errors with friendly messages | |
switch (error.code) { | |
case 'EACCES': | |
app.log.error(bind + ' requires elevated privileges'); | |
process.exit(1); | |
break; | |
case 'EADDRINUSE': | |
app.log.error(bind + ' is already in use'); | |
process.exit(1); | |
break; | |
default: | |
throw error; | |
} | |
}); | |
server.once('listening', function onListening() { | |
var addr = server.address(); | |
var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; | |
debug('Listening on ' + bind); | |
app.log.info('Server configuration', { | |
config: JSON.parse(JSON.stringify(app.config)), | |
process: { | |
pid: process.pid, | |
uid: process.getuid ? process.getuid() : null, | |
gid: process.getgid ? process.getgid() : null, | |
cwd: process.cwd(), | |
execPath: process.execPath, | |
version: process.version, | |
argv: process.argv, | |
memoryUsage: process.memoryUsage() | |
}, | |
os: { | |
loadavg: os.loadavg(), | |
uptime: os.uptime() | |
} | |
}); | |
app.log.info(moment().format('ddd ll HH:mm:ss Z')); | |
// app.log.info('Momonga version %s', app.momonga.version); | |
app.log.info('Starting %s server at http://127.0.0.1:%s', utils.env(), addr.port); | |
app.log.info('To shut down momonga, press <cntl> + c at any time.'); | |
resolve(); | |
}); | |
}); | |
}; | |
var afterRunApplication = function afterRunApplication() { | |
return new Promise(function (resolve, reject) { | |
app.emit('start:after'); | |
async.eachSeries(app.hook('afterstart'), function runHook(hook, cb) { | |
try { | |
var timeoutMs = app.config.hooks.hookTimeout || 2000; | |
var timer = setTimeout(function runTimer() { | |
app.log.error('Hook is taking unusually long to execute its callback (%d milliseconds).', | |
timeoutMs); | |
}, timeoutMs); | |
hook(app, function onComplete() { | |
clearTimeout(timer); | |
cb(); | |
}); | |
} catch (e1) { | |
reject(err); | |
} | |
}, function (err) { | |
if (err) { | |
reject(err); | |
} | |
resolve(); | |
}); | |
}); | |
}; | |
var execHooks = function execHooks(hookname) { | |
return new Promise(function (resolve, reject) { | |
async.eachSeries(app.hook(hookname), function runHook(hook, cb) { | |
try { | |
var timeoutMs = app.config.hooks.hookTimeout || 2000; | |
var timer = setTimeout(function () { | |
app.log.error('Hook is taking unusually long to execute its callback (%d milliseconds).', | |
timeoutMs); | |
}, timeoutMs); | |
var onComplete = function (err) { | |
clearTimeout(timer); | |
cb(err); | |
}; | |
var prom = hook(app, onComplete); | |
if (prom instanceof Promise) { | |
prom.then(onComplete).catch(onComplete); | |
} | |
} catch (e1) { | |
cb(e1); | |
} | |
}, function onComplete(err) { | |
if (err) { | |
reject(err); | |
} | |
resolve(); | |
}); | |
}); | |
}; | |
var runDomain = function runDomain() { | |
return new Promise(function (resolve, reject) { | |
var d = domain.create(); | |
d.once('error', reject); | |
d.run(function onRun() { | |
try { | |
var order = [ | |
'logger', | |
'responses', | |
'policies', | |
'services', | |
'validators', | |
'controllers', | |
'models', | |
'routes', | |
'auth' | |
]; | |
async.eachSeries(order, function (hookname, cb) { | |
app.emit('before:' + hookname); | |
execHooks('before' + hookname) | |
.then(function () { | |
if (_.isString(hookname)) { | |
try { | |
return require(path.resolve(__dirname, 'hooks', hookname))(app, cb); | |
} catch (e1) { | |
return cb(e1); | |
} | |
} else { | |
execHooks('after' + hookname) | |
.then(function () { | |
app.emit('after:' + hookname); | |
cb(); | |
}).catch(cb); | |
} | |
}) | |
.catch(cb); | |
}, function (err) { | |
if (err) { | |
reject(err); | |
} | |
resolve(); | |
}); | |
} catch (err) { | |
reject(err); | |
} | |
}); | |
}); | |
}; | |
var trace = function trace() { | |
return new Promise(function (resolve) { | |
app.server.on('timeout', function (socket) {}); | |
app.configure('prod', function () { | |
var timeOut = 2 * 60 * 1000; | |
app.server.setTimeout(timeOut); | |
app.log.debug('Currently server timeout is %s', app.server.timeout); | |
}); | |
app.configure(function () { | |
app.set('json spaces', 2); | |
var timeOut = 30 * 1000; | |
app.server.setTimeout(timeOut); | |
app.log.debug('Currently server timeout is %s', app.server.timeout); | |
app.get('/package.json', function (req, res) { | |
return res.status(200).send(app.config.pkg); | |
}); | |
app.get('/config.json', function (req, res) { | |
return res.status(200).send(app.config); | |
}); | |
app.get('/config/:property.json', function (req, res) { | |
if (_.has(app.config, req.params.property)) { | |
return res.status(200).send(app.config[req.params.property]); | |
} else { | |
return res.status(404).send(); | |
} | |
}); | |
app.get('/ping', function (req, res) { | |
res.set('Connection', 'close'); | |
setTimeout(function () { | |
return res.send('pong'); | |
}, +req.query.timeout || 0); | |
}); | |
app.post('/rs', function (req, res) { | |
app.restart(); | |
return res.status(202).send(); | |
}); | |
app.use(function (req, res, next) { | |
app.log.warn('Url not found', { | |
url: req.url, | |
method: req.method, | |
headers: req.headers, | |
body: req.body, | |
query: req.query, | |
params: req.params, | |
wantsJSON: req.wantsJSON | |
}); | |
next(); | |
}); | |
app.use(function (err, req, res, next) { | |
app.log.warn('requestException', exception.getAllInfo(err)); | |
next(err); | |
}); | |
}); | |
resolve(); | |
}); | |
}; | |
cb = cb || function uncaughtException(err) { | |
if (err) { | |
console.error(err.stack); | |
app.log.error('uncaughtException', exception.getAllInfo(err)); | |
} | |
}; | |
var loadConfig = require('./config/load'); | |
loadConfig(app, conf) | |
.then(setupLogger) | |
.then(prepareHooks) | |
.then(initialize) | |
.then(beforeStartApplication) | |
.then(runDomain) | |
// .then(freezeconfig) | |
.then(loadBootScripts) | |
.then(runApplication) | |
.then(afterRunApplication) | |
.then(trace) | |
.then(function () { | |
cb(null, app); | |
}) | |
.catch(cb); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment