diff --git a/lib/cli/doctor/index.js b/lib/cli/doctor/index.js index 596dc5d6..2fe3017e 100644 --- a/lib/cli/doctor/index.js +++ b/lib/cli/doctor/index.js @@ -7,5 +7,180 @@ module.exports.builder = function(yargs) { } module.exports.handler = function() { - return require('../../util/doctor').run() + var cp = require('child_process') + var os = require('os') + var util = require('util') + + var semver = require('semver') + var Promise = require('bluebird') + var zmq = require('zmq') + + var pkg = require('../../../package') + var log = require('../../util/logger').createLogger('cli:doctor') + + function CheckError() { + Error.captureStackTrace(this, this.constructor) + this.name = 'CheckError' + this.message = util.format.apply(util, arguments) + } + + util.inherits(CheckError, Error) + + function call(command, args, options) { + return new Promise(function(resolve, reject) { + var proc = cp.spawn(command, args, options) + var stdout = [] + + proc.stdout.on('readable', function() { + var chunk + while ((chunk = proc.stdout.read())) { + stdout.push(chunk) + } + }) + + proc.on('error', reject) + + proc.on('exit', function(code, signal) { + if (signal) { + reject(new CheckError('Exited with signal %s', signal)) + } + else if (code === 0) { + resolve(Buffer.concat(stdout).toString()) + } + else { + reject(new CheckError('Exited with status %s', code)) + } + }) + }) + } + + function check(label, fn) { + return Promise.try(function() { + function Check() { + } + + Check.prototype.call = function(command, args, options) { + return call(command, args, options).catch(function(err) { + if (err.code === 'ENOENT') { + throw new CheckError( + '%s is not installed (`%s` is missing)' + , label + , command + ) + } + + throw err + }) + } + + Check.prototype.extract = function(what, re) { + return function(input) { + return Promise.try(function() { + var match = re.exec(input) + if (!match) { + throw new CheckError(util.format('%s %s cannot be detected', label, what)) + } + return match[1] + }) + } + } + + Check.prototype.version = function(wantedVersion) { + return function(currentVersion) { + return Promise.try(function() { + log.info('Using %s %s', label, currentVersion) + var sanitizedVersion = currentVersion.replace(/~.*/, '') + return semver.satisfies(sanitizedVersion, wantedVersion) + }) + .then(function(satisfied) { + if (!satisfied) { + throw new CheckError( + '%s is currently %s but needs to be %s' + , label + , currentVersion + , wantedVersion + ) + } + }) + } + } + + return fn(new Check()) + .catch(CheckError, function(err) { + log.error(err.message) + }) + .catch(function(err) { + log.error('Unexpected error checking %s: %s', label, err) + }) + }) + } + + function checkOSArch() { + log.info('OS Arch: %s', os.arch()) + } + + function checkOSPlatform() { + log.info('OS Platform: %s', os.platform()) + if (os.platform() === 'win32') { + log.warn('STF has never been tested on Windows. Contributions are welcome!') + } + } + + function checkOSRelease() { + log.info('OS Platform: %s', os.release()) + } + + function checkNodeVersion() { + log.info('Using Node %s', process.versions.node) + } + + function checkLocalRethinkDBVersion() { + return check('RethinkDB', function(checker) { + return checker.call('rethinkdb', ['--version']) + .then(checker.extract('version', /rethinkdb ([^\s]+)/)) + .then(checker.version(pkg.externalDependencies.rethinkdb)) + }) + } + + function checkGraphicsMagick() { + return check('GraphicsMagick', function(checker) { + return checker.call('gm', ['-version']) + .then(checker.extract('version', /GraphicsMagick ([^\s]+)/)) + .then(checker.version(pkg.externalDependencies.gm)) + }) + } + + function checkZeroMQ() { + return check('ZeroMQ', function(checker) { + return checker.version(pkg.externalDependencies.zeromq)(zmq.version) + }) + } + + function checkProtoBuf() { + return check('ProtoBuf', function(checker) { + return checker.call('protoc', ['--version']) + .then(checker.extract('version', /libprotoc ([^\s]+)/)) + .then(checker.version(pkg.externalDependencies.protobuf)) + }) + } + + function checkADB() { + return check('ADB', function(checker) { + return checker.call('adb', ['version']) + .then(checker.extract('version', /Android Debug Bridge version ([^\s]+)/)) + .then(checker.version(pkg.externalDependencies.adb)) + }) + } + + return Promise.all([ + checkOSArch() + , checkOSPlatform() + , checkOSRelease() + , checkNodeVersion() + , checkLocalRethinkDBVersion() + , checkGraphicsMagick() + , checkZeroMQ() + , checkProtoBuf() + , checkADB() + ]) } diff --git a/lib/util/doctor.js b/lib/util/doctor.js deleted file mode 100644 index 476ff25d..00000000 --- a/lib/util/doctor.js +++ /dev/null @@ -1,189 +0,0 @@ -var os = require('os') -var semver = require('semver') -var childProcess = require('child_process') -var rethinkdbPkg = require('rethinkdb/package') -var zmq = require('zmq') - -var pkg = require('../../package') -var log = require('./logger').createLogger('util:doctor') - -var doctor = module.exports = Object.create(null) - -function unsupportedVersion(desc, ver, range, callback) { - if (!semver.satisfies(ver, range)) { - log.error( - 'Current %s version is not supported, it has to be %s' - , desc - , range - ) - if (callback) { - return callback() - } - } -} - -function execHasErrors(error, stderr) { - if (error) { - if (error.code === 'ENOENT') { - log.warn('Executable was not found') - } - else { - log.error(error) - } - return true - } - if (stderr) { - log.error('There was an error with: %s', stderr) - } - return false -} - -function execCommand(command, param, desc, match, callback) { - childProcess.execFile(command, [param], - function(error, stdout, stderr) { - if (!execHasErrors(error, stderr)) { - if (stdout) { - var result = stdout.replace(match, '$1') - if (result) { - log.info('%s %s', desc, result) - if (callback) { - return callback(result) - } - } - else { - log.error('There was an error with: %s', stdout) - } - } - } - } - ) -} - -doctor.checkOSArch = function() { - log.info('OS Arch: %s', os.arch()) -} - -doctor.checkOSPlatform = function() { - log.info('OS Platform: %s', os.platform()) - if (os.platform() === 'win32') { - log.warn('STF has never been tested on Windows. Contributions are welcome!') - } -} - -doctor.checkOSRelease = function() { - log.info('OS Platform: %s', os.release()) -} - -doctor.checkNodeVersion = function() { - log.info('Using Node %s', process.versions.node) - if (pkg.engineStrict) { - unsupportedVersion('Node', process.versions.node, pkg.engines.node) - } -} - -doctor.checkRethinkDBClient = function() { - log.info('Using RethinkDB client %s', rethinkdbPkg.version) -} - -doctor.checkLocalRethinkDBServer = function() { - execCommand( - 'rethinkdb' - , '--version' - , 'Local RethinkDB server' - , /rethinkdb (.*?) \(.*\)\n?/gm - , function(ver) { - unsupportedVersion( - 'Local RethinkDB server' - , ver - , pkg.externalDependencies.rethinkdb - ) - }) -} - -doctor.checkGraphicsMagick = function() { - execCommand( - 'gm' - , '-version' - , 'GraphicsMagick' - , /GraphicsMagick ((.|\n)*?) (.|\n)*/g - , function(ver) { - unsupportedVersion('GraphicsMagick', ver, pkg.externalDependencies.gm) - } - ) -} - -doctor.checkZeroMQ = function() { - log.info('Using ZeroMQ %s', zmq.version) - - unsupportedVersion('ZeroMQ', zmq.version, pkg.externalDependencies.zeromq) -} - -doctor.checkProtoBuf = function() { - execCommand( - 'protoc' - , '--version' - , 'ProtoBuf' - , /^libprotoc (.*)\n$/g - , function(ver) { - unsupportedVersion( - 'ProtoBuf' - , ver - , pkg.externalDependencies.protobuf - ) - }) -} - -doctor.checkADB = function() { - execCommand( - 'adb' - , 'version' - , 'Local ADB' - , /Android Debug Bridge version (\d+\.\d+\.\d+)(\n.*)?/g - , function(ver) { - unsupportedVersion('AD', ver, pkg.externalDependencies.adb) - } - ) -} - -doctor.run = function() { - // Check OS architecture - doctor.checkOSArch() - - // Check OS platform - doctor.checkOSPlatform() - - // Check OS release - doctor.checkOSRelease() - - // Check node version - doctor.checkNodeVersion() - - // Check rethinkdb client - doctor.checkRethinkDBClient() - - // Check local rethinkdb server - doctor.checkLocalRethinkDBServer() - - // Check graphicsmagick - doctor.checkGraphicsMagick() - - // Check zeromq - doctor.checkZeroMQ() - - // Check protobuf - doctor.checkProtoBuf() - - // Check adb - doctor.checkADB() - - // TODO: - // Check yasm - // Check pkg-config - // Check python2 - // Exit on errors - // Run on stf local - - // Only for stf local: - // Check if rethinkdb is running - // Check if adb server is running -}