diff --git a/lib/device.js b/lib/device.js new file mode 100644 index 00000000..5d251d8a --- /dev/null +++ b/lib/device.js @@ -0,0 +1,8 @@ +var assert = require('assert') + +assert.ok(process.env.ANDROID_SERIAL, + 'Missing environment variable ANDROID_SERIAL') + +var log = require('./util/logger') + .setGlobalIdentifier(process.env.ANDROID_SERIAL) + .createLogger('device') diff --git a/lib/provider.js b/lib/provider.js index 7144932e..c0d43dae 100644 --- a/lib/provider.js +++ b/lib/provider.js @@ -1,8 +1,12 @@ +var path = require('path') +var fork = require('child_process').fork + var adb = require('adbkit') var async = require('async') var log = require('./util/logger').createLogger('provider') var client = adb.createClient() +var workers = {} client.trackDevices(function(err, tracker) { if (err) { @@ -14,13 +18,117 @@ client.trackDevices(function(err, tracker) { tracker.on('add', function(device) { log.info('Found device "%s" (%s)', device.id, device.type) + maybeConnect(device) }) tracker.on('change', function(device) { log.info('Device "%s" is now "%s"', device.id, device.type) + maybeConnect(device) || maybeDisconnect(device) }) tracker.on('remove', function(device) { log.info('Lost device "%s" (%s)', device.id, device.type) + maybeDisconnect(device) }) }) + +function isConnectable(device) { + switch (device.type) { + case 'device': + case 'emulator': + return true + default: + return false + } +} + +function isConnected(device) { + return workers[device.id] && workers[device.id].proc +} + +function maybeConnect(device) { + if (isConnectable(device) && !isConnected(device)) { + log.debug('Spawning worker for device "%s"', device.id) + var proc = fork(path.join(__dirname, 'device'), { + env: { + ANDROID_SERIAL: device.id + } + }) + proc.on('error', function(err) { + log.error('Worker of device "%s" had an error: %s', + device.id, err.message) + }) + proc.on('exit', function(code, signal) { + var data = workers[device.id] + delete workers[device.id] + if (code === 0) { + log.info('Worker of device "%s" stopped cleanly', device.id) + } + else { + log.error('Worker of device "%s" had a dirty exit', device.id) + if (Date.now() - data.started < 10000) { + log.error('Worker of device "%s" failed in less than 10 seconds,' + + ' will not attempt to restart', device.id) + } + else { + log.info('Restarting worker of "%s"', device.id) + maybeConnect(device) + } + } + }) + workers[device.id] = { + device: device + , proc: proc + , started: Date.now() + } + return true + } + return false +} + +function maybeDisconnect(device) { + if (isConnected(device)) { + log.info('Releasing worker of %s', device.id) + gracefullyKillWorker(device.id, function() { /* noop */ }) + return true + } + return false +} + +function gracefullyKillWorker(id, done) { + var proc = workers[id] + , timer + + timer = setTimeout(function() { + log.error('Worker of "%s" did not stop in time', id) + proc.removeListener('exit', onExit) + proc.kill('SIGKILL') + done() + }, 10000) + + function onExit() { + clearTimeout(timer) + done() + } + + proc.once('exit', onExit) + proc.kill('SIGTERM') +} + +function gracefullyExit() { + log.info('Stopping all workers') + async.each(Object.keys(workers), gracefullyKillWorker, function(err) { + log.info('All cleaned up') + process.exit(0) + }) +} + +process.on('SIGINT', function(e) { + log.debug('Received SIGINT') + gracefullyExit() +}) + +process.on('SIGTERM', function(e) { + log.debug('Received SIGTERM') + gracefullyExit() +}) diff --git a/lib/util/logger.js b/lib/util/logger.js index 2fe9949e..5270a02e 100644 --- a/lib/util/logger.js +++ b/lib/util/logger.js @@ -1,15 +1,15 @@ var util = require('util') +var colors = require('colors') function Log(tag, stream) { this.tag = tag - this.stream = stream || process.stderr this.levels = { - DEBUG: 'DBG' - , VERBOSE: 'VRB' - , INFO: 'INF' - , WARNING: 'WRN' - , ERROR: 'ERR' - , FATAL: 'FTL' + DEBUG: 'DBG'.grey + , VERBOSE: 'VRB'.cyan + , INFO: 'INF'.green + , WARNING: 'WRN'.yellow + , ERROR: 'ERR'.red + , FATAL: 'FTL'.red } } @@ -38,23 +38,24 @@ Log.prototype.fatal = function() { } Log.prototype._format = function(priority, args) { - return util.format('%s %s/%s %d %s\n', - Log.globalPrefix, priority, this.tag, process.pid, + return util.format('%s/%s %d [%s] %s', + priority, this.tag, process.pid, Log.globalIdentifier, util.format.apply(util, args)) } Log.prototype._write = function(out) { - this.stream.write(out) + console.error(out) } -Log.globalPrefix = '*' +Log.globalIdentifier = '*' Log.createLogger = function(tag) { return new Log(tag) } -Log.setGlobalPrefix = function(prefix) { - Log.globalPrefix = prefix +Log.setGlobalIdentifier = function(identifier) { + Log.globalIdentifier = identifier + return Log } exports = module.exports = Log diff --git a/test/util/logger.js b/test/util/logger.js index 47a7bd59..a73e1908 100644 --- a/test/util/logger.js +++ b/test/util/logger.js @@ -9,8 +9,8 @@ describe('Logger', function() { expect(logger).itself.to.respondTo('createLogger') }) - it('should have a setGlobalPrefix method', function() { - expect(logger).itself.to.respondTo('setGlobalPrefix') + it('should have a setGlobalIdentifier method', function() { + expect(logger).itself.to.respondTo('setGlobalIdentifier') }) })