UNPKG

guvnor

Version:

A node process manager that isn't spanners all the way down

432 lines (351 loc) 13.1 kB
var Autowire = require('wantsit').Autowire var async = require('async') var EventEmitter = require('wildemitter') var util = require('util') var Guvnor = function () { EventEmitter.call(this, { wildcard: true, delimiter: ':' }) this._config = Autowire this._logger = Autowire this._processService = Autowire this._fs = Autowire this._usage = Autowire this._cpuStats = Autowire this._remoteUserService = Autowire this._nodeInspectorWrapper = Autowire this._os = Autowire this._appService = Autowire this._pem = Autowire this._ini = Autowire this._posix = Autowire this._etc_passwd = Autowire this._processInfoStore = Autowire this._processInfoStoreFactory = Autowire // write out process file on exit process.on('exit', function () { if (!this._config.guvnor.autoresume) { return } this._processInfoStore.saveSync() }.bind(this)) } util.inherits(Guvnor, EventEmitter) Guvnor.prototype.afterPropertiesSet = function (done) { process.title = 'guvnor' var tasks = [] if (this._config.guvnor.autoresume) { tasks = this._processInfoStore.all().map(function (processInfo) { return function (callback) { this._processService.startProcess(processInfo, function (error) { if (error) { this._logger.error('Error resuming process', processInfo.name, error) } // don't pass the error callback because if we do, we'll abort resuming the rest of the processes callback() }.bind(this)) }.bind(this) }.bind(this)) } async.series(tasks, done) } /** * Start a new NodeJS process * * @param {String} script The path to the NodeJS script to start * @param {Object} options * @param {Number} [options.instances] The number of instances to start (1) * @param {String} [options.name] Name to give the process (script filename) * @param {String|Number} [options.user] User name or uid to start the process as (current user) * @param {String|Number} [options.group] Group name or gid to start the process as (current group) * @param {Boolean} [options.restartOnError] Restart the process automatically when it exits abnormally (true) * @param {Number} [options.restartRetries] Number of times the process can be restarted when crashing (5) * @oaram {Number} [options.crashRecoveryPeriod] The time before the process is considered to not be crashing (5000ms) * @param {Object} [options.env] Process environment key/value pairs * @param {Function} callback Called on successful process start or on startup error * @returns {Number} PID of the process that was started */ Guvnor.prototype.startProcess = function (userDetails, script, options, callback) { var appInfo = this._appService.findByName(script) if (appInfo) { options.script = appInfo.path options.app = appInfo.id options.name = appInfo.name } if (!options.user) { options.user = userDetails.name } if (!options.group) { options.group = userDetails.group } this._processService.startProcess(script, options, callback) } // this is exposed as an admin rpc function.. Guvnor.prototype.startProcessAsUser = function (userDetails, script, options, callback) { this.startProcess(userDetails, script, options, callback) } Guvnor.prototype.stopProcess = function (userDetails, id, callback) { var processInfo = this._processService.findById(id) if (!processInfo) { return callback(new Error('No process found for id ' + id)) } if (processInfo.status === 'running') { return callback(new Error('Process ' + processInfo.name + ' is running, use it\'s RPC service to kill it')) } // if the user is root, or the user is the one that owns the process or is in the same group as the process owner if (userDetails.name === this._config.guvnor.user || userDetails.name === processInfo.user || userDetails.groups.indexOf(processInfo.group) !== -1) { if (processInfo.process && processInfo.process.kill) { processInfo.process.emit('process:stopping') processInfo.process.kill() callback() return } callback(new Error('Could not kill process')) } else { var error = new Error('Permission denied') error.code = 'EPERM' callback(error) } } Guvnor.prototype.removeProcess = function (userDetails, id, callback) { this._processService.removeProcess(id, callback) } Guvnor.prototype.getServerStatus = function (userDetails, callback) { this._cpuStats(function (error, stats) { var status = { time: Date.now(), uptime: this._os.uptime(), freeMemory: this._os.freemem(), totalMemory: this._os.totalmem(), cpus: this._os.cpus(), debuggerPort: this._config.remote.inspector.enabled ? this._nodeInspectorWrapper.debuggerPort : undefined } if (!error) { stats.forEach(function (load, index) { status.cpus[index].load = load }) } callback(error, status) }.bind(this)) } Guvnor.prototype.listUsers = function (userDetails, callback) { this._etc_passwd.getGroups(function listGroups (error, groups) { if (error) { return callback(error) } var users = [] // only return groups without the _ prefix groups.filter(function removeHiddenGroups (group) { return group.groupname.substring(0, 1) !== '_' }).map(function convertToGroupName (group) { return group.groupname }).forEach(function listGroupUsers (group) { this._posix.getgrnam(group).members.filter(function removeHiddenUsers (user) { return user.substring(0, 1) !== '_' }).forEach(function findUserGroups (username) { var user = users.reduce(function addUserToListIfNecessary (prev, current) { if (prev) { return prev } if (current.name === username) { return current } }, null) if (!user) { var pwnam = this._posix.getpwnam(username) var grname = this._posix.getgrnam(pwnam.gid) user = { uid: pwnam.uid, name: pwnam.name, group: grname.name, groups: [] } users.push(user) } user.groups.push(group) }.bind(this)) }.bind(this)) users.forEach(function (user) { if (user.groups.indexOf(user.group) === -1) { user.groups.push(user.group) } }) callback(undefined, users) }.bind(this)) } Guvnor.prototype.listProcesses = function (userDetails, callback) { async.parallel(this._processService.listProcesses().map(function (processInfo) { return function (callback) { var language = processInfo.script.substring(processInfo.script.length - '.coffee'.length) === '.coffee' ? 'coffee' : 'javascript' if (!processInfo.remote || processInfo.status !== 'running') { // this process is not ready yet return callback(undefined, { id: processInfo.id, name: processInfo.name, restarts: processInfo.totalRestarts, status: processInfo.status, script: processInfo.script, debugPort: processInfo.debugPort, user: processInfo.user, group: processInfo.group, cwd: processInfo.cwd, argv: processInfo.argv, execArgv: processInfo.execArgv, pid: processInfo.pid, language: language }) } processInfo.remote.reportStatus(function (error, status) { var processStatus = processInfo.status if (error && error.code === 'TIMEOUT') { processStatus = 'unresponsive' } status = status || {} status.restarts = processInfo.totalRestarts status.id = processInfo.id status.script = processInfo.script status.debugPort = processInfo.debugPort status.status = processStatus status.language = language status.socket = processInfo.socket callback(undefined, status) }) } }), callback) } Guvnor.prototype.findProcessInfoById = function (userDetails, id, callback) { callback(undefined, this._processService.findById(id)) } Guvnor.prototype.findProcessInfoByPid = function (userDetails, pid, callback) { callback(undefined, this._processService.findByPid(pid)) } Guvnor.prototype.findProcessInfoByName = function (userDetails, name, callback) { callback(undefined, this._processService.findByName(name)) } Guvnor.prototype.dumpProcesses = function (userDetails, callback) { this._processInfoStore.save(function (error) { callback(error) this.emit('daemon:dump') }.bind(this)) } Guvnor.prototype.restoreProcesses = function (userDetails, callback) { this._processInfoStoreFactory.create(['processInfoFactory', 'processes.json'], function (error, store) { if (error) return callback(error) async.series(store.all().map(function (processInfo) { return this._processService.startProcess.bind(this._processService, processInfo.script, processInfo) }.bind(this)), function (error, result) { callback(error, result) this.emit('daemon:restore') }.bind(this)) }.bind(this)) } Guvnor.prototype.kill = function (userDetails, callback) { if (callback) { callback() } this.emit('daemon:exit') process.exit(0) } Guvnor.prototype.remoteHostConfig = function (userDetails, callback) { this._remoteUserService.findOrCreateUser(this._config.guvnor.user, function (error, user) { callback( error, this._os.hostname(), this._config.remote.port, this._config.guvnor.user, user.secret ) }.bind(this)) } Guvnor.prototype.addRemoteUser = function (userDetails, userName, callback) { this._remoteUserService.createUser(userName, callback) } Guvnor.prototype.removeRemoteUser = function (userDetails, userName, callback) { if (userName === this._config.guvnor.user) { var error = new Error('Cowardly refusing to delete ' + this._config.guvnor.user) error.code = 'WILLNOTREMOVEGUVNORUSER' return callback(error) } this._remoteUserService.removeUser(userName, callback) } Guvnor.prototype.listRemoteUsers = function (userDetails, callback) { this._remoteUserService.listUsers(callback) } Guvnor.prototype.rotateRemoteUserKeys = function (userDetails, userName, callback) { this._remoteUserService.rotateKeys(userName, callback) } Guvnor.prototype.deployApplication = function (userDetails, name, url, user, onOut, onErr, callback) { this._appService.deploy(name, url, user, onOut, onErr, callback) } Guvnor.prototype.removeApplication = function (userDetails, name, callback) { this._appService.remove(name, callback) } Guvnor.prototype.listApplications = function (userDetails, callback) { this._appService.list(callback) } Guvnor.prototype.findAppByName = function (userDetails, name, callback) { callback(undefined, this._appService.findByName(name)) } Guvnor.prototype.findAppById = function (userDetails, id, callback) { callback(undefined, this._appService.findById(id)) } Guvnor.prototype.switchApplicationRef = function (userDetails, name, ref, onOut, onErr, callback) { this._appService.switchRef(name, ref, onOut, onErr, callback) } Guvnor.prototype.listApplicationRefs = function (userDetails, name, callback) { this._appService.listRefs(name, callback) } Guvnor.prototype.updateApplicationRefs = function (userDetails, name, onOut, onError, callback) { this._appService.updateRefs(name, onOut, onError, callback) } Guvnor.prototype.currentRef = function (userDetails, name, callback) { this._appService.currentRef(name, callback) } Guvnor.prototype.generateRemoteRpcCertificates = function (userDetails, days, callback) { this._pem.createCertificate({ days: days, selfSigned: true }, function (error, keys) { if (error) { callback(error) return } var keyPath = this._config.guvnor.confdir + '/rpc.key' var certPath = this._config.guvnor.confdir + '/rpc.cert' var configPath = this._config.guvnor.confdir + '/guvnor' async.parallel([ this._fs.writeFile.bind(this._fs, keyPath, keys.serviceKey, { mode: parseInt('0600', 8) }), this._fs.writeFile.bind(this._fs, certPath, keys.certificate, { mode: parseInt('0600', 8) }) ], function (error) { if (error) { callback(error) return } this._fs.readFile(configPath, 'utf-8', function (error, result) { if (error && error.code !== 'ENOENT') { callback(error) return } var config = {} if (result) { config = this._ini.parse(result) } config.remote = config.remote || {} config.remote.key = keyPath config.remote.certificate = certPath this._fs.writeFile(configPath, this._ini.stringify(config), { mode: parseInt('0600', 8) }, function (error) { callback(error, configPath) this.emit('daemon:genssl') }.bind(this)) }.bind(this)) }.bind(this)) }.bind(this)) } module.exports = Guvnor