From 9617f52e9bb15bd020a8bb255306b3f39318696c Mon Sep 17 00:00:00 2001 From: Phap Date: Thu, 19 Sep 2019 22:55:46 -0400 Subject: [PATCH] used decaffeinate to convert coffeescript into javascript --- Gruntfile.js | 117 ++++ bench/sync/pull.js | 31 + src/adb.js | 24 + src/adb/auth.js | 106 ++++ src/adb/client.js | 492 +++++++++++++++ src/adb/command.js | 66 ++ src/adb/command/host-serial/forward.js | 36 ++ src/adb/command/host-serial/getdevicepath.js | 27 + src/adb/command/host-serial/getserialno.js | 27 + src/adb/command/host-serial/getstate.js | 27 + src/adb/command/host-serial/listforwards.js | 41 ++ src/adb/command/host-serial/waitfordevice.js | 36 ++ src/adb/command/host-transport/clear.js | 38 ++ src/adb/command/host-transport/framebuffer.js | 103 +++ src/adb/command/host-transport/getfeatures.js | 48 ++ src/adb/command/host-transport/getpackages.js | 48 ++ .../command/host-transport/getproperties.js | 48 ++ src/adb/command/host-transport/install.js | 39 ++ src/adb/command/host-transport/isinstalled.js | 35 ++ .../command/host-transport/listreverses.js | 41 ++ src/adb/command/host-transport/local.js | 26 + src/adb/command/host-transport/log.js | 26 + src/adb/command/host-transport/logcat.js | 34 + src/adb/command/host-transport/monkey.js | 52 ++ src/adb/command/host-transport/reboot.js | 27 + src/adb/command/host-transport/remount.js | 26 + src/adb/command/host-transport/reverse.js | 36 ++ src/adb/command/host-transport/root.js | 43 ++ src/adb/command/host-transport/screencap.js | 38 ++ src/adb/command/host-transport/shell.js | 29 + .../command/host-transport/startactivity.js | 171 +++++ .../command/host-transport/startservice.js | 22 + src/adb/command/host-transport/sync.js | 27 + src/adb/command/host-transport/tcp.js | 26 + src/adb/command/host-transport/tcpip.js | 43 ++ src/adb/command/host-transport/trackjdwp.js | 109 ++++ src/adb/command/host-transport/uninstall.js | 38 ++ src/adb/command/host-transport/usb.js | 43 ++ .../host-transport/waitbootcomplete.js | 32 + src/adb/command/host/connect.js | 47 ++ src/adb/command/host/devices.js | 46 ++ src/adb/command/host/deviceswithpaths.js | 47 ++ src/adb/command/host/disconnect.js | 46 ++ src/adb/command/host/kill.js | 26 + src/adb/command/host/trackdevices.js | 28 + src/adb/command/host/transport.js | 26 + src/adb/command/host/version.js | 33 + src/adb/connection.js | 92 +++ src/adb/dump.js | 16 + src/adb/framebuffer/rgbtransform.js | 59 ++ src/adb/keycode.js | 228 +++++++ src/adb/linetransform.js | 102 +++ src/adb/parser.js | 278 +++++++++ src/adb/proc/stat.js | 139 +++++ src/adb/protocol.js | 38 ++ src/adb/sync.js | 337 ++++++++++ src/adb/sync/entry.js | 27 + src/adb/sync/pulltransfer.js | 34 + src/adb/sync/pushtransfer.js | 42 ++ src/adb/sync/stats.js | 47 ++ src/adb/tcpusb/packet.js | 103 +++ src/adb/tcpusb/packetreader.js | 123 ++++ src/adb/tcpusb/rollingcounter.js | 20 + src/adb/tcpusb/server.js | 67 ++ src/adb/tcpusb/service.js | 182 ++++++ src/adb/tcpusb/servicemap.js | 46 ++ src/adb/tcpusb/socket.js | 283 +++++++++ src/adb/tracker.js | 87 +++ src/adb/util.js | 13 + src/cli.js | 63 ++ tasks/keycode.js | 63 ++ test/adb.js | 31 + test/adb/command/host-serial/waitfordevice.js | 64 ++ test/adb/command/host-transport/clear.js | 86 +++ .../adb/command/host-transport/framebuffer.js | 82 +++ .../adb/command/host-transport/getfeatures.js | 70 +++ .../adb/command/host-transport/getpackages.js | 78 +++ .../command/host-transport/getproperties.js | 72 +++ test/adb/command/host-transport/install.js | 84 +++ .../adb/command/host-transport/isinstalled.js | 76 +++ test/adb/command/host-transport/local.js | 55 ++ test/adb/command/host-transport/log.js | 42 ++ test/adb/command/host-transport/logcat.js | 92 +++ test/adb/command/host-transport/monkey.js | 61 ++ test/adb/command/host-transport/reboot.js | 47 ++ test/adb/command/host-transport/remount.js | 26 + test/adb/command/host-transport/root.js | 50 ++ test/adb/command/host-transport/screencap.js | 88 +++ test/adb/command/host-transport/shell.js | 73 +++ .../command/host-transport/startactivity.js | 590 ++++++++++++++++++ .../command/host-transport/startservice.js | 88 +++ test/adb/command/host-transport/sync.js | 26 + test/adb/command/host-transport/tcp.js | 55 ++ test/adb/command/host-transport/tcpip.js | 61 ++ test/adb/command/host-transport/uninstall.js | 135 ++++ test/adb/command/host-transport/usb.js | 49 ++ .../host-transport/waitbootcomplete.js | 87 +++ test/adb/command/host/connect.js | 78 +++ test/adb/command/host/disconnect.js | 62 ++ test/adb/command/host/version.js | 59 ++ test/adb/framebuffer/rgbtransform.js | 215 +++++++ test/adb/linetransform.js | 197 ++++++ test/adb/parser.js | 452 ++++++++++++++ test/adb/protocol.js | 88 +++ test/adb/sync.js | 254 ++++++++ test/adb/tracker.js | 200 ++++++ test/adb/util.js | 39 ++ test/mock/connection.js | 21 + test/mock/duplex.js | 33 + 109 files changed, 9032 insertions(+) create mode 100644 Gruntfile.js create mode 100644 bench/sync/pull.js create mode 100644 src/adb.js create mode 100644 src/adb/auth.js create mode 100644 src/adb/client.js create mode 100644 src/adb/command.js create mode 100644 src/adb/command/host-serial/forward.js create mode 100644 src/adb/command/host-serial/getdevicepath.js create mode 100644 src/adb/command/host-serial/getserialno.js create mode 100644 src/adb/command/host-serial/getstate.js create mode 100644 src/adb/command/host-serial/listforwards.js create mode 100644 src/adb/command/host-serial/waitfordevice.js create mode 100644 src/adb/command/host-transport/clear.js create mode 100644 src/adb/command/host-transport/framebuffer.js create mode 100644 src/adb/command/host-transport/getfeatures.js create mode 100644 src/adb/command/host-transport/getpackages.js create mode 100644 src/adb/command/host-transport/getproperties.js create mode 100644 src/adb/command/host-transport/install.js create mode 100644 src/adb/command/host-transport/isinstalled.js create mode 100644 src/adb/command/host-transport/listreverses.js create mode 100644 src/adb/command/host-transport/local.js create mode 100644 src/adb/command/host-transport/log.js create mode 100644 src/adb/command/host-transport/logcat.js create mode 100644 src/adb/command/host-transport/monkey.js create mode 100644 src/adb/command/host-transport/reboot.js create mode 100644 src/adb/command/host-transport/remount.js create mode 100644 src/adb/command/host-transport/reverse.js create mode 100644 src/adb/command/host-transport/root.js create mode 100644 src/adb/command/host-transport/screencap.js create mode 100644 src/adb/command/host-transport/shell.js create mode 100644 src/adb/command/host-transport/startactivity.js create mode 100644 src/adb/command/host-transport/startservice.js create mode 100644 src/adb/command/host-transport/sync.js create mode 100644 src/adb/command/host-transport/tcp.js create mode 100644 src/adb/command/host-transport/tcpip.js create mode 100644 src/adb/command/host-transport/trackjdwp.js create mode 100644 src/adb/command/host-transport/uninstall.js create mode 100644 src/adb/command/host-transport/usb.js create mode 100644 src/adb/command/host-transport/waitbootcomplete.js create mode 100644 src/adb/command/host/connect.js create mode 100644 src/adb/command/host/devices.js create mode 100644 src/adb/command/host/deviceswithpaths.js create mode 100644 src/adb/command/host/disconnect.js create mode 100644 src/adb/command/host/kill.js create mode 100644 src/adb/command/host/trackdevices.js create mode 100644 src/adb/command/host/transport.js create mode 100644 src/adb/command/host/version.js create mode 100644 src/adb/connection.js create mode 100644 src/adb/dump.js create mode 100644 src/adb/framebuffer/rgbtransform.js create mode 100644 src/adb/keycode.js create mode 100644 src/adb/linetransform.js create mode 100644 src/adb/parser.js create mode 100644 src/adb/proc/stat.js create mode 100644 src/adb/protocol.js create mode 100644 src/adb/sync.js create mode 100644 src/adb/sync/entry.js create mode 100644 src/adb/sync/pulltransfer.js create mode 100644 src/adb/sync/pushtransfer.js create mode 100644 src/adb/sync/stats.js create mode 100644 src/adb/tcpusb/packet.js create mode 100644 src/adb/tcpusb/packetreader.js create mode 100644 src/adb/tcpusb/rollingcounter.js create mode 100644 src/adb/tcpusb/server.js create mode 100644 src/adb/tcpusb/service.js create mode 100644 src/adb/tcpusb/servicemap.js create mode 100644 src/adb/tcpusb/socket.js create mode 100644 src/adb/tracker.js create mode 100644 src/adb/util.js create mode 100644 src/cli.js create mode 100644 tasks/keycode.js create mode 100644 test/adb.js create mode 100644 test/adb/command/host-serial/waitfordevice.js create mode 100644 test/adb/command/host-transport/clear.js create mode 100644 test/adb/command/host-transport/framebuffer.js create mode 100644 test/adb/command/host-transport/getfeatures.js create mode 100644 test/adb/command/host-transport/getpackages.js create mode 100644 test/adb/command/host-transport/getproperties.js create mode 100644 test/adb/command/host-transport/install.js create mode 100644 test/adb/command/host-transport/isinstalled.js create mode 100644 test/adb/command/host-transport/local.js create mode 100644 test/adb/command/host-transport/log.js create mode 100644 test/adb/command/host-transport/logcat.js create mode 100644 test/adb/command/host-transport/monkey.js create mode 100644 test/adb/command/host-transport/reboot.js create mode 100644 test/adb/command/host-transport/remount.js create mode 100644 test/adb/command/host-transport/root.js create mode 100644 test/adb/command/host-transport/screencap.js create mode 100644 test/adb/command/host-transport/shell.js create mode 100644 test/adb/command/host-transport/startactivity.js create mode 100644 test/adb/command/host-transport/startservice.js create mode 100644 test/adb/command/host-transport/sync.js create mode 100644 test/adb/command/host-transport/tcp.js create mode 100644 test/adb/command/host-transport/tcpip.js create mode 100644 test/adb/command/host-transport/uninstall.js create mode 100644 test/adb/command/host-transport/usb.js create mode 100644 test/adb/command/host-transport/waitbootcomplete.js create mode 100644 test/adb/command/host/connect.js create mode 100644 test/adb/command/host/disconnect.js create mode 100644 test/adb/command/host/version.js create mode 100644 test/adb/framebuffer/rgbtransform.js create mode 100644 test/adb/linetransform.js create mode 100644 test/adb/parser.js create mode 100644 test/adb/protocol.js create mode 100644 test/adb/sync.js create mode 100644 test/adb/tracker.js create mode 100644 test/adb/util.js create mode 100644 test/mock/connection.js create mode 100644 test/mock/duplex.js diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..492a9e17 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,117 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +module.exports = function(grunt) { + + grunt.initConfig({ + pkg: require('./package'), + coffee: { + src: { + options: { + bare: true, + noHeader: true + }, + expand: true, + cwd: 'src', + src: '**/*.coffee', + dest: 'lib', + ext: '.js' + }, + index: { + src: 'index.coffee', + dest: 'index.js' + } + }, + clean: { + lib: { + src: 'lib' + }, + index: { + src: 'index.js' + } + }, + coffeelint: { + options: { + indentation: { + level: 'ignore' + }, + no_backticks: { + level: 'ignore' + } + }, + src: { + src: '<%= coffee.src.cwd %>/<%= coffee.src.src %>' + }, + index: { + src: '<%= coffee.index.src %>' + }, + test: { + src: 'test/**/*.coffee' + }, + tasks: { + src: 'tasks/**/*.coffee' + }, + gruntfile: { + src: 'Gruntfile.coffee' + } + }, + jsonlint: { + packagejson: { + src: 'package.json' + } + }, + watch: { + src: { + files: '<%= coffee.src.cwd %>/<%= coffee.src.src %>', + tasks: ['coffeelint:src', 'test'] + }, + index: { + files: '<%= coffee.index.src %>', + tasks: ['coffeelint:index', 'test'] + }, + test: { + files: '<%= coffeelint.test.src %>', + tasks: ['coffeelint:test', 'test'] + }, + gruntfile: { + files: '<%= coffeelint.gruntfile.src %>', + tasks: ['coffeelint:gruntfile'] + }, + packagejson: { + files: '<%= jsonlint.packagejson.src %>', + tasks: ['jsonlint:packagejson'] + } + }, + exec: { + mocha: { + options: [ + '--compilers coffee:coffee-script/register', + '--reporter spec', + '--colors', + '--recursive' + ], + cmd: './node_modules/.bin/mocha <%= exec.mocha.options.join(" ") %>' + } + }, + keycode: { + generate: { + dest: 'src/adb/keycode.coffee' + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-coffee'); + grunt.loadNpmTasks('grunt-coffeelint'); + grunt.loadNpmTasks('grunt-jsonlint'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-notify'); + grunt.loadNpmTasks('grunt-exec'); + grunt.loadTasks('./tasks'); + + grunt.registerTask('test', ['jsonlint', 'coffeelint', 'exec:mocha']); + grunt.registerTask('build', ['coffee']); + return grunt.registerTask('default', ['test']); +}; diff --git a/bench/sync/pull.js b/bench/sync/pull.js new file mode 100644 index 00000000..7d6876c0 --- /dev/null +++ b/bench/sync/pull.js @@ -0,0 +1,31 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Bench = require('bench'); +const {spawn} = require('child_process'); + +const Adb = require('../..'); + +const deviceId = process.env.DEVICE_ID; + +module.exports = { + compareCount: 3, + compare: { + "pull /dev/graphics/fb0 using ADB CLI"(done) { + const proc = spawn('adb', + ['-s', deviceId, 'pull', '/dev/graphics/fb0', '/dev/null']); + return proc.stdout.on('end', done); + }, + "pull /dev/graphics/fb0 using client.pull()"(done) { + const client = Adb.createClient(); + return client.pull(deviceId, '/dev/graphics/fb0', function(err, stream) { + stream.resume(); + return stream.on('end', done); + }); + } + } +}; + +Bench.runMain(); diff --git a/src/adb.js b/src/adb.js new file mode 100644 index 00000000..a991100b --- /dev/null +++ b/src/adb.js @@ -0,0 +1,24 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require('./adb/client'); +const Keycode = require('./adb/keycode'); +const util = require('./adb/util'); + +class Adb { + static createClient(options) { + if (options == null) { options = {}; } + if (!options.host) { options.host = process.env.ADB_HOST; } + if (!options.port) { options.port = process.env.ADB_PORT; } + return new Client(options); + } +} + +Adb.Keycode = Keycode; + +Adb.util = util; + +module.exports = Adb; diff --git a/src/adb/auth.js b/src/adb/auth.js new file mode 100644 index 00000000..250ebd5a --- /dev/null +++ b/src/adb/auth.js @@ -0,0 +1,106 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Promise = require('bluebird'); +const forge = require('node-forge'); +const { + BigInteger +} = forge.jsbn; + +/* +The stucture of an ADB RSAPublicKey is as follows: + + *define RSANUMBYTES 256 // 2048 bit key length + *define RSANUMWORDS (RSANUMBYTES / sizeof(uint32_t)) + + typedef struct RSAPublicKey { + int len; // Length of n[] in number of uint32_t + uint32_t n0inv; // -1 / n[0] mod 2^32 + uint32_t n[RSANUMWORDS]; // modulus as little endian array + uint32_t rr[RSANUMWORDS]; // R^2 as little endian array + int exponent; // 3 or 65537 + } RSAPublicKey; + +*/ +var Auth = (function() { + let RE = undefined; + let readPublicKeyFromStruct = undefined; + Auth = class Auth { + static initClass() { + // coffeelint: disable=max_line_length + RE = /^((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)\0?( .*|)\s*$/; + // coffeelint: enable=max_line_length + + readPublicKeyFromStruct = function(struct, comment) { + if (!struct.length) { throw new Error("Invalid public key"); } + + // Keep track of what we've read already + let offset = 0; + + // Get len + const len = struct.readUInt32LE(offset) * 4; + offset += 4; + + if (struct.length !== (4 + 4 + len + len + 4)) { + throw new Error("Invalid public key"); + } + + // Skip n0inv, we don't need it + offset += 4; + + // Get n + const n = new Buffer(len); + struct.copy(n, 0, offset, offset + len); + [].reverse.call(n); + offset += len; + + // Skip rr, we don't need it + offset += len; + + // Get e + const e = struct.readUInt32LE(offset); + + if ((e !== 3) && (e !== 65537)) { + throw new Error(`Invalid exponent ${e}, only 3 and 65537 are supported`); + } + + // Restore the public key + const key = forge.pki.setRsaPublicKey( + new BigInteger(n.toString('hex'), 16), + new BigInteger(e.toString(), 10) + ); + + // It will be difficult to retrieve the fingerprint later as it's based + // on the complete struct data, so let's just extend the key with it. + const md = forge.md.md5.create(); + md.update(struct.toString('binary')); + key.fingerprint = md.digest().toHex().match(/../g).join(':'); + + // Expose comment for the same reason + key.comment = comment; + + return key; + }; +} + + static parsePublicKey(buffer) { + return new Promise(function(resolve, reject) { + let match; + if (match = RE.exec(buffer)) { + const struct = new Buffer(match[1], 'base64'); + const comment = match[2].trim(); + return resolve(readPublicKeyFromStruct(struct, comment)); + } else { + return reject(new Error("Unrecognizable public key format")); + } + }); +} +}; + Auth.initClass(); + return Auth; +})(); + +module.exports = Auth; diff --git a/src/adb/client.js b/src/adb/client.js new file mode 100644 index 00000000..a206177f --- /dev/null +++ b/src/adb/client.js @@ -0,0 +1,492 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Monkey = require('adbkit-monkey'); +const Logcat = require('adbkit-logcat'); +const Promise = require('bluebird'); +const debug = require('debug')('adb:client'); + +const Connection = require('./connection'); +const Sync = require('./sync'); +const Parser = require('./parser'); +const ProcStat = require('./proc/stat'); + +const HostVersionCommand = require('./command/host/version'); +const HostConnectCommand = require('./command/host/connect'); +const HostDevicesCommand = require('./command/host/devices'); +const HostDevicesWithPathsCommand = require('./command/host/deviceswithpaths'); +const HostDisconnectCommand = require('./command/host/disconnect'); +const HostTrackDevicesCommand = require('./command/host/trackdevices'); +const HostKillCommand = require('./command/host/kill'); +const HostTransportCommand = require('./command/host/transport'); + +const ClearCommand = require('./command/host-transport/clear'); +const FrameBufferCommand = require('./command/host-transport/framebuffer'); +const GetFeaturesCommand = require('./command/host-transport/getfeatures'); +const GetPackagesCommand = require('./command/host-transport/getpackages'); +const GetPropertiesCommand = require('./command/host-transport/getproperties'); +const InstallCommand = require('./command/host-transport/install'); +const IsInstalledCommand = require('./command/host-transport/isinstalled'); +const ListReversesCommand = require('./command/host-transport/listreverses'); +const LocalCommand = require('./command/host-transport/local'); +const LogcatCommand = require('./command/host-transport/logcat'); +const LogCommand = require('./command/host-transport/log'); +const MonkeyCommand = require('./command/host-transport/monkey'); +const RebootCommand = require('./command/host-transport/reboot'); +const RemountCommand = require('./command/host-transport/remount'); +const RootCommand = require('./command/host-transport/root'); +const ReverseCommand = require('./command/host-transport/reverse'); +const ScreencapCommand = require('./command/host-transport/screencap'); +const ShellCommand = require('./command/host-transport/shell'); +const StartActivityCommand = require('./command/host-transport/startactivity'); +const StartServiceCommand = require('./command/host-transport/startservice'); +const SyncCommand = require('./command/host-transport/sync'); +const TcpCommand = require('./command/host-transport/tcp'); +const TcpIpCommand = require('./command/host-transport/tcpip'); +const TrackJdwpCommand = require('./command/host-transport/trackjdwp'); +const UninstallCommand = require('./command/host-transport/uninstall'); +const UsbCommand = require('./command/host-transport/usb'); +const WaitBootCompleteCommand = require('./command/host-transport/waitbootcomplete'); + +const ForwardCommand = require('./command/host-serial/forward'); +const GetDevicePathCommand = require('./command/host-serial/getdevicepath'); +const GetSerialNoCommand = require('./command/host-serial/getserialno'); +const GetStateCommand = require('./command/host-serial/getstate'); +const ListForwardsCommand = require('./command/host-serial/listforwards'); +const WaitForDeviceCommand = require('./command/host-serial/waitfordevice'); + +const TcpUsbServer = require('./tcpusb/server'); + +var Client = (function() { + let NoUserOptionError = undefined; + Client = class Client { + static initClass() { + + NoUserOptionError = err => err.message.indexOf('--user') !== -1; + } + constructor(options) { + if (options == null) { options = {}; } + this.options = options; + if (!this.options.port) { this.options.port = 5037; } + if (!this.options.bin) { this.options.bin = 'adb'; } + } + + createTcpUsbBridge(serial, options) { + return new TcpUsbServer(this, serial, options); + } + + connection() { + let connectListener, errorListener; + const resolver = Promise.defer(); + var conn = new Connection(this.options) + .on('error', (errorListener = err => resolver.reject(err))).on('connect', (connectListener = () => resolver.resolve(conn))).connect(); + return resolver.promise.finally(function() { + conn.removeListener('error', errorListener); + return conn.removeListener('connect', connectListener); + }); + } + + version(callback) { + return this.connection() + .then(conn => new HostVersionCommand(conn) + .execute()).nodeify(callback); + } + + connect(host, port, callback) { + if (port == null) { port = 5555; } + if (typeof port === 'function') { + callback = port; + port = 5555; + } + if (host.indexOf(':') !== -1) { + [host, port] = Array.from(host.split(':', 2)); + } + return this.connection() + .then(conn => new HostConnectCommand(conn) + .execute(host, port)).nodeify(callback); + } + + disconnect(host, port, callback) { + if (port == null) { port = 5555; } + if (typeof port === 'function') { + callback = port; + port = 5555; + } + if (host.indexOf(':') !== -1) { + [host, port] = Array.from(host.split(':', 2)); + } + return this.connection() + .then(conn => new HostDisconnectCommand(conn) + .execute(host, port)).nodeify(callback); + } + + listDevices(callback) { + return this.connection() + .then(conn => new HostDevicesCommand(conn) + .execute()).nodeify(callback); + } + + listDevicesWithPaths(callback) { + return this.connection() + .then(conn => new HostDevicesWithPathsCommand(conn) + .execute()).nodeify(callback); + } + + trackDevices(callback) { + return this.connection() + .then(conn => new HostTrackDevicesCommand(conn) + .execute()).nodeify(callback); + } + + kill(callback) { + return this.connection() + .then(conn => new HostKillCommand(conn) + .execute()).nodeify(callback); + } + + getSerialNo(serial, callback) { + return this.connection() + .then(conn => new GetSerialNoCommand(conn) + .execute(serial)).nodeify(callback); + } + + getDevicePath(serial, callback) { + return this.connection() + .then(conn => new GetDevicePathCommand(conn) + .execute(serial)).nodeify(callback); + } + + getState(serial, callback) { + return this.connection() + .then(conn => new GetStateCommand(conn) + .execute(serial)).nodeify(callback); + } + + getProperties(serial, callback) { + return this.transport(serial) + .then(transport => new GetPropertiesCommand(transport) + .execute()).nodeify(callback); + } + + getFeatures(serial, callback) { + return this.transport(serial) + .then(transport => new GetFeaturesCommand(transport) + .execute()).nodeify(callback); + } + + getPackages(serial, callback) { + return this.transport(serial) + .then(transport => new GetPackagesCommand(transport) + .execute()).nodeify(callback); + } + + getDHCPIpAddress(serial, iface, callback) { + if (iface == null) { iface = 'wlan0'; } + if (typeof iface === 'function') { + callback = iface; + iface = 'wlan0'; + } + return this.getProperties(serial) + .then(function(properties) { + let ip; + if (ip = properties[`dhcp.${iface}.ipaddress`]) { return ip; } + throw new Error(`Unable to find ipaddress for '${iface}'`); + }); + } + + forward(serial, local, remote, callback) { + return this.connection() + .then(conn => new ForwardCommand(conn) + .execute(serial, local, remote)).nodeify(callback); + } + + listForwards(serial, callback) { + return this.connection() + .then(conn => new ListForwardsCommand(conn) + .execute(serial)).nodeify(callback); + } + + reverse(serial, remote, local, callback) { + return this.transport(serial) + .then(transport => new ReverseCommand(transport) + .execute(remote, local) + .nodeify(callback)); + } + + listReverses(serial, callback) { + return this.transport(serial) + .then(transport => new ListReversesCommand(transport) + .execute()).nodeify(callback); + } + + transport(serial, callback) { + return this.connection() + .then(conn => new HostTransportCommand(conn) + .execute(serial) + .return(conn)).nodeify(callback); + } + + shell(serial, command, callback) { + return this.transport(serial) + .then(transport => new ShellCommand(transport) + .execute(command)).nodeify(callback); + } + + reboot(serial, callback) { + return this.transport(serial) + .then(transport => new RebootCommand(transport) + .execute()).nodeify(callback); + } + + remount(serial, callback) { + return this.transport(serial) + .then(transport => new RemountCommand(transport) + .execute()).nodeify(callback); + } + + root(serial, callback) { + return this.transport(serial) + .then(transport => new RootCommand(transport) + .execute()).nodeify(callback); + } + + trackJdwp(serial, callback) { + return this.transport(serial) + .then(transport => new TrackJdwpCommand(transport) + .execute()).nodeify(callback); + } + + framebuffer(serial, format, callback) { + if (format == null) { format = 'raw'; } + if (typeof format === 'function') { + callback = format; + format = 'raw'; + } + return this.transport(serial) + .then(transport => new FrameBufferCommand(transport) + .execute(format)).nodeify(callback); + } + + screencap(serial, callback) { + return this.transport(serial) + .then(transport => { + return new ScreencapCommand(transport) + .execute() + .catch(err => { + debug(`Emulating screencap command due to '${err}'`); + return this.framebuffer(serial, 'png'); + }); + }).nodeify(callback); + } + + openLocal(serial, path, callback) { + return this.transport(serial) + .then(transport => new LocalCommand(transport) + .execute(path)).nodeify(callback); + } + + openLog(serial, name, callback) { + return this.transport(serial) + .then(transport => new LogCommand(transport) + .execute(name)).nodeify(callback); + } + + openTcp(serial, port, host, callback) { + if (typeof host === 'function') { + callback = host; + host = undefined; + } + return this.transport(serial) + .then(transport => new TcpCommand(transport) + .execute(port, host)).nodeify(callback); + } + + openMonkey(serial, port, callback) { + if (port == null) { port = 1080; } + if (typeof port === 'function') { + callback = port; + port = 1080; + } + var tryConnect = times => { + return this.openTcp(serial, port) + .then(stream => Monkey.connectStream(stream)).catch(function(err) { + if (times -= 1) { + debug(`Monkey can't be reached, trying ${times} more times`); + return Promise.delay(100) + .then(() => tryConnect(times)); + } else { + throw err; + } + }); + }; + return tryConnect(1) + .catch(err => { + return this.transport(serial) + .then(transport => new MonkeyCommand(transport) + .execute(port)).then(out => tryConnect(20) + .then(monkey => monkey.once('end', () => out.end()))); + }).nodeify(callback); + } + + openLogcat(serial, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + return this.transport(serial) + .then(transport => new LogcatCommand(transport) + .execute(options)).then(stream => Logcat.readStream(stream, + {fixLineFeeds: false})).nodeify(callback); + } + + openProcStat(serial, callback) { + return this.syncService(serial) + .then(sync => new ProcStat(sync)).nodeify(callback); + } + + clear(serial, pkg, callback) { + return this.transport(serial) + .then(transport => new ClearCommand(transport) + .execute(pkg)).nodeify(callback); + } + + install(serial, apk, callback) { + const temp = Sync.temp(typeof apk === 'string' ? apk : '_stream.apk'); + return this.push(serial, apk, temp) + .then(transfer => { + let endListener, errorListener; + const resolver = Promise.defer(); + + transfer.on('error', (errorListener = err => resolver.reject(err)) + ); + + transfer.on('end', (endListener = () => { + return resolver.resolve(this.installRemote(serial, temp)); + }) + ); + + return resolver.promise.finally(function() { + transfer.removeListener('error', errorListener); + return transfer.removeListener('end', endListener); + }); + }).nodeify(callback); + } + + installRemote(serial, apk, callback) { + return this.transport(serial) + .then(transport => { + return new InstallCommand(transport) + .execute(apk) + .then(() => { + return this.shell(serial, ['rm', '-f', apk]); + }) + .then(stream => new Parser(stream) + .readAll()).then(out => true); + }).nodeify(callback); + } + + uninstall(serial, pkg, callback) { + return this.transport(serial) + .then(transport => new UninstallCommand(transport) + .execute(pkg)).nodeify(callback); + } + + isInstalled(serial, pkg, callback) { + return this.transport(serial) + .then(transport => new IsInstalledCommand(transport) + .execute(pkg)).nodeify(callback); + } + + startActivity(serial, options, callback) { + return this.transport(serial) + .then(transport => new StartActivityCommand(transport) + .execute(options)).catch(NoUserOptionError, () => { + options.user = null; + return this.startActivity(serial, options); + }).nodeify(callback); + } + + startService(serial, options, callback) { + return this.transport(serial) + .then(function(transport) { + if (!options.user && (options.user !== null)) { options.user = 0; } + return new StartServiceCommand(transport) + .execute(options);}).catch(NoUserOptionError, () => { + options.user = null; + return this.startService(serial, options); + }).nodeify(callback); + } + + syncService(serial, callback) { + return this.transport(serial) + .then(transport => new SyncCommand(transport) + .execute()).nodeify(callback); + } + + stat(serial, path, callback) { + return this.syncService(serial) + .then(sync => sync.stat(path) + .finally(() => sync.end())).nodeify(callback); + } + + readdir(serial, path, callback) { + return this.syncService(serial) + .then(sync => sync.readdir(path) + .finally(() => sync.end())).nodeify(callback); + } + + pull(serial, path, callback) { + return this.syncService(serial) + .then(sync => sync.pull(path) + .on('end', () => sync.end())).nodeify(callback); + } + + push(serial, contents, path, mode, callback) { + if (typeof mode === 'function') { + callback = mode; + mode = undefined; + } + return this.syncService(serial) + .then(sync => sync.push(contents, path, mode) + .on('end', () => sync.end())).nodeify(callback); + } + + tcpip(serial, port, callback) { + if (port == null) { port = 5555; } + if (typeof port === 'function') { + callback = port; + port = 5555; + } + return this.transport(serial) + .then(transport => new TcpIpCommand(transport) + .execute(port)).nodeify(callback); + } + + usb(serial, callback) { + return this.transport(serial) + .then(transport => new UsbCommand(transport) + .execute()).nodeify(callback); + } + + waitBootComplete(serial, callback) { + return this.transport(serial) + .then(transport => new WaitBootCompleteCommand(transport) + .execute()).nodeify(callback); + } + + waitForDevice(serial, callback) { + return this.connection() + .then(conn => new WaitForDeviceCommand(conn) + .execute(serial)).nodeify(callback); + } + }; + Client.initClass(); + return Client; +})(); + +module.exports = Client; diff --git a/src/adb/command.js b/src/adb/command.js new file mode 100644 index 00000000..09b156a6 --- /dev/null +++ b/src/adb/command.js @@ -0,0 +1,66 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const debug = require('debug')('adb:command'); + +const Parser = require('./parser'); +const Protocol = require('./protocol'); + +var Command = (function() { + let RE_SQUOT = undefined; + let RE_ESCAPE = undefined; + Command = class Command { + static initClass() { + RE_SQUOT = /'/g; + RE_ESCAPE = /([$`\\!"])/g; + } + + constructor(connection) { + this.connection = connection; + this.parser = this.connection.parser; + this.protocol = Protocol; + } + + execute() { + throw new Exception('Missing implementation'); + } + + _send(data) { + const encoded = Protocol.encodeData(data); + debug(`Send '${encoded}'`); + this.connection.write(encoded); + return this; + } + + // Note that this is just for convenience, not security. + _escape(arg) { + switch (typeof arg) { + case 'number': + return arg; + default: + return "'" + arg.toString().replace(RE_SQUOT, "'\"'\"'") + "'"; + } + } + + // Note that this is just for convenience, not security. Also, for some + // incomprehensible reason, some Lenovo devices (e.g. Lenovo A806) behave + // differently when arguments are given inside single quotes. See + // https://github.com/openstf/stf/issues/471 for more information. So that's + // why we now use double quotes here. + _escapeCompat(arg) { + switch (typeof arg) { + case 'number': + return arg; + default: + return '"' + arg.toString().replace(RE_ESCAPE, '\\$1') + '"'; + } + } + }; + Command.initClass(); + return Command; +})(); + +module.exports = Command; diff --git a/src/adb/command/host-serial/forward.js b/src/adb/command/host-serial/forward.js new file mode 100644 index 00000000..d679cb7d --- /dev/null +++ b/src/adb/command/host-serial/forward.js @@ -0,0 +1,36 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class ForwardCommand extends Command { + execute(serial, local, remote) { + this._send(`host-serial:${serial}:forward:${local};${remote}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return true; + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = ForwardCommand; diff --git a/src/adb/command/host-serial/getdevicepath.js b/src/adb/command/host-serial/getdevicepath.js new file mode 100644 index 00000000..6480ed7d --- /dev/null +++ b/src/adb/command/host-serial/getdevicepath.js @@ -0,0 +1,27 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class GetDevicePathCommand extends Command { + execute(serial) { + this._send(`host-serial:${serial}:get-devpath`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readValue() + .then(value => value.toString()); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = GetDevicePathCommand; diff --git a/src/adb/command/host-serial/getserialno.js b/src/adb/command/host-serial/getserialno.js new file mode 100644 index 00000000..3fba17ee --- /dev/null +++ b/src/adb/command/host-serial/getserialno.js @@ -0,0 +1,27 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class GetSerialNoCommand extends Command { + execute(serial) { + this._send(`host-serial:${serial}:get-serialno`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readValue() + .then(value => value.toString()); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = GetSerialNoCommand; diff --git a/src/adb/command/host-serial/getstate.js b/src/adb/command/host-serial/getstate.js new file mode 100644 index 00000000..189f9399 --- /dev/null +++ b/src/adb/command/host-serial/getstate.js @@ -0,0 +1,27 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class GetStateCommand extends Command { + execute(serial) { + this._send(`host-serial:${serial}:get-state`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readValue() + .then(value => value.toString()); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = GetStateCommand; diff --git a/src/adb/command/host-serial/listforwards.js b/src/adb/command/host-serial/listforwards.js new file mode 100644 index 00000000..1e3f8c37 --- /dev/null +++ b/src/adb/command/host-serial/listforwards.js @@ -0,0 +1,41 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class ListForwardsCommand extends Command { + execute(serial) { + this._send(`host-serial:${serial}:list-forward`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readValue() + .then(value => { + return this._parseForwards(value); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _parseForwards(value) { + const forwards = []; + for (let forward of Array.from(value.toString().split('\n'))) { + if (forward) { + const [serial, local, remote] = Array.from(forward.split(/\s+/)); + forwards.push({serial, local, remote}); + } + } + return forwards; + } +} + +module.exports = ListForwardsCommand; diff --git a/src/adb/command/host-serial/waitfordevice.js b/src/adb/command/host-serial/waitfordevice.js new file mode 100644 index 00000000..3c1d2c77 --- /dev/null +++ b/src/adb/command/host-serial/waitfordevice.js @@ -0,0 +1,36 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class WaitForDeviceCommand extends Command { + execute(serial) { + this._send(`host-serial:${serial}:wait-for-any`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return serial; + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = WaitForDeviceCommand; diff --git a/src/adb/command/host-transport/clear.js b/src/adb/command/host-transport/clear.js new file mode 100644 index 00000000..2f744a54 --- /dev/null +++ b/src/adb/command/host-transport/clear.js @@ -0,0 +1,38 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class ClearCommand extends Command { + execute(pkg) { + this._send(`shell:pm clear ${pkg}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.searchLine(/^(Success|Failed)$/) + .finally(() => { + return this.parser.end(); + }).then(function(result) { + switch (result[0]) { + case 'Success': + return true; + case 'Failed': + // Unfortunately, the command may stall at this point and we + // have to kill the connection. + throw new Error(`Package '${pkg}' could not be cleared`); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = ClearCommand; diff --git a/src/adb/command/host-transport/framebuffer.js b/src/adb/command/host-transport/framebuffer.js new file mode 100644 index 00000000..a6cc9672 --- /dev/null +++ b/src/adb/command/host-transport/framebuffer.js @@ -0,0 +1,103 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Assert = require('assert'); +const {spawn} = require('child_process'); +const debug = require('debug')('adb:command:framebuffer'); + +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const RgbTransform = require('../../framebuffer/rgbtransform'); + +class FrameBufferCommand extends Command { + execute(format) { + this._send('framebuffer:'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readBytes(52) + .then(header => { + const meta = this._parseHeader(header); + switch (format) { + case 'raw': + var stream = this.parser.raw(); + stream.meta = meta; + return stream; + default: + stream = this._convert(meta, format); + stream.meta = meta; + return stream; + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _convert(meta, format, raw) { + debug(`Converting raw framebuffer stream into ${format.toUpperCase()}`); + switch (meta.format) { + case 'rgb': case 'rgba': + break; + // Known to be supported by GraphicsMagick + default: + debug(`Silently transforming '${meta.format}' into 'rgb' for \`gm\``); + var transform = new RgbTransform(meta); + meta.format = 'rgb'; + raw = this.parser.raw().pipe(transform); + } + const proc = spawn('gm', [ + 'convert', + '-size', + `${meta.width}x${meta.height}`, + `${meta.format}:-`, + `${format}:-` + ]); + raw.pipe(proc.stdin); + return proc.stdout; + } + + _parseHeader(header) { + const meta = {}; + let offset = 0; + meta.version = header.readUInt32LE(offset); + if (meta.version === 16) { + throw new Error('Old-style raw images are not supported'); + } + offset += 4; + meta.bpp = header.readUInt32LE(offset); + offset += 4; + meta.size = header.readUInt32LE(offset); + offset += 4; + meta.width = header.readUInt32LE(offset); + offset += 4; + meta.height = header.readUInt32LE(offset); + offset += 4; + meta.red_offset = header.readUInt32LE(offset); + offset += 4; + meta.red_length = header.readUInt32LE(offset); + offset += 4; + meta.blue_offset = header.readUInt32LE(offset); + offset += 4; + meta.blue_length = header.readUInt32LE(offset); + offset += 4; + meta.green_offset = header.readUInt32LE(offset); + offset += 4; + meta.green_length = header.readUInt32LE(offset); + offset += 4; + meta.alpha_offset = header.readUInt32LE(offset); + offset += 4; + meta.alpha_length = header.readUInt32LE(offset); + meta.format = meta.blue_offset === 0 ? 'bgr' : 'rgb'; + if ((meta.bpp === 32) || meta.alpha_length) { meta.format += 'a'; } + return meta; + } +} + +module.exports = FrameBufferCommand; diff --git a/src/adb/command/host-transport/getfeatures.js b/src/adb/command/host-transport/getfeatures.js new file mode 100644 index 00000000..dcde19b9 --- /dev/null +++ b/src/adb/command/host-transport/getfeatures.js @@ -0,0 +1,48 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +var GetFeaturesCommand = (function() { + let RE_FEATURE = undefined; + GetFeaturesCommand = class GetFeaturesCommand extends Command { + static initClass() { + RE_FEATURE = /^feature:(.*?)(?:=(.*?))?\r?$/gm; + } + + execute() { + this._send('shell:pm list features 2>/dev/null'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAll() + .then(data => { + return this._parseFeatures(data.toString()); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _parseFeatures(value) { + let match; + const features = {}; + while ((match = RE_FEATURE.exec(value))) { + features[match[1]] = match[2] || true; + } + return features; + } + }; + GetFeaturesCommand.initClass(); + return GetFeaturesCommand; +})(); + +module.exports = GetFeaturesCommand; diff --git a/src/adb/command/host-transport/getpackages.js b/src/adb/command/host-transport/getpackages.js new file mode 100644 index 00000000..ade4e753 --- /dev/null +++ b/src/adb/command/host-transport/getpackages.js @@ -0,0 +1,48 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +var GetPackagesCommand = (function() { + let RE_PACKAGE = undefined; + GetPackagesCommand = class GetPackagesCommand extends Command { + static initClass() { + RE_PACKAGE = /^package:(.*?)\r?$/gm; + } + + execute() { + this._send('shell:pm list packages 2>/dev/null'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAll() + .then(data => { + return this._parsePackages(data.toString()); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _parsePackages(value) { + let match; + const features = []; + while ((match = RE_PACKAGE.exec(value))) { + features.push(match[1]); + } + return features; + } + }; + GetPackagesCommand.initClass(); + return GetPackagesCommand; +})(); + +module.exports = GetPackagesCommand; diff --git a/src/adb/command/host-transport/getproperties.js b/src/adb/command/host-transport/getproperties.js new file mode 100644 index 00000000..48cca048 --- /dev/null +++ b/src/adb/command/host-transport/getproperties.js @@ -0,0 +1,48 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +var GetPropertiesCommand = (function() { + let RE_KEYVAL = undefined; + GetPropertiesCommand = class GetPropertiesCommand extends Command { + static initClass() { + RE_KEYVAL = /^\[([\s\S]*?)\]: \[([\s\S]*?)\]\r?$/gm; + } + + execute() { + this._send('shell:getprop'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAll() + .then(data => { + return this._parseProperties(data.toString()); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _parseProperties(value) { + let match; + const properties = {}; + while ((match = RE_KEYVAL.exec(value))) { + properties[match[1]] = match[2]; + } + return properties; + } + }; + GetPropertiesCommand.initClass(); + return GetPropertiesCommand; +})(); + +module.exports = GetPropertiesCommand; diff --git a/src/adb/command/host-transport/install.js b/src/adb/command/host-transport/install.js new file mode 100644 index 00000000..106801dc --- /dev/null +++ b/src/adb/command/host-transport/install.js @@ -0,0 +1,39 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class InstallCommand extends Command { + execute(apk) { + this._send(`shell:pm install -r ${this._escapeCompat(apk)}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.searchLine(/^(Success|Failure \[(.*?)\])$/) + .then(function(match) { + if (match[1] === 'Success') { + return true; + } else { + const code = match[2]; + const err = new Error(`${apk} could not be installed [${code}]`); + err.code = code; + throw err; + }}).finally(() => { + // Consume all remaining content to "naturally" close the + // connection. + return this.parser.readAll(); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = InstallCommand; diff --git a/src/adb/command/host-transport/isinstalled.js b/src/adb/command/host-transport/isinstalled.js new file mode 100644 index 00000000..c0e68665 --- /dev/null +++ b/src/adb/command/host-transport/isinstalled.js @@ -0,0 +1,35 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const Parser = require('../../parser'); + +class IsInstalledCommand extends Command { + execute(pkg) { + this._send(`shell:pm path ${pkg} 2>/dev/null`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAscii(8) + .then(reply => { + switch (reply) { + case 'package:': + return true; + default: + return this.parser.unexpected(reply, "'package:'"); + } + }).catch(Parser.PrematureEOFError, err => false); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = IsInstalledCommand; diff --git a/src/adb/command/host-transport/listreverses.js b/src/adb/command/host-transport/listreverses.js new file mode 100644 index 00000000..e2d1c825 --- /dev/null +++ b/src/adb/command/host-transport/listreverses.js @@ -0,0 +1,41 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class ListReversesCommand extends Command { + execute() { + this._send("reverse:list-forward"); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readValue() + .then(value => { + return this._parseReverses(value); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _parseReverses(value) { + const reverses = []; + for (let reverse of Array.from(value.toString().split('\n'))) { + if (reverse) { + const [serial, remote, local] = Array.from(reverse.split(/\s+/)); + reverses.push({remote, local}); + } + } + return reverses; + } +} + +module.exports = ListReversesCommand; diff --git a/src/adb/command/host-transport/local.js b/src/adb/command/host-transport/local.js new file mode 100644 index 00000000..14dc5bcb --- /dev/null +++ b/src/adb/command/host-transport/local.js @@ -0,0 +1,26 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class LocalCommand extends Command { + execute(path) { + this._send(/:/.test(path) ? path : `localfilesystem:${path}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.raw(); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = LocalCommand; diff --git a/src/adb/command/host-transport/log.js b/src/adb/command/host-transport/log.js new file mode 100644 index 00000000..a170d409 --- /dev/null +++ b/src/adb/command/host-transport/log.js @@ -0,0 +1,26 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class LogCommand extends Command { + execute(name) { + this._send(`log:${name}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.raw(); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = LogCommand; diff --git a/src/adb/command/host-transport/logcat.js b/src/adb/command/host-transport/logcat.js new file mode 100644 index 00000000..f184586a --- /dev/null +++ b/src/adb/command/host-transport/logcat.js @@ -0,0 +1,34 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const LineTransform = require('../../linetransform'); + +class LogcatCommand extends Command { + execute(options) { + // For some reason, LG G Flex requires a filter spec with the -B option. + // It doesn't actually use it, though. Regardless of the spec we always get + // all events on all devices. + if (options == null) { options = {}; } + let cmd = 'logcat -B *:I 2>/dev/null'; + if (options.clear) { cmd = `logcat -c 2>/dev/null && ${cmd}`; } + this._send(`shell:echo && ${cmd}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.raw().pipe(new LineTransform({autoDetect: true})); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = LogcatCommand; diff --git a/src/adb/command/host-transport/monkey.js b/src/adb/command/host-transport/monkey.js new file mode 100644 index 00000000..0d867c09 --- /dev/null +++ b/src/adb/command/host-transport/monkey.js @@ -0,0 +1,52 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Promise = require('bluebird'); + +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class MonkeyCommand extends Command { + execute(port) { + // Some devices have broken /sdcard (i.e. /mnt/sdcard), which monkey will + // attempt to use to write log files to. We can cheat and set the location + // with an environment variable, because most logs use + // Environment.getLegacyExternalStorageDirectory() like they should. There + // are some hardcoded logs, though. Anyway, this should enable most things. + // Check https://github.com/android/platform_frameworks_base/blob/master/ + // core/java/android/os/Environment.java for the variables. + this._send(`shell:EXTERNAL_STORAGE=/data/local/tmp monkey --port ${port} -v`); + + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + // The monkey command is a bit weird in that it doesn't look like + // it starts in daemon mode, but it actually does. So even though + // the command leaves the terminal "hanging", Ctrl-C (or just + // ending the connection) will not end the daemon. HOWEVER, on + // some devices, such as SO-02C by Sony, it is required to leave + // the command hanging around. In any case, if the command exits + // by itself, it means that something went wrong. + return this.parser.searchLine(/^:Monkey:/) + // On some devices (such as F-08D by Fujitsu), the monkey + // command gives no output no matter how many verbose flags you + // give it. So we use a fallback timeout. + .timeout(1000) + .then(() => { + return this.parser.raw(); + }).catch(Promise.TimeoutError, err => { + return this.parser.raw(); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = MonkeyCommand; diff --git a/src/adb/command/host-transport/reboot.js b/src/adb/command/host-transport/reboot.js new file mode 100644 index 00000000..74ff941e --- /dev/null +++ b/src/adb/command/host-transport/reboot.js @@ -0,0 +1,27 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class RebootCommand extends Command { + execute() { + this._send('reboot:'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAll() + .return(true); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = RebootCommand; diff --git a/src/adb/command/host-transport/remount.js b/src/adb/command/host-transport/remount.js new file mode 100644 index 00000000..a05089f7 --- /dev/null +++ b/src/adb/command/host-transport/remount.js @@ -0,0 +1,26 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class RemountCommand extends Command { + execute() { + this._send('remount:'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return true; + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = RemountCommand; diff --git a/src/adb/command/host-transport/reverse.js b/src/adb/command/host-transport/reverse.js new file mode 100644 index 00000000..73495727 --- /dev/null +++ b/src/adb/command/host-transport/reverse.js @@ -0,0 +1,36 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class ReverseCommand extends Command { + execute(remote, local) { + this._send(`reverse:forward:${remote};${local}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return true; + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = ReverseCommand; diff --git a/src/adb/command/host-transport/root.js b/src/adb/command/host-transport/root.js new file mode 100644 index 00000000..f1a88e99 --- /dev/null +++ b/src/adb/command/host-transport/root.js @@ -0,0 +1,43 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +var RootCommand = (function() { + let RE_OK = undefined; + RootCommand = class RootCommand extends Command { + static initClass() { + RE_OK = /restarting adbd as root/; + } + + execute() { + this._send('root:'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAll() + .then(function(value) { + if (RE_OK.test(value)) { + return true; + } else { + throw new Error(value.toString().trim()); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + }; + RootCommand.initClass(); + return RootCommand; +})(); + +module.exports = RootCommand; diff --git a/src/adb/command/host-transport/screencap.js b/src/adb/command/host-transport/screencap.js new file mode 100644 index 00000000..cbd40971 --- /dev/null +++ b/src/adb/command/host-transport/screencap.js @@ -0,0 +1,38 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Promise = require('bluebird'); + +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const Parser = require('../../parser'); +const LineTransform = require('../../linetransform'); + +class ScreencapCommand extends Command { + execute() { + this._send('shell:echo && screencap -p 2>/dev/null'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + var transform = new LineTransform; + return this.parser.readBytes(1) + .then(chunk => { + transform = new LineTransform({autoDetect: true}); + transform.write(chunk); + return this.parser.raw().pipe(transform); + }).catch(Parser.PrematureEOFError, function() { + throw new Error('No support for the screencap command'); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = ScreencapCommand; diff --git a/src/adb/command/host-transport/shell.js b/src/adb/command/host-transport/shell.js new file mode 100644 index 00000000..299afbc3 --- /dev/null +++ b/src/adb/command/host-transport/shell.js @@ -0,0 +1,29 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class ShellCommand extends Command { + execute(command) { + if (Array.isArray(command)) { + command = command.map(this._escape).join(' '); + } + this._send(`shell:${command}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.raw(); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = ShellCommand; diff --git a/src/adb/command/host-transport/startactivity.js b/src/adb/command/host-transport/startactivity.js new file mode 100644 index 00000000..e00619ca --- /dev/null +++ b/src/adb/command/host-transport/startactivity.js @@ -0,0 +1,171 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const Parser = require('../../parser'); + +var StartActivityCommand = (function() { + let RE_ERROR = undefined; + let EXTRA_TYPES = undefined; + StartActivityCommand = class StartActivityCommand extends Command { + static initClass() { + RE_ERROR = /^Error: (.*)$/; + EXTRA_TYPES = { + string: 's', + null: 'sn', + bool: 'z', + int: 'i', + long: 'l', + float: 'l', + uri: 'u', + component: 'cn' + }; + } + + execute(options) { + const args = this._intentArgs(options); + if (options.debug) { + args.push('-D'); + } + if (options.wait) { + args.push('-W'); + } + if (options.user || (options.user === 0)) { + args.push('--user', this._escape(options.user)); + } + return this._run('start', args); + } + + _run(command, args) { + this._send(`shell:am ${command} ${args.join(' ')}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.searchLine(RE_ERROR) + .finally(() => { + return this.parser.end(); + }).then(function(match) { + throw new Error(match[1]);}) + .catch(Parser.PrematureEOFError, err => true); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _intentArgs(options) { + const args = []; + if (options.extras) { + args.push.apply(args, this._formatExtras(options.extras)); + } + if (options.action) { + args.push('-a', this._escape(options.action)); + } + if (options.data) { + args.push('-d', this._escape(options.data)); + } + if (options.mimeType) { + args.push('-t', this._escape(options.mimeType)); + } + if (options.category) { + if (Array.isArray(options.category)) { + options.category.forEach(category => { + return args.push('-c', this._escape(category)); + }); + } else { + args.push('-c', this._escape(options.category)); + } + } + if (options.component) { + args.push('-n', this._escape(options.component)); + } + if (options.flags) { + args.push('-f', this._escape(options.flags)); + } + return args; + } + + _formatExtras(extras) { + if (!extras) { return []; } + if (Array.isArray(extras)) { + return extras.reduce((all, extra) => { + return all.concat(this._formatLongExtra(extra)); + } + , []); + } else { + return Object.keys(extras).reduce((all, key) => { + return all.concat(this._formatShortExtra(key, extras[key])); + } + , []); + } + } + + _formatShortExtra(key, value) { + let sugared = + {key}; + if (value === null) { + sugared.type = 'null'; + } else if (Array.isArray(value)) { + throw new Error(`Refusing to format array value '${key}' using short \ +syntax; empty array would cause unpredictable results due to unknown \ +type. Please use long syntax instead.` + ); + } else { + switch (typeof value) { + case 'string': + sugared.type = 'string'; + sugared.value = value; + break; + case 'boolean': + sugared.type = 'bool'; + sugared.value = value; + break; + case 'number': + sugared.type = 'int'; + sugared.value = value; + break; + case 'object': + sugared = value; + sugared.key = key; + break; + } + } + return this._formatLongExtra(sugared); + } + + _formatLongExtra(extra) { + const args = []; + if (!extra.type) { extra.type = 'string'; } + const type = EXTRA_TYPES[extra.type]; + if (!type) { + throw new Error(`Unsupported type '${extra.type}' for extra \ +'${extra.key}'` + ); + } + if (extra.type === 'null') { + args.push(`--e${type}`); + args.push(this._escape(extra.key)); + } else if (Array.isArray(extra.value)) { + args.push(`--e${type}a`); + args.push(this._escape(extra.key)); + args.push(this._escape(extra.value.join(','))); + } else { + args.push(`--e${type}`); + args.push(this._escape(extra.key)); + args.push(this._escape(extra.value)); + } + return args; + } + }; + StartActivityCommand.initClass(); + return StartActivityCommand; +})(); + +module.exports = StartActivityCommand; diff --git a/src/adb/command/host-transport/startservice.js b/src/adb/command/host-transport/startservice.js new file mode 100644 index 00000000..ec558648 --- /dev/null +++ b/src/adb/command/host-transport/startservice.js @@ -0,0 +1,22 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const Parser = require('../../parser'); + +const StartActivityCommand = require('./startactivity'); + +class StartServiceCommand extends StartActivityCommand { + execute(options) { + const args = this._intentArgs(options); + if (options.user || (options.user === 0)) { + args.push('--user', this._escape(options.user)); + } + return this._run('startservice', args); + } +} + +module.exports = StartServiceCommand; diff --git a/src/adb/command/host-transport/sync.js b/src/adb/command/host-transport/sync.js new file mode 100644 index 00000000..b4fb9999 --- /dev/null +++ b/src/adb/command/host-transport/sync.js @@ -0,0 +1,27 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const Sync = require('../../sync'); + +class SyncCommand extends Command { + execute() { + this._send('sync:'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return new Sync(this.connection); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = SyncCommand; diff --git a/src/adb/command/host-transport/tcp.js b/src/adb/command/host-transport/tcp.js new file mode 100644 index 00000000..9a35af0b --- /dev/null +++ b/src/adb/command/host-transport/tcp.js @@ -0,0 +1,26 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class TcpCommand extends Command { + execute(port, host) { + this._send(`tcp:${port}` + (host ? `:${host}` : '')); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.raw(); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = TcpCommand; diff --git a/src/adb/command/host-transport/tcpip.js b/src/adb/command/host-transport/tcpip.js new file mode 100644 index 00000000..bb8f0c2f --- /dev/null +++ b/src/adb/command/host-transport/tcpip.js @@ -0,0 +1,43 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +var TcpIpCommand = (function() { + let RE_OK = undefined; + TcpIpCommand = class TcpIpCommand extends Command { + static initClass() { + RE_OK = /restarting in/; + } + + execute(port) { + this._send(`tcpip:${port}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAll() + .then(function(value) { + if (RE_OK.test(value)) { + return port; + } else { + throw new Error(value.toString().trim()); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + }; + TcpIpCommand.initClass(); + return TcpIpCommand; +})(); + +module.exports = TcpIpCommand; diff --git a/src/adb/command/host-transport/trackjdwp.js b/src/adb/command/host-transport/trackjdwp.js new file mode 100644 index 00000000..44951d87 --- /dev/null +++ b/src/adb/command/host-transport/trackjdwp.js @@ -0,0 +1,109 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {EventEmitter} = require('events'); + +const Promise = require('bluebird'); + +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const Parser = require('../../parser'); + +var TrackJdwpCommand = (function() { + let Tracker = undefined; + TrackJdwpCommand = class TrackJdwpCommand extends Command { + static initClass() { + + Tracker = class Tracker extends EventEmitter { + constructor(command) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.command = command; + this.pids = []; + this.pidMap = Object.create(null); + this.reader = this.read() + .catch(Parser.PrematureEOFError, err => { + return this.emit('end'); + }).catch(Promise.CancellationError, err => { + this.command.connection.end(); + return this.emit('end'); + }).catch(err => { + this.command.connection.end(); + this.emit('error', err); + return this.emit('end'); + }); + } + + read() { + return this.command.parser.readValue() + .cancellable() + .then(list => { + let maybeEmpty; + const pids = list.toString().split('\n'); + if (maybeEmpty = pids.pop()) { pids.push(maybeEmpty); } + return this.update(pids); + }); + } + + update(newList) { + let pid; + const changeSet = { + removed: [], + added: [] + }; + const newMap = Object.create(null); + for (pid of Array.from(newList)) { + if (!this.pidMap[pid]) { + changeSet.added.push(pid); + this.emit('add', pid); + newMap[pid] = pid; + } + } + for (pid of Array.from(this.pids)) { + if (!newMap[pid]) { + changeSet.removed.push(pid); + this.emit('remove', pid); + } + } + this.pids = newList; + this.pidMap = newMap; + this.emit('changeSet', changeSet, newList); + return this; + } + + end() { + this.reader.cancel(); + return this; + } + }; + } + execute() { + this._send('track-jdwp'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return new Tracker(this); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + }; + TrackJdwpCommand.initClass(); + return TrackJdwpCommand; +})(); + +module.exports = TrackJdwpCommand; diff --git a/src/adb/command/host-transport/uninstall.js b/src/adb/command/host-transport/uninstall.js new file mode 100644 index 00000000..482e74cd --- /dev/null +++ b/src/adb/command/host-transport/uninstall.js @@ -0,0 +1,38 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class UninstallCommand extends Command { + execute(pkg) { + this._send(`shell:pm uninstall ${pkg}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.searchLine(/^(Success|Failure.*|.*Unknown package:.*)$/) + .then(function(match) { + if (match[1] === 'Success') { + return true; + } else { + // Either way, the package was uninstalled or doesn't exist, + // which is good enough for us. + return true; + }}).finally(() => { + // Consume all remaining content to "naturally" close the + // connection. + return this.parser.readAll(); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, "OKAY or FAIL"); + } + }); + } +} + +module.exports = UninstallCommand; diff --git a/src/adb/command/host-transport/usb.js b/src/adb/command/host-transport/usb.js new file mode 100644 index 00000000..20de5b84 --- /dev/null +++ b/src/adb/command/host-transport/usb.js @@ -0,0 +1,43 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +var UsbCommand = (function() { + let RE_OK = undefined; + UsbCommand = class UsbCommand extends Command { + static initClass() { + RE_OK = /restarting in/; + } + + execute() { + this._send('usb:'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readAll() + .then(function(value) { + if (RE_OK.test(value)) { + return true; + } else { + throw new Error(value.toString().trim()); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + }; + UsbCommand.initClass(); + return UsbCommand; +})(); + +module.exports = UsbCommand; diff --git a/src/adb/command/host-transport/waitbootcomplete.js b/src/adb/command/host-transport/waitbootcomplete.js new file mode 100644 index 00000000..7259533c --- /dev/null +++ b/src/adb/command/host-transport/waitbootcomplete.js @@ -0,0 +1,32 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const debug = require('debug')('adb:command:waitboot'); + +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class WaitBootCompleteCommand extends Command { + execute() { + this._send( + 'shell:while getprop sys.boot_completed 2>/dev/null; do sleep 1; done'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.searchLine(/^1$/) + .finally(() => { + return this.parser.end(); + }).then(() => true); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = WaitBootCompleteCommand; diff --git a/src/adb/command/host/connect.js b/src/adb/command/host/connect.js new file mode 100644 index 00000000..afbb15d1 --- /dev/null +++ b/src/adb/command/host/connect.js @@ -0,0 +1,47 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +var ConnectCommand = (function() { + let RE_OK = undefined; + ConnectCommand = class ConnectCommand extends Command { + static initClass() { + // Possible replies: + // "unable to connect to 192.168.2.2:5555" + // "connected to 192.168.2.2:5555" + // "already connected to 192.168.2.2:5555" + RE_OK = /connected to|already connected/; + } + + execute(host, port) { + this._send(`host:connect:${host}:${port}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readValue() + .then(function(value) { + if (RE_OK.test(value)) { + return `${host}:${port}`; + } else { + throw new Error(value.toString()); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + }; + ConnectCommand.initClass(); + return ConnectCommand; +})(); + +module.exports = ConnectCommand; diff --git a/src/adb/command/host/devices.js b/src/adb/command/host/devices.js new file mode 100644 index 00000000..7634ae69 --- /dev/null +++ b/src/adb/command/host/devices.js @@ -0,0 +1,46 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class HostDevicesCommand extends Command { + execute() { + this._send('host:devices'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this._readDevices(); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _readDevices() { + return this.parser.readValue() + .then(value => { + return this._parseDevices(value); + }); + } + + _parseDevices(value) { + const devices = []; + if (!value.length) { return devices; } + for (let line of Array.from(value.toString('ascii').split('\n'))) { + if (line) { + const [id, type] = Array.from(line.split('\t')); + devices.push({id, type}); + } + } + return devices; + } +} + +module.exports = HostDevicesCommand; diff --git a/src/adb/command/host/deviceswithpaths.js b/src/adb/command/host/deviceswithpaths.js new file mode 100644 index 00000000..5bfcc185 --- /dev/null +++ b/src/adb/command/host/deviceswithpaths.js @@ -0,0 +1,47 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class HostDevicesWithPathsCommand extends Command { + execute() { + this._send('host:devices-l'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this._readDevices(); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + + _readDevices() { + return this.parser.readValue() + .then(value => { + return this._parseDevices(value); + }); + } + + _parseDevices(value) { + const devices = []; + if (!value.length) { return devices; } + for (let line of Array.from(value.toString('ascii').split('\n'))) { + if (line) { + // For some reason, the columns are separated by spaces instead of tabs + const [id, type, path] = Array.from(line.split(/\s+/)); + devices.push({id, type, path}); + } + } + return devices; + } +} + +module.exports = HostDevicesWithPathsCommand; diff --git a/src/adb/command/host/disconnect.js b/src/adb/command/host/disconnect.js new file mode 100644 index 00000000..0c959318 --- /dev/null +++ b/src/adb/command/host/disconnect.js @@ -0,0 +1,46 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +var DisconnectCommand = (function() { + let RE_OK = undefined; + DisconnectCommand = class DisconnectCommand extends Command { + static initClass() { + // Possible replies: + // "No such device 192.168.2.2:5555" + // "" + RE_OK = /^$/; + } + + execute(host, port) { + this._send(`host:disconnect:${host}:${port}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readValue() + .then(function(value) { + if (RE_OK.test(value)) { + return `${host}:${port}`; + } else { + throw new Error(value.toString()); + } + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } + }; + DisconnectCommand.initClass(); + return DisconnectCommand; +})(); + +module.exports = DisconnectCommand; diff --git a/src/adb/command/host/kill.js b/src/adb/command/host/kill.js new file mode 100644 index 00000000..8f4b31f8 --- /dev/null +++ b/src/adb/command/host/kill.js @@ -0,0 +1,26 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class HostKillCommand extends Command { + execute() { + this._send('host:kill'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return true; + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = HostKillCommand; diff --git a/src/adb/command/host/trackdevices.js b/src/adb/command/host/trackdevices.js new file mode 100644 index 00000000..f53b4d58 --- /dev/null +++ b/src/adb/command/host/trackdevices.js @@ -0,0 +1,28 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); +const Tracker = require('../../tracker'); +const HostDevicesCommand = require('./devices'); + +class HostTrackDevicesCommand extends HostDevicesCommand { + execute() { + this._send('host:track-devices'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return new Tracker(this); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = HostTrackDevicesCommand; diff --git a/src/adb/command/host/transport.js b/src/adb/command/host/transport.js new file mode 100644 index 00000000..9efc1dcb --- /dev/null +++ b/src/adb/command/host/transport.js @@ -0,0 +1,26 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class HostTransportCommand extends Command { + execute(serial) { + this._send(`host:transport:${serial}`); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return true; + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} + +module.exports = HostTransportCommand; diff --git a/src/adb/command/host/version.js b/src/adb/command/host/version.js new file mode 100644 index 00000000..af6c916e --- /dev/null +++ b/src/adb/command/host/version.js @@ -0,0 +1,33 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Command = require('../../command'); +const Protocol = require('../../protocol'); + +class HostVersionCommand extends Command { + execute() { + this._send('host:version'); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readValue() + .then(value => { + return this._parseVersion(value); + }); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this._parseVersion(reply); + } + }); + } + + _parseVersion(version) { + return parseInt(version, 16); + } +} + +module.exports = HostVersionCommand; diff --git a/src/adb/connection.js b/src/adb/connection.js new file mode 100644 index 00000000..37d3ee4a --- /dev/null +++ b/src/adb/connection.js @@ -0,0 +1,92 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Net = require('net'); +const debug = require('debug')('adb:connection'); +const {EventEmitter} = require('events'); +const {execFile} = require('child_process'); + +const Parser = require('./parser'); +const dump = require('./dump'); + +class Connection extends EventEmitter { + constructor(options) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.options = options; + this.socket = null; + this.parser = null; + this.triedStarting = false; + } + + connect() { + this.socket = Net.connect(this.options); + this.socket.setNoDelay(true); + this.parser = new Parser(this.socket); + this.socket.on('connect', () => { + return this.emit('connect'); + }); + this.socket.on('end', () => { + return this.emit('end'); + }); + this.socket.on('drain', () => { + return this.emit('drain'); + }); + this.socket.on('timeout', () => { + return this.emit('timeout'); + }); + this.socket.on('error', err => { + return this._handleError(err); + }); + this.socket.on('close', hadError => { + return this.emit('close', hadError); + }); + return this; + } + + end() { + this.socket.end(); + return this; + } + + write(data, callback) { + this.socket.write(dump(data), callback); + return this; + } + + startServer(callback) { + debug(`Starting ADB server via '${this.options.bin} start-server'`); + return this._exec(['start-server'], {}, callback); + } + + _exec(args, options, callback) { + debug(`CLI: ${this.options.bin} ${args.join(' ')}`); + execFile(this.options.bin, args, options, callback); + return this; + } + + _handleError(err) { + if ((err.code === 'ECONNREFUSED') && !this.triedStarting) { + debug("Connection was refused, let's try starting the server once"); + this.triedStarting = true; + this.startServer(err => { + if (err) { return this._handleError(err); } + return this.connect(); + }); + } else { + debug(`Connection had an error: ${err.message}`); + this.emit('error', err); + this.end(); + } + } +} + +module.exports = Connection; diff --git a/src/adb/dump.js b/src/adb/dump.js new file mode 100644 index 00000000..70ad54f8 --- /dev/null +++ b/src/adb/dump.js @@ -0,0 +1,16 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const fs = require('fs'); + +if (process.env.ADBKIT_DUMP) { + const out = fs.createWriteStream('adbkit.dump'); + module.exports = function(chunk) { + out.write(chunk); + return chunk; + }; +} else { + module.exports = chunk => chunk; +} diff --git a/src/adb/framebuffer/rgbtransform.js b/src/adb/framebuffer/rgbtransform.js new file mode 100644 index 00000000..a8b102e8 --- /dev/null +++ b/src/adb/framebuffer/rgbtransform.js @@ -0,0 +1,59 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Assert = require('assert'); +const Stream = require('stream'); + +class RgbTransform extends Stream.Transform { + constructor(meta, options) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.meta = meta; + this._buffer = new Buffer(''); + Assert.ok(((this.meta.bpp === 24) || (this.meta.bpp === 32)), + 'Only 24-bit and 32-bit raw images with 8-bits per color are supported'); + this._r_pos = this.meta.red_offset / 8; + this._g_pos = this.meta.green_offset / 8; + this._b_pos = this.meta.blue_offset / 8; + this._a_pos = this.meta.alpha_offset / 8; + this._pixel_bytes = this.meta.bpp / 8; + super(options); + } + + _transform(chunk, encoding, done) { + if (this._buffer.length) { + this._buffer = Buffer.concat([this._buffer, chunk], this._buffer.length + chunk.length); + } else { + this._buffer = chunk; + } + let sourceCursor = 0; + let targetCursor = 0; + const target = this._pixel_bytes === 3 + ? this._buffer + : new Buffer(Math.max(4, (chunk.length / this._pixel_bytes) * 3)); + while ((this._buffer.length - sourceCursor) >= this._pixel_bytes) { + const r = this._buffer[sourceCursor + this._r_pos]; + const g = this._buffer[sourceCursor + this._g_pos]; + const b = this._buffer[sourceCursor + this._b_pos]; + target[targetCursor + 0] = r; + target[targetCursor + 1] = g; + target[targetCursor + 2] = b; + sourceCursor += this._pixel_bytes; + targetCursor += 3; + } + if (targetCursor) { + this.push(target.slice(0, targetCursor)); + this._buffer = this._buffer.slice(sourceCursor); + } + done(); + } +} + +module.exports = RgbTransform; diff --git a/src/adb/keycode.js b/src/adb/keycode.js new file mode 100644 index 00000000..a88e10e8 --- /dev/null +++ b/src/adb/keycode.js @@ -0,0 +1,228 @@ +// Generated by `grunt keycode` on Tue, 26 Nov 2013 08:02:49 GMT +// KeyEvent.java Copyright (C) 2006 The Android Open Source Project + +module.exports = { + KEYCODE_UNKNOWN: 0, + KEYCODE_SOFT_LEFT: 1, + KEYCODE_SOFT_RIGHT: 2, + KEYCODE_HOME: 3, + KEYCODE_BACK: 4, + KEYCODE_CALL: 5, + KEYCODE_ENDCALL: 6, + KEYCODE_0: 7, + KEYCODE_1: 8, + KEYCODE_2: 9, + KEYCODE_3: 10, + KEYCODE_4: 11, + KEYCODE_5: 12, + KEYCODE_6: 13, + KEYCODE_7: 14, + KEYCODE_8: 15, + KEYCODE_9: 16, + KEYCODE_STAR: 17, + KEYCODE_POUND: 18, + KEYCODE_DPAD_UP: 19, + KEYCODE_DPAD_DOWN: 20, + KEYCODE_DPAD_LEFT: 21, + KEYCODE_DPAD_RIGHT: 22, + KEYCODE_DPAD_CENTER: 23, + KEYCODE_VOLUME_UP: 24, + KEYCODE_VOLUME_DOWN: 25, + KEYCODE_POWER: 26, + KEYCODE_CAMERA: 27, + KEYCODE_CLEAR: 28, + KEYCODE_A: 29, + KEYCODE_B: 30, + KEYCODE_C: 31, + KEYCODE_D: 32, + KEYCODE_E: 33, + KEYCODE_F: 34, + KEYCODE_G: 35, + KEYCODE_H: 36, + KEYCODE_I: 37, + KEYCODE_J: 38, + KEYCODE_K: 39, + KEYCODE_L: 40, + KEYCODE_M: 41, + KEYCODE_N: 42, + KEYCODE_O: 43, + KEYCODE_P: 44, + KEYCODE_Q: 45, + KEYCODE_R: 46, + KEYCODE_S: 47, + KEYCODE_T: 48, + KEYCODE_U: 49, + KEYCODE_V: 50, + KEYCODE_W: 51, + KEYCODE_X: 52, + KEYCODE_Y: 53, + KEYCODE_Z: 54, + KEYCODE_COMMA: 55, + KEYCODE_PERIOD: 56, + KEYCODE_ALT_LEFT: 57, + KEYCODE_ALT_RIGHT: 58, + KEYCODE_SHIFT_LEFT: 59, + KEYCODE_SHIFT_RIGHT: 60, + KEYCODE_TAB: 61, + KEYCODE_SPACE: 62, + KEYCODE_SYM: 63, + KEYCODE_EXPLORER: 64, + KEYCODE_ENVELOPE: 65, + KEYCODE_ENTER: 66, + KEYCODE_DEL: 67, + KEYCODE_GRAVE: 68, + KEYCODE_MINUS: 69, + KEYCODE_EQUALS: 70, + KEYCODE_LEFT_BRACKET: 71, + KEYCODE_RIGHT_BRACKET: 72, + KEYCODE_BACKSLASH: 73, + KEYCODE_SEMICOLON: 74, + KEYCODE_APOSTROPHE: 75, + KEYCODE_SLASH: 76, + KEYCODE_AT: 77, + KEYCODE_NUM: 78, + KEYCODE_HEADSETHOOK: 79, + KEYCODE_FOCUS: 80, + KEYCODE_PLUS: 81, + KEYCODE_MENU: 82, + KEYCODE_NOTIFICATION: 83, + KEYCODE_SEARCH: 84, + KEYCODE_MEDIA_PLAY_PAUSE: 85, + KEYCODE_MEDIA_STOP: 86, + KEYCODE_MEDIA_NEXT: 87, + KEYCODE_MEDIA_PREVIOUS: 88, + KEYCODE_MEDIA_REWIND: 89, + KEYCODE_MEDIA_FAST_FORWARD: 90, + KEYCODE_MUTE: 91, + KEYCODE_PAGE_UP: 92, + KEYCODE_PAGE_DOWN: 93, + KEYCODE_PICTSYMBOLS: 94, + KEYCODE_SWITCH_CHARSET: 95, + KEYCODE_BUTTON_A: 96, + KEYCODE_BUTTON_B: 97, + KEYCODE_BUTTON_C: 98, + KEYCODE_BUTTON_X: 99, + KEYCODE_BUTTON_Y: 100, + KEYCODE_BUTTON_Z: 101, + KEYCODE_BUTTON_L1: 102, + KEYCODE_BUTTON_R1: 103, + KEYCODE_BUTTON_L2: 104, + KEYCODE_BUTTON_R2: 105, + KEYCODE_BUTTON_THUMBL: 106, + KEYCODE_BUTTON_THUMBR: 107, + KEYCODE_BUTTON_START: 108, + KEYCODE_BUTTON_SELECT: 109, + KEYCODE_BUTTON_MODE: 110, + KEYCODE_ESCAPE: 111, + KEYCODE_FORWARD_DEL: 112, + KEYCODE_CTRL_LEFT: 113, + KEYCODE_CTRL_RIGHT: 114, + KEYCODE_CAPS_LOCK: 115, + KEYCODE_SCROLL_LOCK: 116, + KEYCODE_META_LEFT: 117, + KEYCODE_META_RIGHT: 118, + KEYCODE_FUNCTION: 119, + KEYCODE_SYSRQ: 120, + KEYCODE_BREAK: 121, + KEYCODE_MOVE_HOME: 122, + KEYCODE_MOVE_END: 123, + KEYCODE_INSERT: 124, + KEYCODE_FORWARD: 125, + KEYCODE_MEDIA_PLAY: 126, + KEYCODE_MEDIA_PAUSE: 127, + KEYCODE_MEDIA_CLOSE: 128, + KEYCODE_MEDIA_EJECT: 129, + KEYCODE_MEDIA_RECORD: 130, + KEYCODE_F1: 131, + KEYCODE_F2: 132, + KEYCODE_F3: 133, + KEYCODE_F4: 134, + KEYCODE_F5: 135, + KEYCODE_F6: 136, + KEYCODE_F7: 137, + KEYCODE_F8: 138, + KEYCODE_F9: 139, + KEYCODE_F10: 140, + KEYCODE_F11: 141, + KEYCODE_F12: 142, + KEYCODE_NUM_LOCK: 143, + KEYCODE_NUMPAD_0: 144, + KEYCODE_NUMPAD_1: 145, + KEYCODE_NUMPAD_2: 146, + KEYCODE_NUMPAD_3: 147, + KEYCODE_NUMPAD_4: 148, + KEYCODE_NUMPAD_5: 149, + KEYCODE_NUMPAD_6: 150, + KEYCODE_NUMPAD_7: 151, + KEYCODE_NUMPAD_8: 152, + KEYCODE_NUMPAD_9: 153, + KEYCODE_NUMPAD_DIVIDE: 154, + KEYCODE_NUMPAD_MULTIPLY: 155, + KEYCODE_NUMPAD_SUBTRACT: 156, + KEYCODE_NUMPAD_ADD: 157, + KEYCODE_NUMPAD_DOT: 158, + KEYCODE_NUMPAD_COMMA: 159, + KEYCODE_NUMPAD_ENTER: 160, + KEYCODE_NUMPAD_EQUALS: 161, + KEYCODE_NUMPAD_LEFT_PAREN: 162, + KEYCODE_NUMPAD_RIGHT_PAREN: 163, + KEYCODE_VOLUME_MUTE: 164, + KEYCODE_INFO: 165, + KEYCODE_CHANNEL_UP: 166, + KEYCODE_CHANNEL_DOWN: 167, + KEYCODE_ZOOM_IN: 168, + KEYCODE_ZOOM_OUT: 169, + KEYCODE_TV: 170, + KEYCODE_WINDOW: 171, + KEYCODE_GUIDE: 172, + KEYCODE_DVR: 173, + KEYCODE_BOOKMARK: 174, + KEYCODE_CAPTIONS: 175, + KEYCODE_SETTINGS: 176, + KEYCODE_TV_POWER: 177, + KEYCODE_TV_INPUT: 178, + KEYCODE_STB_POWER: 179, + KEYCODE_STB_INPUT: 180, + KEYCODE_AVR_POWER: 181, + KEYCODE_AVR_INPUT: 182, + KEYCODE_PROG_RED: 183, + KEYCODE_PROG_GREEN: 184, + KEYCODE_PROG_YELLOW: 185, + KEYCODE_PROG_BLUE: 186, + KEYCODE_APP_SWITCH: 187, + KEYCODE_BUTTON_1: 188, + KEYCODE_BUTTON_2: 189, + KEYCODE_BUTTON_3: 190, + KEYCODE_BUTTON_4: 191, + KEYCODE_BUTTON_5: 192, + KEYCODE_BUTTON_6: 193, + KEYCODE_BUTTON_7: 194, + KEYCODE_BUTTON_8: 195, + KEYCODE_BUTTON_9: 196, + KEYCODE_BUTTON_10: 197, + KEYCODE_BUTTON_11: 198, + KEYCODE_BUTTON_12: 199, + KEYCODE_BUTTON_13: 200, + KEYCODE_BUTTON_14: 201, + KEYCODE_BUTTON_15: 202, + KEYCODE_BUTTON_16: 203, + KEYCODE_LANGUAGE_SWITCH: 204, + KEYCODE_MANNER_MODE: 205, + KEYCODE_3D_MODE: 206, + KEYCODE_CONTACTS: 207, + KEYCODE_CALENDAR: 208, + KEYCODE_MUSIC: 209, + KEYCODE_CALCULATOR: 210, + KEYCODE_ZENKAKU_HANKAKU: 211, + KEYCODE_EISU: 212, + KEYCODE_MUHENKAN: 213, + KEYCODE_HENKAN: 214, + KEYCODE_KATAKANA_HIRAGANA: 215, + KEYCODE_YEN: 216, + KEYCODE_RO: 217, + KEYCODE_KANA: 218, + KEYCODE_ASSIST: 219, + KEYCODE_BRIGHTNESS_DOWN: 220, + KEYCODE_BRIGHTNESS_UP: 221, + KEYCODE_MEDIA_AUDIO_TRACK: 222 +}; diff --git a/src/adb/linetransform.js b/src/adb/linetransform.js new file mode 100644 index 00000000..8755bf8f --- /dev/null +++ b/src/adb/linetransform.js @@ -0,0 +1,102 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); + +class LineTransform extends Stream.Transform { + constructor(options) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + if (options == null) { options = {}; } + this.savedR = null; + this.autoDetect = options.autoDetect || false; + this.transformNeeded = true; + this.skipBytes = 0; + delete options.autoDetect; + super(options); + } + + _nullTransform(chunk, encoding, done) { + this.push(chunk); + done(); + } + + // Sadly, the ADB shell is not very smart. It automatically converts every + // 0x0a ('\n') it can find to 0x0d 0x0a ('\r\n'). This also applies to binary + // content. We could get rid of this behavior by setting `stty raw`, but + // unfortunately it's not available by default (you'd have to install busybox) + // or something similar. On the up side, it really does do this for all line + // feeds, so a simple transform works fine. + _transform(chunk, encoding, done) { + // If auto detection is enabled, check the first byte. The first two + // bytes must be either 0x0a .. or 0x0d 0x0a. This causes a need to skip + // either one or two bytes. The autodetection runs only once. + if (this.autoDetect) { + if (chunk[0] === 0x0a) { + this.transformNeeded = false; + this.skipBytes = 1; + } else { + this.skipBytes = 2; + } + this.autoDetect = false; + } + + // It's technically possible that we may receive the first two bytes + // in two separate chunks. That's why the autodetect bytes are skipped + // here, separately. + if (this.skipBytes) { + const skip = Math.min(chunk.length, this.skipBytes); + chunk = chunk.slice(skip); + this.skipBytes -= skip; + } + + // It's possible that skipping bytes has created an empty chunk. + if (!chunk.length) { return done(); } + + // At this point all bytes that needed to be skipped should have been + // skipped. If transform is not needed, shortcut to null transform. + if (!this.transformNeeded) { return this._nullTransform(chunk, encoding, done); } + + // Ok looks like we're transforming. + let lo = 0; + let hi = 0; + if (this.savedR) { + if (chunk[0] !== 0x0a) { this.push(this.savedR); } + this.savedR = null; + } + const last = chunk.length - 1; + while (hi <= last) { + if (chunk[hi] === 0x0d) { + if (hi === last) { + this.savedR = chunk.slice(last); + break; // Stop hi from incrementing, we want to skip the last byte. + } else if (chunk[hi + 1] === 0x0a) { + this.push(chunk.slice(lo, hi)); + lo = hi + 1; + } + } + hi += 1; + } + if (hi !== lo) { + this.push(chunk.slice(lo, hi)); + } + done(); + } + + // When the stream ends on an '\r', output it as-is (assume binary data). + _flush(done) { + if (this.savedR) { this.push(this.savedR); } + return done(); + } +} + +module.exports = LineTransform; diff --git a/src/adb/parser.js b/src/adb/parser.js new file mode 100644 index 00000000..246c6bda --- /dev/null +++ b/src/adb/parser.js @@ -0,0 +1,278 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Promise = require('bluebird'); + +const Protocol = require('./protocol'); + +class Parser { + constructor(stream) { + this.stream = stream; + this.ended = false; + } + + end() { + let endListener, errorListener; + if (this.ended) { return Promise.resolve(true); } + + const resolver = Promise.defer(); + + const tryRead = () => { + while (this.stream.read()) { + continue; + } + }; + + this.stream.on('readable', tryRead); + + this.stream.on('error', (errorListener = err => resolver.reject(err)) + ); + + this.stream.on('end', (endListener = () => { + this.ended = true; + return resolver.resolve(true); + }) + ); + + this.stream.read(0); + this.stream.end(); + + return resolver.promise.cancellable().finally(() => { + this.stream.removeListener('readable', tryRead); + this.stream.removeListener('error', errorListener); + return this.stream.removeListener('end', endListener); + }); + } + + raw() { + return this.stream; + } + + readAll() { + let endListener, errorListener; + let all = new Buffer(0); + const resolver = Promise.defer(); + + const tryRead = () => { + let chunk; + while ((chunk = this.stream.read())) { + all = Buffer.concat([all, chunk]); + } + if (this.ended) { return resolver.resolve(all); } + }; + + this.stream.on('readable', tryRead); + + this.stream.on('error', (errorListener = err => resolver.reject(err)) + ); + + this.stream.on('end', (endListener = () => { + this.ended = true; + return resolver.resolve(all); + }) + ); + + tryRead(); + + return resolver.promise.cancellable().finally(() => { + this.stream.removeListener('readable', tryRead); + this.stream.removeListener('error', errorListener); + return this.stream.removeListener('end', endListener); + }); + } + + readAscii(howMany) { + return this.readBytes(howMany) + .then(chunk => chunk.toString('ascii')); + } + + readBytes(howMany) { + const resolver = Promise.defer(); + + const tryRead = () => { + if (howMany) { + let chunk; + if (chunk = this.stream.read(howMany)) { + // If the stream ends while still having unread bytes, the read call + // will ignore the limit and just return what it's got. + howMany -= chunk.length; + if (howMany === 0) { return resolver.resolve(chunk); } + } + if (this.ended) { return resolver.reject(new Parser.PrematureEOFError(howMany)); } + } else { + return resolver.resolve(new Buffer(0)); + } + }; + + const endListener = () => { + this.ended = true; + return resolver.reject(new Parser.PrematureEOFError(howMany)); + }; + + const errorListener = err => resolver.reject(err); + + this.stream.on('readable', tryRead); + this.stream.on('error', errorListener); + this.stream.on('end', endListener); + + tryRead(); + + return resolver.promise.cancellable().finally(() => { + this.stream.removeListener('readable', tryRead); + this.stream.removeListener('error', errorListener); + return this.stream.removeListener('end', endListener); + }); + } + + readByteFlow(howMany, targetStream) { + const resolver = Promise.defer(); + + const tryRead = () => { + if (howMany) { + // Try to get the exact amount we need first. If unsuccessful, take + // whatever is available, which will be less than the needed amount. + let chunk; + while ((chunk = this.stream.read(howMany) || this.stream.read())) { + howMany -= chunk.length; + targetStream.write(chunk); + if (howMany === 0) { return resolver.resolve(); } + } + if (this.ended) { return resolver.reject(new Parser.PrematureEOFError(howMany)); } + } else { + return resolver.resolve(); + } + }; + + const endListener = () => { + this.ended = true; + return resolver.reject(new Parser.PrematureEOFError(howMany)); + }; + + const errorListener = err => resolver.reject(err); + + this.stream.on('readable', tryRead); + this.stream.on('error', errorListener); + this.stream.on('end', endListener); + + tryRead(); + + return resolver.promise.cancellable().finally(() => { + this.stream.removeListener('readable', tryRead); + this.stream.removeListener('error', errorListener); + return this.stream.removeListener('end', endListener); + }); + } + + readError() { + return this.readValue() + .then(value => Promise.reject(new Parser.FailError(value.toString()))); + } + + readValue() { + return this.readAscii(4) + .then(value => { + const length = Protocol.decodeLength(value); + return this.readBytes(length); + }); + } + + readUntil(code) { + let skipped = new Buffer(0); + var read = () => { + return this.readBytes(1) + .then(function(chunk) { + if (chunk[0] === code) { + return skipped; + } else { + skipped = Buffer.concat([skipped, chunk]); + return read(); + } + }); + }; + return read(); + } + + searchLine(re) { + return this.readLine() + .then(line => { + let match; + if ((match = re.exec(line))) { + return match; + } else { + return this.searchLine(re); + } + }); + } + + readLine() { + return this.readUntil(0x0a) // '\n' + .then(function(line) { + if (line[line.length - 1] === 0x0d) { // '\r' + return line.slice(0, -1); + } else { + return line; + } + }); + } + + unexpected(data, expected) { + return Promise.reject(new Parser.UnexpectedDataError(data, expected)); + } +} + +Parser.FailError = class FailError extends Error { + constructor(message) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + Error.call(this); + this.name = 'FailError'; + this.message = `Failure: '${message}'`; + Error.captureStackTrace(this, Parser.FailError); + } +}; + +Parser.PrematureEOFError = class PrematureEOFError extends Error { + constructor(howManyMissing) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + Error.call(this); + this.name = 'PrematureEOFError'; + this.message = `Premature end of stream, needed ${howManyMissing} \ +more bytes`; + this.missingBytes = howManyMissing; + Error.captureStackTrace(this, Parser.PrematureEOFError); + } +}; + +Parser.UnexpectedDataError = class UnexpectedDataError extends Error { + constructor(unexpected, expected) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + Error.call(this); + this.name = 'UnexpectedDataError'; + this.message = `Unexpected '${unexpected}', was expecting ${expected}`; + this.unexpected = unexpected; + this.expected = expected; + Error.captureStackTrace(this, Parser.UnexpectedDataError); + } +}; + +module.exports = Parser; diff --git a/src/adb/proc/stat.js b/src/adb/proc/stat.js new file mode 100644 index 00000000..95926319 --- /dev/null +++ b/src/adb/proc/stat.js @@ -0,0 +1,139 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {EventEmitter} = require('events'); +const split = require('split'); + +const Parser = require('../parser'); + +var ProcStat = (function() { + let RE_CPULINE = undefined; + let RE_COLSEP = undefined; + ProcStat = class ProcStat extends EventEmitter { + static initClass() { + RE_CPULINE = /^cpu[0-9]+ .*$/mg; + RE_COLSEP = /\ +/g; + } + + constructor(sync) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.sync = sync; + this.interval = 1000; + this.stats = this._emptyStats(); + this._ignore = {}; + this._timer = setInterval(() => { + return this.update(); + } + , this.interval); + this.update(); + } + + end() { + clearInterval(this._timer); + this.sync.end(); + return this.sync = null; + } + + update() { + return new Parser(this.sync.pull('/proc/stat')) + .readAll() + .then(out => { + return this._parse(out); + }).catch(err => { + this._error(err); + }); + } + + _parse(out) { + let match; + const stats = this._emptyStats(); + while ((match = RE_CPULINE.exec(out))) { + const line = match[0]; + const cols = line.split(RE_COLSEP); + const type = cols.shift(); + if (this._ignore[type] === line) { continue; } + let total = 0; + for (let val of Array.from(cols)) { total += +val; } + stats.cpus[type] = { + line, + user: +cols[0] || 0, + nice: +cols[1] || 0, + system: +cols[2] || 0, + idle: +cols[3] || 0, + iowait: +cols[4] || 0, + irq: +cols[5] || 0, + softirq: +cols[6] || 0, + steal: +cols[7] || 0, + guest: +cols[8] || 0, + guestnice: +cols[9] || 0, + total + }; + } + return this._set(stats); + } + + _set(stats) { + const loads = {}; + let found = false; + for (let id in stats.cpus) { + const cur = stats.cpus[id]; + const old = this.stats.cpus[id]; + if (!old) { continue; } + const ticks = cur.total - old.total; + if (ticks > 0) { + found = true; + // Calculate percentages for everything. For ease of formatting, + // let's do `x / y * 100` as `100 / y * x`. + const m = 100 / ticks; + loads[id] = { + user: Math.floor(m * (cur.user - old.user)), + nice: Math.floor(m * (cur.nice - old.nice)), + system: Math.floor(m * (cur.system - old.system)), + idle: Math.floor(m * (cur.idle - old.idle)), + iowait: Math.floor(m * (cur.iowait - old.iowait)), + irq: Math.floor(m * (cur.irq - old.irq)), + softirq: Math.floor(m * (cur.softirq - old.softirq)), + steal: Math.floor(m * (cur.steal - old.steal)), + guest: Math.floor(m * (cur.guest - old.guest)), + guestnice: Math.floor(m * (cur.guestnice - old.guestnice)), + total: 100 + }; + } else { + // The CPU is either offline (nothing was done) or it mysteriously + // warped back in time (idle stat dropped significantly), causing the + // total tick count to be <0. The latter seems to only happen on + // Galaxy S4 so far. Either way we don't want those anomalies in our + // stats. We'll also ignore the line in the next cycle. This doesn't + // completely eliminate the anomalies, but it helps. + this._ignore[id] = cur.line; + delete stats.cpus[id]; + } + } + if (found) { this.emit('load', loads); } + return this.stats = stats; + } + + _error(err) { + return this.emit('error', err); + } + + _emptyStats() { + return {cpus: {}}; + } + }; + ProcStat.initClass(); + return ProcStat; +})(); + +module.exports = ProcStat; diff --git a/src/adb/protocol.js b/src/adb/protocol.js new file mode 100644 index 00000000..88e63839 --- /dev/null +++ b/src/adb/protocol.js @@ -0,0 +1,38 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +class Protocol { + static initClass() { + this.OKAY = 'OKAY'; + this.FAIL = 'FAIL'; + this.STAT = 'STAT'; + this.LIST = 'LIST'; + this.DENT = 'DENT'; + this.RECV = 'RECV'; + this.DATA = 'DATA'; + this.DONE = 'DONE'; + this.SEND = 'SEND'; + this.QUIT = 'QUIT'; + } + + static decodeLength(length) { + return parseInt(length, 16); + } + + static encodeLength(length) { + return ('0000' + length.toString(16)).slice(-4).toUpperCase(); + } + + static encodeData(data) { + if (!Buffer.isBuffer(data)) { + data = new Buffer(data); + } + return Buffer.concat([new Buffer(Protocol.encodeLength(data.length)), data]); + } +} +Protocol.initClass(); + +module.exports = Protocol; diff --git a/src/adb/sync.js b/src/adb/sync.js new file mode 100644 index 00000000..f9e334a1 --- /dev/null +++ b/src/adb/sync.js @@ -0,0 +1,337 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Fs = require('fs'); +const Path = require('path'); +const Promise = require('bluebird'); +const {EventEmitter} = require('events'); +const debug = require('debug')('adb:sync'); + +const Parser = require('./parser'); +const Protocol = require('./protocol'); +const Stats = require('./sync/stats'); +const Entry = require('./sync/entry'); +const PushTransfer = require('./sync/pushtransfer'); +const PullTransfer = require('./sync/pulltransfer'); + +var Sync = (function() { + let TEMP_PATH = undefined; + let DEFAULT_CHMOD = undefined; + let DATA_MAX_LENGTH = undefined; + Sync = class Sync extends EventEmitter { + static initClass() { + TEMP_PATH = '/data/local/tmp'; + DEFAULT_CHMOD = 0o644; + DATA_MAX_LENGTH = 65536; + } + + static temp(path) { + return `${TEMP_PATH}/${Path.basename(path)}`; + } + + constructor(connection) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.connection = connection; + this.parser = this.connection.parser; + } + + stat(path, callback) { + this._sendCommandWithArg(Protocol.STAT, path); + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.STAT: + return this.parser.readBytes(12) + .then(stat => { + const mode = stat.readUInt32LE(0); + const size = stat.readUInt32LE(4); + const mtime = stat.readUInt32LE(8); + if (mode === 0) { + return this._enoent(path); + } else { + return new Stats(mode, size, mtime); + } + }); + case Protocol.FAIL: + return this._readError(); + default: + return this.parser.unexpected(reply, 'STAT or FAIL'); + } + }).nodeify(callback); + } + + readdir(path, callback) { + const files = []; + + var readNext = () => { + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.DENT: + return this.parser.readBytes(16) + .then(stat => { + const mode = stat.readUInt32LE(0); + const size = stat.readUInt32LE(4); + const mtime = stat.readUInt32LE(8); + const namelen = stat.readUInt32LE(12); + return this.parser.readBytes(namelen) + .then(function(name) { + name = name.toString(); + // Skip '.' and '..' to match Node's fs.readdir(). + if ((name !== '.') && (name !== '..')) { + files.push(new Entry(name, mode, size, mtime)); + } + return readNext(); + }); + }); + case Protocol.DONE: + return this.parser.readBytes(16) + .then(zero => files); + case Protocol.FAIL: + return this._readError(); + default: + return this.parser.unexpected(reply, 'DENT, DONE or FAIL'); + } + }); + }; + + this._sendCommandWithArg(Protocol.LIST, path); + + return readNext() + .nodeify(callback); + } + + push(contents, path, mode) { + if (typeof contents === 'string') { + return this.pushFile(contents, path, mode); + } else { + return this.pushStream(contents, path, mode); + } + } + + pushFile(file, path, mode) { + if (mode == null) { mode = DEFAULT_CHMOD; } + if (!mode) { mode = DEFAULT_CHMOD; } + return this.pushStream(Fs.createReadStream(file), path, mode); + } + + pushStream(stream, path, mode) { + if (mode == null) { mode = DEFAULT_CHMOD; } + mode |= Stats.S_IFREG; + this._sendCommandWithArg(Protocol.SEND, `${path},${mode}`); + return this._writeData(stream, Math.floor(Date.now() / 1000)); + } + + pull(path) { + this._sendCommandWithArg(Protocol.RECV, `${path}`); + return this._readData(); + } + + end() { + this.connection.end(); + return this; + } + + tempFile(path) { + return Sync.temp(path); + } + + _writeData(stream, timeStamp) { + const transfer = new PushTransfer; + + const writeData = () => { + let endListener, errorListener, readableListener; + let resolver = Promise.defer(); + const writer = Promise.resolve() + .cancellable(); + + stream.on('end', (endListener = () => { + return writer.then(() => { + this._sendCommandWithLength(Protocol.DONE, timeStamp); + return resolver.resolve(); + }); + }) + ); + + const waitForDrain = () => { + let drainListener; + resolver = Promise.defer(); + + this.connection.on('drain', (drainListener = () => resolver.resolve()) + ); + + return resolver.promise.finally(() => { + return this.connection.removeListener('drain', drainListener); + }); + }; + + const track = () => transfer.pop(); + + var writeNext = () => { + let chunk; + if (chunk = stream.read(DATA_MAX_LENGTH) || stream.read()) { + this._sendCommandWithLength(Protocol.DATA, chunk.length); + transfer.push(chunk.length); + if (this.connection.write(chunk, track)) { + return writeNext(); + } else { + return waitForDrain() + .then(writeNext); + } + } else { + return Promise.resolve(); + } + }; + + stream.on('readable', (readableListener = () => writer.then(writeNext)) + ); + + stream.on('error', (errorListener = err => resolver.reject(err)) + ); + + return resolver.promise.finally(function() { + stream.removeListener('end', endListener); + stream.removeListener('readable', readableListener); + stream.removeListener('error', errorListener); + return writer.cancel(); + }); + }; + + const readReply = () => { + return this.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + return this.parser.readBytes(4) + .then(zero => true); + case Protocol.FAIL: + return this._readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + }; + + // While I can't think of a case that would break this double-Promise + // writer-reader arrangement right now, it's not immediately obvious + // that the code is correct and it may or may not have some failing + // edge cases. Refactor pending. + + const writer = writeData() + .cancellable() + .catch(Promise.CancellationError, err => { + return this.connection.end(); + }).catch(function(err) { + transfer.emit('error', err); + return reader.cancel(); + }); + + var reader = readReply() + .cancellable() + .catch(Promise.CancellationError, err => true).catch(function(err) { + transfer.emit('error', err); + return writer.cancel();}).finally(() => transfer.end()); + + transfer.on('cancel', function() { + writer.cancel(); + return reader.cancel(); + }); + + return transfer; + } + + _readData() { + let cancelListener; + const transfer = new PullTransfer; + + var readNext = () => { + return this.parser.readAscii(4) + .cancellable() + .then(reply => { + switch (reply) { + case Protocol.DATA: + return this.parser.readBytes(4) + .then(lengthData => { + const length = lengthData.readUInt32LE(0); + return this.parser.readByteFlow(length, transfer) + .then(readNext); + }); + case Protocol.DONE: + return this.parser.readBytes(4) + .then(zero => true); + case Protocol.FAIL: + return this._readError(); + default: + return this.parser.unexpected(reply, 'DATA, DONE or FAIL'); + } + }); + }; + + const reader = readNext() + .catch(Promise.CancellationError, err => { + return this.connection.end(); + }).catch(err => transfer.emit('error', err)).finally(function() { + transfer.removeListener('cancel', cancelListener); + return transfer.end(); + }); + + transfer.on('cancel', (cancelListener = () => reader.cancel()) + ); + + return transfer; + } + + _readError() { + return this.parser.readBytes(4) + .then(length => { + return this.parser.readBytes(length.readUInt32LE(0)) + .then(buf => Promise.reject(new Parser.FailError(buf.toString()))); + }).finally(() => { + return this.parser.end(); + }); + } + + _sendCommandWithLength(cmd, length) { + if (cmd !== Protocol.DATA) { debug(cmd); } + const payload = new Buffer(cmd.length + 4); + payload.write(cmd, 0, cmd.length); + payload.writeUInt32LE(length, cmd.length); + return this.connection.write(payload); + } + + _sendCommandWithArg(cmd, arg) { + debug(`${cmd} ${arg}`); + const payload = new Buffer(cmd.length + 4 + arg.length); + let pos = 0; + payload.write(cmd, pos, cmd.length); + pos += cmd.length; + payload.writeUInt32LE(arg.length, pos); + pos += 4; + payload.write(arg, pos); + return this.connection.write(payload); + } + + _enoent(path) { + const err = new Error(`ENOENT, no such file or directory '${path}'`); + err.errno = 34; + err.code = 'ENOENT'; + err.path = path; + return Promise.reject(err); + } + }; + Sync.initClass(); + return Sync; +})(); + +module.exports = Sync; diff --git a/src/adb/sync/entry.js b/src/adb/sync/entry.js new file mode 100644 index 00000000..eb6cde93 --- /dev/null +++ b/src/adb/sync/entry.js @@ -0,0 +1,27 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stats = require('./stats'); + +class Entry extends Stats { + constructor(name, mode, size, mtime) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.name = name; + super(mode, size, mtime); + } + + toString() { + return this.name; + } +} + +module.exports = Entry; diff --git a/src/adb/sync/pulltransfer.js b/src/adb/sync/pulltransfer.js new file mode 100644 index 00000000..a638aac7 --- /dev/null +++ b/src/adb/sync/pulltransfer.js @@ -0,0 +1,34 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); + +class PullTransfer extends Stream.PassThrough { + constructor() { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.stats = + {bytesTransferred: 0}; + super(); + } + + cancel() { + return this.emit('cancel'); + } + + write(chunk, encoding, callback) { + this.stats.bytesTransferred += chunk.length; + this.emit('progress', this.stats); + return super.write(chunk, encoding, callback); + } +} + +module.exports = PullTransfer; diff --git a/src/adb/sync/pushtransfer.js b/src/adb/sync/pushtransfer.js new file mode 100644 index 00000000..9d6e7c35 --- /dev/null +++ b/src/adb/sync/pushtransfer.js @@ -0,0 +1,42 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {EventEmitter} = require('events'); + +class PushTransfer extends EventEmitter { + constructor() { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this._stack = []; + this.stats = + {bytesTransferred: 0}; + } + + cancel() { + return this.emit('cancel'); + } + + push(byteCount) { + return this._stack.push(byteCount); + } + + pop() { + const byteCount = this._stack.pop(); + this.stats.bytesTransferred += byteCount; + return this.emit('progress', this.stats); + } + + end() { + return this.emit('end'); + } +} + +module.exports = PushTransfer; diff --git a/src/adb/sync/stats.js b/src/adb/sync/stats.js new file mode 100644 index 00000000..429bb1c8 --- /dev/null +++ b/src/adb/sync/stats.js @@ -0,0 +1,47 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Fs = require('fs'); + +class Stats extends Fs.Stats { + static initClass() { + // The following constant were extracted from `man 2 stat` on Ubuntu 12.10. + this.S_IFMT = 0o170000; // bit mask for the file type bit fields + this.S_IFSOCK = 0o140000; // socket + this.S_IFLNK = 0o120000; // symbolic link + this.S_IFREG = 0o100000; // regular file + this.S_IFBLK = 0o060000; // block device + this.S_IFDIR = 0o040000; // directory + this.S_IFCHR = 0o020000; // character device + this.S_IFIFO = 0o010000; // FIFO + this.S_ISUID = 0o004000; // set UID bit + this.S_ISGID = 0o002000; // set-group-ID bit (see below) + this.S_ISVTX = 0o001000; // sticky bit (see below) + this.S_IRWXU = 0o0700; // mask for file owner permissions + this.S_IRUSR = 0o0400; // owner has read permission + this.S_IWUSR = 0o0200; // owner has write permission + this.S_IXUSR = 0o0100; // owner has execute permission + this.S_IRWXG = 0o0070; // mask for group permissions + this.S_IRGRP = 0o0040; + // group has read permission + } + + constructor(mode, size, mtime) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.mode = mode; + this.size = size; + this.mtime = new Date(mtime * 1000); + } +} +Stats.initClass(); + +module.exports = Stats; diff --git a/src/adb/tcpusb/packet.js b/src/adb/tcpusb/packet.js new file mode 100644 index 00000000..e2e3dac3 --- /dev/null +++ b/src/adb/tcpusb/packet.js @@ -0,0 +1,103 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +class Packet { + static initClass() { + this.A_SYNC = 0x434e5953; + this.A_CNXN = 0x4e584e43; + this.A_OPEN = 0x4e45504f; + this.A_OKAY = 0x59414b4f; + this.A_CLSE = 0x45534c43; + this.A_WRTE = 0x45545257; + this.A_AUTH = 0x48545541; + } + + static checksum(data) { + let sum = 0; + if (data) { for (let char of Array.from(data)) { sum += char; } } + return sum; + } + + static magic(command) { + // We need the full uint32 range, which ">>> 0" thankfully allows us to use + return (command ^ 0xffffffff) >>> 0; + } + + static assemble(command, arg0, arg1, data) { + let chunk; + if (data) { + chunk = new Buffer(24 + data.length); + chunk.writeUInt32LE(command, 0); + chunk.writeUInt32LE(arg0, 4); + chunk.writeUInt32LE(arg1, 8); + chunk.writeUInt32LE(data.length, 12); + chunk.writeUInt32LE(Packet.checksum(data), 16); + chunk.writeUInt32LE(Packet.magic(command), 20); + data.copy(chunk, 24); + return chunk; + } else { + chunk = new Buffer(24); + chunk.writeUInt32LE(command, 0); + chunk.writeUInt32LE(arg0, 4); + chunk.writeUInt32LE(arg1, 8); + chunk.writeUInt32LE(0, 12); + chunk.writeUInt32LE(0, 16); + chunk.writeUInt32LE(Packet.magic(command), 20); + return chunk; + } + } + + static swap32(n) { + const buffer = new Buffer(4); + buffer.writeUInt32LE(n, 0); + return buffer.readUInt32BE(0); + } + + constructor(command, arg0, arg1, length, check, magic, data) { + this.command = command; + this.arg0 = arg0; + this.arg1 = arg1; + this.length = length; + this.check = check; + this.magic = magic; + this.data = data; + } + + verifyChecksum() { + return this.check === Packet.checksum(this.data); + } + + verifyMagic() { + return this.magic === Packet.magic(this.command); + } + + toString() { + const type = (() => { switch (this.command) { + case Packet.A_SYNC: + return "SYNC"; + case Packet.A_CNXN: + return "CNXN"; + case Packet.A_OPEN: + return "OPEN"; + case Packet.A_OKAY: + return "OKAY"; + case Packet.A_CLSE: + return "CLSE"; + case Packet.A_WRTE: + return "WRTE"; + case Packet.A_AUTH: + return "AUTH"; + default: + throw new Error("Unknown command {@command}"); + } })(); + return `${type} arg0=${this.arg0} arg1=${this.arg1} length=${this.length}`; + } +} +Packet.initClass(); + +module.exports = Packet; diff --git a/src/adb/tcpusb/packetreader.js b/src/adb/tcpusb/packetreader.js new file mode 100644 index 00000000..8e756d9c --- /dev/null +++ b/src/adb/tcpusb/packetreader.js @@ -0,0 +1,123 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {EventEmitter} = require('events'); + +const Packet = require('./packet'); + +class PacketReader extends EventEmitter { + constructor(stream) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.stream = stream; + super(); + this.inBody = false; + this.buffer = null; + this.packet = null; + this.stream.on('readable', this._tryRead.bind(this)); + this.stream.on('error', err => this.emit('error', err)); + this.stream.on('end', () => this.emit('end')); + setImmediate(this._tryRead.bind(this)); + } + + _tryRead() { + while (this._appendChunk()) { + while (this.buffer) { + if (this.inBody) { + if (!(this.buffer.length >= this.packet.length)) { break; } + this.packet.data = this._consume(this.packet.length); + if (!this.packet.verifyChecksum()) { + this.emit('error', new PacketReader.ChecksumError(this.packet)); + return; + } + this.emit('packet', this.packet); + this.inBody = false; + } else { + if (!(this.buffer.length >= 24)) { break; } + const header = this._consume(24); + this.packet = new Packet( + header.readUInt32LE(0), + header.readUInt32LE(4), + header.readUInt32LE(8), + header.readUInt32LE(12), + header.readUInt32LE(16), + header.readUInt32LE(20), + new Buffer(0) + ); + if (!this.packet.verifyMagic()) { + this.emit('error', new PacketReader.MagicError(this.packet)); + return; + } + if (this.packet.length === 0) { + this.emit('packet', this.packet); + } else { + this.inBody = true; + } + } + } + } + } + + _appendChunk() { + let chunk; + if ((chunk = this.stream.read())) { + if (this.buffer) { + return this.buffer = Buffer.concat([this.buffer, chunk], this.buffer.length + chunk.length); + } else { + return this.buffer = chunk; + } + } else { + return null; + } + } + + _consume(length) { + const chunk = this.buffer.slice(0, length); + this.buffer = length === this.buffer.length ? null : this.buffer.slice(length); + return chunk; + } +} + +PacketReader.ChecksumError = class ChecksumError extends Error { + constructor(packet) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.packet = packet; + Error.call(this); + this.name = 'ChecksumError'; + this.message = "Checksum mismatch"; + Error.captureStackTrace(this, PacketReader.ChecksumError); + } +}; + +PacketReader.MagicError = class MagicError extends Error { + constructor(packet) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.packet = packet; + Error.call(this); + this.name = 'MagicError'; + this.message = "Magic value mismatch"; + Error.captureStackTrace(this, PacketReader.MagicError); + } +}; + +module.exports = PacketReader; diff --git a/src/adb/tcpusb/rollingcounter.js b/src/adb/tcpusb/rollingcounter.js new file mode 100644 index 00000000..e32066ff --- /dev/null +++ b/src/adb/tcpusb/rollingcounter.js @@ -0,0 +1,20 @@ +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +class RollingCounter { + constructor(max, min) { + this.max = max; + if (min == null) { min = 1; } + this.min = min; + this.now = this.min; + } + + next() { + if (!(this.now < this.max)) { this.now = this.min; } + return ++this.now; + } +} + +module.exports = RollingCounter; diff --git a/src/adb/tcpusb/server.js b/src/adb/tcpusb/server.js new file mode 100644 index 00000000..443f8c66 --- /dev/null +++ b/src/adb/tcpusb/server.js @@ -0,0 +1,67 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Net = require('net'); +const {EventEmitter} = require('events'); + +const Socket = require('./socket'); + +class Server extends EventEmitter { + constructor(client, serial, options) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.client = client; + this.serial = serial; + this.options = options; + this.connections = []; + this.server = Net.createServer({allowHalfOpen: true}); + this.server.on('error', err => { + return this.emit('error', err); + }); + this.server.on('listening', () => { + return this.emit('listening'); + }); + this.server.on('close', () => { + return this.emit('close'); + }); + this.server.on('connection', conn => { + const socket = new Socket(this.client, this.serial, conn, this.options); + this.connections.push(socket); + socket.on('error', err => { + // 'conn' is guaranteed to get ended + return this.emit('error', err); + }); + socket.once('end', () => { + // 'conn' is guaranteed to get ended + return this.connections = this.connections.filter(val => val !== socket); + }); + return this.emit('connection', socket); + }); + } + + listen() { + this.server.listen.apply(this.server, arguments); + return this; + } + + close() { + this.server.close(); + return this; + } + + end() { + for (let conn of Array.from(this.connections)) { conn.end(); } + return this; + } +} + +module.exports = Server; diff --git a/src/adb/tcpusb/service.js b/src/adb/tcpusb/service.js new file mode 100644 index 00000000..6ee0f7a8 --- /dev/null +++ b/src/adb/tcpusb/service.js @@ -0,0 +1,182 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {EventEmitter} = require('events'); + +const Promise = require('bluebird'); +const debug = require('debug')('adb:tcpusb:service'); + +const Parser = require('../parser'); +const Protocol = require('../protocol'); +const Packet = require('./packet'); + +class Service extends EventEmitter { + constructor(client, serial, localId, remoteId, socket) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.client = client; + this.serial = serial; + this.localId = localId; + this.remoteId = remoteId; + this.socket = socket; + super(); + this.opened = false; + this.ended = false; + this.transport = null; + this.needAck = false; + } + + end() { + if (this.transport) { this.transport.end(); } + if (this.ended) { return this; } + debug('O:A_CLSE'); + const localId = this.opened ? this.localId : 0; // Zero can only mean a failed open + try { + // We may or may not have gotten here due to @socket ending, so write + // may fail. + this.socket.write(Packet.assemble(Packet.A_CLSE, localId, this.remoteId, null)); + } catch (err) {} + // Let it go + this.transport = null; + this.ended = true; + this.emit('end'); + return this; + } + + handle(packet) { + return Promise.try(() => { + switch (packet.command) { + case Packet.A_OPEN: + return this._handleOpenPacket(packet); + case Packet.A_OKAY: + return this._handleOkayPacket(packet); + case Packet.A_WRTE: + return this._handleWritePacket(packet); + case Packet.A_CLSE: + return this._handleClosePacket(packet); + default: + throw new Error(`Unexpected packet ${packet.command}`); + } + }).catch(err => { + this.emit('error', err); + return this.end(); + }); + } + + _handleOpenPacket(packet) { + debug('I:A_OPEN', packet); + return this.client.transport(this.serial) + .then(transport => { + this.transport = transport; + if (this.ended) { throw new LateTransportError(); } + this.transport.write(Protocol.encodeData( + packet.data.slice(0, -1)) + ); // Discard null byte at end + return this.transport.parser.readAscii(4) + .then(reply => { + switch (reply) { + case Protocol.OKAY: + debug('O:A_OKAY'); + this.socket.write( + Packet.assemble(Packet.A_OKAY, this.localId, this.remoteId, null)); + return this.opened = true; + case Protocol.FAIL: + return this.transport.parser.readError(); + default: + return this.transport.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + }).then(() => { + return new Promise((resolve, reject) => { + this.transport.socket + .on('readable', () => this._tryPush()) + .on('end', resolve) + .on('error', reject); + return this._tryPush(); + }); + }).finally(() => { + return this.end(); + }); + } + + _handleOkayPacket(packet) { + debug('I:A_OKAY', packet); + if (this.ended) { return; } + if (!this.transport) { throw new Service.PrematurePacketError(packet); } + this.needAck = false; + return this._tryPush(); + } + + _handleWritePacket(packet) { + debug('I:A_WRTE', packet); + if (this.ended) { return; } + if (!this.transport) { throw new Service.PrematurePacketError(packet); } + if (packet.data) { this.transport.write(packet.data); } + debug('O:A_OKAY'); + return this.socket.write(Packet.assemble(Packet.A_OKAY, this.localId, this.remoteId, null)); + } + + _handleClosePacket(packet) { + debug('I:A_CLSE', packet); + if (this.ended) { return; } + if (!this.transport) { throw new Service.PrematurePacketError(packet); } + return this.end(); + } + + _tryPush() { + let chunk; + if (this.needAck || this.ended) { return; } + if (chunk = this._readChunk(this.transport.socket)) { + debug('O:A_WRTE'); + this.socket.write(Packet.assemble(Packet.A_WRTE, this.localId, this.remoteId, chunk)); + return this.needAck = true; + } + } + + _readChunk(stream) { + return stream.read(this.socket.maxPayload) || stream.read(); + } +} + +Service.PrematurePacketError = class PrematurePacketError extends Error { + constructor(packet) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.packet = packet; + Error.call(this); + this.name = 'PrematurePacketError'; + this.message = "Premature packet"; + Error.captureStackTrace(this, Service.PrematurePacketError); + } +}; + +Service.LateTransportError = class LateTransportError extends Error { + constructor() { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + Error.call(this); + this.name = 'LateTransportError'; + this.message = "Late transport"; + Error.captureStackTrace(this, Service.LateTransportError); + } +}; + +module.exports = Service; diff --git a/src/adb/tcpusb/servicemap.js b/src/adb/tcpusb/servicemap.js new file mode 100644 index 00000000..deac5b9e --- /dev/null +++ b/src/adb/tcpusb/servicemap.js @@ -0,0 +1,46 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +class ServiceMap { + constructor() { + this.remotes = Object.create(null); + this.count = 0; + } + + end() { + for (let remoteId in this.remotes) { + const remote = this.remotes[remoteId]; + remote.end(); + } + this.remotes = Object.create(null); + this.count = 0; + } + + insert(remoteId, socket) { + if (this.remotes[remoteId]) { + throw new Error(`Remote ID ${remoteId} is already being used`); + } else { + this.count += 1; + return this.remotes[remoteId] = socket; + } + } + + get(remoteId) { + return this.remotes[remoteId] || null; + } + + remove(remoteId) { + let remote; + if (remote = this.remotes[remoteId]) { + delete this.remotes[remoteId]; + this.count -= 1; + return remote; + } else { + return null; + } + } +} + +module.exports = ServiceMap; diff --git a/src/adb/tcpusb/socket.js b/src/adb/tcpusb/socket.js new file mode 100644 index 00000000..bc23300e --- /dev/null +++ b/src/adb/tcpusb/socket.js @@ -0,0 +1,283 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const crypto = require('crypto'); +const {EventEmitter} = require('events'); + +const Promise = require('bluebird'); +const Forge = require('node-forge'); +const debug = require('debug')('adb:tcpusb:socket'); + +const Parser = require('../parser'); +const Protocol = require('../protocol'); +const Auth = require('../auth'); +const Packet = require('./packet'); +const PacketReader = require('./packetreader'); +const Service = require('./service'); +const ServiceMap = require('./servicemap'); +const RollingCounter = require('./rollingcounter'); + +var Socket = (function() { + let UINT32_MAX = undefined; + let UINT16_MAX = undefined; + let AUTH_TOKEN = undefined; + let AUTH_SIGNATURE = undefined; + let AUTH_RSAPUBLICKEY = undefined; + let TOKEN_LENGTH = undefined; + Socket = class Socket extends EventEmitter { + static initClass() { + UINT32_MAX = 0xFFFFFFFF; + UINT16_MAX = 0xFFFF; + + AUTH_TOKEN = 1; + AUTH_SIGNATURE = 2; + AUTH_RSAPUBLICKEY = 3; + + TOKEN_LENGTH = 20; + } + + constructor(client, serial, socket, options) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.client = client; + this.serial = serial; + this.socket = socket; + if (options == null) { options = {}; } + this.options = options; + if (!this.options.auth) { this.options.auth = Promise.resolve(true); } + this.ended = false; + this.socket.setNoDelay(true); + this.reader = new PacketReader(this.socket) + .on('packet', this._handle.bind(this)) + .on('error', err => { + debug(`PacketReader error: ${err.message}`); + return this.end(); + }).on('end', this.end.bind(this)); + this.version = 1; + this.maxPayload = 4096; + this.authorized = false; + this.syncToken = new RollingCounter(UINT32_MAX); + this.remoteId = new RollingCounter(UINT32_MAX); + this.services = new ServiceMap; + this.remoteAddress = this.socket.remoteAddress; + this.token = null; + this.signature = null; + } + + end() { + if (this.ended) { return this; } + // End services first so that they can send a final payload before FIN. + this.services.end(); + this.socket.end(); + this.ended = true; + this.emit('end'); + return this; + } + + _error(err) { + this.emit('error', err); + return this.end(); + } + + _handle(packet) { + if (this.ended) { return; } + this.emit('userActivity', packet); + return Promise.try(() => { + switch (packet.command) { + case Packet.A_SYNC: + return this._handleSyncPacket(packet); + case Packet.A_CNXN: + return this._handleConnectionPacket(packet); + case Packet.A_OPEN: + return this._handleOpenPacket(packet); + case Packet.A_OKAY: case Packet.A_WRTE: case Packet.A_CLSE: + return this._forwardServicePacket(packet); + case Packet.A_AUTH: + return this._handleAuthPacket(packet); + default: + throw new Error(`Unknown command ${packet.command}`); + } + }).catch(Socket.AuthError, () => { + return this.end(); + }).catch(Socket.UnauthorizedError, () => { + return this.end(); + }).catch(err => { + return this._error(err); + }); + } + + _handleSyncPacket(packet) { + // No need to do anything? + debug('I:A_SYNC'); + debug('O:A_SYNC'); + return this.write(Packet.assemble(Packet.A_SYNC, 1, this.syncToken.next(), null)); + } + + _handleConnectionPacket(packet) { + debug('I:A_CNXN', packet); + const version = Packet.swap32(packet.arg0); + this.maxPayload = Math.min(UINT16_MAX, packet.arg1); + return this._createToken() + .then(token => { + this.token = token; + debug(`Created challenge '${this.token.toString('base64')}'`); + debug('O:A_AUTH'); + return this.write(Packet.assemble(Packet.A_AUTH, AUTH_TOKEN, 0, this.token)); + }); + } + + _handleAuthPacket(packet) { + debug('I:A_AUTH', packet); + switch (packet.arg0) { + case AUTH_SIGNATURE: + // Store first signature, ignore the rest + debug(`Received signature '${packet.data.toString('base64')}'`); + if (!this.signature) { this.signature = packet.data; } + debug('O:A_AUTH'); + return this.write(Packet.assemble(Packet.A_AUTH, AUTH_TOKEN, 0, this.token)); + case AUTH_RSAPUBLICKEY: + if (!this.signature) { + throw new Socket.AuthError("Public key sent before signature"); + } + if (!packet.data || !(packet.data.length >= 2)) { + throw new Socket.AuthError("Empty RSA public key"); + } + debug(`Received RSA public key '${packet.data.toString('base64')}'`); + return Auth.parsePublicKey(this._skipNull(packet.data)) + .then(key => { + const digest = this.token.toString('binary'); + const sig = this.signature.toString('binary'); + if (!key.verify(digest, sig)) { + debug("Signature mismatch"); + throw new Socket.AuthError("Signature mismatch"); + } + debug("Signature verified"); + return key; + }).then(key => { + return this.options.auth(key) + .catch(function(err) { + debug("Connection rejected by user-defined auth handler"); + throw new Socket.AuthError("Rejected by user-defined handler"); + }); + }).then(() => { + return this._deviceId(); + }).then(id => { + this.authorized = true; + debug('O:A_CNXN'); + return this.write(Packet.assemble(Packet.A_CNXN, + Packet.swap32(this.version), this.maxPayload, id) + ); + }); + default: + throw new Error(`Unknown authentication method ${packet.arg0}`); + } + } + + _handleOpenPacket(packet) { + if (!this.authorized) { throw new Socket.UnauthorizedError(); } + const remoteId = packet.arg0; + const localId = this.remoteId.next(); + if (!packet.data || !(packet.data.length >= 2)) { + throw new Error("Empty service name"); + } + const name = this._skipNull(packet.data); + debug(`Calling ${name}`); + const service = new Service(this.client, this.serial, localId, remoteId, this); + return new Promise((resolve, reject) => { + service.on('error', reject); + service.on('end', resolve); + this.services.insert(localId, service); + debug(`Handling ${this.services.count} services simultaneously`); + return service.handle(packet); + }).catch(err => true).finally(() => { + this.services.remove(localId); + debug(`Handling ${this.services.count} services simultaneously`); + return service.end(); + }); + } + + _forwardServicePacket(packet) { + let service; + if (!this.authorized) { throw new Socket.UnauthorizedError(); } + const remoteId = packet.arg0; + const localId = packet.arg1; + if ((service = this.services.get(localId))) { + return service.handle(packet); + } else { + return debug("Received a packet to a service that may have been closed already"); + } + } + + write(chunk) { + if (this.ended) { return; } + return this.socket.write(chunk); + } + + _createToken() { + return Promise.promisify(crypto.randomBytes)(TOKEN_LENGTH); + } + + _skipNull(data) { + return data.slice(0, -1); // Discard null byte at end + } + + _deviceId() { + debug("Loading device properties to form a standard device ID"); + return this.client.getProperties(this.serial) + .then(function(properties) { + const id = (([ + 'ro.product.name', + 'ro.product.model', + 'ro.product.device' + ]).map((prop) => `${prop}=${properties[prop]};`)).join(''); + return new Buffer(`device::${id}\0`); + }); + } + }; + Socket.initClass(); + return Socket; +})(); + +Socket.AuthError = class AuthError extends Error { + constructor(message) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.message = message; + Error.call(this); + this.name = 'AuthError'; + Error.captureStackTrace(this, Socket.AuthError); + } +}; + +Socket.UnauthorizedError = class UnauthorizedError extends Error { + constructor() { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + Error.call(this); + this.name = 'UnauthorizedError'; + this.message = "Unauthorized access"; + Error.captureStackTrace(this, Socket.UnauthorizedError); + } +}; + +module.exports = Socket; diff --git a/src/adb/tracker.js b/src/adb/tracker.js new file mode 100644 index 00000000..f3011309 --- /dev/null +++ b/src/adb/tracker.js @@ -0,0 +1,87 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {EventEmitter} = require('events'); +const Promise = require('bluebird'); + +const Parser = require('./parser'); + +class Tracker extends EventEmitter { + constructor(command) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super(); } + let thisFn = (() => { return this; }).toString(); + let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1]; + eval(`${thisName} = this;`); + } + this.command = command; + this.deviceList = []; + this.deviceMap = {}; + this.reader = this.read() + .catch(Promise.CancellationError, () => true).catch(Parser.PrematureEOFError, function() { + throw new Error('Connection closed'); + }).catch(err => { + this.emit('error', err); + + }).finally(() => { + return this.command.parser.end() + .then(() => { + return this.emit('end'); + }); + }); + } + + read() { + return this.command._readDevices() + .cancellable() + .then(list => { + this.update(list); + return this.read(); + }); + } + + update(newList) { + let device; + const changeSet = { + removed: [], + changed: [], + added: [] + }; + const newMap = {}; + for (device of Array.from(newList)) { + const oldDevice = this.deviceMap[device.id]; + if (oldDevice) { + if (oldDevice.type !== device.type) { + changeSet.changed.push(device); + this.emit('change', device, oldDevice); + } + } else { + changeSet.added.push(device); + this.emit('add', device); + } + newMap[device.id] = device; + } + for (device of Array.from(this.deviceList)) { + if (!newMap[device.id]) { + changeSet.removed.push(device); + this.emit('remove', device); + } + } + this.emit('changeSet', changeSet); + this.deviceList = newList; + this.deviceMap = newMap; + return this; + } + + end() { + this.reader.cancel(); + return this; + } +} + +module.exports = Tracker; diff --git a/src/adb/util.js b/src/adb/util.js new file mode 100644 index 00000000..a6260879 --- /dev/null +++ b/src/adb/util.js @@ -0,0 +1,13 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Parser = require('./parser'); +const Auth = require('./auth'); + +module.exports.readAll = (stream, callback) => new Parser(stream).readAll(stream) + .nodeify(callback); + +module.exports.parsePublicKey = (keyString, callback) => Auth.parsePublicKey(keyString) + .nodeify(callback); diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 00000000..ec55e48f --- /dev/null +++ b/src/cli.js @@ -0,0 +1,63 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const fs = require('fs'); +const program = require('commander'); +const Promise = require('bluebird'); +const forge = require('node-forge'); + +const pkg = require('../package'); +const Adb = require('./adb'); +const Auth = require('./adb/auth'); +const PacketReader = require('./adb/tcpusb/packetreader'); + +Promise.longStackTraces(); + +program + .version(pkg.version); + +program + .command('pubkey-convert ') + .option('-f, --format ', 'format (pem or openssh)', String, 'pem') + .description('Converts an ADB-generated public key into PEM format.') + .action((file, options) => Auth.parsePublicKey(fs.readFileSync(file)) + .then(function(key) { + switch (options.format.toLowerCase()) { + case 'pem': + return console.log(forge.pki.publicKeyToPem(key).trim()); + case 'openssh': + return console.log(forge.ssh.publicKeyToOpenSSH(key, 'adbkey').trim()); + default: + console.error(`Unsupported format '${options.format}'`); + return process.exit(1); + } +})); + +program + .command('pubkey-fingerprint ') + .description('Outputs the fingerprint of an ADB-generated public key.') + .action(file => Auth.parsePublicKey(fs.readFileSync(file)) + .then(key => console.log('%s %s', key.fingerprint, key.comment))); + +program + .command('usb-device-to-tcp ') + .option('-p, --port ', 'port number', String, 6174) + .description('Provides an USB device over TCP using a translating proxy.') + .action(function(serial, options) { + const adb = Adb.createClient(); + const server = adb.createTcpUsbBridge(serial, {auth() { return Promise.resolve(); }}) + .on('listening', () => console.info('Connect with `adb connect localhost:%d`', options.port)).on('error', err => console.error(`An error occured: ${err.message}`)); + return server.listen(options.port); +}); + +program + .command('parse-tcp-packets ') + .description('Parses ADB TCP packets from the given file.') + .action(function(file, options) { + const reader = new PacketReader(fs.createReadStream(file)); + return reader.on('packet', packet => console.log(packet.toString())); +}); + +program.parse(process.argv); diff --git a/tasks/keycode.js b/tasks/keycode.js new file mode 100644 index 00000000..4f28f5b6 --- /dev/null +++ b/tasks/keycode.js @@ -0,0 +1,63 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Https = require('https'); + +module.exports = grunt => grunt.registerMultiTask('keycode', 'Updates KeyEvent mapping.', function() { + + const repo_path = '/android/platform_frameworks_base/master'; + const done = this.async(); + const options = this.options({ + original: { + hostname: 'raw.github.com', + path: `${repo_path}/core/java/android/view/KeyEvent.java`, + method: 'GET' + }, + regex: /public static final int (KEYCODE_[^\s]+)\s*=\s*([0-9]+);/g + }); + + return grunt.util.async.forEach(this.files, function(file, next) { + const req = Https.request(options.original, function(res) { + if (res.statusCode !== 200) { + grunt.fail.warn( + `Unable to retrieve KeyEvent.java (HTTP ${res.statusCode})`); + return next(); + } + + let raw = new Buffer(''); + + res.on('data', chunk => raw = Buffer.concat([raw, chunk])); + + return res.on('end', function() { + let match; + const code = raw.toString(); + const date = new Date().toUTCString(); + const coffee = []; + coffee.push(`# Generated by \`grunt keycode\` on ${date}`); + coffee.push( + "# KeyEvent.java Copyright (C) 2006 The Android Open Source Project"); + coffee.push(''); + coffee.push('module.exports ='); + + while ((match = options.regex.exec(code))) { + coffee.push(` ${match[1]}: ${match[2]}`); + } + + coffee.push(''); + + grunt.file.write(file.dest, coffee.join('\n')); + grunt.log.ok(`File ${file.dest} created`); + + return next(); + }); + }); + + req.on('error', next); + + return req.end(); + } + + , done); +}); diff --git a/test/adb.js b/test/adb.js new file mode 100644 index 00000000..7ce3089f --- /dev/null +++ b/test/adb.js @@ -0,0 +1,31 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {expect} = require('chai'); + +const Adb = require('../src/adb'); +const Client = require('../src/adb/client'); +const Keycode = require('../src/adb/keycode'); +const util = require('../src/adb/util'); + +describe('Adb', function() { + + it("should expose Keycode", function(done) { + expect(Adb).to.have.property('Keycode'); + expect(Adb.Keycode).to.equal(Keycode); + return done(); + }); + + it("should expose util", function(done) { + expect(Adb).to.have.property('util'); + expect(Adb.util).to.equal(util); + return done(); + }); + + return describe('@createClient(options)', () => it("should return a Client instance", function(done) { + expect(Adb.createClient()).to.be.an.instanceOf(Client); + return done(); + })); +}); diff --git a/test/adb/command/host-serial/waitfordevice.js b/test/adb/command/host-serial/waitfordevice.js new file mode 100644 index 00000000..3a4b2934 --- /dev/null +++ b/test/adb/command/host-serial/waitfordevice.js @@ -0,0 +1,64 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const WaitForDeviceCommand = + require('../../../../src/adb/command/host-serial/waitfordevice'); + +describe('WaitForDeviceCommand', function() { + + it("should send 'host-serial::wait-for-any'", function(done) { + const conn = new MockConnection; + const cmd = new WaitForDeviceCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('host-serial:abba:wait-for-any').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute('abba') + .then(() => done()); + }); + + it("should resolve with id when the device is connected", function(done) { + const conn = new MockConnection; + const cmd = new WaitForDeviceCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute('abba') + .then(function(id) { + expect(id).to.equal('abba'); + return done(); + }); + }); + + return it("should reject with error if unable to connect", function(done) { + const conn = new MockConnection; + const cmd = new WaitForDeviceCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.FAIL); + conn.socket.causeRead( + Protocol.encodeData('not sure how this might happen')); + return conn.socket.causeEnd(); + }); + return cmd.execute('abba') + .catch(function(err) { + expect(err.message).to.contain('not sure how this might happen'); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/clear.js b/test/adb/command/host-transport/clear.js new file mode 100644 index 00000000..0cd5f391 --- /dev/null +++ b/test/adb/command/host-transport/clear.js @@ -0,0 +1,86 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const ClearCommand = require('../../../../src/adb/command/host-transport/clear'); + +describe('ClearCommand', function() { + + it("should send 'pm clear '", function(done) { + const conn = new MockConnection; + const cmd = new ClearCommand(conn); + conn.socket.on('write', function(chunk) { + expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:pm clear foo.bar.c').toString()); + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo.bar.c') + .then(() => done()); + }); + + it("should succeed on 'Success'", function(done) { + const conn = new MockConnection; + const cmd = new ClearCommand(conn); + conn.socket.on('write', function(chunk) { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo.bar.c') + .then(() => done()); + }); + + it("should error on 'Failed'", function(done) { + const conn = new MockConnection; + const cmd = new ClearCommand(conn); + conn.socket.on('write', function(chunk) { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Failed\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo.bar.c') + .catch(function(err) { + expect(err).to.be.an.instanceof(Error); + return done(); + }); + }); + + it(`should error on 'Failed' even if connection not closed by \ +device`, function(done) { + const conn = new MockConnection; + const cmd = new ClearCommand(conn); + conn.socket.on('write', function(chunk) { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeRead('Failed\r\n'); + }); + return cmd.execute('foo.bar.c') + .catch(function(err) { + expect(err).to.be.an.instanceof(Error); + return done(); + }); + }); + + return it("should ignore irrelevant lines", function(done) { + const conn = new MockConnection; + const cmd = new ClearCommand(conn); + conn.socket.on('write', function(chunk) { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Open: foo error\n\n'); + conn.socket.causeRead('Success\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo.bar.c') + .then(() => done()); + }); +}); diff --git a/test/adb/command/host-transport/framebuffer.js b/test/adb/command/host-transport/framebuffer.js new file mode 100644 index 00000000..2ce5ad4d --- /dev/null +++ b/test/adb/command/host-transport/framebuffer.js @@ -0,0 +1,82 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const FrameBufferCommand = + require('../../../../src/adb/command/host-transport/framebuffer'); + +describe('FrameBufferCommand', function() { + + it("should send 'framebuffer:'", function(done) { + const conn = new MockConnection; + const cmd = new FrameBufferCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('framebuffer:').toString())); + setImmediate(function() { + const meta = new Buffer(52); + meta.fill(0); + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(meta); + return conn.socket.causeEnd(); + }); + return cmd.execute('raw') + .then(() => done()); + }); + + return it(`should parse meta header and return it as the 'meta' \ +property of the stream`, function(done) { + const conn = new MockConnection; + const cmd = new FrameBufferCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('framebuffer:').toString())); + setImmediate(function() { + const meta = new Buffer(52); + let offset = 0; + meta.writeUInt32LE(1, offset); + meta.writeUInt32LE(32, (offset += 4)); + meta.writeUInt32LE(819200, (offset += 4)); + meta.writeUInt32LE(640, (offset += 4)); + meta.writeUInt32LE(320, (offset += 4)); + meta.writeUInt32LE(0, (offset += 4)); + meta.writeUInt32LE(8, (offset += 4)); + meta.writeUInt32LE(16, (offset += 4)); + meta.writeUInt32LE(8, (offset += 4)); + meta.writeUInt32LE(8, (offset += 4)); + meta.writeUInt32LE(8, (offset += 4)); + meta.writeUInt32LE(24, (offset += 4)); + meta.writeUInt32LE(8, (offset += 4)); + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(meta); + return conn.socket.causeEnd(); + }); + return cmd.execute('raw') + .then(function(stream) { + expect(stream).to.have.property('meta'); + expect(stream.meta).to.eql({ + version: 1, + bpp: 32, + size: 819200, + width: 640, + height: 320, + red_offset: 0, + red_length: 8, + blue_offset: 16, + blue_length: 8, + green_offset: 8, + green_length: 8, + alpha_offset: 24, + alpha_length: 8, + format: 'rgba' + }); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/getfeatures.js b/test/adb/command/host-transport/getfeatures.js new file mode 100644 index 00000000..f471ea07 --- /dev/null +++ b/test/adb/command/host-transport/getfeatures.js @@ -0,0 +1,70 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const GetFeaturesCommand = + require('../../../../src/adb/command/host-transport/getfeatures'); + +describe('GetFeaturesCommand', function() { + + it("should send 'pm list features'", function(done) { + const conn = new MockConnection; + const cmd = new GetFeaturesCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:pm list features 2>/dev/null').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(() => done()); + }); + + it("should return an empty object for an empty feature list", function(done) { + const conn = new MockConnection; + const cmd = new GetFeaturesCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(features) { + expect(Object.keys(features)).to.be.empty; + return done(); + }); + }); + + return it("should return a map of features", function(done) { + const conn = new MockConnection; + const cmd = new GetFeaturesCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(`\ +feature:reqGlEsVersion=0x20000 +feature:foo\r +feature:bar\ +` + ); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(features) { + expect(Object.keys(features)).to.have.length(3); + expect(features).to.eql({ + reqGlEsVersion: '0x20000', + foo: true, + bar: true + }); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/getpackages.js b/test/adb/command/host-transport/getpackages.js new file mode 100644 index 00000000..b951b68c --- /dev/null +++ b/test/adb/command/host-transport/getpackages.js @@ -0,0 +1,78 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const GetPackagesCommand = + require('../../../../src/adb/command/host-transport/getpackages'); + +describe('GetPackagesCommand', function() { + + it("should send 'pm list packages'", function(done) { + const conn = new MockConnection; + const cmd = new GetPackagesCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:pm list packages 2>/dev/null').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(() => done()); + }); + + it("should return an empty array for an empty package list", function(done) { + const conn = new MockConnection; + const cmd = new GetPackagesCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(packages) { + expect(packages).to.be.empty; + return done(); + }); + }); + + return it("should return an array of packages", function(done) { + const conn = new MockConnection; + const cmd = new GetPackagesCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(`\ +package:com.google.android.gm +package:com.google.android.inputmethod.japanese +package:com.google.android.tag\r +package:com.google.android.GoogleCamera +package:com.google.android.youtube +package:com.google.android.apps.magazines +package:com.google.earth\ +` + ); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(packages) { + expect(packages).to.have.length(7); + expect(packages).to.eql([ + 'com.google.android.gm', + 'com.google.android.inputmethod.japanese', + 'com.google.android.tag', + 'com.google.android.GoogleCamera', + 'com.google.android.youtube', + 'com.google.android.apps.magazines', + 'com.google.earth' + ]); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/getproperties.js b/test/adb/command/host-transport/getproperties.js new file mode 100644 index 00000000..2d4950e1 --- /dev/null +++ b/test/adb/command/host-transport/getproperties.js @@ -0,0 +1,72 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const GetPropertiesCommand = + require('../../../../src/adb/command/host-transport/getproperties'); + +describe('GetPropertiesCommand', function() { + + it("should send 'getprop'", function(done) { + const conn = new MockConnection; + const cmd = new GetPropertiesCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:getprop').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(() => done()); + }); + + it("should return an empty object for an empty property list", function(done) { + const conn = new MockConnection; + const cmd = new GetPropertiesCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(properties) { + expect(Object.keys(properties)).to.be.empty; + return done(); + }); + }); + + return it("should return a map of properties", function(done) { + const conn = new MockConnection; + const cmd = new GetPropertiesCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(`\ +[ro.product.locale.region]: [US] +[ro.product.manufacturer]: [samsung]\r +[ro.product.model]: [SC-04E] +[ro.product.name]: [SC-04E]\ +` + ); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(properties) { + expect(Object.keys(properties)).to.have.length(4); + expect(properties).to.eql({ + 'ro.product.locale.region': 'US', + 'ro.product.manufacturer': 'samsung', + 'ro.product.model': 'SC-04E', + 'ro.product.name': 'SC-04E' + }); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/install.js b/test/adb/command/host-transport/install.js new file mode 100644 index 00000000..5266893f --- /dev/null +++ b/test/adb/command/host-transport/install.js @@ -0,0 +1,84 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const InstallCommand = + require('../../../../src/adb/command/host-transport/install'); + +describe('InstallCommand', function() { + + it("should send 'pm install -r '", function(done) { + const conn = new MockConnection; + const cmd = new InstallCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:pm install -r "foo"').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); + + it("should succeed when command responds with 'Success'", function(done) { + const conn = new MockConnection; + const cmd = new InstallCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); + + it("should reject if command responds with 'Failure [REASON]'", function(done) { + const conn = new MockConnection; + const cmd = new InstallCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Failure [BAR]\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .catch(err => done()); + }); + + it("should give detailed reason in rejection's code property", function(done) { + const conn = new MockConnection; + const cmd = new InstallCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Failure [ALREADY_EXISTS]\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .catch(function(err) { + expect(err.code).to.equal('ALREADY_EXISTS'); + return done(); + }); + }); + + return it("should ignore any other data", function(done) { + const conn = new MockConnection; + const cmd = new InstallCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('open: Permission failed\r\n'); + conn.socket.causeRead('Success\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); +}); diff --git a/test/adb/command/host-transport/isinstalled.js b/test/adb/command/host-transport/isinstalled.js new file mode 100644 index 00000000..e369908d --- /dev/null +++ b/test/adb/command/host-transport/isinstalled.js @@ -0,0 +1,76 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const IsInstalledCommand = + require('../../../../src/adb/command/host-transport/isinstalled'); + +describe('IsInstalledCommand', function() { + + it("should send 'pm path '", function(done) { + const conn = new MockConnection; + const cmd = new IsInstalledCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData("shell:pm path foo 2>/dev/null").toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('package:foo\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); + + it("should resolve with true if package returned by command", function(done) { + const conn = new MockConnection; + const cmd = new IsInstalledCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('package:bar\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(function(found) { + expect(found).to.be.true; + return done(); + }); + }); + + it("should resolve with false if no package returned", function(done) { + const conn = new MockConnection; + const cmd = new IsInstalledCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(function(found) { + expect(found).to.be.false; + return done(); + }); + }); + + return it("should fail if any other data is received", function(done) { + const conn = new MockConnection; + const cmd = new IsInstalledCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('open: Permission failed\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .catch(function(err) { + expect(err).to.be.an.instanceof(Error); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/local.js b/test/adb/command/host-transport/local.js new file mode 100644 index 00000000..f556ede8 --- /dev/null +++ b/test/adb/command/host-transport/local.js @@ -0,0 +1,55 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const LocalCommand = require('../../../../src/adb/command/host-transport/local'); + +describe('LocalCommand', function() { + + it("should send 'localfilesystem:'", function(done) { + const conn = new MockConnection; + const cmd = new LocalCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('localfilesystem:/foo.sock').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute('/foo.sock') + .then(stream => done()); + }); + + it("should send ':' if prefixed with ':'", function(done) { + const conn = new MockConnection; + const cmd = new LocalCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('localabstract:/foo.sock').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute('localabstract:/foo.sock') + .then(stream => done()); + }); + + return it("should resolve with the stream", function(done) { + const conn = new MockConnection; + const cmd = new LocalCommand(conn); + setImmediate(() => conn.socket.causeRead(Protocol.OKAY)); + return cmd.execute('/foo.sock') + .then(function(stream) { + stream.end(); + expect(stream).to.be.an.instanceof(Stream.Readable); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/log.js b/test/adb/command/host-transport/log.js new file mode 100644 index 00000000..212b8a79 --- /dev/null +++ b/test/adb/command/host-transport/log.js @@ -0,0 +1,42 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const LogCommand = require('../../../../src/adb/command/host-transport/log'); + +describe('LogCommand', function() { + + it("should send 'log:'", function(done) { + const conn = new MockConnection; + const cmd = new LogCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('log:main').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute('main') + .then(stream => done()); + }); + + return it("should resolve with the log stream", function(done) { + const conn = new MockConnection; + const cmd = new LogCommand(conn); + setImmediate(() => conn.socket.causeRead(Protocol.OKAY)); + return cmd.execute('main') + .then(function(stream) { + stream.end(); + expect(stream).to.be.an.instanceof(Stream.Readable); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/logcat.js b/test/adb/command/host-transport/logcat.js new file mode 100644 index 00000000..3e24038f --- /dev/null +++ b/test/adb/command/host-transport/logcat.js @@ -0,0 +1,92 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Promise = require('bluebird'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const Parser = require('../../../../src/adb/parser'); +const LogcatCommand = require('../../../../src/adb/command/host-transport/logcat'); + +describe('LogcatCommand', function() { + + it("should send 'echo && logcat -B *:I'", function(done) { + const conn = new MockConnection; + const cmd = new LogcatCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:echo && \ +logcat -B *:I 2>/dev/null`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(stream => done()); + }); + + it(`should send 'echo && logcat -c && logcat -B *:I' if options.clear \ +is set`, function(done) { + const conn = new MockConnection; + const cmd = new LogcatCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:echo && logcat -c 2>/dev/null && \ +logcat -B *:I 2>/dev/null`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute({clear: true}) + .then(stream => done()); + }); + + it("should resolve with the logcat stream", function(done) { + const conn = new MockConnection; + const cmd = new LogcatCommand(conn); + setImmediate(() => conn.socket.causeRead(Protocol.OKAY)); + return cmd.execute() + .then(function(stream) { + stream.end(); + expect(stream).to.be.an.instanceof(Stream.Readable); + return done(); + }); + }); + + it("should perform CRLF transformation by default", function(done) { + const conn = new MockConnection; + const cmd = new LogcatCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('\r\nfoo\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(stream => new Parser(stream).readAll()).then(function(out) { + expect(out.toString()).to.equal('foo\n'); + return done(); + }); + }); + + return it("should not perform CRLF transformation if not needed", function(done) { + const conn = new MockConnection; + const cmd = new LogcatCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('\nfoo\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(stream => new Parser(stream).readAll()).then(function(out) { + expect(out.toString()).to.equal('foo\r\n'); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/monkey.js b/test/adb/command/host-transport/monkey.js new file mode 100644 index 00000000..7dd75855 --- /dev/null +++ b/test/adb/command/host-transport/monkey.js @@ -0,0 +1,61 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Promise = require('bluebird'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const MonkeyCommand = require('../../../../src/adb/command/host-transport/monkey'); + +describe('MonkeyCommand', function() { + + it("should send 'monkey --port -v'", function(done) { + const conn = new MockConnection; + const cmd = new MonkeyCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:EXTERNAL_STORAGE=/data/local/tmp monkey \ +--port 1080 -v`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeRead(':Monkey: foo\n'); + }); + return cmd.execute(1080) + .then(stream => done()); + }); + + it("should resolve with the output stream", function(done) { + const conn = new MockConnection; + const cmd = new MonkeyCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeRead(':Monkey: foo\n'); + }); + return cmd.execute(1080) + .then(function(stream) { + stream.end(); + expect(stream).to.be.an.instanceof(Stream.Readable); + return done(); + }); + }); + + return it(`should resolve after a timeout if result can't be judged from \ +output`, function(done) { + const conn = new MockConnection; + const cmd = new MonkeyCommand(conn); + setImmediate(() => conn.socket.causeRead(Protocol.OKAY)); + return cmd.execute(1080) + .then(function(stream) { + stream.end(); + expect(stream).to.be.an.instanceof(Stream.Readable); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/reboot.js b/test/adb/command/host-transport/reboot.js new file mode 100644 index 00000000..b46b8f62 --- /dev/null +++ b/test/adb/command/host-transport/reboot.js @@ -0,0 +1,47 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const RebootCommand = require('../../../../src/adb/command/host-transport/reboot'); + +describe('RebootCommand', function() { + + it("should send 'reboot:'", function(done) { + const conn = new MockConnection; + const cmd = new RebootCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('reboot:').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(() => done()); + }); + + return it("should send wait for the connection to end", function(done) { + const conn = new MockConnection; + const cmd = new RebootCommand(conn); + let ended = false; + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('reboot:').toString())); + setImmediate(() => conn.socket.causeRead(Protocol.OKAY)); + setImmediate(function() { + ended = true; + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function() { + expect(ended).to.be.true; + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/remount.js b/test/adb/command/host-transport/remount.js new file mode 100644 index 00000000..bd3caa71 --- /dev/null +++ b/test/adb/command/host-transport/remount.js @@ -0,0 +1,26 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const RemountCommand = require('../../../../src/adb/command/host-transport/remount'); + +describe('RemountCommand', () => it("should send 'remount:'", function(done) { + const conn = new MockConnection; + const cmd = new RemountCommand(conn); + conn.socket.on('write', function(chunk) { + expect(chunk.toString()).to.equal( + Protocol.encodeData('remount:').toString()); + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(() => done()); +})); diff --git a/test/adb/command/host-transport/root.js b/test/adb/command/host-transport/root.js new file mode 100644 index 00000000..4bca99ca --- /dev/null +++ b/test/adb/command/host-transport/root.js @@ -0,0 +1,50 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const RootCommand = require('../../../../src/adb/command/host-transport/root'); + +describe('RootCommand', function() { + + it("should send 'root:'", function(done) { + const conn = new MockConnection; + const cmd = new RootCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('root:').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead("restarting adbd as root\n"); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(val) { + expect(val).to.be.true; + return done(); + }); + }); + + return it("should reject on unexpected reply", function(done) { + const conn = new MockConnection; + const cmd = new RootCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead("adbd cannot run as root in production builds\n"); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .catch(function(err) { + expect(err.message).to.eql( + 'adbd cannot run as root in production builds'); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/screencap.js b/test/adb/command/host-transport/screencap.js new file mode 100644 index 00000000..eb20f820 --- /dev/null +++ b/test/adb/command/host-transport/screencap.js @@ -0,0 +1,88 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const Parser = require('../../../../src/adb/parser'); +const ScreencapCommand = + require('../../../../src/adb/command/host-transport/screencap'); + +describe('ScreencapCommand', function() { + + it("should send 'screencap -p'", function(done) { + const conn = new MockConnection; + const cmd = new ScreencapCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:echo && screencap -p 2>/dev/null').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('\r\nlegit image'); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(stream => done()); + }); + + it("should resolve with the PNG stream", function(done) { + const conn = new MockConnection; + const cmd = new ScreencapCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('\r\nlegit image'); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(stream => new Parser(stream).readAll()).then(function(out) { + expect(out.toString()).to.equal('legit image'); + return done(); + }); + }); + + it("should reject if command not supported", function(done) { + const conn = new MockConnection; + const cmd = new ScreencapCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .catch(err => done()); + }); + + it("should perform CRLF transformation by default", function(done) { + const conn = new MockConnection; + const cmd = new ScreencapCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('\r\nfoo\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(stream => new Parser(stream).readAll()).then(function(out) { + expect(out.toString()).to.equal('foo\n'); + return done(); + }); + }); + + return it("should not perform CRLF transformation if not needed", function(done) { + const conn = new MockConnection; + const cmd = new ScreencapCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('\nfoo\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(stream => new Parser(stream).readAll()).then(function(out) { + expect(out.toString()).to.equal('foo\r\n'); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/shell.js b/test/adb/command/host-transport/shell.js new file mode 100644 index 00000000..07a2e714 --- /dev/null +++ b/test/adb/command/host-transport/shell.js @@ -0,0 +1,73 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const Parser = require('../../../../src/adb/parser'); +const ShellCommand = + require('../../../../src/adb/command/host-transport/shell'); + +describe('ShellCommand', function() { + + it("should pass String commands as-is", function(done) { + const conn = new MockConnection; + const cmd = new ShellCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:foo \'bar').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo \'bar') + .then(out => done()); + }); + + it("should escape Array commands", function(done) { + const conn = new MockConnection; + const cmd = new ShellCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData("shell:'foo' ''\"'\"'bar'\"'\"'' '\"'").toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute(['foo', '\'bar\'', '"']) + .then(out => done()); + }); + + it("should not escape numbers in arguments", function(done) { + const conn = new MockConnection; + const cmd = new ShellCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData("shell:'foo' 67").toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute(['foo', 67]) + .then(out => done()); + }); + + return it(`should reject with FailError on ADB failure (not command \ +failure)`, function(done) { + const conn = new MockConnection; + const cmd = new ShellCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData("shell:'foo'").toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.FAIL); + conn.socket.causeRead(Protocol.encodeData('mystery')); + return conn.socket.causeEnd(); + }); + return cmd.execute(['foo']) + .catch(Parser.FailError, err => done()); + }); +}); diff --git a/test/adb/command/host-transport/startactivity.js b/test/adb/command/host-transport/startactivity.js new file mode 100644 index 00000000..dfe07ef4 --- /dev/null +++ b/test/adb/command/host-transport/startactivity.js @@ -0,0 +1,590 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const StartActivityCommand = require( + '../../../../src/adb/command/host-transport/startactivity'); + +describe('StartActivityCommand', function() { + + it("should succeed when 'Success' returned", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = + {component: 'com.dummy.component/.Main'}; + return cmd.execute(options) + .then(() => done()); + }); + + it("should fail when 'Error' returned", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Error: foo\n'); + return conn.socket.causeEnd(); + }); + const options = + {component: 'com.dummy.component/.Main'}; + return cmd.execute(options) + .catch(function(err) { + expect(err).to.be.be.an.instanceOf(Error); + return done(); + }); + }); + + it("should send 'am start -n '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = + {component: 'com.dummy.component/.Main'}; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -W -D --user 0 -n '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +-n 'com.dummy.component/.Main' \ +-D \ +-W \ +--user 0`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + user: 0, + wait: true, + debug: true + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -a '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +-a 'foo.ACTION_BAR'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = + {action: "foo.ACTION_BAR"}; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -d '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +-d 'foo://bar'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = + {data: "foo://bar"}; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -t '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +-t 'text/plain'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = + {mimeType: "text/plain"}; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -c '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +-c 'android.intent.category.LAUNCHER'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = + {category: "android.intent.category.LAUNCHER"}; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -c -c '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +-c 'android.intent.category.LAUNCHER' \ +-c 'android.intent.category.DEFAULT'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = { + category: [ + "android.intent.category.LAUNCHER", + "android.intent.category.DEFAULT" + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -f '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +-f ${0x10210000}`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = + {flags: 0x10210000}; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -n --es '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--es 'key1' 'value1' \ +--es 'key2' 'value2' \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = { + component: "com.dummy.component/.Main", + extras: [{ + key: 'key1', + value: 'value1' + } + , { + key: 'key2', + value: 'value2' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -n --ei '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--ei 'key1' 1 \ +--ei 'key2' 2 \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + extras: [{ + key: 'key1', + value: 1, + type: 'int' + } + , { + key: 'key2', + value: 2, + type: 'int' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -n --ez '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--ez 'key1' 'true' \ +--ez 'key2' 'false' \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = { + component: "com.dummy.component/.Main", + extras: [{ + key: 'key1', + value: true, + type: 'bool' + } + , { + key: 'key2', + value: false, + type: 'bool' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -n --el '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--el 'key1' 1 \ +--el 'key2' '2' \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + extras: [{ + key: 'key1', + value: 1, + type: 'long' + } + , { + key: 'key2', + value: '2', + type: 'long' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -n --eu '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--eu 'key1' 'http://example.org' \ +--eu 'key2' 'http://example.org' \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + extras: [{ + key: 'key1', + value: 'http://example.org', + type: 'uri' + } + , { + key: 'key2', + value: 'http://example.org', + type: 'uri' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -n --es '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--es 'key1' 'a' \ +--es 'key2' 'b' \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + extras: [{ + key: 'key1', + value: 'a', + type: 'string' + } + , { + key: 'key2', + value: 'b', + type: 'string' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -n --eia '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--eia 'key1' '2,3' \ +--ela 'key2' '20,30' \ +--ei 'key3' 5 \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + extras: [{ + key: 'key1', + value: [ + 2, + 3 + ], + type: 'int' + } + , { + key: 'key2', + value: [ + 20, + 30 + ], + type: 'long' + } + , { + key: 'key3', + value: 5, + type: 'int' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should send 'am start -n --esn '", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--esn 'key1' \ +--esn 'key2' \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + extras: [{ + key: 'key1', + type: 'null' + } + , { + key: 'key2', + type: 'null' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + it("should throw when calling with an unknown extra type", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + const options = { + component: 'com.dummy.component/.Main', + extras: [{ + key: 'key1', + value: 'value1', + type: 'nonexisting' + } + ] + }; + expect(() => cmd.execute(options, function() {})).to.throw; + return done(); + }); + + it("should accept mixed types of extras", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am start \ +--ez 'key1' 'true' \ +--es 'key2' 'somestr' \ +--es 'key3' 'defaultType' \ +--ei 'key4' 3 \ +--el 'key5' '4' \ +--eu 'key6' 'http://example.org' \ +--esn 'key7' \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + extras: [{ + key: 'key1', + value: true, + type: 'bool' + } + , { + key: 'key2', + value: 'somestr', + type: 'string' + } + , { + key: 'key3', + value: 'defaultType' + } + , { + key: 'key4', + value: 3, + type: 'int' + } + , { + key: 'key5', + value: '4', + type: 'long' + } + , { + key: 'key6', + value: 'http://example.org', + type: 'uri' + } + , { + key: 'key7', + type: 'null' + } + ] + }; + return cmd.execute(options) + .then(() => done()); + }); + + return it("should map short extras to long extras", function(done) { + const conn = new MockConnection; + const cmd = new StartActivityCommand(conn); + const short = cmd._formatExtras({ + someString: 'bar', + someInt: 5, + someUrl: { + type: 'uri', + value: 'http://example.org' + }, + someArray: { + type: 'int', + value: [1, 2] + }, + someNull: null + }); + const long = cmd._formatExtras([{ + key: 'someString', + value: 'bar', + type: 'string' + } + , { + key: 'someInt', + value: 5, + type: 'int' + } + , { + key: 'someUrl', + value: 'http://example.org', + type: 'uri' + } + , { + key: 'someArray', + value: [1, 2], + type: 'int' + } + , { + key: 'someNull', + type: 'null' + } + ]); + expect(short).to.eql(long); + return done(); + }); +}); diff --git a/test/adb/command/host-transport/startservice.js b/test/adb/command/host-transport/startservice.js new file mode 100644 index 00000000..7aee3ac2 --- /dev/null +++ b/test/adb/command/host-transport/startservice.js @@ -0,0 +1,88 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const StartServiceCommand = require( + '../../../../src/adb/command/host-transport/startservice'); + +describe('StartServiceCommand', function() { + + it("should succeed when 'Success' returned", function(done) { + const conn = new MockConnection; + const cmd = new StartServiceCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success'); + return conn.socket.causeEnd(); + }); + const options = + {component: 'com.dummy.component/.Main'}; + return cmd.execute(options) + .then(() => done()); + }); + + it("should fail when 'Error' returned", function(done) { + const conn = new MockConnection; + const cmd = new StartServiceCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Error: foo\n'); + return conn.socket.causeEnd(); + }); + const options = + {component: 'com.dummy.component/.Main'}; + return cmd.execute(options) + .catch(function(err) { + expect(err).to.be.be.an.instanceOf(Error); + return done(); + }); + }); + + it("should send 'am startservice --user 0 -n '", function(done) { + const conn = new MockConnection; + const cmd = new StartServiceCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am startservice \ +-n 'com.dummy.component/.Main' \ +--user 0`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = { + component: 'com.dummy.component/.Main', + user: 0 + }; + return cmd.execute(options) + .then(() => done()); + }); + + return it("should not send user option if not set'", function(done) { + const conn = new MockConnection; + const cmd = new StartServiceCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(`shell:am startservice \ +-n 'com.dummy.component/.Main'`).toString() + )); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\n'); + return conn.socket.causeEnd(); + }); + const options = + {component: 'com.dummy.component/.Main'}; + return cmd.execute(options) + .then(() => done()); + }); +}); diff --git a/test/adb/command/host-transport/sync.js b/test/adb/command/host-transport/sync.js new file mode 100644 index 00000000..5e07b1a3 --- /dev/null +++ b/test/adb/command/host-transport/sync.js @@ -0,0 +1,26 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const SyncCommand = require('../../../../src/adb/command/host-transport/sync'); + +describe('SyncCommand', () => it("should send 'sync:'", function(done) { + const conn = new MockConnection; + const cmd = new SyncCommand(conn); + conn.socket.on('write', function(chunk) { + expect(chunk.toString()).to.equal( + Protocol.encodeData('sync:').toString()); + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(() => done()); +})); diff --git a/test/adb/command/host-transport/tcp.js b/test/adb/command/host-transport/tcp.js new file mode 100644 index 00000000..e13cb37e --- /dev/null +++ b/test/adb/command/host-transport/tcp.js @@ -0,0 +1,55 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const TcpCommand = require('../../../../src/adb/command/host-transport/tcp'); + +describe('TcpCommand', function() { + + it("should send 'tcp:' when no host given", function(done) { + const conn = new MockConnection; + const cmd = new TcpCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('tcp:8080').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute(8080) + .then(stream => done()); + }); + + it("should send 'tcp::' when host given", function(done) { + const conn = new MockConnection; + const cmd = new TcpCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('tcp:8080:127.0.0.1').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute(8080, '127.0.0.1') + .then(stream => done()); + }); + + return it("should resolve with the tcp stream", function(done) { + const conn = new MockConnection; + const cmd = new TcpCommand(conn); + setImmediate(() => conn.socket.causeRead(Protocol.OKAY)); + return cmd.execute(8080) + .then(function(stream) { + stream.end(); + expect(stream).to.be.an.instanceof(Stream.Readable); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/tcpip.js b/test/adb/command/host-transport/tcpip.js new file mode 100644 index 00000000..669cc0ec --- /dev/null +++ b/test/adb/command/host-transport/tcpip.js @@ -0,0 +1,61 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const TcpIpCommand = require('../../../../src/adb/command/host-transport/tcpip'); + +describe('TcpIpCommand', function() { + + it("should send 'tcp:'", function(done) { + const conn = new MockConnection; + const cmd = new TcpIpCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('tcpip:5555').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead("restarting in TCP mode port: 5555\n"); + return conn.socket.causeEnd(); + }); + return cmd.execute(5555) + .then(() => done()); + }); + + it("should resolve with the port", function(done) { + const conn = new MockConnection; + const cmd = new TcpIpCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead("restarting in TCP mode port: 5555\n"); + return conn.socket.causeEnd(); + }); + return cmd.execute(5555) + .then(function(port) { + expect(port).to.equal(5555); + return done(); + }); + }); + + return it("should reject on unexpected reply", function(done) { + const conn = new MockConnection; + const cmd = new TcpIpCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead("not sure what this could be\n"); + return conn.socket.causeEnd(); + }); + return cmd.execute(5555) + .catch(function(err) { + expect(err.message).to.eql('not sure what this could be'); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/uninstall.js b/test/adb/command/host-transport/uninstall.js new file mode 100644 index 00000000..3d7dac55 --- /dev/null +++ b/test/adb/command/host-transport/uninstall.js @@ -0,0 +1,135 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const Parser = require('../../../../src/adb/parser'); +const UninstallCommand = + require('../../../../src/adb/command/host-transport/uninstall'); + +describe('UninstallCommand', function() { + + it("should succeed when command responds with 'Success'", function(done) { + const conn = new MockConnection; + const cmd = new UninstallCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:pm uninstall foo').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Success\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); + + it("should succeed even if command responds with 'Failure'", function(done) { + const conn = new MockConnection; + const cmd = new UninstallCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:pm uninstall foo').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Failure\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); + + it(`should succeed even if command responds with 'Failure' \ +with info in standard format`, function(done) { + const conn = new MockConnection; + const cmd = new UninstallCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:pm uninstall foo').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Failure [DELETE_FAILED_INTERNAL_ERROR]\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); + + it(`should succeed even if command responds with 'Failure' \ +with info info in weird format`, function(done) { + const conn = new MockConnection; + const cmd = new UninstallCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Failure - not installed for 0\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); + + it("should succeed even if command responds with a buggy exception", function(done) { + const conn = new MockConnection; + const cmd = new UninstallCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + // coffeelint: disable=max_line_length + conn.socket.causeRead(`\ + +Exception occurred while dumping: +java.lang.IllegalArgumentException: Unknown package: foo + at com.android.server.pm.Settings.isOrphaned(Settings.java:4134) + at com.android.server.pm.PackageManagerService.isOrphaned(PackageManagerService.java:18066) + at com.android.server.pm.PackageManagerService.deletePackage(PackageManagerService.java:15483) + at com.android.server.pm.PackageInstallerService.uninstall(PackageInstallerService.java:888) + at com.android.server.pm.PackageManagerShellCommand.runUninstall(PackageManagerShellCommand.java:765) + at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:113) + at android.os.ShellCommand.exec(ShellCommand.java:94) + at com.android.server.pm.PackageManagerService.onShellCommand(PackageManagerService.java:18324) + at android.os.Binder.shellCommand(Binder.java:468) + at android.os.Binder.onTransact(Binder.java:367) + at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:2387) + at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:3019) + at android.os.Binder.execTransact(Binder.java:565)\ +` + ); + // coffeelint: enable=max_line_length + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); + + it(`should reject with Parser.PrematureEOFError if stream ends \ +before match`, function(done) { + const conn = new MockConnection; + const cmd = new UninstallCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('Hello. Is it me you are looking for?\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .catch(Parser.PrematureEOFError, err => done()); + }); + + return it("should ignore any other data", function(done) { + const conn = new MockConnection; + const cmd = new UninstallCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('shell:pm uninstall foo').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('open: Permission failed\r\n'); + conn.socket.causeRead('Failure\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute('foo') + .then(() => done()); + }); +}); diff --git a/test/adb/command/host-transport/usb.js b/test/adb/command/host-transport/usb.js new file mode 100644 index 00000000..173274c7 --- /dev/null +++ b/test/adb/command/host-transport/usb.js @@ -0,0 +1,49 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const UsbCommand = require('../../../../src/adb/command/host-transport/usb'); + +describe('UsbCommand', function() { + + it("should send 'usb:'", function(done) { + const conn = new MockConnection; + const cmd = new UsbCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('usb:').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead("restarting in USB mode\n"); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(val) { + expect(val).to.be.true; + return done(); + }); + }); + + return it("should reject on unexpected reply", function(done) { + const conn = new MockConnection; + const cmd = new UsbCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead("invalid port\n"); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .catch(function(err) { + expect(err.message).to.eql('invalid port'); + return done(); + }); + }); +}); diff --git a/test/adb/command/host-transport/waitbootcomplete.js b/test/adb/command/host-transport/waitbootcomplete.js new file mode 100644 index 00000000..6bf1f5e9 --- /dev/null +++ b/test/adb/command/host-transport/waitbootcomplete.js @@ -0,0 +1,87 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const Parser = require('../../../../src/adb/parser'); +const WaitBootCompleteCommand = + require('../../../../src/adb/command/host-transport/waitbootcomplete'); + +describe('WaitBootCompleteCommand', function() { + + it("should send a while loop with boot check", function(done) { + const conn = new MockConnection; + const cmd = new WaitBootCompleteCommand(conn); + const want = + 'shell:while getprop sys.boot_completed 2>/dev/null; do sleep 1; done'; + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData(want).toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('1\r\n'); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(() => done()); + }); + + it(`should reject with Parser.PrematureEOFError if connection cuts \ +prematurely`, function(done) { + const conn = new MockConnection; + const cmd = new WaitBootCompleteCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(() => done(new Error('Succeeded even though it should not'))).catch(Parser.PrematureEOFError, err => done()); + }); + + it("should not return until boot is complete", function(done) { + const conn = new MockConnection; + const cmd = new WaitBootCompleteCommand(conn); + let sent = false; + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + conn.socket.causeRead('\r\n'); + return setTimeout(function() { + sent = true; + return conn.socket.causeRead('1\r\n'); + } + , 50); + }); + return cmd.execute() + .then(function() { + expect(sent).to.be.true; + return done(); + }); + }); + + return it("should close connection when done", function(done) { + const conn = new MockConnection; + const cmd = new WaitBootCompleteCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + return conn.socket.causeRead('1\r\n'); + }); + conn.socket.on('end', () => done()); + return cmd.execute(); + }); +}); diff --git a/test/adb/command/host/connect.js b/test/adb/command/host/connect.js new file mode 100644 index 00000000..f091c7d5 --- /dev/null +++ b/test/adb/command/host/connect.js @@ -0,0 +1,78 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const ConnectCommand = require('../../../../src/adb/command/host/connect'); + +describe('ConnectCommand', function() { + + it("should send 'host:connect::'", function(done) { + const conn = new MockConnection; + const cmd = new ConnectCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('host:connect:192.168.2.2:5555').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.encodeData('connected to 192.168.2.2:5555')); + return conn.socket.causeEnd(); + }); + return cmd.execute('192.168.2.2', 5555) + .then(() => done()); + }); + + it("should resolve with the new device id if connected", function(done) { + const conn = new MockConnection; + const cmd = new ConnectCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.encodeData('connected to 192.168.2.2:5555')); + return conn.socket.causeEnd(); + }); + return cmd.execute('192.168.2.2', 5555) + .then(function(val) { + expect(val).to.be.equal('192.168.2.2:5555'); + return done(); + }); + }); + + it("should resolve with the new device id if already connected", function(done) { + const conn = new MockConnection; + const cmd = new ConnectCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead( + Protocol.encodeData('already connected to 192.168.2.2:5555')); + return conn.socket.causeEnd(); + }); + return cmd.execute('192.168.2.2', 5555) + .then(function(val) { + expect(val).to.be.equal('192.168.2.2:5555'); + return done(); + }); + }); + + return it("should reject with error if unable to connect", function(done) { + const conn = new MockConnection; + const cmd = new ConnectCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead( + Protocol.encodeData('unable to connect to 192.168.2.2:5555')); + return conn.socket.causeEnd(); + }); + return cmd.execute('192.168.2.2', 5555) + .catch(function(err) { + expect(err.message).to.eql('unable to connect to 192.168.2.2:5555'); + return done(); + }); + }); +}); diff --git a/test/adb/command/host/disconnect.js b/test/adb/command/host/disconnect.js new file mode 100644 index 00000000..e1e7d14a --- /dev/null +++ b/test/adb/command/host/disconnect.js @@ -0,0 +1,62 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const DisconnectCommand = require('../../../../src/adb/command/host/disconnect'); + +describe('DisconnectCommand', function() { + + it("should send 'host:disconnect::'", function(done) { + const conn = new MockConnection; + const cmd = new DisconnectCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('host:disconnect:192.168.2.2:5555').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.encodeData('')); + return conn.socket.causeEnd(); + }); + return cmd.execute('192.168.2.2', 5555) + .then(() => done()); + }); + + it("should resolve with the new device id if disconnected", function(done) { + const conn = new MockConnection; + const cmd = new DisconnectCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.encodeData('')); + return conn.socket.causeEnd(); + }); + return cmd.execute('192.168.2.2', 5555) + .then(function(val) { + expect(val).to.be.equal('192.168.2.2:5555'); + return done(); + }); + }); + + return it("should reject with error if unable to disconnect", function(done) { + const conn = new MockConnection; + const cmd = new DisconnectCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead( + Protocol.encodeData('No such device 192.168.2.2:5555')); + return conn.socket.causeEnd(); + }); + return cmd.execute('192.168.2.2', 5555) + .catch(function(err) { + expect(err.message).to.eql('No such device 192.168.2.2:5555'); + return done(); + }); + }); +}); diff --git a/test/adb/command/host/version.js b/test/adb/command/host/version.js new file mode 100644 index 00000000..5112bc73 --- /dev/null +++ b/test/adb/command/host/version.js @@ -0,0 +1,59 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const MockConnection = require('../../../mock/connection'); +const Protocol = require('../../../../src/adb/protocol'); +const HostVersionCommand = require('../../../../src/adb/command/host/version'); + +describe('HostVersionCommand', function() { + + it("should send 'host:version'", function(done) { + const conn = new MockConnection; + const cmd = new HostVersionCommand(conn); + conn.socket.on('write', chunk => expect(chunk.toString()).to.equal( + Protocol.encodeData('host:version').toString())); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.encodeData('0000')); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(version => done()); + }); + + it("should resolve with version", function(done) { + const conn = new MockConnection; + const cmd = new HostVersionCommand(conn); + setImmediate(function() { + conn.socket.causeRead(Protocol.OKAY); + conn.socket.causeRead(Protocol.encodeData(((0x1234)).toString(16))); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(version) { + expect(version).to.equal(0x1234); + return done(); + }); + }); + + return it("should handle old-style version", function(done) { + const conn = new MockConnection; + const cmd = new HostVersionCommand(conn); + setImmediate(function() { + conn.socket.causeRead(((0x1234)).toString(16)); + return conn.socket.causeEnd(); + }); + return cmd.execute() + .then(function(version) { + expect(version).to.equal(0x1234); + return done(); + }); + }); +}); diff --git a/test/adb/framebuffer/rgbtransform.js b/test/adb/framebuffer/rgbtransform.js new file mode 100644 index 00000000..c75e882d --- /dev/null +++ b/test/adb/framebuffer/rgbtransform.js @@ -0,0 +1,215 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const RgbTransform = require('../../../src/adb/framebuffer/rgbtransform'); + +describe('RgbTransform', function() { + + it("should transform BGRA into RGB", function(done) { + const meta = { + bpp: 32, + red_offset: 16, + red_length: 8, + green_offset: 8, + green_length: 8, + blue_offset: 0, + blue_length: 8, + alpha_offset: 24, + alpha_length: 8 + }; + const pixel = new Buffer(4); + pixel.writeUInt8(50, 0); + pixel.writeUInt8(100, 1); + pixel.writeUInt8(150, 2); + pixel.writeUInt8(200, 3); + const stream = new Stream.PassThrough; + const transform = new RgbTransform(meta); + stream.pipe(transform); + transform.on('data', function(chunk) { + expect(chunk).to.have.length(3); + expect(chunk.readUInt8(0)).to.equal(150); + expect(chunk.readUInt8(1)).to.equal(100); + expect(chunk.readUInt8(2)).to.equal(50); + return done(); + }); + stream.write(pixel); + return stream.end(); + }); + + it("should transform BGR into RGB", function(done) { + const meta = { + bpp: 32, + red_offset: 16, + red_length: 8, + green_offset: 8, + green_length: 8, + blue_offset: 0, + blue_length: 8, + alpha_offset: 0, + alpha_length: 0 + }; + const pixel = new Buffer(4); + pixel.writeUInt8(50, 0); + pixel.writeUInt8(100, 1); + pixel.writeUInt8(150, 2); + const stream = new Stream.PassThrough; + const transform = new RgbTransform(meta); + stream.pipe(transform); + transform.on('data', function(chunk) { + expect(chunk).to.have.length(3); + expect(chunk.readUInt8(0)).to.equal(150); + expect(chunk.readUInt8(1)).to.equal(100); + expect(chunk.readUInt8(2)).to.equal(50); + return done(); + }); + stream.write(pixel); + return stream.end(); + }); + + it("should transform RGB into RGB", function(done) { + const meta = { + bpp: 24, + red_offset: 0, + red_length: 8, + green_offset: 8, + green_length: 8, + blue_offset: 16, + blue_length: 8, + alpha_offset: 0, + alpha_length: 0 + }; + const pixel = new Buffer(3); + pixel.writeUInt8(50, 0); + pixel.writeUInt8(100, 1); + pixel.writeUInt8(150, 2); + const stream = new Stream.PassThrough; + const transform = new RgbTransform(meta); + stream.pipe(transform); + transform.on('data', function(chunk) { + expect(chunk).to.have.length(3); + expect(chunk.readUInt8(0)).to.equal(50); + expect(chunk.readUInt8(1)).to.equal(100); + expect(chunk.readUInt8(2)).to.equal(150); + return done(); + }); + stream.write(pixel); + return stream.end(); + }); + + it("should transform RGBA into RGB", function(done) { + const meta = { + bpp: 32, + red_offset: 0, + red_length: 8, + green_offset: 8, + green_length: 8, + blue_offset: 16, + blue_length: 8, + alpha_offset: 24, + alpha_length: 8 + }; + const pixel = new Buffer(4); + pixel.writeUInt8(50, 0); + pixel.writeUInt8(100, 1); + pixel.writeUInt8(150, 2); + pixel.writeUInt8(200, 3); + const stream = new Stream.PassThrough; + const transform = new RgbTransform(meta); + stream.pipe(transform); + transform.on('data', function(chunk) { + expect(chunk).to.have.length(3); + expect(chunk.readUInt8(0)).to.equal(50); + expect(chunk.readUInt8(1)).to.equal(100); + expect(chunk.readUInt8(2)).to.equal(150); + return done(); + }); + stream.write(pixel); + return stream.end(); + }); + + it("should wait for a complete pixel before transforming", function(done) { + const meta = { + bpp: 32, + red_offset: 0, + red_length: 8, + green_offset: 8, + green_length: 8, + blue_offset: 16, + blue_length: 8, + alpha_offset: 24, + alpha_length: 8 + }; + const pixel = new Buffer(4); + pixel.writeUInt8(50, 0); + pixel.writeUInt8(100, 1); + pixel.writeUInt8(150, 2); + pixel.writeUInt8(200, 3); + const stream = new Stream.PassThrough; + const transform = new RgbTransform(meta); + stream.pipe(transform); + transform.on('data', function(chunk) { + expect(chunk).to.have.length(3); + expect(chunk.readUInt8(0)).to.equal(50); + expect(chunk.readUInt8(1)).to.equal(100); + expect(chunk.readUInt8(2)).to.equal(150); + return done(); + }); + stream.write(pixel.slice(0, 2)); + stream.write(pixel.slice(2, 3)); + stream.write(pixel.slice(3, 4)); + return stream.end(); + }); + + return it("should transform a stream of multiple pixels", function(done) { + const meta = { + bpp: 32, + red_offset: 16, + red_length: 8, + green_offset: 8, + green_length: 8, + blue_offset: 0, + blue_length: 8, + alpha_offset: 24, + alpha_length: 8 + }; + const pixel1 = new Buffer(4); + pixel1.writeUInt8(50, 0); + pixel1.writeUInt8(100, 1); + pixel1.writeUInt8(150, 2); + pixel1.writeUInt8(200, 3); + const pixel2 = new Buffer(4); + pixel2.writeUInt8(51, 0); + pixel2.writeUInt8(101, 1); + pixel2.writeUInt8(151, 2); + pixel2.writeUInt8(201, 3); + const stream = new Stream.PassThrough; + const transform = new RgbTransform(meta); + stream.pipe(transform); + let all = new Buffer(''); + transform.on('data', chunk => all = Buffer.concat([all, chunk])); + transform.on('end', function() { + expect(all).to.have.length(15); + expect(all.readUInt8(0)).to.equal(150); + expect(all.readUInt8(1)).to.equal(100); + expect(all.readUInt8(2)).to.equal(50); + expect(all.readUInt8(3)).to.equal(151); + expect(all.readUInt8(4)).to.equal(101); + expect(all.readUInt8(5)).to.equal(51); + return done(); + }); + stream.write(pixel1); + stream.write(pixel2); + stream.write(pixel1); + stream.write(pixel2); + stream.write(pixel1); + return stream.end(); + }); +}); diff --git a/test/adb/linetransform.js b/test/adb/linetransform.js new file mode 100644 index 00000000..318980c7 --- /dev/null +++ b/test/adb/linetransform.js @@ -0,0 +1,197 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = Chai; + +const LineTransform = require('../../src/adb/linetransform'); +const MockDuplex = require('../mock/duplex'); + +describe('LineTransform', function() { + + it("should implement stream.Transform", function(done) { + expect(new LineTransform).to.be.an.instanceOf(Stream.Transform); + return done(); + }); + + describe('with autoDetect', function() { + it("should not modify data if first byte is 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform({autoDetect: true}); + transform.on('data', function(data) { + expect(data.toString()).to.equal('bar\r\n'); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead('\nbar\r\n'); + return duplex.causeEnd(); + }); + + it("should not include initial 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform({autoDetect: true}); + let buffer = new Buffer(''); + transform.on('data', data => buffer = Buffer.concat([buffer, data])); + transform.on('end', function() { + expect(buffer.toString()).to.equal('bar\r\n'); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead('\nbar\r\n'); + return duplex.causeEnd(); + }); + + it("should not include initial 0x0d 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform({autoDetect: true}); + let buffer = new Buffer(''); + transform.on('data', data => buffer = Buffer.concat([buffer, data])); + transform.on('end', function() { + expect(buffer.toString()).to.equal('bar\n'); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead('\r\nbar\r\n'); + return duplex.causeEnd(); + }); + + it(`should not include initial 0x0d 0x0a even if in separate \ +chunks`, function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform({autoDetect: true}); + let buffer = new Buffer(''); + transform.on('data', data => buffer = Buffer.concat([buffer, data])); + transform.on('end', function() { + expect(buffer.toString()).to.equal('bar\n'); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead('\r'); + duplex.causeRead('\nbar\r\n'); + return duplex.causeEnd(); + }); + + return it("should transform as usual if first byte is not 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform({autoDetect: true}); + let buffer = new Buffer(''); + transform.on('data', data => buffer = Buffer.concat([buffer, data])); + transform.on('end', function() { + expect(buffer.toString()).to.equal('bar\nfoo'); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead('\r\nbar\r\nfoo'); + return duplex.causeEnd(); + }); + }); + + describe('without autoDetect', () => it("should transform as usual even if first byte is 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform; + let buffer = new Buffer(''); + transform.on('data', data => buffer = Buffer.concat([buffer, data])); + transform.on('end', function() { + expect(buffer.toString()).to.equal('\n\nbar\nfoo'); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead('\n\r\nbar\r\nfoo'); + return duplex.causeEnd(); + })); + + it("should not modify data that does not have 0x0d 0x0a in it", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform; + transform.on('data', function(data) { + expect(data.toString()).to.equal('foo'); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead('foo'); + return duplex.causeEnd(); + }); + + it("should not remove 0x0d if not followed by 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform; + transform.on('data', function(data) { + expect(data.length).to.equal(2); + expect(data[0]).to.equal(0x0d); + expect(data[1]).to.equal(0x05); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead(new Buffer([0x0d, 0x05])); + return duplex.causeEnd(); + }); + + it("should remove 0x0d if followed by 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform; + transform.on('data', function(data) { + expect(data.length).to.equal(2); + expect(data[0]).to.equal(0x0a); + expect(data[1]).to.equal(0x97); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead(new Buffer([0x0d, 0x0a, 0x97])); + return duplex.causeEnd(); + }); + + it("should push 0x0d without 0x0a if last in stream", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform; + transform.on('data', function(data) { + expect(data.length).to.equal(1); + expect(data[0]).to.equal(0x0d); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead(new Buffer([0x0d])); + return duplex.causeEnd(); + }); + + it("should push saved 0x0d if next chunk does not start with 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform; + let buffer = new Buffer(''); + transform.on('data', data => buffer = Buffer.concat([buffer, data])); + transform.on('end', function() { + expect(buffer).to.have.length(3); + expect(buffer[0]).to.equal(0x62); + expect(buffer[1]).to.equal(0x0d); + expect(buffer[2]).to.equal(0x37); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead(new Buffer([0x62, 0x0d])); + duplex.causeRead(new Buffer([0x37])); + duplex.causeEnd(); + return duplex.end(); + }); + + return it("should remove saved 0x0d if next chunk starts with 0x0a", function(done) { + const duplex = new MockDuplex; + const transform = new LineTransform; + let buffer = new Buffer(''); + transform.on('data', data => buffer = Buffer.concat([buffer, data])); + transform.on('end', function() { + expect(buffer).to.have.length(2); + expect(buffer[0]).to.equal(0x62); + expect(buffer[1]).to.equal(0x0a); + return done(); + }); + duplex.pipe(transform); + duplex.causeRead(new Buffer([0x62, 0x0d])); + duplex.causeRead(new Buffer([0x0a])); + duplex.causeEnd(); + return duplex.end(); + }); +}); diff --git a/test/adb/parser.js b/test/adb/parser.js new file mode 100644 index 00000000..a72146df --- /dev/null +++ b/test/adb/parser.js @@ -0,0 +1,452 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Promise = require('bluebird'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = require('chai'); + +const Parser = require('../../src/adb/parser'); + +describe('Parser', function() { + + describe('end()', () => it("should end the stream and consume all remaining data", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + stream.write('F'); + stream.write('O'); + stream.write('O'); + return parser.end() + .then(() => done()); + })); + + describe('readAll()', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const promise = parser.readAll(); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should read all remaining content until the stream ends", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readAll() + .then(function(buf) { + expect(buf.length).to.equal(3); + expect(buf.toString()).to.equal('FOO'); + return done(); + }); + stream.write('F'); + stream.write('O'); + stream.write('O'); + return stream.end(); + }); + + return it(`should resolve with an empty Buffer if the stream has already ended \ +and there's nothing more to read`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readAll() + .then(function(buf) { + expect(buf.length).to.equal(0); + return done(); + }); + return stream.end(); + }); + }); + + describe('readBytes(howMany)', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const promise = parser.readBytes(1); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should read as many bytes as requested", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readBytes(4) + .then(function(buf) { + expect(buf.length).to.equal(4); + expect(buf.toString()).to.equal('OKAY'); + return parser.readBytes(2) + .then(function(buf) { + expect(buf).to.have.length(2); + expect(buf.toString()).to.equal('FA'); + return done(); + }); + }); + return stream.write('OKAYFAIL'); + }); + + it("should wait for enough data to appear", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readBytes(5) + .then(function(buf) { + expect(buf.toString()).to.equal('BYTES'); + return done(); + }); + return Promise.delay(50) + .then(() => stream.write('BYTES')); + }); + + it(`should keep data waiting even when nothing has been \ +requested`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + stream.write('FOO'); + return Promise.delay(50) + .then(() => parser.readBytes(2) + .then(function(buf) { + expect(buf.length).to.equal(2); + expect(buf.toString()).to.equal('FO'); + return done(); + })); + }); + + return it(`should reject with Parser.PrematureEOFError if stream ends \ +before enough bytes can be read`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + stream.write('F'); + parser.readBytes(10) + .catch(Parser.PrematureEOFError, function(err) { + expect(err.missingBytes).to.equal(9); + return done(); + }); + return stream.end(); + }); + }); + + describe('readByteFlow(maxHowMany, targetStream)', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const target = new Stream.PassThrough; + const promise = parser.readByteFlow(1, target); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should read as many bytes as requested", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const target = new Stream.PassThrough; + parser.readByteFlow(4, target) + .then(function() { + expect(target.read()).to.eql(new Buffer('OKAY')); + return parser.readByteFlow(2, target) + .then(function() { + expect(target.read()).to.eql(new Buffer('FA')); + return done(); + });}).catch(done); + return stream.write('OKAYFAIL'); + }); + + return it("should progress with new/partial chunk until maxHowMany", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const target = new Stream.PassThrough; + parser.readByteFlow(3, target) + .then(function() { + expect(target.read()).to.eql(new Buffer('PIE')); + return done();}).catch(done); + const b1 = new Buffer('P'); + const b2 = new Buffer('I'); + const b3 = new Buffer('ES'); + const b4 = new Buffer('R'); + stream.write(b1); + stream.write(b2); + stream.write(b3); + return stream.write(b4); + }); + }); + + describe('readAscii(howMany)', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const promise = parser.readAscii(1); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should read as many ascii characters as requested", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readAscii(4) + .then(function(str) { + expect(str.length).to.equal(4); + expect(str).to.equal('OKAY'); + return done(); + }); + return stream.write('OKAYFAIL'); + }); + + return it(`should reject with Parser.PrematureEOFError if stream ends \ +before enough bytes can be read`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + stream.write('FOO'); + parser.readAscii(7) + .catch(Parser.PrematureEOFError, function(err) { + expect(err.missingBytes).to.equal(4); + return done(); + }); + return stream.end(); + }); + }); + + describe('readValue()', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const promise = parser.readValue(); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should read a protocol value as a Buffer", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readValue() + .then(function(value) { + expect(value).to.be.an.instanceOf(Buffer); + expect(value).to.have.length(4); + expect(value.toString()).to.equal('001f'); + return done(); + }); + return stream.write('0004001f'); + }); + + it("should return an empty value", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readValue() + .then(function(value) { + expect(value).to.be.an.instanceOf(Buffer); + expect(value).to.have.length(0); + return done(); + }); + return stream.write('0000'); + }); + + return it(`should reject with Parser.PrematureEOFError if stream ends \ +before the value can be read`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readValue() + .catch(Parser.PrematureEOFError, err => done()); + stream.write('00ffabc'); + return stream.end(); + }); + }); + + describe('readError()', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const promise = parser.readError(); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should reject with Parser.FailError using the value", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readError() + .catch(Parser.FailError, function(err) { + expect(err.message).to.equal("Failure: 'epic failure'"); + return done(); + }); + return stream.write('000cepic failure'); + }); + + return it(`should reject with Parser.PrematureEOFError if stream ends \ +before the error can be read`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readError() + .catch(Parser.PrematureEOFError, err => done()); + stream.write('000cepic'); + return stream.end(); + }); + }); + + describe('searchLine(re)', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const promise = parser.searchLine(/foo/); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should return the re.exec match of the matching line", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.searchLine(/za(p)/) + .then(function(line) { + expect(line[0]).to.equal('zap'); + expect(line[1]).to.equal('p'); + expect(line.input).to.equal('zip zap'); + return done(); + }); + return stream.write('foo bar\nzip zap\npip pop\n'); + }); + + return it(`should reject with Parser.PrematureEOFError if stream ends \ +before a line is found`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.searchLine(/nope/) + .catch(Parser.PrematureEOFError, err => done()); + stream.write('foo bar'); + return stream.end(); + }); + }); + + describe('readLine()', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const promise = parser.readLine(); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should skip a line terminated by \\n", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readLine() + .then(() => parser.readBytes(7) + .then(function(buf) { + expect(buf.toString()).to.equal('zip zap'); + return done(); + })); + return stream.write('foo bar\nzip zap\npip pop'); + }); + + it("should return skipped line", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readLine() + .then(function(buf) { + expect(buf.toString()).to.equal('foo bar'); + return done(); + }); + return stream.write('foo bar\nzip zap\npip pop'); + }); + + it("should strip trailing \\r", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readLine() + .then(function(buf) { + expect(buf.toString()).to.equal('foo bar'); + return done(); + }); + return stream.write('foo bar\r\n'); + }); + + return it(`should reject with Parser.PrematureEOFError if stream ends \ +before a line is found`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readLine() + .catch(Parser.PrematureEOFError, err => done()); + stream.write('foo bar'); + return stream.end(); + }); + }); + + describe('readUntil(code)', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const promise = parser.readUntil(0xa0); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + it("should return any characters before given value", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readUntil('p'.charCodeAt(0)) + .then(function(buf) { + expect(buf.toString()).to.equal('foo bar\nzi'); + return done(); + }); + return stream.write('foo bar\nzip zap\npip pop'); + }); + + return it(`should reject with Parser.PrematureEOFError if stream ends \ +before a line is found`, function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + parser.readUntil('z'.charCodeAt(0)) + .catch(Parser.PrematureEOFError, err => done()); + stream.write('ho ho'); + return stream.end(); + }); + }); + + describe('raw()', () => it("should return the resumed raw stream", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + const raw = parser.raw(); + expect(raw).to.equal(stream); + raw.on('data', () => done()); + return raw.write('foo'); + })); + + return describe('unexpected(data, expected)', () => it("should reject with Parser.UnexpectedDataError", function(done) { + const stream = new Stream.PassThrough; + const parser = new Parser(stream); + return parser.unexpected('foo', "'bar' or end of stream") + .catch(Parser.UnexpectedDataError, function(err) { + expect(err.message).to.equal(`Unexpected 'foo', was expecting 'bar' \ +or end of stream` + ); + expect(err.unexpected).to.equal('foo'); + expect(err.expected).to.equal("'bar' or end of stream"); + return done(); + }); + })); +}); diff --git a/test/adb/protocol.js b/test/adb/protocol.js new file mode 100644 index 00000000..5b518d2f --- /dev/null +++ b/test/adb/protocol.js @@ -0,0 +1,88 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {expect} = require('chai'); + +const Protocol = require('../../src/adb/protocol'); + +describe('Protocol', function() { + + it("should expose a 'FAIL' property", function(done) { + expect(Protocol).to.have.property('FAIL'); + expect(Protocol.FAIL).to.equal('FAIL'); + return done(); + }); + + it("should expose an 'OKAY' property", function(done) { + expect(Protocol).to.have.property('OKAY'); + expect(Protocol.OKAY).to.equal('OKAY'); + return done(); + }); + + describe('@decodeLength(length)', function() { + + it("should return a Number", function(done) { + expect(Protocol.decodeLength('0x0046')).to.be.a('number'); + return done(); + }); + + return it("should accept a hexadecimal string", function(done) { + expect(Protocol.decodeLength('0x5887')).to.equal(0x5887); + return done(); + }); + }); + + describe('@encodeLength(length)', function() { + + it("should return a String", function(done) { + expect(Protocol.encodeLength(27)).to.be.a('string'); + return done(); + }); + + it("should return a valid hexadecimal number", function(done) { + expect(parseInt(Protocol.encodeLength(32), 16)).to.equal(32); + expect(parseInt(Protocol.encodeLength(9999), 16)).to.equal(9999); + return done(); + }); + + it("should return uppercase hexadecimal digits", function(done) { + expect(Protocol.encodeLength(0x0abc)).to.equal('0ABC'); + return done(); + }); + + it("should pad short values with zeroes for a 4-byte size", function(done) { + expect(Protocol.encodeLength(1)).to.have.length(4); + expect(Protocol.encodeLength(2)).to.have.length(4); + expect(Protocol.encodeLength(57)).to.have.length(4); + return done(); + }); + + return it("should return 0000 for 0 length", function(done) { + expect(Protocol.encodeLength(0)).to.equal('0000'); + return done(); + }); + }); + + return describe('@encodeData(data)', function() { + + it("should return a Buffer", function(done) { + expect(Protocol.encodeData(new Buffer(''))).to.be.an.instanceOf(Buffer); + return done(); + }); + + it("should accept a string or a Buffer", function(done) { + expect(Protocol.encodeData('')).to.be.an.instanceOf(Buffer); + expect(Protocol.encodeData(new Buffer(''))).to.be.an.instanceOf(Buffer); + return done(); + }); + + return it("should prefix data with length", function(done) { + const data = Protocol.encodeData(new Buffer(0x270F)); + expect(data).to.have.length(0x270F + 4); + expect(data.toString('ascii', 0, 4)).to.equal('270F'); + return done(); + }); + }); +}); diff --git a/test/adb/sync.js b/test/adb/sync.js new file mode 100644 index 00000000..902b61de --- /dev/null +++ b/test/adb/sync.js @@ -0,0 +1,254 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Fs = require('fs'); +const Stream = require('stream'); +const Promise = require('bluebird'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect, assert} = Chai; + +const Adb = require('../../src/adb'); +const Sync = require('../../src/adb/sync'); +const Stats = require('../../src/adb/sync/stats'); +const Entry = require('../../src/adb/sync/entry'); +const PushTransfer = require('../../src/adb/sync/pushtransfer'); +const PullTransfer = require('../../src/adb/sync/pulltransfer'); +const MockConnection = require('../mock/connection'); + +// This test suite is a bit special in that it requires a connected Android +// device (or many devices). All will be tested. +describe('Sync', function() { + // By default, skip tests that require a device. + let dt = xit; + if (process.env.RUN_DEVICE_TESTS) { dt = it; } + + const SURELY_EXISTING_FILE = '/system/build.prop'; + const SURELY_EXISTING_PATH = '/'; + const SURELY_NONEXISTING_PATH = '/non-existing-path'; + const SURELY_WRITABLE_FILE = '/data/local/tmp/_sync.test'; + + let client = null; + let deviceList = null; + + const forEachSyncDevice = function(iterator, done) { + assert(deviceList.length > 0, + 'At least one connected Android device is required'); + + const promises = deviceList.map(device => client.syncService(device.id) + .then(function(sync) { + expect(sync).to.be.an.instanceof(Sync); + return Promise.cast(iterator(sync)) + .finally(() => sync.end()); + })); + + return Promise.all(promises) + .then(() => done()).catch(done); + }; + + before(function(done) { + client = Adb.createClient(); + return client.listDevices() + .then(function(devices) { + deviceList = devices; + return done(); + }); + }); + + describe('end()', () => it("should end the sync connection", function() { + const conn = new MockConnection; + const sync = new Sync(conn); + Sinon.stub(conn, 'end'); + sync.end(); + return expect(conn.end).to.have.been.called; + })); + + describe('push(contents, path[, mode])', function() { + + it("should call pushStream when contents is a Stream", function() { + const conn = new MockConnection; + const sync = new Sync(conn); + const stream = new Stream.PassThrough; + Sinon.stub(sync, 'pushStream'); + sync.push(stream, 'foo'); + return expect(sync.pushStream).to.have.been.called; + }); + + it("should call pushFile when contents is a String", function() { + const conn = new MockConnection; + const sync = new Sync(conn); + const stream = new Stream.PassThrough; + Sinon.stub(sync, 'pushFile'); + sync.push(__filename, 'foo'); + return expect(sync.pushFile).to.have.been.called; + }); + + return it("should return a PushTransfer instance", function() { + const conn = new MockConnection; + const sync = new Sync(conn); + const stream = new Stream.PassThrough; + const transfer = sync.push(stream, 'foo'); + expect(transfer).to.be.an.instanceof(PushTransfer); + return transfer.cancel(); + }); + }); + + describe('pushStream(stream, path[, mode])', function() { + + it("should return a PushTransfer instance", function() { + const conn = new MockConnection; + const sync = new Sync(conn); + const stream = new Stream.PassThrough; + const transfer = sync.pushStream(stream, 'foo'); + expect(transfer).to.be.an.instanceof(PushTransfer); + return transfer.cancel(); + }); + + return dt("should be able to push >65536 byte chunks without error", done => forEachSyncDevice(sync => new Promise(function(resolve, reject) { + const stream = new Stream.PassThrough; + const content = new Buffer(1000000); + const transfer = sync.pushStream(stream, SURELY_WRITABLE_FILE); + transfer.on('error', reject); + transfer.on('end', resolve); + stream.write(content); + return stream.end(); + }) + , done)); + }); + + describe('pull(path)', function() { + + dt("should retrieve the same content pushStream() pushed", done => forEachSyncDevice(sync => new Promise(function(resolve, reject) { + const stream = new Stream.PassThrough; + const content = 'ABCDEFGHI' + Date.now(); + let transfer = sync.pushStream(stream, SURELY_WRITABLE_FILE); + expect(transfer).to.be.an.instanceof(PushTransfer); + transfer.on('error', reject); + transfer.on('end', function() { + transfer = sync.pull(SURELY_WRITABLE_FILE); + expect(transfer).to.be.an.instanceof(PullTransfer); + transfer.on('error', reject); + return transfer.on('readable', function() { + let chunk; + while ((chunk = transfer.read())) { + expect(chunk).to.not.be.null; + expect(chunk.toString()).to.equal(content); + return resolve(); + } + }); + }); + stream.write(content); + return stream.end(); + }) + , done)); + + dt("should emit error for non-existing files", done => forEachSyncDevice(sync => new Promise(function(resolve, reject) { + const transfer = sync.pull(SURELY_NONEXISTING_PATH); + return transfer.on('error', resolve); + }) + , done)); + + dt("should return a PullTransfer instance", done => forEachSyncDevice(function(sync) { + const rval = sync.pull(SURELY_EXISTING_FILE); + expect(rval).to.be.an.instanceof(PullTransfer); + return rval.cancel(); + } + , done)); + + return describe('Stream', () => dt("should emit 'end' when pull is done", done => forEachSyncDevice(sync => new Promise(function(resolve, reject) { + const transfer = sync.pull(SURELY_EXISTING_FILE); + transfer.on('error', reject); + transfer.on('end', resolve); + return transfer.resume(); + }) + , done))); + }); + + return describe('stat(path)', function() { + + dt("should return a Promise", done => forEachSyncDevice(function(sync) { + const rval = sync.stat(SURELY_EXISTING_PATH); + expect(rval).to.be.an.instanceof(Promise); + return rval; + } + , done)); + + dt("should call with an ENOENT error if the path does not exist", done => forEachSyncDevice(sync => sync.stat(SURELY_NONEXISTING_PATH) + .then(function(stats) { + throw new Error('Should not reach success branch');}).catch(function(err) { + expect(err).to.be.an.instanceof(Error); + expect(err.code).to.equal('ENOENT'); + expect(err.errno).to.equal(34); + return expect(err.path).to.equal(SURELY_NONEXISTING_PATH); + }) + , done)); + + dt("should call with an fs.Stats instance for an existing path", done => forEachSyncDevice(sync => sync.stat(SURELY_EXISTING_PATH) + .then(stats => expect(stats).to.be.an.instanceof(Fs.Stats)) + , done)); + + describe('Stats', function() { + + it("should implement Fs.Stats", function(done) { + expect(new Stats(0, 0, 0)).to.be.an.instanceof(Fs.Stats); + return done(); + }); + + dt("should set the `.mode` property for isFile() etc", done => forEachSyncDevice(sync => sync.stat(SURELY_EXISTING_FILE) + .then(function(stats) { + expect(stats).to.be.an.instanceof(Fs.Stats); + expect(stats.mode).to.be.above(0); + expect(stats.isFile()).to.be.true; + return expect(stats.isDirectory()).to.be.false; + }) + , done)); + + dt("should set the `.size` property", done => forEachSyncDevice(sync => sync.stat(SURELY_EXISTING_FILE) + .then(function(stats) { + expect(stats).to.be.an.instanceof(Fs.Stats); + expect(stats.isFile()).to.be.true; + return expect(stats.size).to.be.above(0); + }) + , done)); + + return dt("should set the `.mtime` property", done => forEachSyncDevice(sync => sync.stat(SURELY_EXISTING_FILE) + .then(function(stats) { + expect(stats).to.be.an.instanceof(Fs.Stats); + return expect(stats.mtime).to.be.an.instanceof(Date); + }) + , done)); + }); + + return describe('Entry', function() { + + it("should implement Stats", function(done) { + expect(new Entry('foo', 0, 0, 0)).to.be.an.instanceof(Stats); + return done(); + }); + + dt("should set the `.name` property", done => forEachSyncDevice(sync => sync.readdir(SURELY_EXISTING_PATH) + .then(function(files) { + expect(files).to.be.an('Array'); + return files.forEach(function(file) { + expect(file.name).to.not.be.null; + return expect(file).to.be.an.instanceof(Entry); + }); + }) + , done)); + + return dt("should set the Stats properties", done => forEachSyncDevice(sync => sync.readdir(SURELY_EXISTING_PATH) + .then(function(files) { + expect(files).to.be.an('Array'); + return files.forEach(function(file) { + expect(file.mode).to.not.be.null; + expect(file.size).to.not.be.null; + return expect(file.mtime).to.not.be.null; + }); + }) + , done)); + }); + }); +}); diff --git a/test/adb/tracker.js b/test/adb/tracker.js new file mode 100644 index 00000000..0dabea7b --- /dev/null +++ b/test/adb/tracker.js @@ -0,0 +1,200 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = require('chai'); + +const Parser = require('../../src/adb/parser'); +const Tracker = require('../../src/adb/tracker'); +const Protocol = require('../../src/adb/protocol'); +const HostTrackDevicesCommand = require('../../src/adb/command/host/trackdevices'); + +describe('Tracker', function() { + + beforeEach(function() { + this.writer = new Stream.PassThrough; + this.conn = { + parser: new Parser(this.writer), + end() {} + }; + this.cmd = new HostTrackDevicesCommand(this.conn); + return this.tracker = new Tracker(this.cmd); + }); + + it("should emit 'add' when a device is added", function(done) { + const spy = Sinon.spy(); + this.tracker.on('add', spy); + const device1 = { + id: 'a', + type: 'device' + }; + const device2 = { + id: 'b', + type: 'device' + }; + this.tracker.update([device1, device2]); + expect(spy).to.have.been.calledTwice; + expect(spy).to.have.been.calledWith(device1); + expect(spy).to.have.been.calledWith(device2); + return done(); + }); + + it("should emit 'remove' when a device is removed", function(done) { + const spy = Sinon.spy(); + this.tracker.on('remove', spy); + const device1 = { + id: 'a', + type: 'device' + }; + const device2 = { + id: 'b', + type: 'device' + }; + this.tracker.update([device1, device2]); + this.tracker.update([device1]); + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(device2); + return done(); + }); + + it("should emit 'change' when a device changes", function(done) { + const spy = Sinon.spy(); + this.tracker.on('change', spy); + const deviceOld = { + id: 'a', + type: 'device' + }; + const deviceNew = { + id: 'a', + type: 'offline' + }; + this.tracker.update([deviceOld]); + this.tracker.update([deviceNew]); + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(deviceNew, deviceOld); + return done(); + }); + + it("should emit 'changeSet' with all changes", function(done) { + const spy = Sinon.spy(); + this.tracker.on('changeSet', spy); + const device1 = { + id: 'a', + type: 'device' + }; + const device2 = { + id: 'b', + type: 'device' + }; + const device3 = { + id: 'c', + type: 'device' + }; + const device3New = { + id: 'c', + type: 'offline' + }; + const device4 = { + id: 'd', + type: 'offline' + }; + this.tracker.update([device1, device2, device3]); + this.tracker.update([device1, device3New, device4]); + expect(spy).to.have.been.calledTwice; + expect(spy).to.have.been.calledWith({ + added: [device1, device2, device3], + changed: [], + removed: []}); + expect(spy).to.have.been.calledWith({ + added: [device4], + changed: [device3New], + removed: [device2]}); + return done(); + }); + + it("should emit 'error' and 'end' when connection ends", function(done) { + this.tracker.on('error', () => { + return this.tracker.on('end', () => done()); + }); + return this.writer.end(); + }); + + it("should read devices from socket", function(done) { + const spy = Sinon.spy(); + this.tracker.on('changeSet', spy); + const device1 = { + id: 'a', + type: 'device' + }; + const device2 = { + id: 'b', + type: 'device' + }; + const device3 = { + id: 'c', + type: 'device' + }; + const device3New = { + id: 'c', + type: 'offline' + }; + const device4 = { + id: 'd', + type: 'offline' + }; + this.writer.write(Protocol.encodeData(`\ +a\tdevice +b\tdevice +c\tdevice\ +` + ) + ); + this.writer.write(Protocol.encodeData(`\ +a\tdevice +c\toffline +d\toffline\ +` + ) + ); + return setTimeout(function() { + expect(spy).to.have.been.calledTwice; + expect(spy).to.have.been.calledWith({ + added: [device1, device2, device3], + changed: [], + removed: []}); + expect(spy).to.have.been.calledWith({ + added: [device4], + changed: [device3New], + removed: [device2]}); + return done(); + } + , 10); + }); + + return describe('end()', function() { + + it("should close the connection", function(done) { + Sinon.spy(this.conn.parser, 'end'); + this.tracker.on('end', () => { + expect(this.conn.parser.end).to.have.been.calledOnce; + return done(); + }); + return this.tracker.end(); + }); + + return it("should not cause an error to be emit", function(done) { + const spy = Sinon.spy(); + this.tracker.on('error', spy); + this.tracker.on('end', function() { + expect(spy).to.not.have.been.called; + return done(); + }); + return this.tracker.end(); + }); + }); +}); diff --git a/test/adb/util.js b/test/adb/util.js new file mode 100644 index 00000000..7c72ccd2 --- /dev/null +++ b/test/adb/util.js @@ -0,0 +1,39 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); +const Promise = require('bluebird'); +const Sinon = require('sinon'); +const Chai = require('chai'); +Chai.use(require('sinon-chai')); +const {expect} = require('chai'); + +const util = require('../../src/adb/util'); + +describe('util', () => describe('readAll(stream)', function() { + + it("should return a cancellable Promise", function(done) { + const stream = new Stream.PassThrough; + const promise = util.readAll(stream); + expect(promise).to.be.an.instanceOf(Promise); + expect(promise.isCancellable()).to.be.true; + promise.catch(Promise.CancellationError, err => done()); + return promise.cancel(); + }); + + return it("should read all remaining content until the stream ends", function(done) { + const stream = new Stream.PassThrough; + util.readAll(stream) + .then(function(buf) { + expect(buf.length).to.equal(3); + expect(buf.toString()).to.equal('FOO'); + return done(); + }); + stream.write('F'); + stream.write('O'); + stream.write('O'); + return stream.end(); + }); +})); diff --git a/test/mock/connection.js b/test/mock/connection.js new file mode 100644 index 00000000..07265ef5 --- /dev/null +++ b/test/mock/connection.js @@ -0,0 +1,21 @@ +const Parser = require('../../src/adb/parser'); +const MockDuplex = require('./duplex'); + +class MockConnection { + constructor() { + this.socket = new MockDuplex; + this.parser = new Parser(this.socket); + } + + end() { + this.socket.causeEnd(); + return this; + } + + write() { + this.socket.write.apply(this.socket, arguments); + return this; + } +} + +module.exports = MockConnection; diff --git a/test/mock/duplex.js b/test/mock/duplex.js new file mode 100644 index 00000000..cd7f6004 --- /dev/null +++ b/test/mock/duplex.js @@ -0,0 +1,33 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream'); + +class MockDuplex extends Stream.Duplex { + _read(size) {} + + _write(chunk, encoding, callback) { + this.emit('write', chunk, encoding, callback); + callback(null); + } + + causeRead(chunk) { + if (!Buffer.isBuffer(chunk)) { + chunk = new Buffer(chunk); + } + this.push(chunk); + } + + causeEnd() { + this.push(null); + } + + end() { + this.causeEnd(); // In order to better emulate socket streams + return Stream.Duplex.prototype.end.apply(this, arguments); + } +} + +module.exports = MockDuplex;