diff --git a/lib/cli.js b/lib/cli.js index 5c57efbf..0e7e23e4 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -109,7 +109,7 @@ program if (!options.provider) { this.missingArgument('--provider') } - if (!options.provider) { + if (!options.ports) { this.missingArgument('--ports') } diff --git a/lib/roles/device.js b/lib/roles/device.js index da3eba4e..70655d76 100644 --- a/lib/roles/device.js +++ b/lib/roles/device.js @@ -1,680 +1,35 @@ -var assert = require('assert') -var util = require('util') -var http = require('http') - -var Promise = require('bluebird') -var zmq = require('zmq') -var adbkit = require('adbkit') -var monkey = require('adbkit-monkey') -var request = Promise.promisifyAll(require('request')) -var httpProxy = require('http-proxy') -var split = require('split') +var syrup = require('syrup') var logger = require('../util/logger') -var wire = require('../wire') -var wireutil = require('../wire/util') -var wirerouter = require('../wire/router') -var devutil = require('../util/devutil') -var pathutil = require('../util/pathutil') -var promiseutil = require('../util/promiseutil') -var Vitals = require('../util/vitals') -var ChannelManager = require('../wire/channelmanager') -var keyutil = require('../util/keyutil') -var inputAgent = require('../services/inputagent') module.exports = function(options) { - var log = logger.createLogger('device') - , identity = Object.create(null) - , display = Object.create(null) - , vendor = Object.create(null) - , owner = null - , solo = wireutil.makePrivateChannel() - , channels = new ChannelManager() - , vitals = new Vitals() - , ports = { - http: 2870 - , input: 2820 - , stats: 2830 - , forward: 2810 - } - , services = { - input: null - , logcat: null - } - // Show serial number in logs logger.setGlobalIdentifier(options.serial) - // Output - var push = zmq.socket('push') - options.endpoints.push.forEach(function(endpoint) { - log.info('Sending output to %s', endpoint) - push.connect(endpoint) - }) - - // Panic if necessary - Promise.onPossiblyUnhandledRejection(function(err, promise) { - log.fatal('Unhandled rejection', err.stack) - selfDestruct() - }) - - // Forward all logs - logger.on('entry', function(entry) { - push.send([wireutil.log, - wireutil.makeDeviceLogMessage(options.serial, entry)]) - }) - - // Adb - var adb = adbkit.createClient() - - // Input - var sub = zmq.socket('sub') - options.endpoints.sub.forEach(function(endpoint) { - log.info('Receiving input from %s', endpoint) - sub.connect(endpoint) - }) - - // Establish always-on channels - ;[wireutil.global, solo].forEach(function(channel) { - log.info('Subscribing to permanent channel "%s"', channel) - sub.subscribe(channel) - channels.register(channel, Infinity) - }) - - // Unsubscribe from temporary channels when they timeout - channels.on('timeout', function(channel) { - log.info('Channel "%s" timed out', channel) - if (channel === owner.group) { - leaveGroup() - } - }) - - // Closure of vital functionality - vitals.on('end', function(name) { - log.fatal(util.format('Vital utility "%s" has ended', name)) - selfDestruct() - }) - - // Error in vital utility - vitals.on('error', function(name, err) { - log.fatal(util.format('Vital utility "%s" had an error', err.stack)) - selfDestruct() - }) - - promiseutil.periodicNotify(adb.waitBootComplete(options.serial), 1000) - .progressed(function() { - log.info('Waiting for boot to complete') - }) - .then(function() { - log.info('Gathering properties') - return adb.getProperties(options.serial) - }) - .then(function(properties) { - log.info('Solving identity') - return identity = devutil.makeIdentity(options.serial, properties) - }) - .then(function() { - vendor = devutil.vendorFiles(identity) - return Promise.all(Object.keys(vendor).map(function(id) { - var res = vendor[id] - log.info(util.format('Pushing vendor file "%s"', res.dest)) - return adb.push(options.serial, res.src, res.dest, res.mode) - .then(function(transfer) { - return new Promise(function(resolve, reject) { - transfer.on('error', reject) - transfer.on('end', resolve) - }) - }) - })) - }) - .then(function() { - log.info('Checking if any processes from a previous run are still up') - return Promise.all([ - devutil.killProcsByComm( - adb - , options.serial - , vendor.bin.comm - , vendor.bin.dest - ) - , devutil.killProcsByComm( - adb - , options.serial - , 'app_process' - , 'app_process' - ) - ]) - }) - .then(function() { - log.info('Launching HTTP API') - return devutil.ensureUnusedPort(adb, options.serial, 2870) - .then(function(port) { - var log = logger.createLogger('device:remote:http') - return adb.shell(options.serial, [ - vendor.bin.dest - , '--lib', vendor.lib.dest - , '--listen-http', port - ]) - .then(function(out) { - vitals.register('device:remote:http:shell', out) - out.pipe(split()) - .on('data', function(chunk) { - log.info(chunk) - }) - }) - .then(function() { - return devutil.waitForPort(adb, options.serial, port) - }) - .then(function(conn) { - var ours = options.ports.pop() - , everyones = options.ports.pop() - , url = util.format('http://127.0.0.1:%d', ours) - - // Don't need the connection - conn.end() - - log.info('Opening device HTTP API forwarder on "%s"', url) - - return adb.forward( - options.serial - , util.format('tcp:%d', ours) - , util.format('tcp:%d', port) - ) - .then(function() { - return request.getAsync({ - url: util.format('%s/api/v1/displays/0', url) - , json: true - }) - }) - .then(function(args) { - assert.ok('id' in args[1], 'Invalid response from HTTP API') - identity.display = args[1] - }) - .then(function() { - log.info( - 'Opening HTTP API proxy on "http://%s:%s"' - , options.publicIp - , everyones - ) - - var resolver = Promise.defer() - - function resolve() { - vitals.register('device:http:proxy', proxyServer) - resolver.resolve() - } - - function reject(err) { - resolver.reject(err) - } - - var proxy = httpProxy.createProxyServer({ - target: url - , ws: false - , xfwd: false - }) - - var proxyServer = http.createServer(proxy.web) - .listen(everyones) - - proxyServer.on('listening', resolve) - proxyServer.on('error', reject) - - return resolver.promise.finally(function() { - proxyServer.removeListener('listening', resolve) - proxyServer.removeListener('error', reject) - }) - }) - .then(function() { - identity.display.url = util.format( - 'http://%s:%s/api/v1/displays/0/screenshot.jpg' - , options.publicIp - , everyones - ) - }) - }) - }) - }) - .then(function() { - log.info('Launching InputAgent') - return devutil.ensureUnusedPort(adb, options.serial, 1090) - .then(function(port) { - var log = logger.createLogger('device:inputAgent') - return promiseutil.periodicNotify( - inputAgent.openAgent(adb, options.serial) - , 1000 - ) - .progressed(function() { - log.info('Waiting for InputAgent') - }) - .then(function(out) { - vitals.register('device:inputAgent:shell', out) - out.pipe(split()) - .on('data', function(chunk) { - log.info(chunk) - }) - return port - }) - }) - .then(function(port) { - return devutil.waitForPort(adb, options.serial, port) - }) - .then(function(conn) { - services.inputAgentSocket = vitals.register( - 'device:inputAgent:socket' - , conn - ) - }) - }) - .then(function(apk) { - log.info('Launching InputService') - return inputAgent.stopService(adb, options.serial) - .then(function() { - return devutil.waitForPortToFree(adb, options.serial, 1100) - }) - .then(function(port) { - var log = logger.createLogger('device:inputService') - return inputAgent.openService(adb, options.serial) - .then(function() { - return promiseutil.periodicNotify( - devutil.waitForPort(adb, options.serial, port) - , 1000 - ) - .progressed(function() { - log.info('Waiting for InputService') - }) - }) - }) - .then(function(conn) { - services.inputServiceSocket = vitals.register( - 'device:inputService:socket' - , conn - ) - }) - }) - .then(function() { - log.info('Launching TouchService') - return devutil.ensureUnusedPort(adb, options.serial, 2820) - .then(function(port) { - var log = logger.createLogger('device:remote:touch') - return adb.shell(options.serial, [ - vendor.bin.dest - , '--lib', vendor.lib.dest - , '--listen-input', port - ]) - .then(function(out) { - vitals.register('device:remote:touch:shell', out) - out.pipe(split()) - .on('data', function(chunk) { - log.info(chunk) - }) - return port - }) - }) - .then(function(port) { - return devutil.waitForPort(adb, options.serial, port) - }) - .then(function(conn) { - return monkey.connectStream(conn) - }) - .then(function(monkey) { - services.touch = vitals.register( - 'device:remote:touch:monkey' - , Promise.promisifyAll(monkey) - ) - }) - }) - .then(function() { - log.info('Launching stats service') - return devutil.ensureUnusedPort(adb, options.serial, 2830) - .then(function(port) { - var log = logger.createLogger('device:remote:stats') - return adb.shell(options.serial, [ - vendor.bin.dest - , '--lib', vendor.lib.dest - , '--listen-stats', port - ]) - .then(function(out) { - vitals.register('device:remote:stats:shell', out) - out.pipe(split()) - .on('data', function(chunk) { - log.info(chunk) - }) - }) - }) - }) - .then(function() { - log.info('Launching logcat service') - return adb.openLogcat(options.serial) - .then(function(logcat) { - services.logcat = vitals.register('device:logcat', logcat) - resetLogcat() - }) - }) - .then(function() { - log.info('Ready for instructions') - process.send('ready') - poke() - }) - .catch(function(err) { - log.fatal('Setup failed', err.stack) - selfDestruct() - }) - - sub.on('message', wirerouter() - .on('message', function(channel) { - channels.keepalive(channel) - }) - .on(wire.ProbeMessage, function(channel, message) { - push.send([wireutil.global, - wireutil.makeDeviceIdentityMessage(options.serial, identity)]) - }) - .on(wire.GroupMessage, function(channel, message) { - var seq = 0 - if (devutil.matchesRequirements(identity, message.requirements)) { - if (!isGrouped()) { - joinGroup(message.owner, message.timeout) - push.send([ - channel - , wireutil.envelope(new wire.TransactionDoneMessage( - options.serial - , seq++ - , true - )) - ]) - } - else if (isOwnedBy(message.owner)) { - push.send([ - channel - , wireutil.envelope(new wire.TransactionDoneMessage( - options.serial - , seq++ - , true - )) - ]) - } - else { - push.send([ - channel - , wireutil.envelope(new wire.TransactionDoneMessage( - options.serial - , seq++ - , false - )) - ]) - } - } - }) - .on(wire.UngroupMessage, function(channel, message) { - var seq = 0 - if (devutil.matchesRequirements(identity, message.requirements)) { - if (isGrouped()) { - leaveGroup() - push.send([ - channel - , wireutil.envelope(new wire.TransactionDoneMessage( - options.serial - , seq++ - , true - )) - ]) - } - else { - push.send([ - channel - , wireutil.envelope(new wire.TransactionDoneMessage( - options.serial - , seq++ - , true - )) - ]) - } - } - }) - .on(wire.TouchDownMessage, function(channel, message) { - services.touch.touchDownAsync(message.x, message.y) - .catch(function(err) { - log.error('touchDown failed', err.stack) - }) - }) - .on(wire.TouchMoveMessage, function(channel, message) { - services.touch.touchMoveAsync(message.x, message.y) - .catch(function(err) { - log.error('touchMove failed', err.stack) - }) - }) - .on(wire.TouchUpMessage, function(channel, message) { - services.touch.touchUpAsync(message.x, message.y) - .catch(function(err) { - log.error('touchUp failed', err.stack) - }) - }) - .on(wire.TapMessage, function(channel, message) { - services.touch.tapAsync(message.x, message.y) - .catch(function(err) { - log.error('tap failed', err.stack) - }) - }) - .on(wire.KeyDownMessage, function(channel, message) { - inputAgent.sendInputEvent(services.inputAgentSocket, { - action: 0 - , keyCode: keyutil.unwire(message.keyCode) - }) - }) - .on(wire.KeyUpMessage, function(channel, message) { - inputAgent.sendInputEvent(services.inputAgentSocket, { - action: 1 - , keyCode: keyutil.unwire(message.keyCode) - }) - }) - .on(wire.KeyPressMessage, function(channel, message) { - inputAgent.sendInputEvent(services.inputAgentSocket, { - action: 2 - , keyCode: keyutil.unwire(message.keyCode) - }) - }) - .on(wire.TypeMessage, function(channel, message) { - inputAgent.sendInputEvent(services.inputAgentSocket, { - action: 3 - , keyCode: 0 - , text: message.text - }) - }) - .on(wire.LogcatApplyFiltersMessage, function(channel, message) { - resetLogcat() - message.filters.forEach(function(filter) { - services.logcat.include(filter.tag, filter.priority) - }) - }) - .on(wire.ShellCommandMessage, function(channel, message) { - var router = this - , seq = 0 - - log.info('Running shell command "%s"', message.command) - adb.shell(options.serial, message.command) - .then(function(stream) { - var resolver = Promise.defer() - , timer - - function keepAliveListener(channel, message) { - clearTimeout(timer) - timer = setTimeout(forceStop, message.timeout) + return syrup() + // We want to send logs before anything else start happening + .dependency(require('./device/plugins/logsender')) + .define(function(options) { + return syrup() + .dependency(require('./device/plugins/solo')) + .dependency(require('./device/plugins/heartbeat')) + .dependency(require('./device/plugins/display')) + .dependency(require('./device/plugins/http')) + .dependency(require('./device/plugins/input')) + .dependency(require('./device/plugins/logcat')) + .dependency(require('./device/plugins/shell')) + .dependency(require('./device/plugins/touch')) + .dependency(require('./device/plugins/owner')) + .define(function(options, solo) { + var log = logger.createLogger('device') + if (process.send) { + // Only if we have a parent process + process.send('ready') } - - function readableListener() { - var chunk - while (chunk = stream.read()) { - push.send([ - channel - , wireutil.envelope(new wire.TransactionProgressMessage( - options.serial - , seq++ - , chunk - )) - ]) - } - } - - function endListener() { - push.send([ - channel - , wireutil.envelope(new wire.TransactionDoneMessage( - options.serial - , seq++ - , true - )) - ]) - resolver.resolve() - } - - function errorListener(err) { - resolver.reject(err) - } - - function forceStop() { - stream.end() - } - - stream.on('readable', readableListener) - stream.on('end', endListener) - stream.on('error', errorListener) - - sub.subscribe(channel) - router.on(wire.ShellKeepAliveMessage, keepAliveListener) - - timer = setTimeout(forceStop, message.timeout) - - return resolver.promise.finally(function() { - stream.removeListener('readable', readableListener) - stream.removeListener('end', endListener) - stream.removeListener('error', errorListener) - sub.unsubscribe(channel) - router.removeListener(wire.ShellKeepAliveMessage, keepAliveListener) - clearTimeout(timer) - }) - }) - .error(function(err) { - log.error('Shell command "%s" failed due to "%s"' - , message.command, err.message) - push.send([ - channel - , wireutil.envelope(new wire.TransactionDoneMessage( - options.serial - , seq++ - , false - , err.message - )) - ]) + log.info('Fully operational') + return solo.poke() }) + .consume(options) }) - .handler()) - - function poke() { - push.send([ - wireutil.global - , wireutil.envelope(new wire.DevicePokeMessage( - options.serial - , solo - )) - ]) - } - - function isGrouped() { - return !!owner - } - - function resetLogcat() { - services.logcat - .resetFilters() - .excludeAll() - } - - function logcatListener(entry) { - push.send([ - owner.group - , wireutil.envelope(new wire.DeviceLogcatEntryMessage( - options.serial - , entry.date.getTime() / 1000 - , entry.pid - , entry.tid - , entry.priority - , entry.tag - , entry.message - )) - ]) - } - - function isOwnedBy(someOwner) { - return owner && owner.group == someOwner.group - } - - function joinGroup(newOwner, timeout) { - log.info('Now owned by "%s"', newOwner.email) - log.info('Subscribing to group channel "%s"', newOwner.group) - channels.register(newOwner.group, timeout) - sub.subscribe(newOwner.group) - push.send([ - wireutil.global - , wireutil.envelope(new wire.JoinGroupMessage( - options.serial - , newOwner - )) - ]) - services.logcat.on('entry', logcatListener) - inputAgent.acquireWakeLock(services.inputServiceSocket) - inputAgent.unlock(services.inputServiceSocket) - owner = newOwner - } - - function leaveGroup() { - log.info('No longer owned by "%s"', owner.email) - log.info('Unsubscribing from group channel "%s"', owner.group) - channels.unregister(owner.group) - sub.unsubscribe(owner.group) - push.send([ - wireutil.global - , wireutil.envelope(new wire.LeaveGroupMessage( - options.serial - , owner - )) - ]) - services.logcat.removeListener('entry', logcatListener) - inputAgent.releaseWakeLock(services.inputServiceSocket) - inputAgent.lock(services.inputServiceSocket) - owner = null - } - - function heartbeat() { - push.send([ - wireutil.heartbeat - , wireutil.envelope(new wire.DeviceHeartbeatMessage( - options.serial - )) - ]) - setTimeout(heartbeat, options.heartbeatInterval) - } - - function selfDestruct() { - process.exit(1) - } - - function gracefullyExit() { - if (isGrouped()) { - leaveGroup() - Promise.delay(500).then(gracefullyExit) - } - else { - log.info('Bye') - process.exit(0) - } - } - - process.on('SIGINT', function() { - gracefullyExit() - }) - - process.on('SIGTERM', function() { - gracefullyExit() - }) - - heartbeat() + .consume(options) } diff --git a/lib/roles/device/plugins/adb.js b/lib/roles/device/plugins/adb.js new file mode 100644 index 00000000..23bec729 --- /dev/null +++ b/lib/roles/device/plugins/adb.js @@ -0,0 +1,25 @@ +var syrup = require('syrup') + +var adbkit = require('adbkit') + +var logger = require('../../../util/logger') +var promiseutil = require('../../../util/promiseutil') + +module.exports = syrup() + .define(function(options) { + var log = logger.createLogger('device:plugins:adb') + var adb = adbkit.createClient() + + function ensureBootComplete() { + return promiseutil.periodicNotify( + adb.waitBootComplete(options.serial) + , 1000 + ) + .progressed(function() { + log.info('Waiting for boot to complete') + }) + } + + return ensureBootComplete() + .return(adb) + }) diff --git a/lib/roles/device/plugins/channels.js b/lib/roles/device/plugins/channels.js new file mode 100644 index 00000000..f0eaf609 --- /dev/null +++ b/lib/roles/device/plugins/channels.js @@ -0,0 +1,14 @@ +var syrup = require('syrup') + +var logger = require('../../../util/logger') +var ChannelManager = require('../../../wire/channelmanager') + +module.exports = syrup() + .define(function(options, router) { + var log = logger.createLogger('device:plugins:channels') + var channels = new ChannelManager() + channels.on('timeout', function(channel) { + log.info('Channel "%s" timed out', channel) + }) + return channels + }) diff --git a/lib/roles/device/plugins/display.js b/lib/roles/device/plugins/display.js new file mode 100644 index 00000000..9af6a71d --- /dev/null +++ b/lib/roles/device/plugins/display.js @@ -0,0 +1,16 @@ +var syrup = require('syrup') + +var logger = require('../../../util/logger') + +module.exports = syrup() + .dependency(require('./http')) + .define(function(options, http) { + var log = logger.createLogger('device:plugins:display') + + function fetch() { + log.info('Fetching display info') + return http.getDisplay(0) + } + + return fetch() + }) diff --git a/lib/roles/device/plugins/heartbeat.js b/lib/roles/device/plugins/heartbeat.js new file mode 100644 index 00000000..493a59cd --- /dev/null +++ b/lib/roles/device/plugins/heartbeat.js @@ -0,0 +1,20 @@ +var syrup = require('syrup') + +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') + +module.exports = syrup() + .dependency(require('./push')) + .define(function(options, push) { + function heartbeat() { + push.send([ + wireutil.heartbeat + , wireutil.envelope(new wire.DeviceHeartbeatMessage( + options.serial + )) + ]) + setTimeout(heartbeat, options.heartbeatInterval) + } + + heartbeat() + }) diff --git a/lib/roles/device/plugins/http.js b/lib/roles/device/plugins/http.js new file mode 100644 index 00000000..803d9eb4 --- /dev/null +++ b/lib/roles/device/plugins/http.js @@ -0,0 +1,144 @@ +var util = require('util') +var assert = require('assert') +var http = require('http') + +var Promise = require('bluebird') +var syrup = require('syrup') +var request = Promise.promisifyAll(require('request')) +var httpProxy = require('http-proxy') +var split = require('split') + +var logger = require('../../../util/logger') +var devutil = require('../../../util/devutil') + +module.exports = syrup() + .dependency(require('./adb')) + .dependency(require('./quit')) + .dependency(require('../resources/remote')) + .define(function(options, adb, quit, remote) { + var log = logger.createLogger('device:plugins:http') + + var service = { + port: 2870 + , privateUrl: null + , publicUrl: null + } + + function openService() { + log.info('Launching HTTP API') + return devutil.ensureUnusedPort(adb, options.serial, service.port) + .then(function() { + var log = logger.createLogger('device:remote:http') + return adb.shell(options.serial, [ + remote.bin + , '--lib', remote.lib + , '--listen-http', service.port + ]) + .then(function(out) { + out.pipe(split()) + .on('data', function(chunk) { + log.info('Remote says: "%s"', chunk) + }) + .on('error', function(err) { + log.fatal('Remote shell had an error', err.stack) + quit.fatal() + }) + .on('end', function() { + log.fatal('Remote shell ended') + quit.fatal() + }) + }) + .then(function() { + return devutil.waitForPort(adb, options.serial, service.port) + }) + .then(function(conn) { + var ours = options.ports.pop() + , everyones = options.ports.pop() + , url = util.format('http://127.0.0.1:%d', ours) + + // Don't need the connection + conn.end() + + log.info('Opening device HTTP API forwarder on "%s"', url) + + service.privateUrl = url + service.publicUrl = util.format( + 'http://%s:%s' + , options.publicIp + , everyones + ) + + return adb.forward( + options.serial + , util.format('tcp:%d', ours) + , util.format('tcp:%d', service.port) + ) + .then(function() { + log.info( + 'Opening HTTP API proxy on "http://%s:%s"' + , options.publicIp + , everyones + ) + + var resolver = Promise.defer() + + function resolve() { + proxyServer + .on('error', function(err) { + log.fatal('Proxy server had an error', err.stack) + quit.fatal() + }) + resolver.resolve() + } + + function reject(err) { + resolver.reject(err) + } + + var proxy = httpProxy.createProxyServer({ + target: url + , ws: false + , xfwd: false + }) + + var proxyServer = http.createServer(proxy.web) + .listen(everyones) + + proxyServer.on('listening', resolve) + proxyServer.on('error', reject) + + return resolver.promise.finally(function() { + proxyServer.removeListener('listening', resolve) + proxyServer.removeListener('error', reject) + }) + }) + }) + }) + } + + return openService() + .then(function() { + return { + getDisplay: function(id) { + return request.getAsync({ + url: util.format( + '%s/api/v1/displays/%d' + , service.privateUrl + , id + ) + , json: true + }) + .then(function(args) { + var display = args[1] + assert.ok('id' in display, 'Invalid response from HTTP API') + display.url = util.format( + '%s/api/v1/displays/%d/screenshot.jpg' + , service.publicUrl + , id + ) + return display + }) + } + } + }) + }) diff --git a/lib/roles/device/plugins/identity.js b/lib/roles/device/plugins/identity.js new file mode 100644 index 00000000..d65eaf3a --- /dev/null +++ b/lib/roles/device/plugins/identity.js @@ -0,0 +1,20 @@ +var syrup = require('syrup') + +var devutil = require('../../../util/devutil') +var logger = require('../../../util/logger') + +module.exports = syrup() + .dependency(require('./adb')) + .define(function(options, adb) { + var log = logger.createLogger('device:plugins:identity') + + function solve() { + log.info('Solving identity') + return adb.getProperties(options.serial) + .then(function(properties) { + return devutil.makeIdentity(options.serial, properties) + }) + } + + return solve() + }) diff --git a/lib/roles/device/plugins/input.js b/lib/roles/device/plugins/input.js new file mode 100644 index 00000000..ec87eed8 --- /dev/null +++ b/lib/roles/device/plugins/input.js @@ -0,0 +1,224 @@ +var util = require('util') + +var syrup = require('syrup') +var split = require('split') +var ByteBuffer = require('protobufjs/node_modules/bytebuffer') + +var wire = require('../../../wire') +var devutil = require('../../../util/devutil') +var keyutil = require('../../../util/keyutil') +var streamutil = require('../../../util/streamutil') +var logger = require('../../../util/logger') + +module.exports = syrup() + .dependency(require('./adb')) + .dependency(require('./router')) + .dependency(require('./quit')) + .dependency(require('../resources/inputagent')) + .define(function(options, adb, router, quit, apk) { + var log = logger.createLogger('device:plugins:input') + + var agent = { + socket: null + , port: 1090 + } + + var service = { + socket: null + , port: 1100 + , startAction: 'jp.co.cyberagent.stf.input.agent.InputService.ACTION_START' + , stopAction: 'jp.co.cyberagent.stf.input.agent.InputService.ACTION_STOP' + } + + function openAgent() { + log.info('Launching input agent') + return stopAgent() + .then(function() { + return devutil.ensureUnusedPort(adb, options.serial, agent.port) + }) + .then(function() { + return adb.shell(options.serial, util.format( + "export CLASSPATH='%s';" + + " exec app_process /system/bin '%s'" + , apk.path + , apk.main + )) + }) + .then(function(out) { + out.pipe(split()) + .on('data', function(chunk) { + log.info('Agent says: "%s"', chunk) + }) + .on('error', function(err) { + log.fatal('InputAgent shell had an error', err.stack) + quit.fatal() + }) + .on('end', function() { + log.fatal('InputAgent shell ended') + quit.fatal() + }) + }) + .then(function() { + return devutil.waitForPort(adb, options.serial, agent.port) + }) + .then(function(conn) { + agent.socket = conn + conn.on('error', function(err) { + log.fatal('InputAgent socket had an error', err.stack) + quit.fatal() + }) + conn.on('end', function() { + log.fatal('InputAgent socket ended') + quit.fatal() + }) + }) + } + + function stopAgent() { + return devutil.killProcsByComm( + adb + , options.serial + , 'app_process' + , 'app_process' + ) + } + + function callService(intent) { + return adb.shell(options.serial, util.format( + 'am startservice --user 0 %s' + , intent + )) + .then(function(out) { + return streamutil.findLine(out, /^Error/) + .finally(function() { + out.end() + }) + .then(function(line) { + if (line.indexOf('--user') !== -1) { + return adb.shell(options.serial, util.format( + 'am startservice %s' + , intent + )) + .then(function() { + return streamutil.findLine(out, /^Error/) + .finally(function() { + out.end() + }) + .then(function(line) { + throw new Error(util.format( + 'Service had an error: "%s"' + , line + )) + }) + .catch(streamutil.NoSuchLineError, function() { + return true + }) + }) + } + else { + throw new Error(util.format( + 'Service had an error: "%s"' + , line + )) + } + }) + .catch(streamutil.NoSuchLineError, function() { + return true + }) + }) + } + + function openService() { + log.info('Launching input service') + return stopService() + .then(function() { + return devutil.waitForPortToFree(adb, options.serial, service.port) + }) + .then(function() { + return callService(util.format("-a '%s'", service.startAction)) + }) + .then(function() { + return devutil.waitForPort(adb, options.serial, service.port) + }) + .then(function(conn) { + service.socket = conn + conn.on('error', function(err) { + log.fatal('InputService socket had an error', err.stack) + quit.fatal() + }) + conn.on('end', function() { + log.fatal('InputService socket ended') + quit.fatal() + }) + }) + } + + function stopService() { + return callService(util.format("-a '%s'", service.stopAction)) + } + + function sendInputEvent(event) { + var lengthBuffer = new ByteBuffer() + , messageBuffer = new resource.proto.InputEvent(event).encode() + + // Delimiter + lengthBuffer.writeVarint32(messageBuffer.length) + + agent.socket.write(Buffer.concat([ + lengthBuffer.toBuffer() + , messageBuffer.toBuffer() + ])) + } + + return openAgent() + .then(openService) + .then(function() { + router + .on(wire.KeyDownMessage, function(channel, message) { + sendInputEvent({ + action: 0 + , keyCode: keyutil.unwire(message.keyCode) + }) + }) + .on(wire.KeyUpMessage, function(channel, message) { + sendInputEvent({ + action: 1 + , keyCode: keyutil.unwire(message.keyCode) + }) + }) + .on(wire.KeyPressMessage, function(channel, message) { + sendInputEvent({ + action: 2 + , keyCode: keyutil.unwire(message.keyCode) + }) + }) + .on(wire.TypeMessage, function(channel, message) { + sendInputEvent({ + action: 3 + , keyCode: 0 + , text: message.text + }) + }) + + return { + unlock: function() { + service.socket.write('unlock\n') + } + , lock: function() { + service.socket.write('lock\n') + } + , acquireWakeLock: function() { + service.socket.write('acquire wake lock\n') + } + , releaseWakeLock: function() { + service.socket.write('release wake lock\n') + } + , identity: function() { + service.socket.write(util.format( + 'show identity %s\n' + , options.serial + )) + } + } + }) + }) diff --git a/lib/roles/device/plugins/logcat.js b/lib/roles/device/plugins/logcat.js new file mode 100644 index 00000000..514aa578 --- /dev/null +++ b/lib/roles/device/plugins/logcat.js @@ -0,0 +1,67 @@ +var syrup = require('syrup') + +var logger = require('../../../util/logger') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') + +module.exports = syrup() + .dependency(require('./adb')) + .dependency(require('./router')) + .dependency(require('./quit')) + .define(function(options, adb, router, quit) { + var log = logger.createLogger('device:plugins:logcat') + + function openService() { + log.info('Launching logcat service') + return adb.openLogcat(options.serial) + .then(function(logcat) { + return logcat + .on('error', function(err) { + log.fatal('Logcat had an error', err) + quit.fatal() + }) + .on('end', function() { + log.fatal('Logcat ended') + quit.fatal() + }) + }) + } + + return openService() + .then(function(logcat) { + function reset() { + logcat + .resetFilters() + .excludeAll() + } + + function entryListener(entry) { + push.send([ + owner.group + , wireutil.envelope(new wire.DeviceLogcatEntryMessage( + options.serial + , entry.date.getTime() / 1000 + , entry.pid + , entry.tid + , entry.priority + , entry.tag + , entry.message + )) + ]) + } + + logcat.on('entry', entryListener) + + router + .on(wire.LogcatApplyFiltersMessage, function(channel, message) { + reset() + message.filters.forEach(function(filter) { + logcat.include(filter.tag, filter.priority) + }) + }) + + reset() + + return logcat + }) + }) diff --git a/lib/roles/device/plugins/logsender.js b/lib/roles/device/plugins/logsender.js new file mode 100644 index 00000000..c2214868 --- /dev/null +++ b/lib/roles/device/plugins/logsender.js @@ -0,0 +1,27 @@ +var syrup = require('syrup') + +var logger = require('../../../util/logger') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') + +module.exports = syrup() + .dependency(require('./push')) + .define(function(options, push) { + // Forward all logs + logger.on('entry', function(entry) { + push.send([ + wireutil.log + , wireutil.envelope(new wire.DeviceLogMessage( + options.serial + , entry.timestamp / 1000 + , entry.priority + , entry.tag + , entry.pid + , entry.message + , entry.identifier + )) + ]) + }) + + return logger + }) diff --git a/lib/roles/device/plugins/owner.js b/lib/roles/device/plugins/owner.js new file mode 100644 index 00000000..bf06fa9e --- /dev/null +++ b/lib/roles/device/plugins/owner.js @@ -0,0 +1,142 @@ +var Promise = require('bluebird') +var syrup = require('syrup') + +var logger = require('../../../util/logger') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') +var devutil = require('../../../util/devutil') + +module.exports = syrup() + .dependency(require('./router')) + .dependency(require('./identity')) + .dependency(require('./push')) + .dependency(require('./sub')) + .dependency(require('./channels')) + .dependency(require('./input')) + .dependency(require('./quit')) + .define(function(options, router, identity, push, sub, channels, input, quit) { + var log = logger.createLogger('device:plugins:owner') + var owner = null + + function isGrouped() { + return !!owner + } + + function isOwnedBy(someOwner) { + return owner && owner.group == someOwner.group + } + + function joinGroup(newOwner, timeout) { + log.info('Now owned by "%s"', newOwner.email) + log.info('Subscribing to group channel "%s"', newOwner.group) + channels.register(newOwner.group, timeout) + sub.subscribe(newOwner.group) + push.send([ + wireutil.global + , wireutil.envelope(new wire.JoinGroupMessage( + options.serial + , newOwner + )) + ]) + input.acquireWakeLock(services.inputServiceSocket) + input.unlock(services.inputServiceSocket) + owner = newOwner + } + + function leaveGroup() { + log.info('No longer owned by "%s"', owner.email) + log.info('Unsubscribing from group channel "%s"', owner.group) + channels.unregister(owner.group) + sub.unsubscribe(owner.group) + push.send([ + wireutil.global + , wireutil.envelope(new wire.LeaveGroupMessage( + options.serial + , owner + )) + ]) + input.releaseWakeLock(services.inputServiceSocket) + input.lock(services.inputServiceSocket) + owner = null + } + + channels.on('timeout', function(channel) { + if (owner && channel === owner.group) { + leaveGroup() + } + }) + + router + .on(wire.GroupMessage, function(channel, message) { + var seq = 0 + if (devutil.matchesRequirements(identity, message.requirements)) { + if (!isGrouped()) { + joinGroup(message.owner, message.timeout) + push.send([ + channel + , wireutil.envelope(new wire.TransactionDoneMessage( + options.serial + , seq++ + , true + )) + ]) + } + else if (isOwnedBy(message.owner)) { + push.send([ + channel + , wireutil.envelope(new wire.TransactionDoneMessage( + options.serial + , seq++ + , true + )) + ]) + } + else { + push.send([ + channel + , wireutil.envelope(new wire.TransactionDoneMessage( + options.serial + , seq++ + , false + )) + ]) + } + } + }) + .on(wire.UngroupMessage, function(channel, message) { + var seq = 0 + if (devutil.matchesRequirements(identity, message.requirements)) { + if (isGrouped()) { + leaveGroup() + push.send([ + channel + , wireutil.envelope(new wire.TransactionDoneMessage( + options.serial + , seq++ + , true + )) + ]) + } + else { + push.send([ + channel + , wireutil.envelope(new wire.TransactionDoneMessage( + options.serial + , seq++ + , true + )) + ]) + } + } + }) + + quit.observe(function() { + if (isGrouped()) { + leaveGroup() + return Promise.delay(500) + } + else { + return true + } + }) + }) diff --git a/lib/roles/device/plugins/push.js b/lib/roles/device/plugins/push.js new file mode 100644 index 00000000..3660cbe9 --- /dev/null +++ b/lib/roles/device/plugins/push.js @@ -0,0 +1,19 @@ +var syrup = require('syrup') + +var zmq = require('zmq') + +var logger = require('../../../util/logger') + +module.exports = syrup() + .define(function(options) { + var log = logger.createLogger('device:plugins:push') + + // Output + var push = zmq.socket('push') + options.endpoints.push.forEach(function(endpoint) { + log.info('Sending output to %s', endpoint) + push.connect(endpoint) + }) + + return push + }) diff --git a/lib/roles/device/plugins/quit.js b/lib/roles/device/plugins/quit.js new file mode 100644 index 00000000..20fd6d9f --- /dev/null +++ b/lib/roles/device/plugins/quit.js @@ -0,0 +1,38 @@ +var Promise = require('bluebird') +var syrup = require('syrup') + +var logger = require('../../../util/logger') + +module.exports = syrup() + .define(function(options) { + var log = logger.createLogger('device:plugins:quit') + var cleanup = [] + + function graceful() { + log.info('Winding down for graceful exit') + + var wait = Promise.all(cleanup.map(function(fn) { + return fn() + })) + + return wait.then(function() { + process.exit(0) + }) + } + + function fatal() { + log.fatal('Shutting down due to fatal error') + process.exit(1) + } + + process.on('SIGINT', graceful) + process.on('SIGTERM', graceful) + + return { + graceful: graceful + , fatal: fatal + , observe: function(promise) { + cleanup.push(promise) + } + } + }) diff --git a/lib/roles/device/plugins/router.js b/lib/roles/device/plugins/router.js new file mode 100644 index 00000000..87c3cb2d --- /dev/null +++ b/lib/roles/device/plugins/router.js @@ -0,0 +1,15 @@ +var syrup = require('syrup') + +var wirerouter = require('../../../wire/router') + +module.exports = syrup() + .dependency(require('./sub')) + .dependency(require('./channels')) + .define(function(options, sub, channels) { + var router = wirerouter() + sub.on('message', router.handler()) + router.on('message', function(channel) { + channels.keepalive(channel) + }) + return router + }) diff --git a/lib/roles/device/plugins/shell.js b/lib/roles/device/plugins/shell.js new file mode 100644 index 00000000..b0606170 --- /dev/null +++ b/lib/roles/device/plugins/shell.js @@ -0,0 +1,96 @@ +var Promise = require('bluebird') +var syrup = require('syrup') + +var logger = require('../../../util/logger') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') + +module.exports = syrup() + .dependency(require('./adb')) + .dependency(require('./router')) + .dependency(require('./push')) + .dependency(require('./sub')) + .define(function(options, adb, router, push, sub) { + var log = logger.createLogger('device:plugins:shell') + + router.on(wire.ShellCommandMessage, function(channel, message) { + var seq = 0 + + log.info('Running shell command "%s"', message.command) + + adb.shell(options.serial, message.command) + .then(function(stream) { + var resolver = Promise.defer() + , timer + + function keepAliveListener(channel, message) { + clearTimeout(timer) + timer = setTimeout(forceStop, message.timeout) + } + + function readableListener() { + var chunk + while (chunk = stream.read()) { + push.send([ + channel + , wireutil.envelope(new wire.TransactionProgressMessage( + options.serial + , seq++ + , chunk + )) + ]) + } + } + + function endListener() { + push.send([ + channel + , wireutil.envelope(new wire.TransactionDoneMessage( + options.serial + , seq++ + , true + )) + ]) + resolver.resolve() + } + + function errorListener(err) { + resolver.reject(err) + } + + function forceStop() { + stream.end() + } + + stream.on('readable', readableListener) + stream.on('end', endListener) + stream.on('error', errorListener) + + sub.subscribe(channel) + router.on(wire.ShellKeepAliveMessage, keepAliveListener) + + timer = setTimeout(forceStop, message.timeout) + + return resolver.promise.finally(function() { + stream.removeListener('readable', readableListener) + stream.removeListener('end', endListener) + stream.removeListener('error', errorListener) + sub.unsubscribe(channel) + router.removeListener(wire.ShellKeepAliveMessage, keepAliveListener) + clearTimeout(timer) + }) + }) + .error(function(err) { + log.error('Shell command "%s" failed', message.command, err.stack) + push.send([ + channel + , wireutil.envelope(new wire.TransactionDoneMessage( + options.serial + , seq++ + , false + , err.message + )) + ]) + }) + }) + }) diff --git a/lib/roles/device/plugins/solo.js b/lib/roles/device/plugins/solo.js new file mode 100644 index 00000000..e8029570 --- /dev/null +++ b/lib/roles/device/plugins/solo.js @@ -0,0 +1,32 @@ +var syrup = require('syrup') + +var logger = require('../../../util/logger') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') + +module.exports = syrup() + .dependency(require('./sub')) + .dependency(require('./push')) + .dependency(require('./channels')) + .define(function(options, sub, push, channels) { + var log = logger.createLogger('device:plugins:solo') + var channel = wireutil.makePrivateChannel() + + log.info('Subscribing to permanent channel "%s"', channel) + sub.subscribe(channel) + channels.register(channel, Infinity) + + + return { + channel: channel + , poke: function() { + push.send([ + wireutil.global + , wireutil.envelope(new wire.DevicePokeMessage( + options.serial + , channel + )) + ]) + } + } + }) diff --git a/lib/roles/device/plugins/stats.js b/lib/roles/device/plugins/stats.js new file mode 100644 index 00000000..c0bc24a8 --- /dev/null +++ b/lib/roles/device/plugins/stats.js @@ -0,0 +1,61 @@ +var syrup = require('syrup') +var split = require('split') + +var logger = require('../../../util/logger') +var devutil = require('../../../util/devutil') + +module.exports = syrup() + .dependency(require('./adb')) + .dependency(require('./quit')) + .dependency(require('../resources/remote')) + .define(function(options, adb, quit, remote) { + var log = logger.createLogger('device:plugins:stats') + + var service = { + port: 2830 + } + + function openService() { + return devutil.ensureUnusedPort(adb, options.serial, service.port) + .then(function(port) { + return adb.shell(options.serial, [ + remote.bin + , '--lib', remote.lib + , '--listen-stats', service.port + ]) + .then(function(out) { + out.pipe(split()) + .on('data', function(chunk) { + log.info('Remote says: "%s"', chunk) + }) + .on('error', function(err) { + log.fatal('Remote shell had an error', err.stack) + quit.fatal() + }) + .on('end', function() { + log.fatal('Remote shell ended') + quit.fatal() + }) + }) + }) + .then(function() { + return devutil.waitForPort(adb, options.serial, service.port) + }) + .then(function(conn) { + conn.pipe(split()) + .on('error', function(err) { + log.fatal('Remote had an error', err.stack) + quit.fatal() + }) + .on('end', function() { + log.fatal('Remote ended') + quit.fatal() + }) + }) + } + + return openService() + .then(function() { + return {} + }) + }) diff --git a/lib/roles/device/plugins/sub.js b/lib/roles/device/plugins/sub.js new file mode 100644 index 00000000..b6f3ab30 --- /dev/null +++ b/lib/roles/device/plugins/sub.js @@ -0,0 +1,28 @@ +var syrup = require('syrup') + +var zmq = require('zmq') + +var logger = require('../../../util/logger') +var wireutil = require('../../../wire/util') + +module.exports = syrup() + .dependency(require('./channels')) + .define(function(options, channels) { + var log = logger.createLogger('device:plugins:sub') + + // Input + var sub = zmq.socket('sub') + options.endpoints.sub.forEach(function(endpoint) { + log.info('Receiving input from %s', endpoint) + sub.connect(endpoint) + }) + + // Establish always-on channels + ;[wireutil.global].forEach(function(channel) { + log.info('Subscribing to permanent channel "%s"', channel) + sub.subscribe(channel) + channels.register(channel, Infinity) + }) + + return sub + }) diff --git a/lib/roles/device/plugins/touch.js b/lib/roles/device/plugins/touch.js new file mode 100644 index 00000000..39fd4546 --- /dev/null +++ b/lib/roles/device/plugins/touch.js @@ -0,0 +1,95 @@ +var Promise = require('bluebird') +var syrup = require('syrup') +var split = require('split') +var monkey = require('adbkit-monkey') + +var wire = require('../../../wire') +var devutil = require('../../../util/devutil') +var logger = require('../../../util/logger') + +module.exports = syrup() + .dependency(require('./adb')) + .dependency(require('./router')) + .dependency(require('./quit')) + .dependency(require('../resources/remote')) + .define(function(options, adb, router, quit, remote) { + var log = logger.createLogger('device:plugins:touch') + + var service = { + port: 2820 + } + + function openService() { + log.info('Launching touch service') + return devutil.ensureUnusedPort(adb, options.serial, service.port) + .then(function() { + return adb.shell(options.serial, [ + remote.bin + , '--lib', remote.lib + , '--listen-input', service.port + ]) + }) + .then(function(out) { + out.pipe(split()) + .on('data', function(chunk) { + log.info('Remote says: "%s"', chunk) + }) + .on('error', function(err) { + log.fatal('Remote had an error', err.stack) + quit.fatal() + }) + .on('end', function() { + log.fatal('Remote ended') + quit.fatal() + }) + }) + .then(function() { + return devutil.waitForPort(adb, options.serial, service.port) + }) + .then(function(conn) { + return Promise.promisifyAll(monkey.connectStream(conn)) + }) + .then(function(monkey) { + monkey + .on('error', function(err) { + log.fatal('Monkey had an error', err.stack) + quit.fatal() + }) + .on('end', function() { + log.fatal('Monkey ended') + quit.fatal() + }) + }) + } + + return openService() + .then(function(monkey) { + router + .on(wire.TouchDownMessage, function(channel, message) { + monkey.touchDownAsync(message.x, message.y) + .catch(function(err) { + log.error('touchDown failed', err.stack) + }) + }) + .on(wire.TouchMoveMessage, function(channel, message) { + monkey.touchMoveAsync(message.x, message.y) + .catch(function(err) { + log.error('touchMove failed', err.stack) + }) + }) + .on(wire.TouchUpMessage, function(channel, message) { + monkey.touchUpAsync(message.x, message.y) + .catch(function(err) { + log.error('touchUp failed', err.stack) + }) + }) + .on(wire.TapMessage, function(channel, message) { + monkey.tapAsync(message.x, message.y) + .catch(function(err) { + log.error('tap failed', err.stack) + }) + }) + + return {} + }) + }) diff --git a/lib/roles/device/resources/inputagent.js b/lib/roles/device/resources/inputagent.js new file mode 100644 index 00000000..8969be8b --- /dev/null +++ b/lib/roles/device/resources/inputagent.js @@ -0,0 +1,83 @@ +var util = require('util') + +var syrup = require('syrup') +var ProtoBuf = require('protobufjs') +var semver = require('semver') + +var pathutil = require('../../../util/pathutil') +var streamutil = require('../../../util/streamutil') +var logger = require('../../../util/logger') + +module.exports = syrup() + .dependency(require('../plugins/adb')) + .define(function(options, adb) { + var log = logger.createLogger('device:resources:inputagent') + + var resource = { + requiredVersion: '~0.1.2' + , pkg: 'jp.co.cyberagent.stf.input.agent' + , main: 'jp.co.cyberagent.stf.input.agent.InputAgent' + , apk: pathutil.vendor('InputAgent/InputAgent.apk') + , proto: ProtoBuf.loadProtoFile( + pathutil.vendor('InputAgent/proto/agent.proto') + ).build().jp.co.cyberagent.stf.input.agent.proto + } + + function getPath() { + return adb.shell(options.serial, ['pm', 'path', resource.pkg]) + .then(function(out) { + return streamutil.findLine(out, (/^package:/)) + .then(function(line) { + return line.substr(8) + }) + }) + } + + function install() { + log.info('Checking whether we need to install InputAgent.apk') + return getPath() + .then(function(installedPath) { + log.info('Running version check') + return adb.shell(options.serial, util.format( + "export CLASSPATH='%s';" + + " exec app_process /system/bin '%s' --version" + , installedPath + , resource.main + )) + .then(function(out) { + return streamutil.readAll(out) + .timeout(10000) + .then(function(buffer) { + var version = buffer.toString() + if (semver.satisfies(version, resource.requiredVersion)) { + return installedPath + } + else { + throw new Error(util.format( + 'Incompatible version %s' + , version + )) + } + }) + }) + }) + .catch(function(err) { + log.info('Installing InputAgent.apk') + return adb.install(options.serial, resource.apk) + .then(function() { + return getPath() + }) + }) + } + + return install() + .then(function(path) { + log.info('InputAgent.apk up to date') + return { + path: path + , pkg: resource.pkg + , main: resource.main + , proto: resource.proto + } + }) + }) diff --git a/lib/roles/device/resources/remote.js b/lib/roles/device/resources/remote.js new file mode 100644 index 00000000..a5afd566 --- /dev/null +++ b/lib/roles/device/resources/remote.js @@ -0,0 +1,85 @@ +var util = require('util') + +var Promise = require('bluebird') +var syrup = require('syrup') + +var logger = require('../../../util/logger') +var pathutil = require('../../../util/pathutil') +var devutil = require('../../../util/devutil') +var streamutil = require('../../../util/streamutil') + +module.exports = syrup() + .dependency(require('../plugins/adb')) + .dependency(require('../plugins/identity')) + .define(function(options, adb, identity) { + var log = logger.createLogger('device:resources:remote') + + var resources = { + bin: { + src: pathutil.vendor(util.format( + 'remote/libs/%s/remote', identity.abi)) + , dest: '/data/local/tmp/remote' + , comm: 'remote' + , mode: 0755 + } + , lib: { + src: pathutil.vendor(util.format( + 'remote/external/android-%d/remote_external.so', identity.sdk)) + , dest: '/data/local/tmp/remote_external.so' + , mode: 0755 + } + } + + function installResource(res) { + return adb.push(options.serial, res.src, res.dest, res.mode) + .then(function(transfer) { + return new Promise(function(resolve, reject) { + transfer.on('error', reject) + transfer.on('end', resolve) + }) + }) + .return(res) + } + + function ensureNotBusy(res) { + return adb.shell(options.serial, [res.dest, '--help']) + .then(function(out) { + return streamutil.findLine(out, (/text file busy/i)) + .then(function(line) { + log.info('Binary is busy, will retry') + return Promise.delay(100) + }) + .then(function() { + return ensureNotBusy(res) + }) + .catch(streamutil.NoSuchLineError, function() { + return res + }) + }) + } + + function installAll() { + return Promise.all([ + installResource(resources.bin).then(ensureNotBusy) + , installResource(resources.lib) + ]) + } + + function stop() { + return devutil.killProcsByComm( + adb + , options.serial + , resources.bin.comm + , resources.bin.dest + ) + } + + return stop() + .then(installAll) + .then(function() { + return { + bin: resources.bin.dest + , lib: resources.lib.dest + } + }) + }) diff --git a/lib/util/devutil.js b/lib/util/devutil.js index f7e583a6..179e0924 100644 --- a/lib/util/devutil.js +++ b/lib/util/devutil.js @@ -6,7 +6,6 @@ var semver = require('semver') var minimatch = require('minimatch') var wire = require('../wire') -var pathutil = require('./pathutil') var devutil = module.exports = Object.create(null) @@ -46,24 +45,6 @@ devutil.matchesRequirements = function(capabilities, requirements) { }) } -devutil.vendorFiles = function(identity) { - return { - bin: { - src: pathutil.vendor(util.format( - 'remote/libs/%s/remote', identity.abi)) - , dest: '/data/local/tmp/remote' - , comm: 'remote' - , mode: 0755 - } - , lib: { - src: pathutil.vendor(util.format( - 'remote/external/android-%d/remote_external.so', identity.sdk)) - , dest: '/data/local/tmp/remote_external.so' - , mode: 0755 - } - } -} - devutil.ensureUnusedPort = function(adb, serial, port) { return adb.openTcp(serial, port) .then(function(conn) { @@ -99,6 +80,7 @@ devutil.waitForPortToFree = function(adb, serial, port) { } function errorListener(err) { + console.log('ERR', err) resolver.reject(err) } diff --git a/lib/wire/util.js b/lib/wire/util.js index bc1b4d59..a5bc7e9f 100644 --- a/lib/wire/util.js +++ b/lib/wire/util.js @@ -38,17 +38,6 @@ var wireutil = { ) .encodeNB() } -, makeDeviceLogMessage: function(serial, entry) { - return wireutil.envelope(new wire.DeviceLogMessage( - serial - , entry.timestamp / 1000 - , entry.priority - , entry.tag - , entry.pid - , entry.message - , entry.identifier - )) - } , makeDeviceIdentityMessage: function(serial, identity) { return wireutil.envelope(new wire.DeviceIdentityMessage( serial