UNPKG

guvnor

Version:

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

469 lines (389 loc) 18.2 kB
var Autowire = require('wantsit').Autowire var async = require('async') var splitargs = require('splitargs') var semver = require('semver') var CLI = function () { this._connectOrStart = Autowire this._running = Autowire this._logger = Autowire this._config = Autowire this._posix = Autowire this._commander = Autowire this._package = Autowire this._execSync = Autowire this._prompt = Autowire this._running = Autowire this._fs = Autowire this._os = Autowire this._child_process = Autowire this._apps = Autowire this._cluster = Autowire this._daemon = Autowire this._processes = Autowire this._remote = Autowire this._user = Autowire this._group = Autowire } CLI.prototype.afterPropertiesSet = function () { this._logger.debug('Loaded config from', this._config._rcfiles) async.series([ this._checkNodeVersion.bind(this), this._checkGuvnorUser.bind(this), this._checkGuvnorGroup.bind(this) ], function (error) { if (error) { throw error } this._setUpCommander() }.bind(this)) } CLI.prototype._setUpCommander = function () { // var allowUnknownOptions = process.env.GUVNOR_ALLOW_UKNOWN_OPTION !== undefined var allowUnknownOptions = true this._commander .version(this._package.version) .allowUnknownOption(allowUnknownOptions) this._commander .command('list') .description('List all running processes') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.list.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('start <scriptOrAppName>') .description('Start a process') .option('-u, --user <user>', 'The user to start a process as') .option('-g, --group <group>', 'The group to start a process as') .option('-i, --instances <instances>', 'How many instances of the process to start', parseInt) .option('-n, --name <name>', 'What name to give the process. If omitted and there is a package.json in the same directory as your script, it will be loaded and the name property used automatically.') .option('-a, --argv <argv>', 'A space separated list of arguments to pass to a process', splitargs) .option('-e, --execArgv <execArgv>', 'A space separated list of arguments to pass to the node executable', splitargs) .option('-d, --debug', 'Pause the process at the start of execution and wait for a debugger to be attached') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.start.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('workers <pidOrName> <workers>') .description('Set the number of workers managed by the cluster manager with the passed pid/name') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._cluster.setClusterWorkers.bind(this._cluster)) .allowUnknownOption(allowUnknownOptions) this._commander .command('stop <pidOrName...>') .description('Stop one or more processes') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.stop.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('remove <pidOrName...>') .description('Remove one or more processes') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.remove.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('restart <pidOrName...>') .description('Restart one or more processes') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.restart.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('send <pidOrName> <event> [args...]') .description('Causes process.emit(event, args[0], args[1]...) to occur in the process') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.send.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('heapdump <pidOrName>') .description('Write out a snapshot of the processes memory for inspection') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.heapdump.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('gc <pidOrName>') .description('Force garbage collection to occur in the process with the passed pid/name') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.gc.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('signal <pidOrName> <signal>') .description('Sends a signal to a process (SIGUSR1, SIGINT, SIGHUP, SIGTERM, etc)') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.signal.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('write <pidOrName> <string>') .description('Write a string to stdin of the managed process') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.write.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('logs [pidOrNames]') .description('Show realtime process logs, optionally filtering by pid/name') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._daemon.logs.bind(this._daemon)) .allowUnknownOption(allowUnknownOptions) this._commander .command('kill') .description('Stop all processes and kill the daemon') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._daemon.kill.bind(this._daemon)) .allowUnknownOption(allowUnknownOptions) this._commander .command('dump') .description('Dumps process data to ' + this._config.guvnor.confdir + '/processes.json') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._daemon.dump.bind(this._daemon)) .allowUnknownOption(allowUnknownOptions) this._commander .command('restore') .description('Restores processes from ' + this._config.guvnor.confdir + '/processes.json') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._daemon.restore.bind(this._daemon)) .allowUnknownOption(allowUnknownOptions) this._commander .command('config <path>') .description('Print a config option') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._daemon.config.bind(this._daemon)) .allowUnknownOption(allowUnknownOptions) this._commander .command('status') .description('Returns whether the daemon is running or not') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._daemon.status.bind(this._daemon)) .allowUnknownOption(allowUnknownOptions) this._commander .command('remoteconfig [hostname]') .description('Prints the remote host config for guvnor-web') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._remote.remoteHostConfig.bind(this._remote)) .allowUnknownOption(allowUnknownOptions) this._commander .command('useradd <username>') .description('Adds a user for use with guvnor-web') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._remote.addRemoteUser.bind(this._remote)) .allowUnknownOption(allowUnknownOptions) this._commander .command('rmuser <username>') .description('Removes a user from the guvnor-web user list') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._remote.deleteRemoteUser.bind(this._remote)) .allowUnknownOption(allowUnknownOptions) this._commander .command('lsusers') .description('Prints out all remote users') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._remote.listRemoteUsers.bind(this._remote)) .allowUnknownOption(allowUnknownOptions) this._commander .command('reset <username>') .description('Generate a new secret for the passed user') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._remote.rotateRemoteUserKeys.bind(this._remote)) .allowUnknownOption(allowUnknownOptions) this._commander .command('genssl [days]') .description('Generates self-signed SSL certificates for use between guvnor and guvnor-web that will expire in the passed number of days (defaults to 365)') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._remote.generateSSLCertificate.bind(this._remote)) .allowUnknownOption(allowUnknownOptions) this._commander .command('install <url> [appName]') .description('Installs an application from a git repository') .option('-u, --user <user>', 'The user to install as - n.b. the current user must be able to su to that user') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._apps.installApplication.bind(this._apps)) .allowUnknownOption(allowUnknownOptions) this._commander .command('lsapps') .description('List applications that have been deployed from git repositories') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._apps.listApplications.bind(this._apps)) .allowUnknownOption(allowUnknownOptions) this._commander .command('rmapp <appName>') .description('Remote deployed application') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._apps.removeApplication.bind(this._apps)) .allowUnknownOption(allowUnknownOptions) this._commander .command('lsrefs <appName>') .description('Lists app refs available to be started') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._apps.listRefs.bind(this._apps)) .allowUnknownOption(allowUnknownOptions) this._commander .command('updaterefs <appName>') .description('Updates app refs available to be started') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._apps.updateRefs.bind(this._apps)) .allowUnknownOption(allowUnknownOptions) this._commander .command('setref <appName> <ref>') .description('Checks out the app at the passed ref') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._apps.setRef.bind(this._apps)) .allowUnknownOption(allowUnknownOptions) this._commander .command('ref <appName>') .description('Prints the current ref of the passed app') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._apps.currentRef.bind(this._apps)) .allowUnknownOption(allowUnknownOptions) this._commander .command('web') .description('Starts guvnor-web as a managed process') .option('-u, --user <user>', 'The user to start a process as') .option('-g, --group <group>', 'The group to start a process as') .option('-d, --debug', 'Pause the process at the start of execution and wait for a debugger to be attached') .option('-v, --verbose', 'Prints detailed internal logging output') .action(this._processes.startWebMonitor.bind(this._processes)) .allowUnknownOption(allowUnknownOptions) this._commander .command('*') .description('') .action(this.unknown.bind(this)) .allowUnknownOption(allowUnknownOptions) var program = this._commander.parse(process.argv) // No command if (program.rawArgs.reduce(function (previous, current) { if (current.substring(0, 1) !== '-') { return previous + 1 } return previous }, 0) === 2) { this._processes.list() } } CLI.prototype._checkNodeVersion = function (callback) { var error if (!semver.satisfies(process.versions.node, '>=' + this._config.guvnor.minnodeversion)) { error = new Error('Please use node ' + this._config.guvnor.minnodeversion + ' or later') } callback(error) } CLI.prototype._checkGuvnorUser = function (callback) { if (this._user.name === this._config.guvnor.user) { return callback() } var guvnorGroup try { guvnorGroup = this._posix.getgrnam(this._config.guvnor.group) } catch (e) { if (e.message !== 'group id does not exist') { return callback(e) } this._logger.error('Guvnor has been configured to run as %s:%s but the %s group does not exist.', this._config.guvnor.user, this._config.guvnor.group, this._config.guvnor.group) this._logger.error('') this._logger.error('To fix this run guvnor as %s and it will attempt to create the %s group.', this._config.guvnor.user, this._config.guvnor.group) this._logger.error('') return callback(new Error(this._config.guvnor.group + ' group does not exist')) } if (guvnorGroup.members.indexOf(this._user.name) === -1) { this._logger.error('Guvnor has been configured to run as %s:%s but user %s is not in the group %s', this._config.guvnor.user, this._config.guvnor.group, this._user.name, this._config.guvnor.group) this._logger.error('') if (this._os.platform() === 'linux') { this._logger.error('To fix this run:') this._logger.error('') this._logger.error('$ sudo usermod -G %s -a %s', this._config.guvnor.group, this._user.name) this._logger.error('') } else if (this._os.platform() === 'darwin') { this._logger.error('To fix this run:') this._logger.error('') this._logger.error('$ sudo dscl . append /Groups/%s GroupMembership %s', this._config.guvnor.group, this._user.name) this._logger.error('') } else { this._logger.error('To fix this add % to the % group.', this._user.name, this._config.guvnor.group) this._logger.error('') } this._logger.error('Note you may need to log out and in again for the new group membership to take effect.') this._logger.error('') return callback(new Error('User is in the wrong group')) } this._running(function (running) { callback(running ? undefined : new Error('The daemon is not running. Please run this command as ' + this._config.guvnor.user + ' to start the daemon.')) }.bind(this)) } CLI.prototype._checkGuvnorGroup = function (callback) { try { this._posix.getgrnam(this._config.guvnor.group) return callback() } catch (error) { if (error.message === 'group id does not exist') { this._logger.warn("Guvnor has been configured to start with the group '%s' but that group does not exist", this._config.guvnor.group) this._prompt.start() this._prompt.get([{ name: "create group '" + this._config.guvnor.group + "' [Y/n]", default: 'Y' }], function (error) { if (error) return callback(error) this._logger.debug('Creating group', this._config.guvnor.group) // ugh http://www.greenend.org.uk/rjk/tech/useradd.html var command // linux try { command = this._execSync('which groupadd').toString().trim() } catch (e) { this._logger.debug('which groupadd failed') } if (command) { this._logger.debug('using groupadd', command) command = command.toString() + ' ' + this._config.guvnor.group this._logger.debug(command) try { this._execSync(command) } catch (e) { error = new Error('Automatically creating group ' + this._config.guvnor.group + ' failed, please create it manually') } return callback(error) } // mac os x try { command = this._execSync('which dscl').toString().trim() } catch (e) { this._logger.debug('which dscl failed') } if (command) { this._logger.debug('using dscl', command) // gids over 500 will appear in the system preferences window var gid = 500 async.series([function (callback) { this._child_process.exec(command + ' . -list /Groups PrimaryGroupID', function (error, stdout) { if (error) return callback(error) stdout.trim().split('\n').forEach(function (line) { line = line.replace(/\s+/g, ' ') var parts = line.split(' ') var existingGid = parseInt(parts[1], 10) if (gid === existingGid) { gid = existingGid + 1 } }) this._logger.debug(this._config.guvnor.group + ' will have gid ' + gid) callback() }.bind(this)) }.bind(this), function (callback) { this._child_process.exec(command + ' . create /Groups/' + this._config.guvnor.group, callback) }.bind(this), function (callback) { this._child_process.exec(command + ' . create /Groups/' + this._config.guvnor.group + ' name ' + this._config.guvnor.group, callback) }.bind(this), function (callback) { this._child_process.exec(command + ' . create /Groups/' + this._config.guvnor.group + ' passwd "*"', callback) }.bind(this), function (callback) { this._child_process.exec(command + ' . create /Groups/' + this._config.guvnor.group + ' gid ' + gid, callback) }.bind(this), function (callback) { this._fs.appendFile('/etc/group', this._config.guvnor.group + ':*:' + gid + ':\n', callback) }.bind(this) ], callback) return } callback(new Error('Automatically creating group ' + this._config.guvnor.group + ' failed, please create it manually')) }.bind(this)) } else { callback(error) } } } CLI.prototype.unknown = function () { this._logger.error("Please specify a known subcommand. See '" + this._commander.name() + " --help' for commands.") } module.exports = CLI