nexus
nexus foo
function Nexus(opts) { var self = this EE2.call(self,{wildcard:true,delimiter:'::',maxListeners:20}) try { self.package = require('./package.json') } catch(e) {} self.apps = {} self.config = initConfig(opts) self.initDb() } Nexus.prototype = new EE2
@param {Function} cb with 2 args: err, version
Nexus.prototype.version = function version(cb) { cb = _.isFunction(cb) ? cb : function() {} if (!this.package || !this.package.version) return cb(new Error('could not read/parse the package.json')) cb(null, this.package.version) }
@param {Function} cb with 2 args: err, version
Nexus.prototype.config = function config(opts, cb) { cb = _.isFunction(arguments[arguments.length-1]) ? arguments[arguments.length-1] : function() {} if (!!this._config) return cb(null, this._config) var self = this , cfg = self._config = {} , cfgFile = {} , cfgPath = home+'/.nexus/config.js' , home = ( process.platform === "win32" ? process.env.USERPROFILE : process.env.HOME ) if (arguments.length < 2) opts = {} if (opts && _.isString(opts)) cfgPath = opts if (opts && _.isObject(opts)) cfg = opts try { fileConfig = require(configPath) } catch (e) {} // no config-file, so we use currConfig or hardcoded defaults var defaultPrefix = path.join(home,'.nexus') cfg.apps = cfg.apps || cfgFile.apps || path.join(defaultPrefix,'apps') cfg.tmp = cfg.tmp || cfgFile.tmp || path.join(defaultPrefix,'tmp') cfg.dbs = cfg.dbs || cfgFile.dbs || path.join(defaultPrefix,'dbs') cfg.logs = cfg.logs || cfgFile.logs || path.join(defaultPrefix,'logs') // client cfg.key = cfg.key || cfgFile.key || null cfg.cert = cfg.cert || cfgFile.cert || null // server cfg.ca = cfg.ca || cfgFile.ca || null cfg.host = cfg.host || cfgFile.host || '0.0.0.0' cfg.port = cfg.port || cfgFile.port || null cfg.socket = cfg.socket || cfgFile.socket || null // remotes cfg.remotes = cfg.remotes || cfgFile.remotes || {} cfg.execTimeout = 1000*60*30 cfg.restartTimeout = 200 cfg.restartTimeoutLimit = 1000*10 var ensureDirs = [cfg.apps, cfg.tmp, cfg.dbs, cfg.logs] if (cfg.ca) ensureDirs.push(cfg.ca) ensureDirs.forEach(function(x){ if (!fs.existsSync(x)) mkdirp.sync(x, 0755) }) // async.map(ensureDirs,function(x, next){ // fs.exists(x, function(e){ // if (!e) return mkdirp(x, 0755, next) // next() // }) // }, function(err){ // cb(err, cfg) // }) }
@param {Object} options
Nexus.prototype.install = function install(opts, cb) { }
list installed apps
ls({'package.name':'foo'},cb)
Nexus.prototype.ls = function ls(filter, cb) { var self = this cb = _.isFunction(arguments[arguments.length-1]) ? arguments[arguments.length-1] : function() {} if ( arguments.length < 2 || !_.isObject(filter) || Object.keys(filter).length==0) filter = null fs.readdir(self.config.apps, function(err, dirs){ if (err) return cb(err) async.map(dirs, function(x, next){ var app = {} app.name = x var appPath = path.join(self._config.apps,x) var pkgPath = path.join(appPath, 'package.json') var gitPath = path.join(appPath, '.git') var tasks = async.series([checkPackage,checkGit], function(err, data){ if (err) return cb(err) next(err, app) }) function checkPackage(next){ fs.exists(pkgPath,function(exists){ if (!exists) return next() fs.readFile(pkgPath,function(err,data){ if (err) return next() try { app.package = JSON.parse(data) } catch(e) { app.package = 'INVALID' } next() }) }) } function checkGit(next){ fs.exists(gitPath,function(exists){ if (!exists) return next() var getDiff = 'git diff' var getCommit = 'git log --no-color | head -n1' cp.exec(getCommit, {cwd:appPath}, function(err, stdout, stderr){ if (err || stderr) return next() // ignore.. app.git = {} app.git.commit = stdout.trim().split(/\s+/)[1] next() }) }) } }, function(err, d){ var result = [] _.each(d,function(data){ if (!filter) return result.push(data) var filteredData = objFilter(filter, data) if (filteredData) result.push(filteredData) }) cb(null, result) }) }) }
get information about running apps
filter all apps with name "foo" and also show the id:
ps( { name : 'foo', id : true }, cb )
Nexus.prototype.ps = function ps(filter, cb) { var self = this cb = _.isFunction(arguments[arguments.length-1]) ? arguments[arguments.length-1] : function() {} if ( arguments.length < 2 || !_.isObject(filter) || Object.keys(filter).length==0 ) filter = null if (Object.keys(self.apps).length == 0) return cb(null, []) var result = [] async.map(Object.keys(self.apps),function(x,next){ self.apps[x].info(function(err,data){ if (!filter) { result.push(data) return next() } var filteredData = objFilter(filter, data) if (filteredData) result.push(filteredData) next() }) }, function(err){ cb(null, result) }) }
start an application
start({name:'someInstalledApp',env:{},command:'node foo',max:3},cb)
Nexus.prototype.start = function start(opts, cb) { debug('nexus.start',opts) opts = _.isObject(opts) ? opts : {} cb = arguments[arguments.length - 1] cb = _.isFunction(cb) ? cb : function(){} if (!opts.name || !_.isString(opts.name)) return cb(new Error('invalid options, no app defined')) var self = this opts.cwd = path.join(self._config.apps, opts.name) fs.exists(opts.cwd, function(e){ if (!e) return cb(new Error('invalid name, the cwd doesnt exist: '+opts.cwd)) var id = genId(10) while (self.apps[id]) id = genId() opts.id = id opts.restartTimeout = self._config.restartTimeout opts.restartTimeoutLimit = self._config.restartTimeoutLimit if (!opts.command) return cb(new Error('invalid options, no command defined')) // #TODO // 1) check for opts.command // 2) check for nexus.json.start // 3) check for package.json.scripts.start new App(opts, function(err,app){ if (err) return cb(err) self.apps[id] = app self.apps[id].onAny(function(){ var args = [].slice.call(arguments) args.unshift('app::'+id+'::'+this.event) self.emit.apply(self, args) }) self.apps[id].start(cb) }) }) }
@param {String|Array} id(s) of apps
Nexus.prototype.restart = function start(ids, cb) { ids = _.isString(ids) ? [ids] : _.isArray(ids) ? ids : null cb = arguments[arguments.length - 1] cb = _.isFunction(cb) ? cb : function(){} var self = this if (!ids) return cb(new Error('invalid options, missing id(s)')) async.map(ids,function(id,next){ id = id.toString() if (!self.apps[id]) return next() self.apps[id].restart(next) }, cb) }
@param {String|Array} id(s) of apps
Nexus.prototype.stop = function stop(ids, cb) { var self = this ids = _.isString(ids) ? [ids] : _.isArray(ids) ? ids : null cb = arguments[arguments.length - 1] cb = _.isFunction(cb) ? cb : function(){} if (!ids || ids.length==0) return cb(new Error('invalid options, missing id(s)')) async.map(ids,function(id,next){ if (!self.apps[id]) return next() self.apps[id].stop(function(err,data){ if (err) return cb(err) self.apps[id].removeAllListeners() delete self.apps[id] next(null,data) }) },cb) }
@param {Function} cb with 2 args: err, version
Nexus.prototype.stopall = function stopall(cb) { var self = this var appIds = Object.keys(this.apps) if (appIds.length == 0) return cb(null, []) this.stop(appIds, cb) }
@param {Object} options
Nexus.prototype.exec = function exec(opts, cb) { var self = this opts = opts || {} cb = _.isFunction(arguments[arguments.length-1]) ? arguments[arguments.length-1] : function() {} if (!opts.command) return cb(new Error('invalid arguments, missing command')) if (_.isArray(opts.command)) opts.command = opts.command.join(' ') if (!_.isString(opts.command)) return cb(new Error('invalid arguments, invalid command (must be array or string)')) ;['kill','stdout','stderr'].forEach(function(x){ opts[x] = opts[x] && _.isFunction(opts[x]) ? opts[x] : function(){} }) var sh = (process.platform === "win32") ? 'cmd' : 'sh' var shFlag = (process.platform === "win32") ? '/c' : '-c' var cwd = opts.name ? path.join(self._config.apps,opts.name) : self._config.apps fs.exists(cwd,function(exists){ if (!exists) return cb(new Error('invalid arguments, cwd does not exist: '+cwd)) var child = cp.spawn(sh, [shFlag, opts.command], {cwd:cwd}) opts.kill(kill) setTimeout(kill, self._config.execTimeout) child.stdout.on('data',function(d){ opts.stdout(d.toString().replace(/\n$/, '')) }) child.stderr.on('data',function(d){ opts.stderr(d.toString().replace(/\n$/, '')) }) child.on('exit',cb) child.on('error',function(e){console.log('exec cp-error',opts,e)}) function kill() { pstree(child.pid, function(err, children){ var tokill = [] children.map(function(p){ tokill.push(p.PID) }) tokill.unshift(child.pid) tokill.forEach(function(x){ process.kill(x) }) }) } }) } Nexus.prototype.initDb = function initDb(cb) { cb = _.isFunction(cb) ? cb : function() {} var self = this var db var dbPath = self._config.port ? path.join(self._config.dbs, self._config.port) : self._config.socket ? path.join(self._config.dbs, self._config.socket.replace(/\//g,'_')) : null if (!dbPath) return cb(new Error('cant init db without defined port or socket')) var rebootFlag = self._config.reboot var todo = [unlinkDb, loadDb, subStop, subStart] if (self._config.reboot) todo.unshift(reboot) async.series(todo, function(x,next){x(next)}, cb) function reboot(cb) { var db = dirty(dbPath).on('load',function(){ db.forEach(function(k,v){ console.log('rebooting',v) if (v) nexus.start(v) }) cb() }) } function unlinkDb(cb) { fs.exists(dbPath,function(exists){ if (exists) return fs.unlink(dbPath,cb) cb() }) } function loadDb(cb) { db = dirty(dbPath).on('load',cb) } function subStop(cb) { self.on('app::*::stop',function(){ var id = this.event.split('::')[1] db.rm(id) }) cb() } function subStart(cb) { self.on('app::*::start',function(pid){ var id = this.event.split('::')[1] self.ps({id:id}, function(err, d){ if (err) return db.set(id, d) }) }) cb() } }
@return {Function} dnode-middleware (remote, conn)
Nexus.prototype.dnodeService = function dnodeService() { var self = this return function(remote, conn){ var service = this var subs = {} ;[ 'version', 'install', 'ls', 'ps', 'start', 'restart' , 'stop', 'stopall', 'exec' ].forEach(function(x){ service[x] = Nexus.prototype[x].bind(self) }) service.subscribe = function subscribe(event, emit, cb) { if (event == '*' || event == 'all') event = '**' if (!subs[event]) { subs[event] = function(data){ emit(this.event,data) } self.on(event, subs[event]) } cb && cb() } service.unsubscribe = function unsubscribe(events, cb) { cb = _.isFunction(arguments.length-1) ? arguments.length-1 : function(){} events = _.isString(events) ? [events] : (_.isArray(events) && events.length>0) ? events : Object.keys(subs) events.forEach(function(x){ self.removeListener(x, subs[x]) delete subs[x] }) } conn.once('end',function(){ service.unsubscribe() }) } }
Nexus.prototype.connect = function connect() { var self = this var client = dnode(this.dnodeService()).listen() self.config(function(err,cfg){ readKeys({key:cfg.keys}) }) return server }
Nexus.prototype.listen = function listen() { var server = dnode(this.dnodeService()) server.listen.apply(server,arguments) return server }
@param {Object} options
function App(opts, cb) { debug('new app',opts) opts = _.isObject(opts) ? opts : null cb = _.isFunction(cb) ? cb : function(){} if (!opts) return cb(new Error('invalid options')) if (!_.isString(opts.name)) return cb(new Error('invalid options, no app defined')) if (!opts.command) return cb(new Error('invalid options, no command defined')) if (!_.isString(opts.command)) { if (_.isArray(opts.command)) opts.command = opts.command.join(' ') else return cb(new Error('invalid command - must be array or string')) } var self = this EE2.call(self,{wildcard:true,delimiter:'::',maxListeners:20}) cb = _.isFunction(cb) ? cb : function(){} opts = opts || {} self.id = opts.id self.status = 'starting' self.name = opts.name self.command = opts.command self.startedOnce = false self.restartFlag = false self.stopFlag = false // after the app crashed it will restart it after `restartTimeout` (ms) self.restartTimeout = opts.restartTimeout || 200 // everytime the app crashes `restartTimeout` gets increased by 100ms // until it reaches `restartTimeoutLimit` self.restartTimeoutLimit = opts.restartTimeoutLimit || 100000 self.child = null self.crashed = 0 self.max = opts.max self.cwd = opts.cwd self.env = opts.env || {} self.env.NEXUS_ID = self.id if (!_.isString(self.command)) { if (_.isArray(self.command)) self.command = self.command.join(' ') else return cb(new Error('invalid command')) } cb(null, self) return self } App.prototype = new EE2
@param {Function} callback
App.prototype.start = function start(cb) { cb = _.isFunction(cb) ? cb : function(){} var self = this self.startedOnce = true if (self.child) return cb(new Error('is already running')) self.status = 'starting' var sh = (process.platform === "win32") ? 'cmd' : 'sh' var shFlag = (process.platform === "win32") ? '/c' : '-c' var child = cp.spawn( sh , [shFlag, self.command] , { cwd : self.cwd , env : self.env } ) self.child = child self.ctime = Date.now() self.status = 'running' self.emit('start', self.child.pid) self.info(cb) child.stdout.on('data',function(d){ self.emit('stdout', d.toString().replace(/\n$/, '')) }) child.stderr.on('data',function(d){ self.emit('stderr', d.toString().replace(/\n$/, '')) }) child.once('exit', function(code, sig){ self.emit('exit', code) self.child = null if ((code != 0) && !self.stopFlag) { self.status = 'crashed' self.crashed++ if (!self.max || self.crashed < self.max) { if ((Date.now()-self.ctime) > 60000) self.restartTimeout = 200 else if (self.restartTimeout < self.restartTimeoutLimit) self.restartTimeout += 100 setTimeout(function(){ self.start() },self.restartTimeout) } } else { // this app has no running child-process, though it can be restarted self.status = 'idle' } }) }
@param {Function} callback
App.prototype.restart = function restart(cb) { var self = this self.status = 'restarting' self.crashed = 0 self.restartFlag = true self.stop(function(){ setTimeout(function(){ self.restartFlag = false self.start(cb) },200) }) }
@param {Function} callback
App.prototype.stop = function stop(cb) { var self = this self.status = 'stopping' self.stopFlag = true if (self.child) { var timer = setTimeout(function(){ cb(new Error( 'tried to kill process (pid:'+self.child.pid+') ' + 'but it did not exit yet' ) ) },5000) self.child.once('exit',function(){ self.stopFlag = false clearTimeout(timer) self.emit('stop') self.info(cb) }) pstree(self.child.pid, function(err, children){ if (err) return cb(err) var pids = children.map(function (p) {return p.PID}) pids.unshift(self.child.pid) cp.spawn('kill', ['-9'].concat(pids)).on('exit',function(){}) }) } else { self.stopFlag = false self.info(cb) } }
@param {Function} callback
App.prototype.info = function info(cb) { cb = _.isFunction(cb) ? cb : function(){} var self = this var r = {} r.id = self.id r.pid = self.child ? self.child.pid : null r.status = self.status r.name = self.name r.command = self.command cb(null, r) }
@param {Object} filter
function objFilter(filter, data) { var result = {} var all = true _.each(filter,function(y,j){ if (!result) return if (_.isBoolean(y)) { all = false var info = objPath(data,j) if (info !== undefined) result[j] = info else result[j] = 'UNDEFINED' } else { y = y.toString() var info = objPath(data,j) if (info != y) result = false else result[j] = info } }) if (result && all) return data return result }
objPath - create/access objects with a key-string
function objPath(obj, keyString, value) { var keys = keyString.split('.') if (obj[keys[0]] === undefined) obj[keys[0]] = {} var data = obj[keys[0]] , keys = keys.slice(1) if (!value) { // get data var value = data for (var i=0, len=keys.length; i
@param {Integer} length
function genId(len) { len = len ? parseInt(len) : 8 var ret = '' var chars = 'ABCDEFGHIJKLMNOPQRSTUVWYXZabcdefghijklmnopqrstuvwyxz0123456789' while (len--) ret += chars[Math.round(Math.random() * (chars.length-1))] return ret }
@param {Object} options
function config(opts) { var cfg = {} , cfgFile = {} , cfgPath = home+'/.nexus/config.js' , home = ( process.platform === "win32" ? process.env.USERPROFILE : process.env.HOME ) if (opts && _.isString(opts)) cfgPath = opts if (opts && _.isObject(opts)) cfg = opts try { cfgFile = require(cfgPath) } catch (e) {} // no config-file, so we use currConfig or hardcoded defaults var defaultPrefix = path.join(home,'.nexus') cfg.apps = cfg.apps || cfgFile.apps || path.join(defaultPrefix,'apps') cfg.tmp = cfg.tmp || cfgFile.tmp || path.join(defaultPrefix,'tmp') cfg.dbs = cfg.dbs || cfgFile.dbs || path.join(defaultPrefix,'dbs') cfg.logs = cfg.logs || cfgFile.logs || path.join(defaultPrefix,'logs') // client cfg.key = cfg.key || cfgFile.key || null cfg.cert = cfg.cert || cfgFile.cert || null // server cfg.ca = cfg.ca || cfgFile.ca || null cfg.host = cfg.host || cfgFile.host || '0.0.0.0' cfg.port = cfg.port || cfgFile.port || null cfg.socket = cfg.socket || cfgFile.socket || null // remotes cfg.remotes = cfg.remotes || cfgFile.remotes || {} cfg.execTimeout = 1000*60*30 cfg.restartTimeout = 200 cfg.restartTimeoutLimit = 1000*10 var ensureDirs = [cfg.apps, cfg.tmp, cfg.dbs, cfg.logs] if (cfg.ca) ensureDirs.push(cfg.ca) ensureDirs.forEach(function(x){ if (!fs.existsSync(x)) mkdirp.sync(x, 0755) }) // async.map(ensureDirs,function(x, next){ // fs.exists(x, function(e){ // if (!e) return mkdirp(x, 0755, next) // next() // }) // }, function(err){ // cb(err, cfg) // }) return cfg } function readKeys(opts, cb) { cb = arguments[arguments.length-1] cb = _.isFunction(cb) ? cb : function(){} opts = opts || {} var result = {} async.parallel([readKey, readCert, readCa], function(err){ if (err) return cb(err) cb(null, result) }) function readKey(next) { if (!opts.key) return next() fs.readFile(opts.key,function(err, data){ if (err) return next(err) result.key = data next() }) } function readCert(next) { if (!opts.key) return next() fs.readFile(opts.key,function(err, data){ if (err) return next(err) result.key = data next() }) } function readCa(next) { if (!opts.ca) return next() fs.readdir(opts.ca,function(err,data){ if (err) return next(err) if (data.length > 0) { result.requestCert = true result.rejectUnauthorized = true async.map(data,function(x, done){ fs.readFile(path.join(opts.ca,x), done) }, function(err, data){ if (err) return next(err) result.ca = data next() }) } else { next() } }) } }