├── .editorconfig ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── bench └── sync │ └── pull.coffee ├── bin └── adbkit ├── index.coffee ├── package.json ├── src ├── adb.coffee ├── adb │ ├── auth.coffee │ ├── client.coffee │ ├── command.coffee │ ├── command │ │ ├── host-serial │ │ │ ├── forward.coffee │ │ │ ├── getdevicepath.coffee │ │ │ ├── getserialno.coffee │ │ │ ├── getstate.coffee │ │ │ ├── listforwards.coffee │ │ │ └── waitfordevice.coffee │ │ ├── host-transport │ │ │ ├── clear.coffee │ │ │ ├── framebuffer.coffee │ │ │ ├── getfeatures.coffee │ │ │ ├── getpackages.coffee │ │ │ ├── getproperties.coffee │ │ │ ├── install.coffee │ │ │ ├── isinstalled.coffee │ │ │ ├── listreverses.coffee │ │ │ ├── local.coffee │ │ │ ├── log.coffee │ │ │ ├── logcat.coffee │ │ │ ├── monkey.coffee │ │ │ ├── reboot.coffee │ │ │ ├── remount.coffee │ │ │ ├── reverse.coffee │ │ │ ├── root.coffee │ │ │ ├── screencap.coffee │ │ │ ├── shell.coffee │ │ │ ├── startactivity.coffee │ │ │ ├── startservice.coffee │ │ │ ├── sync.coffee │ │ │ ├── tcp.coffee │ │ │ ├── tcpip.coffee │ │ │ ├── trackjdwp.coffee │ │ │ ├── uninstall.coffee │ │ │ ├── usb.coffee │ │ │ └── waitbootcomplete.coffee │ │ └── host │ │ │ ├── connect.coffee │ │ │ ├── devices.coffee │ │ │ ├── deviceswithpaths.coffee │ │ │ ├── disconnect.coffee │ │ │ ├── kill.coffee │ │ │ ├── trackdevices.coffee │ │ │ ├── transport.coffee │ │ │ └── version.coffee │ ├── connection.coffee │ ├── dump.coffee │ ├── framebuffer │ │ └── rgbtransform.coffee │ ├── keycode.coffee │ ├── linetransform.coffee │ ├── parser.coffee │ ├── proc │ │ └── stat.coffee │ ├── protocol.coffee │ ├── sync.coffee │ ├── sync │ │ ├── entry.coffee │ │ ├── pulltransfer.coffee │ │ ├── pushtransfer.coffee │ │ └── stats.coffee │ ├── tcpusb │ │ ├── packet.coffee │ │ ├── packetreader.coffee │ │ ├── rollingcounter.coffee │ │ ├── server.coffee │ │ ├── service.coffee │ │ ├── servicemap.coffee │ │ └── socket.coffee │ ├── tracker.coffee │ └── util.coffee └── cli.coffee ├── tasks └── keycode.coffee └── test ├── adb.coffee ├── adb ├── command │ ├── host-serial │ │ └── waitfordevice.coffee │ ├── host-transport │ │ ├── clear.coffee │ │ ├── framebuffer.coffee │ │ ├── getfeatures.coffee │ │ ├── getpackages.coffee │ │ ├── getproperties.coffee │ │ ├── install.coffee │ │ ├── isinstalled.coffee │ │ ├── local.coffee │ │ ├── log.coffee │ │ ├── logcat.coffee │ │ ├── monkey.coffee │ │ ├── reboot.coffee │ │ ├── remount.coffee │ │ ├── root.coffee │ │ ├── screencap.coffee │ │ ├── shell.coffee │ │ ├── startactivity.coffee │ │ ├── startservice.coffee │ │ ├── sync.coffee │ │ ├── tcp.coffee │ │ ├── tcpip.coffee │ │ ├── uninstall.coffee │ │ ├── usb.coffee │ │ └── waitbootcomplete.coffee │ └── host │ │ ├── connect.coffee │ │ ├── disconnect.coffee │ │ └── version.coffee ├── framebuffer │ └── rgbtransform.coffee ├── linetransform.coffee ├── parser.coffee ├── protocol.coffee ├── sync.coffee ├── tracker.coffee └── util.coffee └── mock ├── connection.coffee └── duplex.coffee /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.js] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.json] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.coffee] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.md] 25 | indent_style = space 26 | indent_size = 2 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lib/ 3 | /temp/ 4 | /index.js 5 | /*.tgz 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.editorconfig 2 | /.npmignore 3 | /*.tgz 4 | /npm-debug.log 5 | /Gruntfile.coffee 6 | /index.coffee 7 | /CONTRIBUTING.md 8 | /src/ 9 | /test/ 10 | /bench/ 11 | /tasks/ 12 | /temp/ 13 | /git_hooks/ 14 | *.!sync 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are happy to accept any contributions that make sense and respect the rules listed below. 4 | 5 | ## How to contribute 6 | 7 | 1. Fork the repo. 8 | 2. Create a feature branch for your contribution out of the `master` branch. Only one contribution per branch is accepted. 9 | 3. Implement your contribution while respecting our rules (see below). 10 | 4. If possible, add tests for your contribution to make sure it actually works. 11 | 5. Don't forget to run `npm test` just right before submitting, it also checks for code styling issues. 12 | 6. Submit a pull request against our `master` branch! 13 | 14 | ## Rules 15 | 16 | * **Do** use feature branches. 17 | * **Do** conform to existing coding style so that your contribution fits in. 18 | * **Do** use [EditorConfig] to enforce our [whitespace rules](.editorconfig). If your editor is not supported, enforce the settings manually. 19 | * **Do** run `npm test` for CoffeeLint, JSONLint and unit test coverage. 20 | * **Do not** touch the `version` field in [package.json](package.json). 21 | * **Do not** commit any generated files, unless already in the repo. If absolutely necessary, explain why. 22 | * **Do not** create any top level files or directories. If absolutely necessary, explain why and update [.npmignore](.npmignore). 23 | 24 | ## License 25 | 26 | By contributing your code, you agree to license your contribution under our [LICENSE](LICENSE). 27 | 28 | [editorconfig]: 29 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | grunt.initConfig 4 | pkg: require './package' 5 | coffee: 6 | src: 7 | options: 8 | bare: true 9 | noHeader: true 10 | expand: true 11 | cwd: 'src' 12 | src: '**/*.coffee' 13 | dest: 'lib' 14 | ext: '.js' 15 | index: 16 | src: 'index.coffee' 17 | dest: 'index.js' 18 | clean: 19 | lib: 20 | src: 'lib' 21 | index: 22 | src: 'index.js' 23 | coffeelint: 24 | options: 25 | indentation: 26 | level: 'ignore' 27 | no_backticks: 28 | level: 'ignore' 29 | src: 30 | src: '<%= coffee.src.cwd %>/<%= coffee.src.src %>' 31 | index: 32 | src: '<%= coffee.index.src %>' 33 | test: 34 | src: 'test/**/*.coffee' 35 | tasks: 36 | src: 'tasks/**/*.coffee' 37 | gruntfile: 38 | src: 'Gruntfile.coffee' 39 | jsonlint: 40 | packagejson: 41 | src: 'package.json' 42 | watch: 43 | src: 44 | files: '<%= coffee.src.cwd %>/<%= coffee.src.src %>' 45 | tasks: ['coffeelint:src', 'test'] 46 | index: 47 | files: '<%= coffee.index.src %>' 48 | tasks: ['coffeelint:index', 'test'] 49 | test: 50 | files: '<%= coffeelint.test.src %>', 51 | tasks: ['coffeelint:test', 'test'] 52 | gruntfile: 53 | files: '<%= coffeelint.gruntfile.src %>' 54 | tasks: ['coffeelint:gruntfile'] 55 | packagejson: 56 | files: '<%= jsonlint.packagejson.src %>' 57 | tasks: ['jsonlint:packagejson'] 58 | exec: 59 | mocha: 60 | options: [ 61 | '--compilers coffee:coffee-script/register' 62 | '--reporter spec' 63 | '--colors' 64 | '--recursive' 65 | ], 66 | cmd: './node_modules/.bin/mocha <%= exec.mocha.options.join(" ") %>' 67 | keycode: 68 | generate: 69 | dest: 'src/adb/keycode.coffee' 70 | 71 | grunt.loadNpmTasks 'grunt-contrib-clean' 72 | grunt.loadNpmTasks 'grunt-contrib-coffee' 73 | grunt.loadNpmTasks 'grunt-coffeelint' 74 | grunt.loadNpmTasks 'grunt-jsonlint' 75 | grunt.loadNpmTasks 'grunt-contrib-watch' 76 | grunt.loadNpmTasks 'grunt-notify' 77 | grunt.loadNpmTasks 'grunt-exec' 78 | grunt.loadTasks './tasks' 79 | 80 | grunt.registerTask 'test', ['jsonlint', 'coffeelint', 'exec:mocha'] 81 | grunt.registerTask 'build', ['coffee'] 82 | grunt.registerTask 'default', ['test'] 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2013 CyberAgent, Inc. 2 | Copyright © 2016 The OpenSTF Project 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /bench/sync/pull.coffee: -------------------------------------------------------------------------------- 1 | Bench = require 'bench' 2 | {spawn} = require 'child_process' 3 | 4 | Adb = require '../..' 5 | 6 | deviceId = process.env.DEVICE_ID 7 | 8 | module.exports = 9 | compareCount: 3 10 | compare: 11 | "pull /dev/graphics/fb0 using ADB CLI": (done) -> 12 | proc = spawn 'adb', 13 | ['-s', deviceId, 'pull', '/dev/graphics/fb0', '/dev/null'] 14 | proc.stdout.on 'end', done 15 | "pull /dev/graphics/fb0 using client.pull()": (done) -> 16 | client = Adb.createClient() 17 | client.pull deviceId, '/dev/graphics/fb0', (err, stream) -> 18 | stream.resume() 19 | stream.on 'end', done 20 | 21 | Bench.runMain() 22 | -------------------------------------------------------------------------------- /bin/adbkit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli') 4 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | Path = require 'path' 2 | 3 | module.exports = switch Path.extname __filename 4 | when '.coffee' then require './src/adb' 5 | else require './lib/adb' 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adbkit", 3 | "version": "2.11.1", 4 | "description": "A pure Node.js client for the Android Debug Bridge.", 5 | "keywords": [ 6 | "adb", 7 | "adbkit", 8 | "android", 9 | "logcat", 10 | "monkey" 11 | ], 12 | "bin": { 13 | "adbkit": "./bin/adbkit" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/openstf/adbkit/issues" 17 | }, 18 | "license": "Apache-2.0", 19 | "author": { 20 | "name": "The OpenSTF Project", 21 | "email": "contact@openstf.io", 22 | "url": "https://openstf.io" 23 | }, 24 | "main": "./index", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/openstf/adbkit.git" 28 | }, 29 | "scripts": { 30 | "postpublish": "grunt clean", 31 | "prepublish": "grunt test coffee", 32 | "test": "grunt test" 33 | }, 34 | "dependencies": { 35 | "adbkit-logcat": "^1.1.0", 36 | "adbkit-monkey": "~1.0.1", 37 | "bluebird": "~2.9.24", 38 | "commander": "^2.3.0", 39 | "debug": "~2.6.3", 40 | "node-forge": "^0.7.1", 41 | "split": "~0.3.3" 42 | }, 43 | "devDependencies": { 44 | "bench": "~0.3.5", 45 | "chai": "~2.2.0", 46 | "coffee-script": "~1.9.1", 47 | "grunt": "~0.4.5", 48 | "grunt-cli": "~0.1.13", 49 | "grunt-coffeelint": "0.0.13", 50 | "grunt-contrib-clean": "~0.6.0", 51 | "grunt-contrib-coffee": "~0.13.0", 52 | "grunt-contrib-watch": "~0.6.1", 53 | "grunt-exec": "~0.4.3", 54 | "grunt-jsonlint": "~1.0.4", 55 | "grunt-notify": "~0.4.1", 56 | "mocha": "~2.2.1", 57 | "sinon": "~1.14.1", 58 | "sinon-chai": "~2.7.0", 59 | "coffeelint": "~1.9.3" 60 | }, 61 | "engines": { 62 | "node": ">= 0.10.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/adb.coffee: -------------------------------------------------------------------------------- 1 | Client = require './adb/client' 2 | Keycode = require './adb/keycode' 3 | util = require './adb/util' 4 | 5 | class Adb 6 | @createClient: (options = {}) -> 7 | options.host ||= process.env.ADB_HOST 8 | options.port ||= process.env.ADB_PORT 9 | new Client options 10 | 11 | Adb.Keycode = Keycode 12 | 13 | Adb.util = util 14 | 15 | module.exports = Adb 16 | -------------------------------------------------------------------------------- /src/adb/auth.coffee: -------------------------------------------------------------------------------- 1 | Promise = require 'bluebird' 2 | forge = require 'node-forge' 3 | BigInteger = forge.jsbn.BigInteger 4 | 5 | ### 6 | The stucture of an ADB RSAPublicKey is as follows: 7 | 8 | #define RSANUMBYTES 256 // 2048 bit key length 9 | #define RSANUMWORDS (RSANUMBYTES / sizeof(uint32_t)) 10 | 11 | typedef struct RSAPublicKey { 12 | int len; // Length of n[] in number of uint32_t 13 | uint32_t n0inv; // -1 / n[0] mod 2^32 14 | uint32_t n[RSANUMWORDS]; // modulus as little endian array 15 | uint32_t rr[RSANUMWORDS]; // R^2 as little endian array 16 | int exponent; // 3 or 65537 17 | } RSAPublicKey; 18 | 19 | ### 20 | class Auth 21 | # coffeelint: disable=max_line_length 22 | RE = /^((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)\0?( .*|)\s*$/ 23 | # coffeelint: enable=max_line_length 24 | 25 | readPublicKeyFromStruct = (struct, comment) -> 26 | throw new Error "Invalid public key" unless struct.length 27 | 28 | # Keep track of what we've read already 29 | offset = 0 30 | 31 | # Get len 32 | len = struct.readUInt32LE(offset) * 4 33 | offset += 4 34 | 35 | unless struct.length is 4 + 4 + len + len + 4 36 | throw new Error "Invalid public key" 37 | 38 | # Skip n0inv, we don't need it 39 | offset += 4 40 | 41 | # Get n 42 | n = new Buffer len 43 | struct.copy(n, 0, offset, offset + len) 44 | [].reverse.call(n) 45 | offset += len 46 | 47 | # Skip rr, we don't need it 48 | offset += len 49 | 50 | # Get e 51 | e = struct.readUInt32LE(offset) 52 | 53 | unless e is 3 or e is 65537 54 | throw new Error "Invalid exponent #{e}, only 3 and 65537 are supported" 55 | 56 | # Restore the public key 57 | key = forge.pki.setRsaPublicKey( 58 | new BigInteger(n.toString('hex'), 16) 59 | new BigInteger(e.toString(), 10) 60 | ) 61 | 62 | # It will be difficult to retrieve the fingerprint later as it's based 63 | # on the complete struct data, so let's just extend the key with it. 64 | md = forge.md.md5.create() 65 | md.update struct.toString('binary') 66 | key.fingerprint = md.digest().toHex().match(/../g).join(':') 67 | 68 | # Expose comment for the same reason 69 | key.comment = comment 70 | 71 | return key 72 | 73 | @parsePublicKey = (buffer) -> 74 | new Promise (resolve, reject) -> 75 | if match = RE.exec(buffer) 76 | struct = new Buffer(match[1], 'base64') 77 | comment = match[2].trim() 78 | resolve readPublicKeyFromStruct struct, comment 79 | else 80 | reject new Error "Unrecognizable public key format" 81 | 82 | module.exports = Auth 83 | -------------------------------------------------------------------------------- /src/adb/command.coffee: -------------------------------------------------------------------------------- 1 | debug = require('debug')('adb:command') 2 | 3 | Parser = require './parser' 4 | Protocol = require './protocol' 5 | 6 | class Command 7 | RE_SQUOT = /'/g 8 | RE_ESCAPE = /([$`\\!"])/g 9 | 10 | constructor: (@connection) -> 11 | @parser = @connection.parser 12 | @protocol = Protocol 13 | 14 | execute: -> 15 | throw new Exception 'Missing implementation' 16 | 17 | _send: (data) -> 18 | encoded = Protocol.encodeData data 19 | debug "Send '#{encoded}'" 20 | @connection.write encoded 21 | return this 22 | 23 | # Note that this is just for convenience, not security. 24 | _escape: (arg) -> 25 | switch typeof arg 26 | when 'number' 27 | arg 28 | else 29 | "'" + arg.toString().replace(RE_SQUOT, "'\"'\"'") + "'" 30 | 31 | # Note that this is just for convenience, not security. Also, for some 32 | # incomprehensible reason, some Lenovo devices (e.g. Lenovo A806) behave 33 | # differently when arguments are given inside single quotes. See 34 | # https://github.com/openstf/stf/issues/471 for more information. So that's 35 | # why we now use double quotes here. 36 | _escapeCompat: (arg) -> 37 | switch typeof arg 38 | when 'number' 39 | arg 40 | else 41 | '"' + arg.toString().replace(RE_ESCAPE, '\\$1') + '"' 42 | 43 | module.exports = Command 44 | -------------------------------------------------------------------------------- /src/adb/command/host-serial/forward.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class ForwardCommand extends Command 5 | execute: (serial, local, remote) -> 6 | this._send "host-serial:#{serial}:forward:#{local};#{remote}" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readAscii 4 12 | .then (reply) => 13 | switch reply 14 | when Protocol.OKAY 15 | true 16 | when Protocol.FAIL 17 | @parser.readError() 18 | else 19 | @parser.unexpected reply, 'OKAY or FAIL' 20 | when Protocol.FAIL 21 | @parser.readError() 22 | else 23 | @parser.unexpected reply, 'OKAY or FAIL' 24 | 25 | module.exports = ForwardCommand 26 | -------------------------------------------------------------------------------- /src/adb/command/host-serial/getdevicepath.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class GetDevicePathCommand extends Command 5 | execute: (serial) -> 6 | this._send "host-serial:#{serial}:get-devpath" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readValue() 12 | .then (value) -> 13 | value.toString() 14 | when Protocol.FAIL 15 | @parser.readError() 16 | else 17 | @parser.unexpected reply, 'OKAY or FAIL' 18 | 19 | module.exports = GetDevicePathCommand 20 | -------------------------------------------------------------------------------- /src/adb/command/host-serial/getserialno.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class GetSerialNoCommand extends Command 5 | execute: (serial) -> 6 | this._send "host-serial:#{serial}:get-serialno" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readValue() 12 | .then (value) -> 13 | value.toString() 14 | when Protocol.FAIL 15 | @parser.readError() 16 | else 17 | @parser.unexpected reply, 'OKAY or FAIL' 18 | 19 | module.exports = GetSerialNoCommand 20 | -------------------------------------------------------------------------------- /src/adb/command/host-serial/getstate.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class GetStateCommand extends Command 5 | execute: (serial) -> 6 | this._send "host-serial:#{serial}:get-state" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readValue() 12 | .then (value) -> 13 | value.toString() 14 | when Protocol.FAIL 15 | @parser.readError() 16 | else 17 | @parser.unexpected reply, 'OKAY or FAIL' 18 | 19 | module.exports = GetStateCommand 20 | -------------------------------------------------------------------------------- /src/adb/command/host-serial/listforwards.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class ListForwardsCommand extends Command 5 | execute: (serial) -> 6 | this._send "host-serial:#{serial}:list-forward" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readValue() 12 | .then (value) => 13 | this._parseForwards value 14 | when Protocol.FAIL 15 | @parser.readError() 16 | else 17 | @parser.unexpected reply, 'OKAY or FAIL' 18 | 19 | _parseForwards: (value) -> 20 | forwards = [] 21 | for forward in value.toString().split '\n' 22 | if forward 23 | [serial, local, remote] = forward.split /\s+/ 24 | forwards.push serial: serial, local: local, remote: remote 25 | return forwards 26 | 27 | module.exports = ListForwardsCommand 28 | -------------------------------------------------------------------------------- /src/adb/command/host-serial/waitfordevice.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class WaitForDeviceCommand extends Command 5 | execute: (serial) -> 6 | this._send "host-serial:#{serial}:wait-for-any" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readAscii 4 12 | .then (reply) => 13 | switch reply 14 | when Protocol.OKAY 15 | serial 16 | when Protocol.FAIL 17 | @parser.readError() 18 | else 19 | @parser.unexpected reply, 'OKAY or FAIL' 20 | when Protocol.FAIL 21 | @parser.readError() 22 | else 23 | @parser.unexpected reply, 'OKAY or FAIL' 24 | 25 | module.exports = WaitForDeviceCommand 26 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/clear.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class ClearCommand extends Command 5 | execute: (pkg) -> 6 | this._send "shell:pm clear #{pkg}" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.searchLine /^(Success|Failed)$/ 12 | .finally => 13 | @parser.end() 14 | .then (result) -> 15 | switch result[0] 16 | when 'Success' 17 | true 18 | when 'Failed' 19 | # Unfortunately, the command may stall at this point and we 20 | # have to kill the connection. 21 | throw new Error "Package '#{pkg}' could not be cleared" 22 | when Protocol.FAIL 23 | @parser.readError() 24 | else 25 | @parser.unexpected reply, 'OKAY or FAIL' 26 | 27 | module.exports = ClearCommand 28 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/framebuffer.coffee: -------------------------------------------------------------------------------- 1 | Assert = require 'assert' 2 | {spawn} = require 'child_process' 3 | debug = require('debug')('adb:command:framebuffer') 4 | 5 | Command = require '../../command' 6 | Protocol = require '../../protocol' 7 | RgbTransform = require '../../framebuffer/rgbtransform' 8 | 9 | class FrameBufferCommand extends Command 10 | execute: (format) -> 11 | this._send 'framebuffer:' 12 | @parser.readAscii 4 13 | .then (reply) => 14 | switch reply 15 | when Protocol.OKAY 16 | @parser.readBytes 52 17 | .then (header) => 18 | meta = this._parseHeader header 19 | switch format 20 | when 'raw' 21 | stream = @parser.raw() 22 | stream.meta = meta 23 | stream 24 | else 25 | stream = this._convert meta, format 26 | stream.meta = meta 27 | stream 28 | when Protocol.FAIL 29 | @parser.readError() 30 | else 31 | @parser.unexpected reply, 'OKAY or FAIL' 32 | 33 | _convert: (meta, format, raw) -> 34 | debug "Converting raw framebuffer stream into #{format.toUpperCase()}" 35 | switch meta.format 36 | when 'rgb', 'rgba' 37 | # Known to be supported by GraphicsMagick 38 | else 39 | debug "Silently transforming '#{meta.format}' into 'rgb' for `gm`" 40 | transform = new RgbTransform meta 41 | meta.format = 'rgb' 42 | raw = @parser.raw().pipe transform 43 | proc = spawn 'gm', [ 44 | 'convert' 45 | '-size' 46 | "#{meta.width}x#{meta.height}" 47 | "#{meta.format}:-" 48 | "#{format}:-" 49 | ] 50 | raw.pipe proc.stdin 51 | return proc.stdout 52 | 53 | _parseHeader: (header) -> 54 | meta = {} 55 | offset = 0 56 | meta.version = header.readUInt32LE offset 57 | if meta.version is 16 58 | throw new Error 'Old-style raw images are not supported' 59 | offset += 4 60 | meta.bpp = header.readUInt32LE offset 61 | offset += 4 62 | meta.size = header.readUInt32LE offset 63 | offset += 4 64 | meta.width = header.readUInt32LE offset 65 | offset += 4 66 | meta.height = header.readUInt32LE offset 67 | offset += 4 68 | meta.red_offset = header.readUInt32LE offset 69 | offset += 4 70 | meta.red_length = header.readUInt32LE offset 71 | offset += 4 72 | meta.blue_offset = header.readUInt32LE offset 73 | offset += 4 74 | meta.blue_length = header.readUInt32LE offset 75 | offset += 4 76 | meta.green_offset = header.readUInt32LE offset 77 | offset += 4 78 | meta.green_length = header.readUInt32LE offset 79 | offset += 4 80 | meta.alpha_offset = header.readUInt32LE offset 81 | offset += 4 82 | meta.alpha_length = header.readUInt32LE offset 83 | meta.format = if meta.blue_offset is 0 then 'bgr' else 'rgb' 84 | meta.format += 'a' if meta.bpp is 32 or meta.alpha_length 85 | return meta 86 | 87 | module.exports = FrameBufferCommand 88 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/getfeatures.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class GetFeaturesCommand extends Command 5 | RE_FEATURE = /^feature:(.*?)(?:=(.*?))?\r?$/gm 6 | 7 | execute: -> 8 | this._send 'shell:pm list features 2>/dev/null' 9 | @parser.readAscii 4 10 | .then (reply) => 11 | switch reply 12 | when Protocol.OKAY 13 | @parser.readAll() 14 | .then (data) => 15 | this._parseFeatures data.toString() 16 | when Protocol.FAIL 17 | @parser.readError() 18 | else 19 | @parser.unexpected reply, 'OKAY or FAIL' 20 | 21 | _parseFeatures: (value) -> 22 | features = {} 23 | while match = RE_FEATURE.exec value 24 | features[match[1]] = match[2] or true 25 | return features 26 | 27 | module.exports = GetFeaturesCommand 28 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/getpackages.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class GetPackagesCommand extends Command 5 | RE_PACKAGE = /^package:(.*?)\r?$/gm 6 | 7 | execute: -> 8 | this._send 'shell:pm list packages 2>/dev/null' 9 | @parser.readAscii 4 10 | .then (reply) => 11 | switch reply 12 | when Protocol.OKAY 13 | @parser.readAll() 14 | .then (data) => 15 | this._parsePackages data.toString() 16 | when Protocol.FAIL 17 | @parser.readError() 18 | else 19 | @parser.unexpected reply, 'OKAY or FAIL' 20 | 21 | _parsePackages: (value) -> 22 | features = [] 23 | while match = RE_PACKAGE.exec value 24 | features.push match[1] 25 | return features 26 | 27 | module.exports = GetPackagesCommand 28 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/getproperties.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class GetPropertiesCommand extends Command 5 | RE_KEYVAL = /^\[([\s\S]*?)\]: \[([\s\S]*?)\]\r?$/gm 6 | 7 | execute: -> 8 | this._send 'shell:getprop' 9 | @parser.readAscii 4 10 | .then (reply) => 11 | switch reply 12 | when Protocol.OKAY 13 | @parser.readAll() 14 | .then (data) => 15 | this._parseProperties data.toString() 16 | when Protocol.FAIL 17 | @parser.readError() 18 | else 19 | @parser.unexpected reply, 'OKAY or FAIL' 20 | 21 | _parseProperties: (value) -> 22 | properties = {} 23 | while match = RE_KEYVAL.exec value 24 | properties[match[1]] = match[2] 25 | return properties 26 | 27 | module.exports = GetPropertiesCommand 28 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/install.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class InstallCommand extends Command 5 | execute: (apk) -> 6 | this._send "shell:pm install -r #{this._escapeCompat(apk)}" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.searchLine /^(Success|Failure \[(.*?)\])$/ 12 | .then (match) -> 13 | if match[1] is 'Success' 14 | true 15 | else 16 | code = match[2] 17 | err = new Error "#{apk} could not be installed [#{code}]" 18 | err.code = code 19 | throw err 20 | .finally => 21 | # Consume all remaining content to "naturally" close the 22 | # connection. 23 | @parser.readAll() 24 | when Protocol.FAIL 25 | @parser.readError() 26 | else 27 | @parser.unexpected reply, 'OKAY or FAIL' 28 | 29 | module.exports = InstallCommand 30 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/isinstalled.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | Parser = require '../../parser' 4 | 5 | class IsInstalledCommand extends Command 6 | execute: (pkg) -> 7 | this._send "shell:pm path #{pkg} 2>/dev/null" 8 | @parser.readAscii 4 9 | .then (reply) => 10 | switch reply 11 | when Protocol.OKAY 12 | @parser.readAscii 8 13 | .then (reply) => 14 | switch reply 15 | when 'package:' 16 | true 17 | else 18 | @parser.unexpected reply, "'package:'" 19 | .catch Parser.PrematureEOFError, (err) -> 20 | false 21 | when Protocol.FAIL 22 | @parser.readError() 23 | else 24 | @parser.unexpected reply, 'OKAY or FAIL' 25 | 26 | module.exports = IsInstalledCommand 27 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/listreverses.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class ListReversesCommand extends Command 5 | execute: -> 6 | this._send "reverse:list-forward" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readValue() 12 | .then (value) => 13 | this._parseReverses value 14 | when Protocol.FAIL 15 | @parser.readError() 16 | else 17 | @parser.unexpected reply, 'OKAY or FAIL' 18 | 19 | _parseReverses: (value) -> 20 | reverses = [] 21 | for reverse in value.toString().split '\n' 22 | if reverse 23 | [serial, remote, local] = reverse.split /\s+/ 24 | reverses.push remote: remote, local: local 25 | return reverses 26 | 27 | module.exports = ListReversesCommand 28 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/local.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class LocalCommand extends Command 5 | execute: (path) -> 6 | this._send if /:/.test(path) then path else "localfilesystem:#{path}" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.raw() 12 | when Protocol.FAIL 13 | @parser.readError() 14 | else 15 | @parser.unexpected reply, 'OKAY or FAIL' 16 | 17 | module.exports = LocalCommand 18 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/log.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class LogCommand extends Command 5 | execute: (name) -> 6 | this._send "log:#{name}" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.raw() 12 | when Protocol.FAIL 13 | @parser.readError() 14 | else 15 | @parser.unexpected reply, 'OKAY or FAIL' 16 | 17 | module.exports = LogCommand 18 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/logcat.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | LineTransform = require '../../linetransform' 4 | 5 | class LogcatCommand extends Command 6 | execute: (options = {}) -> 7 | # For some reason, LG G Flex requires a filter spec with the -B option. 8 | # It doesn't actually use it, though. Regardless of the spec we always get 9 | # all events on all devices. 10 | cmd = 'logcat -B *:I 2>/dev/null' 11 | cmd = "logcat -c 2>/dev/null && #{cmd}" if options.clear 12 | this._send "shell:echo && #{cmd}" 13 | @parser.readAscii 4 14 | .then (reply) => 15 | switch reply 16 | when Protocol.OKAY 17 | @parser.raw().pipe new LineTransform autoDetect: true 18 | when Protocol.FAIL 19 | @parser.readError() 20 | else 21 | @parser.unexpected reply, 'OKAY or FAIL' 22 | 23 | module.exports = LogcatCommand 24 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/monkey.coffee: -------------------------------------------------------------------------------- 1 | Promise = require 'bluebird' 2 | 3 | Command = require '../../command' 4 | Protocol = require '../../protocol' 5 | 6 | class MonkeyCommand extends Command 7 | execute: (port) -> 8 | # Some devices have broken /sdcard (i.e. /mnt/sdcard), which monkey will 9 | # attempt to use to write log files to. We can cheat and set the location 10 | # with an environment variable, because most logs use 11 | # Environment.getLegacyExternalStorageDirectory() like they should. There 12 | # are some hardcoded logs, though. Anyway, this should enable most things. 13 | # Check https://github.com/android/platform_frameworks_base/blob/master/ 14 | # core/java/android/os/Environment.java for the variables. 15 | this._send "shell:EXTERNAL_STORAGE=/data/local/tmp monkey --port #{port} -v" 16 | 17 | @parser.readAscii 4 18 | .then (reply) => 19 | switch reply 20 | when Protocol.OKAY 21 | # The monkey command is a bit weird in that it doesn't look like 22 | # it starts in daemon mode, but it actually does. So even though 23 | # the command leaves the terminal "hanging", Ctrl-C (or just 24 | # ending the connection) will not end the daemon. HOWEVER, on 25 | # some devices, such as SO-02C by Sony, it is required to leave 26 | # the command hanging around. In any case, if the command exits 27 | # by itself, it means that something went wrong. 28 | @parser.searchLine /^:Monkey:/ 29 | # On some devices (such as F-08D by Fujitsu), the monkey 30 | # command gives no output no matter how many verbose flags you 31 | # give it. So we use a fallback timeout. 32 | .timeout 1000 33 | .then => 34 | @parser.raw() 35 | .catch Promise.TimeoutError, (err) => 36 | @parser.raw() 37 | when Protocol.FAIL 38 | @parser.readError() 39 | else 40 | @parser.unexpected reply, 'OKAY or FAIL' 41 | 42 | module.exports = MonkeyCommand 43 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/reboot.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class RebootCommand extends Command 5 | execute: -> 6 | this._send 'reboot:' 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readAll() 12 | .return true 13 | when Protocol.FAIL 14 | @parser.readError() 15 | else 16 | @parser.unexpected reply, 'OKAY or FAIL' 17 | 18 | module.exports = RebootCommand 19 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/remount.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class RemountCommand extends Command 5 | execute: -> 6 | this._send 'remount:' 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | true 12 | when Protocol.FAIL 13 | @parser.readError() 14 | else 15 | @parser.unexpected reply, 'OKAY or FAIL' 16 | 17 | module.exports = RemountCommand 18 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/reverse.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class ReverseCommand extends Command 5 | execute: (remote, local) -> 6 | this._send "reverse:forward:#{remote};#{local}" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readAscii 4 12 | .then (reply) => 13 | switch reply 14 | when Protocol.OKAY 15 | true 16 | when Protocol.FAIL 17 | @parser.readError() 18 | else 19 | @parser.unexpected reply, 'OKAY or FAIL' 20 | when Protocol.FAIL 21 | @parser.readError() 22 | else 23 | @parser.unexpected reply, 'OKAY or FAIL' 24 | 25 | module.exports = ReverseCommand 26 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/root.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class RootCommand extends Command 5 | RE_OK = /restarting adbd as root/ 6 | 7 | execute: -> 8 | this._send 'root:' 9 | @parser.readAscii 4 10 | .then (reply) => 11 | switch reply 12 | when Protocol.OKAY 13 | @parser.readAll() 14 | .then (value) -> 15 | if RE_OK.test(value) 16 | true 17 | else 18 | throw new Error value.toString().trim() 19 | when Protocol.FAIL 20 | @parser.readError() 21 | else 22 | @parser.unexpected reply, 'OKAY or FAIL' 23 | 24 | module.exports = RootCommand 25 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/screencap.coffee: -------------------------------------------------------------------------------- 1 | Promise = require 'bluebird' 2 | 3 | Command = require '../../command' 4 | Protocol = require '../../protocol' 5 | Parser = require '../../parser' 6 | LineTransform = require '../../linetransform' 7 | 8 | class ScreencapCommand extends Command 9 | execute: -> 10 | this._send 'shell:echo && screencap -p 2>/dev/null' 11 | @parser.readAscii 4 12 | .then (reply) => 13 | switch reply 14 | when Protocol.OKAY 15 | transform = new LineTransform 16 | @parser.readBytes 1 17 | .then (chunk) => 18 | transform = new LineTransform autoDetect: true 19 | transform.write chunk 20 | @parser.raw().pipe transform 21 | .catch Parser.PrematureEOFError, -> 22 | throw new Error 'No support for the screencap command' 23 | when Protocol.FAIL 24 | @parser.readError() 25 | else 26 | @parser.unexpected reply, 'OKAY or FAIL' 27 | 28 | module.exports = ScreencapCommand 29 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/shell.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class ShellCommand extends Command 5 | execute: (command) -> 6 | if Array.isArray command 7 | command = command.map(this._escape).join ' ' 8 | this._send "shell:#{command}" 9 | @parser.readAscii 4 10 | .then (reply) => 11 | switch reply 12 | when Protocol.OKAY 13 | @parser.raw() 14 | when Protocol.FAIL 15 | @parser.readError() 16 | else 17 | @parser.unexpected reply, 'OKAY or FAIL' 18 | 19 | module.exports = ShellCommand 20 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/startactivity.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | Parser = require '../../parser' 4 | 5 | class StartActivityCommand extends Command 6 | RE_ERROR = /^Error: (.*)$/ 7 | EXTRA_TYPES = 8 | string: 's' 9 | null: 'sn' 10 | bool: 'z' 11 | int: 'i' 12 | long: 'l' 13 | float: 'l' 14 | uri: 'u' 15 | component: 'cn' 16 | 17 | execute: (options) -> 18 | args = this._intentArgs options 19 | if options.debug 20 | args.push '-D' 21 | if options.wait 22 | args.push '-W' 23 | if options.user or options.user is 0 24 | args.push '--user', this._escape options.user 25 | this._run 'start', args 26 | 27 | _run: (command, args) -> 28 | this._send "shell:am #{command} #{args.join ' '}" 29 | @parser.readAscii 4 30 | .then (reply) => 31 | switch reply 32 | when Protocol.OKAY 33 | @parser.searchLine RE_ERROR 34 | .finally => 35 | @parser.end() 36 | .then (match) -> 37 | throw new Error match[1] 38 | .catch Parser.PrematureEOFError, (err) -> 39 | true 40 | when Protocol.FAIL 41 | @parser.readError() 42 | else 43 | @parser.unexpected reply, 'OKAY or FAIL' 44 | 45 | _intentArgs: (options) -> 46 | args = [] 47 | if options.extras 48 | args.push.apply args, this._formatExtras options.extras 49 | if options.action 50 | args.push '-a', this._escape options.action 51 | if options.data 52 | args.push '-d', this._escape options.data 53 | if options.mimeType 54 | args.push '-t', this._escape options.mimeType 55 | if options.category 56 | if Array.isArray options.category 57 | options.category.forEach (category) => 58 | args.push '-c', this._escape category 59 | else 60 | args.push '-c', this._escape options.category 61 | if options.component 62 | args.push '-n', this._escape options.component 63 | if options.flags 64 | args.push '-f', this._escape options.flags 65 | return args 66 | 67 | _formatExtras: (extras) -> 68 | return [] unless extras 69 | if Array.isArray extras 70 | extras.reduce (all, extra) => 71 | all.concat this._formatLongExtra extra 72 | , [] 73 | else 74 | Object.keys(extras).reduce (all, key) => 75 | all.concat this._formatShortExtra key, extras[key] 76 | , [] 77 | 78 | _formatShortExtra: (key, value) -> 79 | sugared = 80 | key: key 81 | if value is null 82 | sugared.type = 'null' 83 | else if Array.isArray value 84 | throw new Error "Refusing to format array value '#{key}' using short 85 | syntax; empty array would cause unpredictable results due to unknown 86 | type. Please use long syntax instead." 87 | else 88 | switch typeof value 89 | when 'string' 90 | sugared.type = 'string' 91 | sugared.value = value 92 | when 'boolean' 93 | sugared.type = 'bool' 94 | sugared.value = value 95 | when 'number' 96 | sugared.type = 'int' 97 | sugared.value = value 98 | when 'object' 99 | sugared = value 100 | sugared.key = key 101 | return this._formatLongExtra sugared 102 | 103 | _formatLongExtra: (extra) -> 104 | args = [] 105 | extra.type = 'string' unless extra.type 106 | type = EXTRA_TYPES[extra.type] 107 | unless type 108 | throw new Error "Unsupported type '#{extra.type}' for extra 109 | '#{extra.key}'" 110 | if extra.type is 'null' 111 | args.push "--e#{type}" 112 | args.push this._escape extra.key 113 | else if Array.isArray extra.value 114 | args.push "--e#{type}a" 115 | args.push this._escape extra.key 116 | args.push this._escape extra.value.join ',' 117 | else 118 | args.push "--e#{type}" 119 | args.push this._escape extra.key 120 | args.push this._escape extra.value 121 | return args 122 | 123 | module.exports = StartActivityCommand 124 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/startservice.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | Parser = require '../../parser' 4 | 5 | StartActivityCommand = require './startactivity' 6 | 7 | class StartServiceCommand extends StartActivityCommand 8 | execute: (options) -> 9 | args = this._intentArgs options 10 | if options.user or options.user is 0 11 | args.push '--user', this._escape options.user 12 | this._run 'startservice', args 13 | 14 | module.exports = StartServiceCommand 15 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/sync.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | Sync = require '../../sync' 4 | 5 | class SyncCommand extends Command 6 | execute: -> 7 | this._send 'sync:' 8 | @parser.readAscii 4 9 | .then (reply) => 10 | switch reply 11 | when Protocol.OKAY 12 | new Sync @connection 13 | when Protocol.FAIL 14 | @parser.readError() 15 | else 16 | @parser.unexpected reply, 'OKAY or FAIL' 17 | 18 | module.exports = SyncCommand 19 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/tcp.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class TcpCommand extends Command 5 | execute: (port, host) -> 6 | this._send "tcp:#{port}" + if host then ":#{host}" else '' 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.raw() 12 | when Protocol.FAIL 13 | @parser.readError() 14 | else 15 | @parser.unexpected reply, 'OKAY or FAIL' 16 | 17 | module.exports = TcpCommand 18 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/tcpip.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class TcpIpCommand extends Command 5 | RE_OK = /restarting in/ 6 | 7 | execute: (port) -> 8 | this._send "tcpip:#{port}" 9 | @parser.readAscii 4 10 | .then (reply) => 11 | switch reply 12 | when Protocol.OKAY 13 | @parser.readAll() 14 | .then (value) -> 15 | if RE_OK.test(value) 16 | port 17 | else 18 | throw new Error value.toString().trim() 19 | when Protocol.FAIL 20 | @parser.readError() 21 | else 22 | @parser.unexpected reply, 'OKAY or FAIL' 23 | 24 | module.exports = TcpIpCommand 25 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/trackjdwp.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | 3 | Promise = require 'bluebird' 4 | 5 | Command = require '../../command' 6 | Protocol = require '../../protocol' 7 | Parser = require '../../parser' 8 | 9 | class TrackJdwpCommand extends Command 10 | execute: -> 11 | this._send 'track-jdwp' 12 | @parser.readAscii 4 13 | .then (reply) => 14 | switch reply 15 | when Protocol.OKAY 16 | new Tracker this 17 | when Protocol.FAIL 18 | @parser.readError() 19 | else 20 | @parser.unexpected reply, 'OKAY or FAIL' 21 | 22 | class Tracker extends EventEmitter 23 | constructor: (@command) -> 24 | @pids = [] 25 | @pidMap = Object.create null 26 | @reader = this.read() 27 | .catch Parser.PrematureEOFError, (err) => 28 | this.emit 'end' 29 | .catch Promise.CancellationError, (err) => 30 | @command.connection.end() 31 | this.emit 'end' 32 | .catch (err) => 33 | @command.connection.end() 34 | this.emit 'error', err 35 | this.emit 'end' 36 | 37 | read: -> 38 | @command.parser.readValue() 39 | .cancellable() 40 | .then (list) => 41 | pids = list.toString().split '\n' 42 | pids.push maybeEmpty if maybeEmpty = pids.pop() 43 | this.update pids 44 | 45 | update: (newList) -> 46 | changeSet = 47 | removed: [] 48 | added: [] 49 | newMap = Object.create null 50 | for pid in newList 51 | unless @pidMap[pid] 52 | changeSet.added.push pid 53 | this.emit 'add', pid 54 | newMap[pid] = pid 55 | for pid in @pids 56 | unless newMap[pid] 57 | changeSet.removed.push pid 58 | this.emit 'remove', pid 59 | @pids = newList 60 | @pidMap = newMap 61 | this.emit 'changeSet', changeSet, newList 62 | return this 63 | 64 | end: -> 65 | @reader.cancel() 66 | return this 67 | 68 | module.exports = TrackJdwpCommand 69 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/uninstall.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class UninstallCommand extends Command 5 | execute: (pkg) -> 6 | this._send "shell:pm uninstall #{pkg}" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.searchLine /^(Success|Failure.*|.*Unknown package:.*)$/ 12 | .then (match) -> 13 | if match[1] is 'Success' 14 | true 15 | else 16 | # Either way, the package was uninstalled or doesn't exist, 17 | # which is good enough for us. 18 | true 19 | .finally => 20 | # Consume all remaining content to "naturally" close the 21 | # connection. 22 | @parser.readAll() 23 | when Protocol.FAIL 24 | @parser.readError() 25 | else 26 | @parser.unexpected reply, "OKAY or FAIL" 27 | 28 | module.exports = UninstallCommand 29 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/usb.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class UsbCommand extends Command 5 | RE_OK = /restarting in/ 6 | 7 | execute: -> 8 | this._send 'usb:' 9 | @parser.readAscii 4 10 | .then (reply) => 11 | switch reply 12 | when Protocol.OKAY 13 | @parser.readAll() 14 | .then (value) -> 15 | if RE_OK.test(value) 16 | true 17 | else 18 | throw new Error value.toString().trim() 19 | when Protocol.FAIL 20 | @parser.readError() 21 | else 22 | @parser.unexpected reply, 'OKAY or FAIL' 23 | 24 | module.exports = UsbCommand 25 | -------------------------------------------------------------------------------- /src/adb/command/host-transport/waitbootcomplete.coffee: -------------------------------------------------------------------------------- 1 | debug = require('debug')('adb:command:waitboot') 2 | 3 | Command = require '../../command' 4 | Protocol = require '../../protocol' 5 | 6 | class WaitBootCompleteCommand extends Command 7 | execute: -> 8 | this._send \ 9 | 'shell:while getprop sys.boot_completed 2>/dev/null; do sleep 1; done' 10 | @parser.readAscii 4 11 | .then (reply) => 12 | switch reply 13 | when Protocol.OKAY 14 | @parser.searchLine /^1$/ 15 | .finally => 16 | @parser.end() 17 | .then -> 18 | true 19 | when Protocol.FAIL 20 | @parser.readError() 21 | else 22 | @parser.unexpected reply, 'OKAY or FAIL' 23 | 24 | module.exports = WaitBootCompleteCommand 25 | -------------------------------------------------------------------------------- /src/adb/command/host/connect.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class ConnectCommand extends Command 5 | # Possible replies: 6 | # "unable to connect to 192.168.2.2:5555" 7 | # "connected to 192.168.2.2:5555" 8 | # "already connected to 192.168.2.2:5555" 9 | RE_OK = /connected to|already connected/ 10 | 11 | execute: (host, port) -> 12 | this._send "host:connect:#{host}:#{port}" 13 | @parser.readAscii 4 14 | .then (reply) => 15 | switch reply 16 | when Protocol.OKAY 17 | @parser.readValue() 18 | .then (value) -> 19 | if RE_OK.test value 20 | "#{host}:#{port}" 21 | else 22 | throw new Error value.toString() 23 | when Protocol.FAIL 24 | @parser.readError() 25 | else 26 | @parser.unexpected reply, 'OKAY or FAIL' 27 | 28 | module.exports = ConnectCommand 29 | -------------------------------------------------------------------------------- /src/adb/command/host/devices.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class HostDevicesCommand extends Command 5 | execute: -> 6 | this._send 'host:devices' 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | this._readDevices() 12 | when Protocol.FAIL 13 | @parser.readError() 14 | else 15 | @parser.unexpected reply, 'OKAY or FAIL' 16 | 17 | _readDevices: -> 18 | @parser.readValue() 19 | .then (value) => 20 | this._parseDevices value 21 | 22 | _parseDevices: (value) -> 23 | devices = [] 24 | return devices unless value.length 25 | for line in value.toString('ascii').split '\n' 26 | if line 27 | [id, type] = line.split '\t' 28 | devices.push id: id, type: type 29 | return devices 30 | 31 | module.exports = HostDevicesCommand 32 | -------------------------------------------------------------------------------- /src/adb/command/host/deviceswithpaths.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class HostDevicesWithPathsCommand extends Command 5 | execute: -> 6 | this._send 'host:devices-l' 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | this._readDevices() 12 | when Protocol.FAIL 13 | @parser.readError() 14 | else 15 | @parser.unexpected reply, 'OKAY or FAIL' 16 | 17 | _readDevices: -> 18 | @parser.readValue() 19 | .then (value) => 20 | this._parseDevices value 21 | 22 | _parseDevices: (value) -> 23 | devices = [] 24 | return devices unless value.length 25 | for line in value.toString('ascii').split '\n' 26 | if line 27 | # For some reason, the columns are separated by spaces instead of tabs 28 | [id, type, path] = line.split /\s+/ 29 | devices.push id: id, type: type, path: path 30 | return devices 31 | 32 | module.exports = HostDevicesWithPathsCommand 33 | -------------------------------------------------------------------------------- /src/adb/command/host/disconnect.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class DisconnectCommand extends Command 5 | # Possible replies: 6 | # "No such device 192.168.2.2:5555" 7 | # "" 8 | RE_OK = /^$/ 9 | 10 | execute: (host, port) -> 11 | this._send "host:disconnect:#{host}:#{port}" 12 | @parser.readAscii 4 13 | .then (reply) => 14 | switch reply 15 | when Protocol.OKAY 16 | @parser.readValue() 17 | .then (value) -> 18 | if RE_OK.test value 19 | "#{host}:#{port}" 20 | else 21 | throw new Error value.toString() 22 | when Protocol.FAIL 23 | @parser.readError() 24 | else 25 | @parser.unexpected reply, 'OKAY or FAIL' 26 | 27 | module.exports = DisconnectCommand 28 | -------------------------------------------------------------------------------- /src/adb/command/host/kill.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class HostKillCommand extends Command 5 | execute: -> 6 | this._send 'host:kill' 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | true 12 | when Protocol.FAIL 13 | @parser.readError() 14 | else 15 | @parser.unexpected reply, 'OKAY or FAIL' 16 | 17 | module.exports = HostKillCommand 18 | -------------------------------------------------------------------------------- /src/adb/command/host/trackdevices.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | Tracker = require '../../tracker' 4 | HostDevicesCommand = require './devices' 5 | 6 | class HostTrackDevicesCommand extends HostDevicesCommand 7 | execute: -> 8 | this._send 'host:track-devices' 9 | @parser.readAscii 4 10 | .then (reply) => 11 | switch reply 12 | when Protocol.OKAY 13 | new Tracker this 14 | when Protocol.FAIL 15 | @parser.readError() 16 | else 17 | @parser.unexpected reply, 'OKAY or FAIL' 18 | 19 | module.exports = HostTrackDevicesCommand 20 | -------------------------------------------------------------------------------- /src/adb/command/host/transport.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class HostTransportCommand extends Command 5 | execute: (serial) -> 6 | this._send "host:transport:#{serial}" 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | true 12 | when Protocol.FAIL 13 | @parser.readError() 14 | else 15 | @parser.unexpected reply, 'OKAY or FAIL' 16 | 17 | module.exports = HostTransportCommand 18 | -------------------------------------------------------------------------------- /src/adb/command/host/version.coffee: -------------------------------------------------------------------------------- 1 | Command = require '../../command' 2 | Protocol = require '../../protocol' 3 | 4 | class HostVersionCommand extends Command 5 | execute: -> 6 | this._send 'host:version' 7 | @parser.readAscii 4 8 | .then (reply) => 9 | switch reply 10 | when Protocol.OKAY 11 | @parser.readValue() 12 | .then (value) => 13 | this._parseVersion value 14 | when Protocol.FAIL 15 | @parser.readError() 16 | else 17 | this._parseVersion reply 18 | 19 | _parseVersion: (version) -> 20 | parseInt version, 16 21 | 22 | module.exports = HostVersionCommand 23 | -------------------------------------------------------------------------------- /src/adb/connection.coffee: -------------------------------------------------------------------------------- 1 | Net = require 'net' 2 | debug = require('debug')('adb:connection') 3 | {EventEmitter} = require 'events' 4 | {execFile} = require 'child_process' 5 | 6 | Parser = require './parser' 7 | dump = require './dump' 8 | 9 | class Connection extends EventEmitter 10 | constructor: (@options) -> 11 | @socket = null 12 | @parser = null 13 | @triedStarting = false 14 | 15 | connect: -> 16 | @socket = Net.connect @options 17 | @socket.setNoDelay true 18 | @parser = new Parser @socket 19 | @socket.on 'connect', => 20 | this.emit 'connect' 21 | @socket.on 'end', => 22 | this.emit 'end' 23 | @socket.on 'drain', => 24 | this.emit 'drain' 25 | @socket.on 'timeout', => 26 | this.emit 'timeout' 27 | @socket.on 'error', (err) => 28 | this._handleError err 29 | @socket.on 'close', (hadError) => 30 | this.emit 'close', hadError 31 | return this 32 | 33 | end: -> 34 | @socket.end() 35 | return this 36 | 37 | write: (data, callback) -> 38 | @socket.write dump(data), callback 39 | return this 40 | 41 | startServer: (callback) -> 42 | debug "Starting ADB server via '#{@options.bin} start-server'" 43 | return this._exec ['start-server'], {}, callback 44 | 45 | _exec: (args, options, callback) -> 46 | debug "CLI: #{@options.bin} #{args.join ' '}" 47 | execFile @options.bin, args, options, callback 48 | return this 49 | 50 | _handleError: (err) -> 51 | if err.code is 'ECONNREFUSED' and not @triedStarting 52 | debug "Connection was refused, let's try starting the server once" 53 | @triedStarting = true 54 | this.startServer (err) => 55 | return this._handleError err if err 56 | this.connect() 57 | else 58 | debug "Connection had an error: #{err.message}" 59 | this.emit 'error', err 60 | this.end() 61 | return 62 | 63 | module.exports = Connection 64 | -------------------------------------------------------------------------------- /src/adb/dump.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | 3 | if process.env.ADBKIT_DUMP 4 | out = fs.createWriteStream 'adbkit.dump' 5 | module.exports = (chunk) -> 6 | out.write chunk 7 | chunk 8 | else 9 | module.exports = (chunk) -> chunk 10 | -------------------------------------------------------------------------------- /src/adb/framebuffer/rgbtransform.coffee: -------------------------------------------------------------------------------- 1 | Assert = require 'assert' 2 | Stream = require 'stream' 3 | 4 | class RgbTransform extends Stream.Transform 5 | constructor: (@meta, options) -> 6 | @_buffer = new Buffer '' 7 | Assert.ok (@meta.bpp is 24 or @meta.bpp is 32), 8 | 'Only 24-bit and 32-bit raw images with 8-bits per color are supported' 9 | @_r_pos = @meta.red_offset / 8 10 | @_g_pos = @meta.green_offset / 8 11 | @_b_pos = @meta.blue_offset / 8 12 | @_a_pos = @meta.alpha_offset / 8 13 | @_pixel_bytes = @meta.bpp / 8 14 | super options 15 | 16 | _transform: (chunk, encoding, done) -> 17 | if @_buffer.length 18 | @_buffer = Buffer.concat [@_buffer, chunk], @_buffer.length + chunk.length 19 | else 20 | @_buffer = chunk 21 | sourceCursor = 0 22 | targetCursor = 0 23 | target = if @_pixel_bytes is 3 \ 24 | then @_buffer 25 | else new Buffer Math.max 4, chunk.length / @_pixel_bytes * 3 26 | while @_buffer.length - sourceCursor >= @_pixel_bytes 27 | r = @_buffer[sourceCursor + @_r_pos] 28 | g = @_buffer[sourceCursor + @_g_pos] 29 | b = @_buffer[sourceCursor + @_b_pos] 30 | target[targetCursor + 0] = r 31 | target[targetCursor + 1] = g 32 | target[targetCursor + 2] = b 33 | sourceCursor += @_pixel_bytes 34 | targetCursor += 3 35 | if targetCursor 36 | this.push target.slice 0, targetCursor 37 | @_buffer = @_buffer.slice sourceCursor 38 | done() 39 | return 40 | 41 | module.exports = RgbTransform 42 | -------------------------------------------------------------------------------- /src/adb/keycode.coffee: -------------------------------------------------------------------------------- 1 | # Generated by `grunt keycode` on Tue, 26 Nov 2013 08:02:49 GMT 2 | # KeyEvent.java Copyright (C) 2006 The Android Open Source Project 3 | 4 | module.exports = 5 | KEYCODE_UNKNOWN: 0 6 | KEYCODE_SOFT_LEFT: 1 7 | KEYCODE_SOFT_RIGHT: 2 8 | KEYCODE_HOME: 3 9 | KEYCODE_BACK: 4 10 | KEYCODE_CALL: 5 11 | KEYCODE_ENDCALL: 6 12 | KEYCODE_0: 7 13 | KEYCODE_1: 8 14 | KEYCODE_2: 9 15 | KEYCODE_3: 10 16 | KEYCODE_4: 11 17 | KEYCODE_5: 12 18 | KEYCODE_6: 13 19 | KEYCODE_7: 14 20 | KEYCODE_8: 15 21 | KEYCODE_9: 16 22 | KEYCODE_STAR: 17 23 | KEYCODE_POUND: 18 24 | KEYCODE_DPAD_UP: 19 25 | KEYCODE_DPAD_DOWN: 20 26 | KEYCODE_DPAD_LEFT: 21 27 | KEYCODE_DPAD_RIGHT: 22 28 | KEYCODE_DPAD_CENTER: 23 29 | KEYCODE_VOLUME_UP: 24 30 | KEYCODE_VOLUME_DOWN: 25 31 | KEYCODE_POWER: 26 32 | KEYCODE_CAMERA: 27 33 | KEYCODE_CLEAR: 28 34 | KEYCODE_A: 29 35 | KEYCODE_B: 30 36 | KEYCODE_C: 31 37 | KEYCODE_D: 32 38 | KEYCODE_E: 33 39 | KEYCODE_F: 34 40 | KEYCODE_G: 35 41 | KEYCODE_H: 36 42 | KEYCODE_I: 37 43 | KEYCODE_J: 38 44 | KEYCODE_K: 39 45 | KEYCODE_L: 40 46 | KEYCODE_M: 41 47 | KEYCODE_N: 42 48 | KEYCODE_O: 43 49 | KEYCODE_P: 44 50 | KEYCODE_Q: 45 51 | KEYCODE_R: 46 52 | KEYCODE_S: 47 53 | KEYCODE_T: 48 54 | KEYCODE_U: 49 55 | KEYCODE_V: 50 56 | KEYCODE_W: 51 57 | KEYCODE_X: 52 58 | KEYCODE_Y: 53 59 | KEYCODE_Z: 54 60 | KEYCODE_COMMA: 55 61 | KEYCODE_PERIOD: 56 62 | KEYCODE_ALT_LEFT: 57 63 | KEYCODE_ALT_RIGHT: 58 64 | KEYCODE_SHIFT_LEFT: 59 65 | KEYCODE_SHIFT_RIGHT: 60 66 | KEYCODE_TAB: 61 67 | KEYCODE_SPACE: 62 68 | KEYCODE_SYM: 63 69 | KEYCODE_EXPLORER: 64 70 | KEYCODE_ENVELOPE: 65 71 | KEYCODE_ENTER: 66 72 | KEYCODE_DEL: 67 73 | KEYCODE_GRAVE: 68 74 | KEYCODE_MINUS: 69 75 | KEYCODE_EQUALS: 70 76 | KEYCODE_LEFT_BRACKET: 71 77 | KEYCODE_RIGHT_BRACKET: 72 78 | KEYCODE_BACKSLASH: 73 79 | KEYCODE_SEMICOLON: 74 80 | KEYCODE_APOSTROPHE: 75 81 | KEYCODE_SLASH: 76 82 | KEYCODE_AT: 77 83 | KEYCODE_NUM: 78 84 | KEYCODE_HEADSETHOOK: 79 85 | KEYCODE_FOCUS: 80 86 | KEYCODE_PLUS: 81 87 | KEYCODE_MENU: 82 88 | KEYCODE_NOTIFICATION: 83 89 | KEYCODE_SEARCH: 84 90 | KEYCODE_MEDIA_PLAY_PAUSE: 85 91 | KEYCODE_MEDIA_STOP: 86 92 | KEYCODE_MEDIA_NEXT: 87 93 | KEYCODE_MEDIA_PREVIOUS: 88 94 | KEYCODE_MEDIA_REWIND: 89 95 | KEYCODE_MEDIA_FAST_FORWARD: 90 96 | KEYCODE_MUTE: 91 97 | KEYCODE_PAGE_UP: 92 98 | KEYCODE_PAGE_DOWN: 93 99 | KEYCODE_PICTSYMBOLS: 94 100 | KEYCODE_SWITCH_CHARSET: 95 101 | KEYCODE_BUTTON_A: 96 102 | KEYCODE_BUTTON_B: 97 103 | KEYCODE_BUTTON_C: 98 104 | KEYCODE_BUTTON_X: 99 105 | KEYCODE_BUTTON_Y: 100 106 | KEYCODE_BUTTON_Z: 101 107 | KEYCODE_BUTTON_L1: 102 108 | KEYCODE_BUTTON_R1: 103 109 | KEYCODE_BUTTON_L2: 104 110 | KEYCODE_BUTTON_R2: 105 111 | KEYCODE_BUTTON_THUMBL: 106 112 | KEYCODE_BUTTON_THUMBR: 107 113 | KEYCODE_BUTTON_START: 108 114 | KEYCODE_BUTTON_SELECT: 109 115 | KEYCODE_BUTTON_MODE: 110 116 | KEYCODE_ESCAPE: 111 117 | KEYCODE_FORWARD_DEL: 112 118 | KEYCODE_CTRL_LEFT: 113 119 | KEYCODE_CTRL_RIGHT: 114 120 | KEYCODE_CAPS_LOCK: 115 121 | KEYCODE_SCROLL_LOCK: 116 122 | KEYCODE_META_LEFT: 117 123 | KEYCODE_META_RIGHT: 118 124 | KEYCODE_FUNCTION: 119 125 | KEYCODE_SYSRQ: 120 126 | KEYCODE_BREAK: 121 127 | KEYCODE_MOVE_HOME: 122 128 | KEYCODE_MOVE_END: 123 129 | KEYCODE_INSERT: 124 130 | KEYCODE_FORWARD: 125 131 | KEYCODE_MEDIA_PLAY: 126 132 | KEYCODE_MEDIA_PAUSE: 127 133 | KEYCODE_MEDIA_CLOSE: 128 134 | KEYCODE_MEDIA_EJECT: 129 135 | KEYCODE_MEDIA_RECORD: 130 136 | KEYCODE_F1: 131 137 | KEYCODE_F2: 132 138 | KEYCODE_F3: 133 139 | KEYCODE_F4: 134 140 | KEYCODE_F5: 135 141 | KEYCODE_F6: 136 142 | KEYCODE_F7: 137 143 | KEYCODE_F8: 138 144 | KEYCODE_F9: 139 145 | KEYCODE_F10: 140 146 | KEYCODE_F11: 141 147 | KEYCODE_F12: 142 148 | KEYCODE_NUM_LOCK: 143 149 | KEYCODE_NUMPAD_0: 144 150 | KEYCODE_NUMPAD_1: 145 151 | KEYCODE_NUMPAD_2: 146 152 | KEYCODE_NUMPAD_3: 147 153 | KEYCODE_NUMPAD_4: 148 154 | KEYCODE_NUMPAD_5: 149 155 | KEYCODE_NUMPAD_6: 150 156 | KEYCODE_NUMPAD_7: 151 157 | KEYCODE_NUMPAD_8: 152 158 | KEYCODE_NUMPAD_9: 153 159 | KEYCODE_NUMPAD_DIVIDE: 154 160 | KEYCODE_NUMPAD_MULTIPLY: 155 161 | KEYCODE_NUMPAD_SUBTRACT: 156 162 | KEYCODE_NUMPAD_ADD: 157 163 | KEYCODE_NUMPAD_DOT: 158 164 | KEYCODE_NUMPAD_COMMA: 159 165 | KEYCODE_NUMPAD_ENTER: 160 166 | KEYCODE_NUMPAD_EQUALS: 161 167 | KEYCODE_NUMPAD_LEFT_PAREN: 162 168 | KEYCODE_NUMPAD_RIGHT_PAREN: 163 169 | KEYCODE_VOLUME_MUTE: 164 170 | KEYCODE_INFO: 165 171 | KEYCODE_CHANNEL_UP: 166 172 | KEYCODE_CHANNEL_DOWN: 167 173 | KEYCODE_ZOOM_IN: 168 174 | KEYCODE_ZOOM_OUT: 169 175 | KEYCODE_TV: 170 176 | KEYCODE_WINDOW: 171 177 | KEYCODE_GUIDE: 172 178 | KEYCODE_DVR: 173 179 | KEYCODE_BOOKMARK: 174 180 | KEYCODE_CAPTIONS: 175 181 | KEYCODE_SETTINGS: 176 182 | KEYCODE_TV_POWER: 177 183 | KEYCODE_TV_INPUT: 178 184 | KEYCODE_STB_POWER: 179 185 | KEYCODE_STB_INPUT: 180 186 | KEYCODE_AVR_POWER: 181 187 | KEYCODE_AVR_INPUT: 182 188 | KEYCODE_PROG_RED: 183 189 | KEYCODE_PROG_GREEN: 184 190 | KEYCODE_PROG_YELLOW: 185 191 | KEYCODE_PROG_BLUE: 186 192 | KEYCODE_APP_SWITCH: 187 193 | KEYCODE_BUTTON_1: 188 194 | KEYCODE_BUTTON_2: 189 195 | KEYCODE_BUTTON_3: 190 196 | KEYCODE_BUTTON_4: 191 197 | KEYCODE_BUTTON_5: 192 198 | KEYCODE_BUTTON_6: 193 199 | KEYCODE_BUTTON_7: 194 200 | KEYCODE_BUTTON_8: 195 201 | KEYCODE_BUTTON_9: 196 202 | KEYCODE_BUTTON_10: 197 203 | KEYCODE_BUTTON_11: 198 204 | KEYCODE_BUTTON_12: 199 205 | KEYCODE_BUTTON_13: 200 206 | KEYCODE_BUTTON_14: 201 207 | KEYCODE_BUTTON_15: 202 208 | KEYCODE_BUTTON_16: 203 209 | KEYCODE_LANGUAGE_SWITCH: 204 210 | KEYCODE_MANNER_MODE: 205 211 | KEYCODE_3D_MODE: 206 212 | KEYCODE_CONTACTS: 207 213 | KEYCODE_CALENDAR: 208 214 | KEYCODE_MUSIC: 209 215 | KEYCODE_CALCULATOR: 210 216 | KEYCODE_ZENKAKU_HANKAKU: 211 217 | KEYCODE_EISU: 212 218 | KEYCODE_MUHENKAN: 213 219 | KEYCODE_HENKAN: 214 220 | KEYCODE_KATAKANA_HIRAGANA: 215 221 | KEYCODE_YEN: 216 222 | KEYCODE_RO: 217 223 | KEYCODE_KANA: 218 224 | KEYCODE_ASSIST: 219 225 | KEYCODE_BRIGHTNESS_DOWN: 220 226 | KEYCODE_BRIGHTNESS_UP: 221 227 | KEYCODE_MEDIA_AUDIO_TRACK: 222 228 | -------------------------------------------------------------------------------- /src/adb/linetransform.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | 3 | class LineTransform extends Stream.Transform 4 | constructor: (options = {}) -> 5 | @savedR = null 6 | @autoDetect = options.autoDetect or false 7 | @transformNeeded = true 8 | @skipBytes = 0 9 | delete options.autoDetect 10 | super options 11 | 12 | _nullTransform: (chunk, encoding, done) -> 13 | this.push chunk 14 | done() 15 | return 16 | 17 | # Sadly, the ADB shell is not very smart. It automatically converts every 18 | # 0x0a ('\n') it can find to 0x0d 0x0a ('\r\n'). This also applies to binary 19 | # content. We could get rid of this behavior by setting `stty raw`, but 20 | # unfortunately it's not available by default (you'd have to install busybox) 21 | # or something similar. On the up side, it really does do this for all line 22 | # feeds, so a simple transform works fine. 23 | _transform: (chunk, encoding, done) -> 24 | # If auto detection is enabled, check the first byte. The first two 25 | # bytes must be either 0x0a .. or 0x0d 0x0a. This causes a need to skip 26 | # either one or two bytes. The autodetection runs only once. 27 | if @autoDetect 28 | if chunk[0] is 0x0a 29 | @transformNeeded = false 30 | @skipBytes = 1 31 | else 32 | @skipBytes = 2 33 | @autoDetect = false 34 | 35 | # It's technically possible that we may receive the first two bytes 36 | # in two separate chunks. That's why the autodetect bytes are skipped 37 | # here, separately. 38 | if @skipBytes 39 | skip = Math.min chunk.length, @skipBytes 40 | chunk = chunk.slice skip 41 | @skipBytes -= skip 42 | 43 | # It's possible that skipping bytes has created an empty chunk. 44 | return done() unless chunk.length 45 | 46 | # At this point all bytes that needed to be skipped should have been 47 | # skipped. If transform is not needed, shortcut to null transform. 48 | return this._nullTransform(chunk, encoding, done) unless @transformNeeded 49 | 50 | # Ok looks like we're transforming. 51 | lo = 0 52 | hi = 0 53 | if @savedR 54 | this.push @savedR unless chunk[0] is 0x0a 55 | @savedR = null 56 | last = chunk.length - 1 57 | while hi <= last 58 | if chunk[hi] is 0x0d 59 | if hi is last 60 | @savedR = chunk.slice last 61 | break # Stop hi from incrementing, we want to skip the last byte. 62 | else if chunk[hi + 1] is 0x0a 63 | this.push chunk.slice lo, hi 64 | lo = hi + 1 65 | hi += 1 66 | unless hi is lo 67 | this.push chunk.slice lo, hi 68 | done() 69 | return 70 | 71 | # When the stream ends on an '\r', output it as-is (assume binary data). 72 | _flush: (done) -> 73 | this.push @savedR if @savedR 74 | done() 75 | 76 | module.exports = LineTransform 77 | -------------------------------------------------------------------------------- /src/adb/parser.coffee: -------------------------------------------------------------------------------- 1 | Promise = require 'bluebird' 2 | 3 | Protocol = require './protocol' 4 | 5 | class Parser 6 | constructor: (@stream) -> 7 | @ended = false 8 | 9 | end: -> 10 | return Promise.resolve(true) if @ended 11 | 12 | resolver = Promise.defer() 13 | 14 | tryRead = => 15 | while @stream.read() 16 | continue 17 | return 18 | 19 | @stream.on 'readable', tryRead 20 | 21 | @stream.on 'error', errorListener = (err) -> 22 | resolver.reject err 23 | 24 | @stream.on 'end', endListener = => 25 | @ended = true 26 | resolver.resolve true 27 | 28 | @stream.read(0) 29 | @stream.end() 30 | 31 | resolver.promise.cancellable().finally => 32 | @stream.removeListener 'readable', tryRead 33 | @stream.removeListener 'error', errorListener 34 | @stream.removeListener 'end', endListener 35 | 36 | raw: -> 37 | @stream 38 | 39 | readAll: -> 40 | all = new Buffer 0 41 | resolver = Promise.defer() 42 | 43 | tryRead = => 44 | while chunk = @stream.read() 45 | all = Buffer.concat [all, chunk] 46 | resolver.resolve all if @ended 47 | 48 | @stream.on 'readable', tryRead 49 | 50 | @stream.on 'error', errorListener = (err) -> 51 | resolver.reject err 52 | 53 | @stream.on 'end', endListener = => 54 | @ended = true 55 | resolver.resolve all 56 | 57 | tryRead() 58 | 59 | resolver.promise.cancellable().finally => 60 | @stream.removeListener 'readable', tryRead 61 | @stream.removeListener 'error', errorListener 62 | @stream.removeListener 'end', endListener 63 | 64 | readAscii: (howMany) -> 65 | this.readBytes howMany 66 | .then (chunk) -> 67 | chunk.toString 'ascii' 68 | 69 | readBytes: (howMany) -> 70 | resolver = Promise.defer() 71 | 72 | tryRead = => 73 | if howMany 74 | if chunk = @stream.read howMany 75 | # If the stream ends while still having unread bytes, the read call 76 | # will ignore the limit and just return what it's got. 77 | howMany -= chunk.length 78 | return resolver.resolve chunk if howMany is 0 79 | resolver.reject new Parser.PrematureEOFError howMany if @ended 80 | else 81 | resolver.resolve new Buffer 0 82 | 83 | endListener = => 84 | @ended = true 85 | resolver.reject new Parser.PrematureEOFError howMany 86 | 87 | errorListener = (err) -> 88 | resolver.reject err 89 | 90 | @stream.on 'readable', tryRead 91 | @stream.on 'error', errorListener 92 | @stream.on 'end', endListener 93 | 94 | tryRead() 95 | 96 | resolver.promise.cancellable().finally => 97 | @stream.removeListener 'readable', tryRead 98 | @stream.removeListener 'error', errorListener 99 | @stream.removeListener 'end', endListener 100 | 101 | readByteFlow: (howMany, targetStream) -> 102 | resolver = Promise.defer() 103 | 104 | tryRead = => 105 | if howMany 106 | # Try to get the exact amount we need first. If unsuccessful, take 107 | # whatever is available, which will be less than the needed amount. 108 | while chunk = @stream.read(howMany) or @stream.read() 109 | howMany -= chunk.length 110 | targetStream.write chunk 111 | return resolver.resolve() if howMany is 0 112 | resolver.reject new Parser.PrematureEOFError howMany if @ended 113 | else 114 | resolver.resolve() 115 | 116 | endListener = => 117 | @ended = true 118 | resolver.reject new Parser.PrematureEOFError howMany 119 | 120 | errorListener = (err) -> 121 | resolver.reject err 122 | 123 | @stream.on 'readable', tryRead 124 | @stream.on 'error', errorListener 125 | @stream.on 'end', endListener 126 | 127 | tryRead() 128 | 129 | resolver.promise.cancellable().finally => 130 | @stream.removeListener 'readable', tryRead 131 | @stream.removeListener 'error', errorListener 132 | @stream.removeListener 'end', endListener 133 | 134 | readError: -> 135 | this.readValue() 136 | .then (value) -> 137 | Promise.reject new Parser.FailError value.toString() 138 | 139 | readValue: -> 140 | this.readAscii 4 141 | .then (value) => 142 | length = Protocol.decodeLength value 143 | this.readBytes length 144 | 145 | readUntil: (code) -> 146 | skipped = new Buffer 0 147 | read = => 148 | this.readBytes 1 149 | .then (chunk) -> 150 | if chunk[0] is code 151 | skipped 152 | else 153 | skipped = Buffer.concat [skipped, chunk] 154 | read() 155 | read() 156 | 157 | searchLine: (re) -> 158 | this.readLine() 159 | .then (line) => 160 | if match = re.exec line 161 | match 162 | else 163 | this.searchLine re 164 | 165 | readLine: -> 166 | this.readUntil 0x0a # '\n' 167 | .then (line) -> 168 | if line[line.length - 1] is 0x0d # '\r' 169 | line.slice 0, -1 170 | else 171 | line 172 | 173 | unexpected: (data, expected) -> 174 | Promise.reject new Parser.UnexpectedDataError data, expected 175 | 176 | class Parser.FailError extends Error 177 | constructor: (message) -> 178 | Error.call this 179 | this.name = 'FailError' 180 | this.message = "Failure: '#{message}'" 181 | Error.captureStackTrace this, Parser.FailError 182 | 183 | class Parser.PrematureEOFError extends Error 184 | constructor: (howManyMissing) -> 185 | Error.call this 186 | this.name = 'PrematureEOFError' 187 | this.message = "Premature end of stream, needed #{howManyMissing} 188 | more bytes" 189 | this.missingBytes = howManyMissing 190 | Error.captureStackTrace this, Parser.PrematureEOFError 191 | 192 | class Parser.UnexpectedDataError extends Error 193 | constructor: (unexpected, expected) -> 194 | Error.call this 195 | this.name = 'UnexpectedDataError' 196 | this.message = "Unexpected '#{unexpected}', was expecting #{expected}" 197 | this.unexpected = unexpected 198 | this.expected = expected 199 | Error.captureStackTrace this, Parser.UnexpectedDataError 200 | 201 | module.exports = Parser 202 | -------------------------------------------------------------------------------- /src/adb/proc/stat.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | split = require 'split' 3 | 4 | Parser = require '../parser' 5 | 6 | class ProcStat extends EventEmitter 7 | RE_CPULINE = /^cpu[0-9]+ .*$/mg 8 | RE_COLSEP = /\ +/g 9 | 10 | constructor: (@sync) -> 11 | @interval = 1000 12 | @stats = this._emptyStats() 13 | @_ignore = {} 14 | @_timer = setInterval => 15 | this.update() 16 | , @interval 17 | this.update() 18 | 19 | end: -> 20 | clearInterval @_timer 21 | @sync.end() 22 | @sync = null 23 | 24 | update: -> 25 | new Parser(@sync.pull '/proc/stat') 26 | .readAll() 27 | .then (out) => 28 | this._parse out 29 | .catch (err) => 30 | this._error err 31 | return 32 | 33 | _parse: (out) -> 34 | stats = this._emptyStats() 35 | while match = RE_CPULINE.exec out 36 | line = match[0] 37 | cols = line.split RE_COLSEP 38 | type = cols.shift() 39 | continue if @_ignore[type] is line 40 | total = 0 41 | total += +val for val in cols 42 | stats.cpus[type] = 43 | line: line 44 | user: +cols[0] or 0 45 | nice: +cols[1] or 0 46 | system: +cols[2] or 0 47 | idle: +cols[3] or 0 48 | iowait: +cols[4] or 0 49 | irq: +cols[5] or 0 50 | softirq: +cols[6] or 0 51 | steal: +cols[7] or 0 52 | guest: +cols[8] or 0 53 | guestnice: +cols[9] or 0 54 | total: total 55 | this._set stats 56 | 57 | _set: (stats) -> 58 | loads = {} 59 | found = false 60 | for id, cur of stats.cpus 61 | old = @stats.cpus[id] 62 | continue unless old 63 | ticks = cur.total - old.total 64 | if ticks > 0 65 | found = true 66 | # Calculate percentages for everything. For ease of formatting, 67 | # let's do `x / y * 100` as `100 / y * x`. 68 | m = 100 / ticks 69 | loads[id] = 70 | user: Math.floor m * (cur.user - old.user) 71 | nice: Math.floor m * (cur.nice - old.nice) 72 | system: Math.floor m * (cur.system - old.system) 73 | idle: Math.floor m * (cur.idle - old.idle) 74 | iowait: Math.floor m * (cur.iowait - old.iowait) 75 | irq: Math.floor m * (cur.irq - old.irq) 76 | softirq: Math.floor m * (cur.softirq - old.softirq) 77 | steal: Math.floor m * (cur.steal - old.steal) 78 | guest: Math.floor m * (cur.guest - old.guest) 79 | guestnice: Math.floor m * (cur.guestnice - old.guestnice) 80 | total: 100 81 | else 82 | # The CPU is either offline (nothing was done) or it mysteriously 83 | # warped back in time (idle stat dropped significantly), causing the 84 | # total tick count to be <0. The latter seems to only happen on 85 | # Galaxy S4 so far. Either way we don't want those anomalies in our 86 | # stats. We'll also ignore the line in the next cycle. This doesn't 87 | # completely eliminate the anomalies, but it helps. 88 | @_ignore[id] = cur.line 89 | delete stats.cpus[id] 90 | this.emit 'load', loads if found 91 | @stats = stats 92 | 93 | _error: (err) -> 94 | this.emit 'error', err 95 | 96 | _emptyStats: -> 97 | cpus: {} 98 | 99 | module.exports = ProcStat 100 | -------------------------------------------------------------------------------- /src/adb/protocol.coffee: -------------------------------------------------------------------------------- 1 | class Protocol 2 | @OKAY = 'OKAY' 3 | @FAIL = 'FAIL' 4 | @STAT = 'STAT' 5 | @LIST = 'LIST' 6 | @DENT = 'DENT' 7 | @RECV = 'RECV' 8 | @DATA = 'DATA' 9 | @DONE = 'DONE' 10 | @SEND = 'SEND' 11 | @QUIT = 'QUIT' 12 | 13 | @decodeLength: (length) -> 14 | parseInt length, 16 15 | 16 | @encodeLength: (length) -> 17 | ('0000' + length.toString 16).slice(-4).toUpperCase() 18 | 19 | @encodeData: (data) -> 20 | unless Buffer.isBuffer data 21 | data = new Buffer data 22 | Buffer.concat [new Buffer(Protocol.encodeLength data.length), data] 23 | 24 | module.exports = Protocol 25 | -------------------------------------------------------------------------------- /src/adb/sync.coffee: -------------------------------------------------------------------------------- 1 | Fs = require 'fs' 2 | Path = require 'path' 3 | Promise = require 'bluebird' 4 | {EventEmitter} = require 'events' 5 | debug = require('debug')('adb:sync') 6 | 7 | Parser = require './parser' 8 | Protocol = require './protocol' 9 | Stats = require './sync/stats' 10 | Entry = require './sync/entry' 11 | PushTransfer = require './sync/pushtransfer' 12 | PullTransfer = require './sync/pulltransfer' 13 | 14 | class Sync extends EventEmitter 15 | TEMP_PATH = '/data/local/tmp' 16 | DEFAULT_CHMOD = 0o644 17 | DATA_MAX_LENGTH = 65536 18 | 19 | @temp: (path) -> 20 | "#{TEMP_PATH}/#{Path.basename path}" 21 | 22 | constructor: (@connection) -> 23 | @parser = @connection.parser 24 | 25 | stat: (path, callback) -> 26 | this._sendCommandWithArg Protocol.STAT, path 27 | @parser.readAscii 4 28 | .then (reply) => 29 | switch reply 30 | when Protocol.STAT 31 | @parser.readBytes 12 32 | .then (stat) => 33 | mode = stat.readUInt32LE 0 34 | size = stat.readUInt32LE 4 35 | mtime = stat.readUInt32LE 8 36 | if mode is 0 37 | this._enoent path 38 | else 39 | new Stats mode, size, mtime 40 | when Protocol.FAIL 41 | this._readError() 42 | else 43 | @parser.unexpected reply, 'STAT or FAIL' 44 | .nodeify callback 45 | 46 | readdir: (path, callback) -> 47 | files = [] 48 | 49 | readNext = => 50 | @parser.readAscii 4 51 | .then (reply) => 52 | switch reply 53 | when Protocol.DENT 54 | @parser.readBytes 16 55 | .then (stat) => 56 | mode = stat.readUInt32LE 0 57 | size = stat.readUInt32LE 4 58 | mtime = stat.readUInt32LE 8 59 | namelen = stat.readUInt32LE 12 60 | @parser.readBytes namelen 61 | .then (name) -> 62 | name = name.toString() 63 | # Skip '.' and '..' to match Node's fs.readdir(). 64 | unless name is '.' or name is '..' 65 | files.push new Entry name, mode, size, mtime 66 | readNext() 67 | when Protocol.DONE 68 | @parser.readBytes 16 69 | .then (zero) -> 70 | files 71 | when Protocol.FAIL 72 | this._readError() 73 | else 74 | @parser.unexpected reply, 'DENT, DONE or FAIL' 75 | 76 | this._sendCommandWithArg Protocol.LIST, path 77 | 78 | readNext() 79 | .nodeify callback 80 | 81 | push: (contents, path, mode) -> 82 | if typeof contents is 'string' 83 | this.pushFile contents, path, mode 84 | else 85 | this.pushStream contents, path, mode 86 | 87 | pushFile: (file, path, mode = DEFAULT_CHMOD) -> 88 | mode or= DEFAULT_CHMOD 89 | this.pushStream Fs.createReadStream(file), path, mode 90 | 91 | pushStream: (stream, path, mode = DEFAULT_CHMOD) -> 92 | mode |= Stats.S_IFREG 93 | this._sendCommandWithArg Protocol.SEND, "#{path},#{mode}" 94 | this._writeData stream, Math.floor(Date.now() / 1000) 95 | 96 | pull: (path) -> 97 | this._sendCommandWithArg Protocol.RECV, "#{path}" 98 | this._readData() 99 | 100 | end: -> 101 | @connection.end() 102 | return this 103 | 104 | tempFile: (path) -> 105 | Sync.temp path 106 | 107 | _writeData: (stream, timeStamp) -> 108 | transfer = new PushTransfer 109 | 110 | writeData = => 111 | resolver = Promise.defer() 112 | writer = Promise.resolve() 113 | .cancellable() 114 | 115 | stream.on 'end', endListener = => 116 | writer.then => 117 | this._sendCommandWithLength Protocol.DONE, timeStamp 118 | resolver.resolve() 119 | 120 | waitForDrain = => 121 | resolver = Promise.defer() 122 | 123 | @connection.on 'drain', drainListener = -> 124 | resolver.resolve() 125 | 126 | resolver.promise.finally => 127 | @connection.removeListener 'drain', drainListener 128 | 129 | track = -> 130 | transfer.pop() 131 | 132 | writeNext = => 133 | if chunk = stream.read(DATA_MAX_LENGTH) or stream.read() 134 | this._sendCommandWithLength Protocol.DATA, chunk.length 135 | transfer.push chunk.length 136 | if @connection.write chunk, track 137 | writeNext() 138 | else 139 | waitForDrain() 140 | .then writeNext 141 | else 142 | Promise.resolve() 143 | 144 | stream.on 'readable', readableListener = -> 145 | writer.then writeNext 146 | 147 | stream.on 'error', errorListener = (err) -> 148 | resolver.reject err 149 | 150 | resolver.promise.finally -> 151 | stream.removeListener 'end', endListener 152 | stream.removeListener 'readable', readableListener 153 | stream.removeListener 'error', errorListener 154 | writer.cancel() 155 | 156 | readReply = => 157 | @parser.readAscii 4 158 | .then (reply) => 159 | switch reply 160 | when Protocol.OKAY 161 | @parser.readBytes 4 162 | .then (zero) -> 163 | true 164 | when Protocol.FAIL 165 | this._readError() 166 | else 167 | @parser.unexpected reply, 'OKAY or FAIL' 168 | 169 | # While I can't think of a case that would break this double-Promise 170 | # writer-reader arrangement right now, it's not immediately obvious 171 | # that the code is correct and it may or may not have some failing 172 | # edge cases. Refactor pending. 173 | 174 | writer = writeData() 175 | .cancellable() 176 | .catch Promise.CancellationError, (err) => 177 | @connection.end() 178 | .catch (err) -> 179 | transfer.emit 'error', err 180 | reader.cancel() 181 | 182 | reader = readReply() 183 | .cancellable() 184 | .catch Promise.CancellationError, (err) -> 185 | true 186 | .catch (err) -> 187 | transfer.emit 'error', err 188 | writer.cancel() 189 | .finally -> 190 | transfer.end() 191 | 192 | transfer.on 'cancel', -> 193 | writer.cancel() 194 | reader.cancel() 195 | 196 | return transfer 197 | 198 | _readData: -> 199 | transfer = new PullTransfer 200 | 201 | readNext = => 202 | @parser.readAscii 4 203 | .cancellable() 204 | .then (reply) => 205 | switch reply 206 | when Protocol.DATA 207 | @parser.readBytes 4 208 | .then (lengthData) => 209 | length = lengthData.readUInt32LE 0 210 | @parser.readByteFlow(length, transfer) 211 | .then readNext 212 | when Protocol.DONE 213 | @parser.readBytes 4 214 | .then (zero) -> 215 | true 216 | when Protocol.FAIL 217 | this._readError() 218 | else 219 | @parser.unexpected reply, 'DATA, DONE or FAIL' 220 | 221 | reader = readNext() 222 | .catch Promise.CancellationError, (err) => 223 | @connection.end() 224 | .catch (err) -> 225 | transfer.emit 'error', err 226 | .finally -> 227 | transfer.removeListener 'cancel', cancelListener 228 | transfer.end() 229 | 230 | transfer.on 'cancel', cancelListener = -> 231 | reader.cancel() 232 | 233 | return transfer 234 | 235 | _readError: -> 236 | @parser.readBytes 4 237 | .then (length) => 238 | @parser.readBytes length.readUInt32LE(0) 239 | .then (buf) -> 240 | Promise.reject new Parser.FailError buf.toString() 241 | .finally => 242 | @parser.end() 243 | 244 | _sendCommandWithLength: (cmd, length) -> 245 | debug cmd unless cmd is Protocol.DATA 246 | payload = new Buffer cmd.length + 4 247 | payload.write cmd, 0, cmd.length 248 | payload.writeUInt32LE length, cmd.length 249 | @connection.write payload 250 | 251 | _sendCommandWithArg: (cmd, arg) -> 252 | debug "#{cmd} #{arg}" 253 | arglen = Buffer.byteLength arg, 'utf-8' 254 | payload = new Buffer cmd.length + 4 + arglen 255 | pos = 0 256 | payload.write cmd, pos, cmd.length 257 | pos += cmd.length 258 | payload.writeUInt32LE arglen, pos 259 | pos += 4 260 | payload.write arg, pos 261 | @connection.write payload 262 | 263 | _enoent: (path) -> 264 | err = new Error "ENOENT, no such file or directory '#{path}'" 265 | err.errno = 34 266 | err.code = 'ENOENT' 267 | err.path = path 268 | Promise.reject err 269 | 270 | module.exports = Sync 271 | -------------------------------------------------------------------------------- /src/adb/sync/entry.coffee: -------------------------------------------------------------------------------- 1 | Stats = require './stats' 2 | 3 | class Entry extends Stats 4 | constructor: (@name, mode, size, mtime) -> 5 | super mode, size, mtime 6 | 7 | toString: -> 8 | @name 9 | 10 | module.exports = Entry 11 | -------------------------------------------------------------------------------- /src/adb/sync/pulltransfer.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | 3 | class PullTransfer extends Stream.PassThrough 4 | constructor: -> 5 | @stats = 6 | bytesTransferred: 0 7 | super() 8 | 9 | cancel: -> 10 | this.emit 'cancel' 11 | 12 | write: (chunk, encoding, callback) -> 13 | @stats.bytesTransferred += chunk.length 14 | this.emit 'progress', @stats 15 | super chunk, encoding, callback 16 | 17 | module.exports = PullTransfer 18 | -------------------------------------------------------------------------------- /src/adb/sync/pushtransfer.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | 3 | class PushTransfer extends EventEmitter 4 | constructor: -> 5 | @_stack = [] 6 | @stats = 7 | bytesTransferred: 0 8 | 9 | cancel: -> 10 | this.emit 'cancel' 11 | 12 | push: (byteCount) -> 13 | @_stack.push byteCount 14 | 15 | pop: -> 16 | byteCount = @_stack.pop() 17 | @stats.bytesTransferred += byteCount 18 | this.emit 'progress', @stats 19 | 20 | end: -> 21 | this.emit 'end' 22 | 23 | module.exports = PushTransfer 24 | -------------------------------------------------------------------------------- /src/adb/sync/stats.coffee: -------------------------------------------------------------------------------- 1 | Fs = require 'fs' 2 | 3 | class Stats extends Fs.Stats 4 | # The following constant were extracted from `man 2 stat` on Ubuntu 12.10. 5 | @S_IFMT = 0o170000 # bit mask for the file type bit fields 6 | @S_IFSOCK = 0o140000 # socket 7 | @S_IFLNK = 0o120000 # symbolic link 8 | @S_IFREG = 0o100000 # regular file 9 | @S_IFBLK = 0o060000 # block device 10 | @S_IFDIR = 0o040000 # directory 11 | @S_IFCHR = 0o020000 # character device 12 | @S_IFIFO = 0o010000 # FIFO 13 | @S_ISUID = 0o004000 # set UID bit 14 | @S_ISGID = 0o002000 # set-group-ID bit (see below) 15 | @S_ISVTX = 0o001000 # sticky bit (see below) 16 | @S_IRWXU = 0o0700 # mask for file owner permissions 17 | @S_IRUSR = 0o0400 # owner has read permission 18 | @S_IWUSR = 0o0200 # owner has write permission 19 | @S_IXUSR = 0o0100 # owner has execute permission 20 | @S_IRWXG = 0o0070 # mask for group permissions 21 | @S_IRGRP = 0o0040 # group has read permission 22 | 23 | constructor: (@mode, @size, mtime) -> 24 | @mtime = new Date mtime * 1000 25 | 26 | module.exports = Stats 27 | -------------------------------------------------------------------------------- /src/adb/tcpusb/packet.coffee: -------------------------------------------------------------------------------- 1 | class Packet 2 | @A_SYNC = 0x434e5953 3 | @A_CNXN = 0x4e584e43 4 | @A_OPEN = 0x4e45504f 5 | @A_OKAY = 0x59414b4f 6 | @A_CLSE = 0x45534c43 7 | @A_WRTE = 0x45545257 8 | @A_AUTH = 0x48545541 9 | 10 | @checksum: (data) -> 11 | sum = 0 12 | sum += char for char in data if data 13 | return sum 14 | 15 | @magic: (command) -> 16 | # We need the full uint32 range, which ">>> 0" thankfully allows us to use 17 | (command ^ 0xffffffff) >>> 0 18 | 19 | @assemble: (command, arg0, arg1, data) -> 20 | if data 21 | chunk = new Buffer 24 + data.length 22 | chunk.writeUInt32LE command, 0 23 | chunk.writeUInt32LE arg0, 4 24 | chunk.writeUInt32LE arg1, 8 25 | chunk.writeUInt32LE data.length, 12 26 | chunk.writeUInt32LE Packet.checksum(data), 16 27 | chunk.writeUInt32LE Packet.magic(command), 20 28 | data.copy chunk, 24 29 | chunk 30 | else 31 | chunk = new Buffer 24 32 | chunk.writeUInt32LE command, 0 33 | chunk.writeUInt32LE arg0, 4 34 | chunk.writeUInt32LE arg1, 8 35 | chunk.writeUInt32LE 0, 12 36 | chunk.writeUInt32LE 0, 16 37 | chunk.writeUInt32LE Packet.magic(command), 20 38 | chunk 39 | 40 | @swap32: (n) -> 41 | buffer = new Buffer(4) 42 | buffer.writeUInt32LE(n, 0) 43 | buffer.readUInt32BE(0) 44 | 45 | constructor: (@command, @arg0, @arg1, @length, @check, @magic, @data) -> 46 | 47 | verifyChecksum: -> 48 | @check is Packet.checksum @data 49 | 50 | verifyMagic: -> 51 | @magic is Packet.magic @command 52 | 53 | toString: -> 54 | type = switch @command 55 | when Packet.A_SYNC 56 | "SYNC" 57 | when Packet.A_CNXN 58 | "CNXN" 59 | when Packet.A_OPEN 60 | "OPEN" 61 | when Packet.A_OKAY 62 | "OKAY" 63 | when Packet.A_CLSE 64 | "CLSE" 65 | when Packet.A_WRTE 66 | "WRTE" 67 | when Packet.A_AUTH 68 | "AUTH" 69 | else 70 | throw new Error "Unknown command {@command}" 71 | "#{type} arg0=#{@arg0} arg1=#{@arg1} length=#{@length}" 72 | 73 | module.exports = Packet 74 | -------------------------------------------------------------------------------- /src/adb/tcpusb/packetreader.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | 3 | Packet = require './packet' 4 | 5 | class PacketReader extends EventEmitter 6 | constructor: (@stream) -> 7 | super() 8 | @inBody = false 9 | @buffer = null 10 | @packet = null 11 | @stream.on 'readable', this._tryRead.bind(this) 12 | @stream.on 'error', (err) => this.emit 'error', err 13 | @stream.on 'end', => this.emit 'end' 14 | setImmediate this._tryRead.bind(this) 15 | 16 | _tryRead: -> 17 | while this._appendChunk() 18 | while @buffer 19 | if @inBody 20 | break unless @buffer.length >= @packet.length 21 | @packet.data = this._consume(@packet.length) 22 | unless @packet.verifyChecksum() 23 | this.emit 'error', new PacketReader.ChecksumError(@packet) 24 | return 25 | this.emit 'packet', @packet 26 | @inBody = false 27 | else 28 | break unless @buffer.length >= 24 29 | header = this._consume(24) 30 | @packet = new Packet( 31 | header.readUInt32LE 0 32 | header.readUInt32LE 4 33 | header.readUInt32LE 8 34 | header.readUInt32LE 12 35 | header.readUInt32LE 16 36 | header.readUInt32LE 20 37 | new Buffer(0) 38 | ) 39 | unless @packet.verifyMagic() 40 | this.emit 'error', new PacketReader.MagicError(@packet) 41 | return 42 | if @packet.length is 0 43 | this.emit 'packet', @packet 44 | else 45 | @inBody = true 46 | 47 | _appendChunk: -> 48 | if chunk = @stream.read() 49 | if @buffer 50 | @buffer = Buffer.concat([@buffer, chunk], @buffer.length + chunk.length) 51 | else 52 | @buffer = chunk 53 | else 54 | null 55 | 56 | _consume: (length) -> 57 | chunk = @buffer.slice(0, length) 58 | @buffer = if length is @buffer.length then null else @buffer.slice(length) 59 | chunk 60 | 61 | class PacketReader.ChecksumError extends Error 62 | constructor: (@packet) -> 63 | Error.call this 64 | @name = 'ChecksumError' 65 | @message = "Checksum mismatch" 66 | Error.captureStackTrace this, PacketReader.ChecksumError 67 | 68 | class PacketReader.MagicError extends Error 69 | constructor: (@packet) -> 70 | Error.call this 71 | @name = 'MagicError' 72 | @message = "Magic value mismatch" 73 | Error.captureStackTrace this, PacketReader.MagicError 74 | 75 | module.exports = PacketReader 76 | -------------------------------------------------------------------------------- /src/adb/tcpusb/rollingcounter.coffee: -------------------------------------------------------------------------------- 1 | class RollingCounter 2 | constructor: (@max, @min = 1) -> 3 | @now = @min 4 | 5 | next: -> 6 | @now = @min unless @now < @max 7 | return ++@now 8 | 9 | module.exports = RollingCounter 10 | -------------------------------------------------------------------------------- /src/adb/tcpusb/server.coffee: -------------------------------------------------------------------------------- 1 | Net = require 'net' 2 | {EventEmitter} = require 'events' 3 | 4 | Socket = require './socket' 5 | 6 | class Server extends EventEmitter 7 | constructor: (@client, @serial, @options) -> 8 | @connections = [] 9 | @server = Net.createServer allowHalfOpen: true 10 | @server.on 'error', (err) => 11 | this.emit 'error', err 12 | @server.on 'listening', => 13 | this.emit 'listening' 14 | @server.on 'close', => 15 | this.emit 'close' 16 | @server.on 'connection', (conn) => 17 | socket = new Socket @client, @serial, conn, @options 18 | @connections.push socket 19 | socket.on 'error', (err) => 20 | # 'conn' is guaranteed to get ended 21 | this.emit 'error', err 22 | socket.once 'end', => 23 | # 'conn' is guaranteed to get ended 24 | @connections = @connections.filter (val) -> val isnt socket 25 | this.emit 'connection', socket 26 | 27 | listen: -> 28 | @server.listen.apply @server, arguments 29 | return this 30 | 31 | close: -> 32 | @server.close() 33 | return this 34 | 35 | end: -> 36 | conn.end() for conn in @connections 37 | return this 38 | 39 | module.exports = Server 40 | -------------------------------------------------------------------------------- /src/adb/tcpusb/service.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | 3 | Promise = require 'bluebird' 4 | debug = require('debug')('adb:tcpusb:service') 5 | 6 | Parser = require '../parser' 7 | Protocol = require '../protocol' 8 | Packet = require './packet' 9 | 10 | class Service extends EventEmitter 11 | constructor: (@client, @serial, @localId, @remoteId, @socket) -> 12 | super() 13 | @opened = false 14 | @ended = false 15 | @transport = null 16 | @needAck = false 17 | 18 | end: -> 19 | @transport.end() if @transport 20 | return this if @ended 21 | debug 'O:A_CLSE' 22 | localId = if @opened then @localId else 0 # Zero can only mean a failed open 23 | try 24 | # We may or may not have gotten here due to @socket ending, so write 25 | # may fail. 26 | @socket.write Packet.assemble(Packet.A_CLSE, localId, @remoteId, null) 27 | catch err 28 | # Let it go 29 | @transport = null 30 | @ended = true 31 | this.emit 'end' 32 | return this 33 | 34 | handle: (packet) -> 35 | Promise.try => 36 | switch packet.command 37 | when Packet.A_OPEN 38 | this._handleOpenPacket(packet) 39 | when Packet.A_OKAY 40 | this._handleOkayPacket(packet) 41 | when Packet.A_WRTE 42 | this._handleWritePacket(packet) 43 | when Packet.A_CLSE 44 | this._handleClosePacket(packet) 45 | else 46 | throw new Error "Unexpected packet #{packet.command}" 47 | .catch (err) => 48 | this.emit 'error', err 49 | this.end() 50 | 51 | _handleOpenPacket: (packet) -> 52 | debug 'I:A_OPEN', packet 53 | @client.transport @serial 54 | .then (@transport) => 55 | throw new LateTransportError() if @ended 56 | @transport.write Protocol.encodeData \ 57 | packet.data.slice(0, -1) # Discard null byte at end 58 | @transport.parser.readAscii 4 59 | .then (reply) => 60 | switch reply 61 | when Protocol.OKAY 62 | debug 'O:A_OKAY' 63 | @socket.write \ 64 | Packet.assemble(Packet.A_OKAY, @localId, @remoteId, null) 65 | @opened = true 66 | when Protocol.FAIL 67 | @transport.parser.readError() 68 | else 69 | @transport.parser.unexpected reply, 'OKAY or FAIL' 70 | .then => 71 | new Promise (resolve, reject) => 72 | @transport.socket 73 | .on 'readable', => this._tryPush() 74 | .on 'end', resolve 75 | .on 'error', reject 76 | this._tryPush() 77 | .finally => 78 | this.end() 79 | 80 | _handleOkayPacket: (packet) -> 81 | debug 'I:A_OKAY', packet 82 | return if @ended 83 | throw new Service.PrematurePacketError(packet) unless @transport 84 | @needAck = false 85 | this._tryPush() 86 | 87 | _handleWritePacket: (packet) -> 88 | debug 'I:A_WRTE', packet 89 | return if @ended 90 | throw new Service.PrematurePacketError(packet) unless @transport 91 | @transport.write packet.data if packet.data 92 | debug 'O:A_OKAY' 93 | @socket.write Packet.assemble(Packet.A_OKAY, @localId, @remoteId, null) 94 | 95 | _handleClosePacket: (packet) -> 96 | debug 'I:A_CLSE', packet 97 | return if @ended 98 | throw new Service.PrematurePacketError(packet) unless @transport 99 | this.end() 100 | 101 | _tryPush: -> 102 | return if @needAck or @ended 103 | if chunk = this._readChunk(@transport.socket) 104 | debug 'O:A_WRTE' 105 | @socket.write Packet.assemble(Packet.A_WRTE, @localId, @remoteId, chunk) 106 | @needAck = true 107 | 108 | _readChunk: (stream) -> 109 | stream.read(@socket.maxPayload) or stream.read() 110 | 111 | class Service.PrematurePacketError extends Error 112 | constructor: (@packet) -> 113 | Error.call this 114 | @name = 'PrematurePacketError' 115 | @message = "Premature packet" 116 | Error.captureStackTrace this, Service.PrematurePacketError 117 | 118 | class Service.LateTransportError extends Error 119 | constructor: -> 120 | Error.call this 121 | @name = 'LateTransportError' 122 | @message = "Late transport" 123 | Error.captureStackTrace this, Service.LateTransportError 124 | 125 | module.exports = Service 126 | -------------------------------------------------------------------------------- /src/adb/tcpusb/servicemap.coffee: -------------------------------------------------------------------------------- 1 | class ServiceMap 2 | constructor: -> 3 | @remotes = Object.create null 4 | @count = 0 5 | 6 | end: -> 7 | for remoteId, remote of @remotes 8 | remote.end() 9 | @remotes = Object.create null 10 | @count = 0 11 | return 12 | 13 | insert: (remoteId, socket) -> 14 | if @remotes[remoteId] 15 | throw new Error "Remote ID #{remoteId} is already being used" 16 | else 17 | @count += 1 18 | @remotes[remoteId] = socket 19 | 20 | get: (remoteId) -> 21 | @remotes[remoteId] or null 22 | 23 | remove: (remoteId) -> 24 | if remote = @remotes[remoteId] 25 | delete @remotes[remoteId] 26 | @count -= 1 27 | remote 28 | else 29 | null 30 | 31 | module.exports = ServiceMap 32 | -------------------------------------------------------------------------------- /src/adb/tcpusb/socket.coffee: -------------------------------------------------------------------------------- 1 | crypto = require 'crypto' 2 | {EventEmitter} = require 'events' 3 | 4 | Promise = require 'bluebird' 5 | Forge = require 'node-forge' 6 | debug = require('debug')('adb:tcpusb:socket') 7 | 8 | Parser = require '../parser' 9 | Protocol = require '../protocol' 10 | Auth = require '../auth' 11 | Packet = require './packet' 12 | PacketReader = require './packetreader' 13 | Service = require './service' 14 | ServiceMap = require './servicemap' 15 | RollingCounter = require './rollingcounter' 16 | 17 | class Socket extends EventEmitter 18 | UINT32_MAX = 0xFFFFFFFF 19 | UINT16_MAX = 0xFFFF 20 | 21 | AUTH_TOKEN = 1 22 | AUTH_SIGNATURE = 2 23 | AUTH_RSAPUBLICKEY = 3 24 | 25 | TOKEN_LENGTH = 20 26 | 27 | constructor: (@client, @serial, @socket, @options = {}) -> 28 | @options.auth or= Promise.resolve true 29 | @ended = false 30 | @socket.setNoDelay true 31 | @reader = new PacketReader @socket 32 | .on 'packet', this._handle.bind(this) 33 | .on 'error', (err) => 34 | debug "PacketReader error: #{err.message}" 35 | this.end() 36 | .on 'end', this.end.bind(this) 37 | @version = 1 38 | @maxPayload = 4096 39 | @authorized = false 40 | @syncToken = new RollingCounter UINT32_MAX 41 | @remoteId = new RollingCounter UINT32_MAX 42 | @services = new ServiceMap 43 | @remoteAddress = @socket.remoteAddress 44 | @token = null 45 | @signature = null 46 | 47 | end: -> 48 | return this if @ended 49 | # End services first so that they can send a final payload before FIN. 50 | @services.end() 51 | @socket.end() 52 | @ended = true 53 | this.emit 'end' 54 | return this 55 | 56 | _error: (err) -> 57 | this.emit 'error', err 58 | this.end() 59 | 60 | _handle: (packet) -> 61 | return if @ended 62 | this.emit 'userActivity', packet 63 | Promise.try => 64 | switch packet.command 65 | when Packet.A_SYNC 66 | this._handleSyncPacket packet 67 | when Packet.A_CNXN 68 | this._handleConnectionPacket packet 69 | when Packet.A_OPEN 70 | this._handleOpenPacket packet 71 | when Packet.A_OKAY, Packet.A_WRTE, Packet.A_CLSE 72 | this._forwardServicePacket packet 73 | when Packet.A_AUTH 74 | this._handleAuthPacket packet 75 | else 76 | throw new Error "Unknown command #{packet.command}" 77 | .catch Socket.AuthError, => 78 | this.end() 79 | .catch Socket.UnauthorizedError, => 80 | this.end() 81 | .catch (err) => 82 | this._error err 83 | 84 | _handleSyncPacket: (packet) -> 85 | # No need to do anything? 86 | debug 'I:A_SYNC' 87 | debug 'O:A_SYNC' 88 | this.write Packet.assemble(Packet.A_SYNC, 1, @syncToken.next(), null) 89 | 90 | _handleConnectionPacket: (packet) -> 91 | debug 'I:A_CNXN', packet 92 | version = Packet.swap32(packet.arg0) 93 | @maxPayload = Math.min UINT16_MAX, packet.arg1 94 | this._createToken() 95 | .then (@token) => 96 | debug "Created challenge '#{@token.toString('base64')}'" 97 | debug 'O:A_AUTH' 98 | this.write Packet.assemble(Packet.A_AUTH, AUTH_TOKEN, 0, @token) 99 | 100 | _handleAuthPacket: (packet) -> 101 | debug 'I:A_AUTH', packet 102 | switch packet.arg0 103 | when AUTH_SIGNATURE 104 | # Store first signature, ignore the rest 105 | debug "Received signature '#{packet.data.toString('base64')}'" 106 | @signature = packet.data unless @signature 107 | debug 'O:A_AUTH' 108 | this.write Packet.assemble(Packet.A_AUTH, AUTH_TOKEN, 0, @token) 109 | when AUTH_RSAPUBLICKEY 110 | unless @signature 111 | throw new Socket.AuthError "Public key sent before signature" 112 | unless packet.data and packet.data.length >= 2 113 | throw new Socket.AuthError "Empty RSA public key" 114 | debug "Received RSA public key '#{packet.data.toString('base64')}'" 115 | Auth.parsePublicKey this._skipNull(packet.data) 116 | .then (key) => 117 | digest = @token.toString 'binary' 118 | sig = @signature.toString 'binary' 119 | unless key.verify digest, sig 120 | debug "Signature mismatch" 121 | throw new Socket.AuthError "Signature mismatch" 122 | debug "Signature verified" 123 | key 124 | .then (key) => 125 | @options.auth key 126 | .catch (err) -> 127 | debug "Connection rejected by user-defined auth handler" 128 | throw new Socket.AuthError "Rejected by user-defined handler" 129 | .then => 130 | this._deviceId() 131 | .then (id) => 132 | @authorized = true 133 | debug 'O:A_CNXN' 134 | this.write Packet.assemble(Packet.A_CNXN, 135 | Packet.swap32(@version), @maxPayload, id) 136 | else 137 | throw new Error "Unknown authentication method #{packet.arg0}" 138 | 139 | _handleOpenPacket: (packet) -> 140 | throw new Socket.UnauthorizedError() unless @authorized 141 | remoteId = packet.arg0 142 | localId = @remoteId.next() 143 | unless packet.data and packet.data.length >= 2 144 | throw new Error "Empty service name" 145 | name = this._skipNull(packet.data) 146 | debug "Calling #{name}" 147 | service = new Service @client, @serial, localId, remoteId, this 148 | new Promise (resolve, reject) => 149 | service.on 'error', reject 150 | service.on 'end', resolve 151 | @services.insert localId, service 152 | debug "Handling #{@services.count} services simultaneously" 153 | service.handle packet 154 | .catch (err) -> 155 | true 156 | .finally => 157 | @services.remove localId 158 | debug "Handling #{@services.count} services simultaneously" 159 | service.end() 160 | 161 | _forwardServicePacket: (packet) -> 162 | throw new Socket.UnauthorizedError() unless @authorized 163 | remoteId = packet.arg0 164 | localId = packet.arg1 165 | if service = @services.get localId 166 | service.handle packet 167 | else 168 | debug "Received a packet to a service that may have been closed already" 169 | 170 | write: (chunk) -> 171 | return if @ended 172 | @socket.write chunk 173 | 174 | _createToken: -> 175 | Promise.promisify(crypto.randomBytes)(TOKEN_LENGTH) 176 | 177 | _skipNull: (data) -> 178 | data.slice 0, -1 # Discard null byte at end 179 | 180 | _deviceId: -> 181 | debug "Loading device properties to form a standard device ID" 182 | @client.getProperties @serial 183 | .then (properties) -> 184 | id = ("#{prop}=#{properties[prop]};" for prop in [ 185 | 'ro.product.name' 186 | 'ro.product.model' 187 | 'ro.product.device' 188 | ]).join('') 189 | new Buffer "device::#{id}\0" 190 | 191 | class Socket.AuthError extends Error 192 | constructor: (@message) -> 193 | Error.call this 194 | @name = 'AuthError' 195 | Error.captureStackTrace this, Socket.AuthError 196 | 197 | class Socket.UnauthorizedError extends Error 198 | constructor: -> 199 | Error.call this 200 | @name = 'UnauthorizedError' 201 | @message = "Unauthorized access" 202 | Error.captureStackTrace this, Socket.UnauthorizedError 203 | 204 | module.exports = Socket 205 | -------------------------------------------------------------------------------- /src/adb/tracker.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | Promise = require 'bluebird' 3 | 4 | Parser = require './parser' 5 | 6 | class Tracker extends EventEmitter 7 | constructor: (@command) -> 8 | @deviceList = [] 9 | @deviceMap = {} 10 | @reader = this.read() 11 | .catch Promise.CancellationError, -> 12 | true 13 | .catch Parser.PrematureEOFError, -> 14 | throw new Error 'Connection closed' 15 | .catch (err) => 16 | this.emit 'error', err 17 | return 18 | .finally => 19 | @command.parser.end() 20 | .then => 21 | this.emit 'end' 22 | 23 | read: -> 24 | @command._readDevices() 25 | .cancellable() 26 | .then (list) => 27 | this.update list 28 | this.read() 29 | 30 | update: (newList) -> 31 | changeSet = 32 | removed: [] 33 | changed: [] 34 | added: [] 35 | newMap = {} 36 | for device in newList 37 | oldDevice = @deviceMap[device.id] 38 | if oldDevice 39 | unless oldDevice.type is device.type 40 | changeSet.changed.push device 41 | this.emit 'change', device, oldDevice 42 | else 43 | changeSet.added.push device 44 | this.emit 'add', device 45 | newMap[device.id] = device 46 | for device in @deviceList 47 | unless newMap[device.id] 48 | changeSet.removed.push device 49 | this.emit 'remove', device 50 | this.emit 'changeSet', changeSet 51 | @deviceList = newList 52 | @deviceMap = newMap 53 | return this 54 | 55 | end: -> 56 | @reader.cancel() 57 | return this 58 | 59 | module.exports = Tracker 60 | -------------------------------------------------------------------------------- /src/adb/util.coffee: -------------------------------------------------------------------------------- 1 | Parser = require './parser' 2 | Auth = require './auth' 3 | 4 | module.exports.readAll = (stream, callback) -> 5 | new Parser(stream).readAll stream 6 | .nodeify callback 7 | 8 | module.exports.parsePublicKey = (keyString, callback) -> 9 | Auth.parsePublicKey keyString 10 | .nodeify callback 11 | -------------------------------------------------------------------------------- /src/cli.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | program = require 'commander' 3 | Promise = require 'bluebird' 4 | forge = require 'node-forge' 5 | 6 | pkg = require '../package' 7 | Adb = require './adb' 8 | Auth = require './adb/auth' 9 | PacketReader = require './adb/tcpusb/packetreader' 10 | 11 | Promise.longStackTraces() 12 | 13 | program 14 | .version pkg.version 15 | 16 | program 17 | .command 'pubkey-convert ' 18 | .option '-f, --format ', 'format (pem or openssh)', String, 'pem' 19 | .description 'Converts an ADB-generated public key into PEM format.' 20 | .action (file, options) -> 21 | Auth.parsePublicKey fs.readFileSync file 22 | .then (key) -> 23 | switch options.format.toLowerCase() 24 | when 'pem' 25 | console.log forge.pki.publicKeyToPem(key).trim() 26 | when 'openssh' 27 | console.log forge.ssh.publicKeyToOpenSSH(key, 'adbkey').trim() 28 | else 29 | console.error "Unsupported format '#{options.format}'" 30 | process.exit 1 31 | 32 | program 33 | .command 'pubkey-fingerprint ' 34 | .description 'Outputs the fingerprint of an ADB-generated public key.' 35 | .action (file) -> 36 | Auth.parsePublicKey fs.readFileSync file 37 | .then (key) -> 38 | console.log '%s %s', key.fingerprint, key.comment 39 | 40 | program 41 | .command 'usb-device-to-tcp ' 42 | .option '-p, --port ', 'port number', String, 6174 43 | .description 'Provides an USB device over TCP using a translating proxy.' 44 | .action (serial, options) -> 45 | adb = Adb.createClient() 46 | server = adb.createTcpUsbBridge(serial, auth: -> Promise.resolve()) 47 | .on 'listening', -> 48 | console.info 'Connect with `adb connect localhost:%d`', options.port 49 | .on 'error', (err) -> 50 | console.error "An error occured: #{err.message}" 51 | server.listen options.port 52 | 53 | program 54 | .command 'parse-tcp-packets ' 55 | .description 'Parses ADB TCP packets from the given file.' 56 | .action (file, options) -> 57 | reader = new PacketReader fs.createReadStream(file) 58 | reader.on 'packet', (packet) -> 59 | console.log packet.toString() 60 | 61 | program.parse process.argv 62 | -------------------------------------------------------------------------------- /tasks/keycode.coffee: -------------------------------------------------------------------------------- 1 | Https = require 'https' 2 | 3 | module.exports = (grunt) -> 4 | 5 | grunt.registerMultiTask 'keycode', 'Updates KeyEvent mapping.', -> 6 | 7 | repo_path = '/android/platform_frameworks_base/master' 8 | done = this.async() 9 | options = this.options 10 | original: 11 | hostname: 'raw.github.com' 12 | path: "#{repo_path}/core/java/android/view/KeyEvent.java" 13 | method: 'GET' 14 | regex: /public static final int (KEYCODE_[^\s]+)\s*=\s*([0-9]+);/g 15 | 16 | grunt.util.async.forEach this.files, (file, next) -> 17 | req = Https.request options.original, (res) -> 18 | unless res.statusCode is 200 19 | grunt.fail.warn \ 20 | "Unable to retrieve KeyEvent.java (HTTP #{res.statusCode})" 21 | return next() 22 | 23 | raw = new Buffer '' 24 | 25 | res.on 'data', (chunk) -> 26 | raw = Buffer.concat [raw, chunk] 27 | 28 | res.on 'end', -> 29 | code = raw.toString() 30 | date = new Date().toUTCString() 31 | coffee = [] 32 | coffee.push "# Generated by `grunt keycode` on #{date}" 33 | coffee.push \ 34 | "# KeyEvent.java Copyright (C) 2006 The Android Open Source Project" 35 | coffee.push '' 36 | coffee.push 'module.exports =' 37 | 38 | while match = options.regex.exec code 39 | coffee.push " #{match[1]}: #{match[2]}" 40 | 41 | coffee.push '' 42 | 43 | grunt.file.write file.dest, coffee.join '\n' 44 | grunt.log.ok "File #{file.dest} created" 45 | 46 | next() 47 | 48 | req.on 'error', next 49 | 50 | req.end() 51 | 52 | , done 53 | -------------------------------------------------------------------------------- /test/adb.coffee: -------------------------------------------------------------------------------- 1 | {expect} = require 'chai' 2 | 3 | Adb = require '../src/adb' 4 | Client = require '../src/adb/client' 5 | Keycode = require '../src/adb/keycode' 6 | util = require '../src/adb/util' 7 | 8 | describe 'Adb', -> 9 | 10 | it "should expose Keycode", (done) -> 11 | expect(Adb).to.have.property 'Keycode' 12 | expect(Adb.Keycode).to.equal Keycode 13 | done() 14 | 15 | it "should expose util", (done) -> 16 | expect(Adb).to.have.property 'util' 17 | expect(Adb.util).to.equal util 18 | done() 19 | 20 | describe '@createClient(options)', -> 21 | 22 | it "should return a Client instance", (done) -> 23 | expect(Adb.createClient()).to.be.an.instanceOf Client 24 | done() 25 | -------------------------------------------------------------------------------- /test/adb/command/host-serial/waitfordevice.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | WaitForDeviceCommand = \ 10 | require '../../../../src/adb/command/host-serial/waitfordevice' 11 | 12 | describe 'WaitForDeviceCommand', -> 13 | 14 | it "should send 'host-serial::wait-for-any'", (done) -> 15 | conn = new MockConnection 16 | cmd = new WaitForDeviceCommand conn 17 | conn.socket.on 'write', (chunk) -> 18 | expect(chunk.toString()).to.equal \ 19 | Protocol.encodeData('host-serial:abba:wait-for-any').toString() 20 | setImmediate -> 21 | conn.socket.causeRead Protocol.OKAY 22 | conn.socket.causeRead Protocol.OKAY 23 | conn.socket.causeEnd() 24 | cmd.execute 'abba' 25 | .then -> 26 | done() 27 | 28 | it "should resolve with id when the device is connected", (done) -> 29 | conn = new MockConnection 30 | cmd = new WaitForDeviceCommand conn 31 | setImmediate -> 32 | conn.socket.causeRead Protocol.OKAY 33 | conn.socket.causeRead Protocol.OKAY 34 | conn.socket.causeEnd() 35 | cmd.execute 'abba' 36 | .then (id) -> 37 | expect(id).to.equal 'abba' 38 | done() 39 | 40 | it "should reject with error if unable to connect", (done) -> 41 | conn = new MockConnection 42 | cmd = new WaitForDeviceCommand conn 43 | setImmediate -> 44 | conn.socket.causeRead Protocol.OKAY 45 | conn.socket.causeRead Protocol.FAIL 46 | conn.socket.causeRead \ 47 | Protocol.encodeData('not sure how this might happen') 48 | conn.socket.causeEnd() 49 | cmd.execute 'abba' 50 | .catch (err) -> 51 | expect(err.message).to.contain 'not sure how this might happen' 52 | done() 53 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/clear.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | ClearCommand = require '../../../../src/adb/command/host-transport/clear' 10 | 11 | describe 'ClearCommand', -> 12 | 13 | it "should send 'pm clear '", (done) -> 14 | conn = new MockConnection 15 | cmd = new ClearCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('shell:pm clear foo.bar.c').toString() 19 | conn.socket.causeRead Protocol.OKAY 20 | conn.socket.causeRead 'Success\r\n' 21 | conn.socket.causeEnd() 22 | cmd.execute 'foo.bar.c' 23 | .then -> 24 | done() 25 | 26 | it "should succeed on 'Success'", (done) -> 27 | conn = new MockConnection 28 | cmd = new ClearCommand conn 29 | conn.socket.on 'write', (chunk) -> 30 | conn.socket.causeRead Protocol.OKAY 31 | conn.socket.causeRead 'Success\r\n' 32 | conn.socket.causeEnd() 33 | cmd.execute 'foo.bar.c' 34 | .then -> 35 | done() 36 | 37 | it "should error on 'Failed'", (done) -> 38 | conn = new MockConnection 39 | cmd = new ClearCommand conn 40 | conn.socket.on 'write', (chunk) -> 41 | conn.socket.causeRead Protocol.OKAY 42 | conn.socket.causeRead 'Failed\r\n' 43 | conn.socket.causeEnd() 44 | cmd.execute 'foo.bar.c' 45 | .catch (err) -> 46 | expect(err).to.be.an.instanceof Error 47 | done() 48 | 49 | it "should error on 'Failed' even if connection not closed by 50 | device", (done) -> 51 | conn = new MockConnection 52 | cmd = new ClearCommand conn 53 | conn.socket.on 'write', (chunk) -> 54 | conn.socket.causeRead Protocol.OKAY 55 | conn.socket.causeRead 'Failed\r\n' 56 | cmd.execute 'foo.bar.c' 57 | .catch (err) -> 58 | expect(err).to.be.an.instanceof Error 59 | done() 60 | 61 | it "should ignore irrelevant lines", (done) -> 62 | conn = new MockConnection 63 | cmd = new ClearCommand conn 64 | conn.socket.on 'write', (chunk) -> 65 | conn.socket.causeRead Protocol.OKAY 66 | conn.socket.causeRead 'Open: foo error\n\n' 67 | conn.socket.causeRead 'Success\r\n' 68 | conn.socket.causeEnd() 69 | cmd.execute 'foo.bar.c' 70 | .then -> 71 | done() 72 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/framebuffer.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | MockConnection = require '../../../mock/connection' 7 | Protocol = require '../../../../src/adb/protocol' 8 | FrameBufferCommand = 9 | require '../../../../src/adb/command/host-transport/framebuffer' 10 | 11 | describe 'FrameBufferCommand', -> 12 | 13 | it "should send 'framebuffer:'", (done) -> 14 | conn = new MockConnection 15 | cmd = new FrameBufferCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('framebuffer:').toString() 19 | setImmediate -> 20 | meta = new Buffer 52 21 | meta.fill 0 22 | conn.socket.causeRead Protocol.OKAY 23 | conn.socket.causeRead meta 24 | conn.socket.causeEnd() 25 | cmd.execute 'raw' 26 | .then -> 27 | done() 28 | 29 | it "should parse meta header and return it as the 'meta' 30 | property of the stream", (done) -> 31 | conn = new MockConnection 32 | cmd = new FrameBufferCommand conn 33 | conn.socket.on 'write', (chunk) -> 34 | expect(chunk.toString()).to.equal \ 35 | Protocol.encodeData('framebuffer:').toString() 36 | setImmediate -> 37 | meta = new Buffer 52 38 | offset = 0 39 | meta.writeUInt32LE 1, offset 40 | meta.writeUInt32LE 32, offset += 4 41 | meta.writeUInt32LE 819200, offset += 4 42 | meta.writeUInt32LE 640, offset += 4 43 | meta.writeUInt32LE 320, offset += 4 44 | meta.writeUInt32LE 0, offset += 4 45 | meta.writeUInt32LE 8, offset += 4 46 | meta.writeUInt32LE 16, offset += 4 47 | meta.writeUInt32LE 8, offset += 4 48 | meta.writeUInt32LE 8, offset += 4 49 | meta.writeUInt32LE 8, offset += 4 50 | meta.writeUInt32LE 24, offset += 4 51 | meta.writeUInt32LE 8, offset += 4 52 | conn.socket.causeRead Protocol.OKAY 53 | conn.socket.causeRead meta 54 | conn.socket.causeEnd() 55 | cmd.execute 'raw' 56 | .then (stream) -> 57 | expect(stream).to.have.property 'meta' 58 | expect(stream.meta).to.eql 59 | version: 1 60 | bpp: 32 61 | size: 819200 62 | width: 640 63 | height: 320 64 | red_offset: 0 65 | red_length: 8 66 | blue_offset: 16 67 | blue_length: 8 68 | green_offset: 8 69 | green_length: 8 70 | alpha_offset: 24 71 | alpha_length: 8 72 | format: 'rgba' 73 | done() 74 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/getfeatures.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | GetFeaturesCommand = 10 | require '../../../../src/adb/command/host-transport/getfeatures' 11 | 12 | describe 'GetFeaturesCommand', -> 13 | 14 | it "should send 'pm list features'", (done) -> 15 | conn = new MockConnection 16 | cmd = new GetFeaturesCommand conn 17 | conn.socket.on 'write', (chunk) -> 18 | expect(chunk.toString()).to.equal \ 19 | Protocol.encodeData('shell:pm list features 2>/dev/null').toString() 20 | setImmediate -> 21 | conn.socket.causeRead Protocol.OKAY 22 | conn.socket.causeEnd() 23 | cmd.execute() 24 | .then -> 25 | done() 26 | 27 | it "should return an empty object for an empty feature list", (done) -> 28 | conn = new MockConnection 29 | cmd = new GetFeaturesCommand conn 30 | setImmediate -> 31 | conn.socket.causeRead Protocol.OKAY 32 | conn.socket.causeEnd() 33 | cmd.execute() 34 | .then (features) -> 35 | expect(Object.keys(features)).to.be.empty 36 | done() 37 | 38 | it "should return a map of features", (done) -> 39 | conn = new MockConnection 40 | cmd = new GetFeaturesCommand conn 41 | setImmediate -> 42 | conn.socket.causeRead Protocol.OKAY 43 | conn.socket.causeRead """ 44 | feature:reqGlEsVersion=0x20000 45 | feature:foo\r 46 | feature:bar 47 | """ 48 | conn.socket.causeEnd() 49 | cmd.execute() 50 | .then (features) -> 51 | expect(Object.keys(features)).to.have.length 3 52 | expect(features).to.eql 53 | reqGlEsVersion: '0x20000' 54 | foo: true 55 | bar: true 56 | done() 57 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/getpackages.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | GetPackagesCommand = 10 | require '../../../../src/adb/command/host-transport/getpackages' 11 | 12 | describe 'GetPackagesCommand', -> 13 | 14 | it "should send 'pm list packages'", (done) -> 15 | conn = new MockConnection 16 | cmd = new GetPackagesCommand conn 17 | conn.socket.on 'write', (chunk) -> 18 | expect(chunk.toString()).to.equal \ 19 | Protocol.encodeData('shell:pm list packages 2>/dev/null').toString() 20 | setImmediate -> 21 | conn.socket.causeRead Protocol.OKAY 22 | conn.socket.causeEnd() 23 | cmd.execute() 24 | .then -> 25 | done() 26 | 27 | it "should return an empty array for an empty package list", (done) -> 28 | conn = new MockConnection 29 | cmd = new GetPackagesCommand conn 30 | setImmediate -> 31 | conn.socket.causeRead Protocol.OKAY 32 | conn.socket.causeEnd() 33 | cmd.execute() 34 | .then (packages) -> 35 | expect(packages).to.be.empty 36 | done() 37 | 38 | it "should return an array of packages", (done) -> 39 | conn = new MockConnection 40 | cmd = new GetPackagesCommand conn 41 | setImmediate -> 42 | conn.socket.causeRead Protocol.OKAY 43 | conn.socket.causeRead """ 44 | package:com.google.android.gm 45 | package:com.google.android.inputmethod.japanese 46 | package:com.google.android.tag\r 47 | package:com.google.android.GoogleCamera 48 | package:com.google.android.youtube 49 | package:com.google.android.apps.magazines 50 | package:com.google.earth 51 | """ 52 | conn.socket.causeEnd() 53 | cmd.execute() 54 | .then (packages) -> 55 | expect(packages).to.have.length 7 56 | expect(packages).to.eql [ 57 | 'com.google.android.gm' 58 | 'com.google.android.inputmethod.japanese' 59 | 'com.google.android.tag' 60 | 'com.google.android.GoogleCamera', 61 | 'com.google.android.youtube', 62 | 'com.google.android.apps.magazines', 63 | 'com.google.earth' 64 | ] 65 | done() 66 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/getproperties.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | GetPropertiesCommand = 10 | require '../../../../src/adb/command/host-transport/getproperties' 11 | 12 | describe 'GetPropertiesCommand', -> 13 | 14 | it "should send 'getprop'", (done) -> 15 | conn = new MockConnection 16 | cmd = new GetPropertiesCommand conn 17 | conn.socket.on 'write', (chunk) -> 18 | expect(chunk.toString()).to.equal \ 19 | Protocol.encodeData('shell:getprop').toString() 20 | setImmediate -> 21 | conn.socket.causeRead Protocol.OKAY 22 | conn.socket.causeEnd() 23 | cmd.execute() 24 | .then -> 25 | done() 26 | 27 | it "should return an empty object for an empty property list", (done) -> 28 | conn = new MockConnection 29 | cmd = new GetPropertiesCommand conn 30 | setImmediate -> 31 | conn.socket.causeRead Protocol.OKAY 32 | conn.socket.causeEnd() 33 | cmd.execute() 34 | .then (properties) -> 35 | expect(Object.keys(properties)).to.be.empty 36 | done() 37 | 38 | it "should return a map of properties", (done) -> 39 | conn = new MockConnection 40 | cmd = new GetPropertiesCommand conn 41 | setImmediate -> 42 | conn.socket.causeRead Protocol.OKAY 43 | conn.socket.causeRead """ 44 | [ro.product.locale.region]: [US] 45 | [ro.product.manufacturer]: [samsung]\r 46 | [ro.product.model]: [SC-04E] 47 | [ro.product.name]: [SC-04E] 48 | """ 49 | conn.socket.causeEnd() 50 | cmd.execute() 51 | .then (properties) -> 52 | expect(Object.keys(properties)).to.have.length 4 53 | expect(properties).to.eql 54 | 'ro.product.locale.region': 'US' 55 | 'ro.product.manufacturer': 'samsung' 56 | 'ro.product.model': 'SC-04E' 57 | 'ro.product.name': 'SC-04E' 58 | done() 59 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/install.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | InstallCommand = 10 | require '../../../../src/adb/command/host-transport/install' 11 | 12 | describe 'InstallCommand', -> 13 | 14 | it "should send 'pm install -r '", (done) -> 15 | conn = new MockConnection 16 | cmd = new InstallCommand conn 17 | conn.socket.on 'write', (chunk) -> 18 | expect(chunk.toString()).to.equal \ 19 | Protocol.encodeData('shell:pm install -r "foo"').toString() 20 | setImmediate -> 21 | conn.socket.causeRead Protocol.OKAY 22 | conn.socket.causeRead 'Success\r\n' 23 | conn.socket.causeEnd() 24 | cmd.execute 'foo' 25 | .then -> 26 | done() 27 | 28 | it "should succeed when command responds with 'Success'", (done) -> 29 | conn = new MockConnection 30 | cmd = new InstallCommand conn 31 | setImmediate -> 32 | conn.socket.causeRead Protocol.OKAY 33 | conn.socket.causeRead 'Success\r\n' 34 | conn.socket.causeEnd() 35 | cmd.execute 'foo' 36 | .then -> 37 | done() 38 | 39 | it "should reject if command responds with 'Failure [REASON]'", (done) -> 40 | conn = new MockConnection 41 | cmd = new InstallCommand conn 42 | setImmediate -> 43 | conn.socket.causeRead Protocol.OKAY 44 | conn.socket.causeRead 'Failure [BAR]\r\n' 45 | conn.socket.causeEnd() 46 | cmd.execute 'foo' 47 | .catch (err) -> 48 | done() 49 | 50 | it "should give detailed reason in rejection's code property", (done) -> 51 | conn = new MockConnection 52 | cmd = new InstallCommand conn 53 | setImmediate -> 54 | conn.socket.causeRead Protocol.OKAY 55 | conn.socket.causeRead 'Failure [ALREADY_EXISTS]\r\n' 56 | conn.socket.causeEnd() 57 | cmd.execute 'foo' 58 | .catch (err) -> 59 | expect(err.code).to.equal 'ALREADY_EXISTS' 60 | done() 61 | 62 | it "should ignore any other data", (done) -> 63 | conn = new MockConnection 64 | cmd = new InstallCommand conn 65 | setImmediate -> 66 | conn.socket.causeRead Protocol.OKAY 67 | conn.socket.causeRead 'open: Permission failed\r\n' 68 | conn.socket.causeRead 'Success\r\n' 69 | conn.socket.causeEnd() 70 | cmd.execute 'foo' 71 | .then -> 72 | done() 73 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/isinstalled.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | IsInstalledCommand = 10 | require '../../../../src/adb/command/host-transport/isinstalled' 11 | 12 | describe 'IsInstalledCommand', -> 13 | 14 | it "should send 'pm path '", (done) -> 15 | conn = new MockConnection 16 | cmd = new IsInstalledCommand conn 17 | conn.socket.on 'write', (chunk) -> 18 | expect(chunk.toString()).to.equal \ 19 | Protocol.encodeData("shell:pm path foo 2>/dev/null").toString() 20 | setImmediate -> 21 | conn.socket.causeRead Protocol.OKAY 22 | conn.socket.causeRead 'package:foo\r\n' 23 | conn.socket.causeEnd() 24 | cmd.execute 'foo' 25 | .then -> 26 | done() 27 | 28 | it "should resolve with true if package returned by command", (done) -> 29 | conn = new MockConnection 30 | cmd = new IsInstalledCommand conn 31 | setImmediate -> 32 | conn.socket.causeRead Protocol.OKAY 33 | conn.socket.causeRead 'package:bar\r\n' 34 | conn.socket.causeEnd() 35 | cmd.execute 'foo' 36 | .then (found) -> 37 | expect(found).to.be.true 38 | done() 39 | 40 | it "should resolve with false if no package returned", (done) -> 41 | conn = new MockConnection 42 | cmd = new IsInstalledCommand conn 43 | setImmediate -> 44 | conn.socket.causeRead Protocol.OKAY 45 | conn.socket.causeEnd() 46 | cmd.execute 'foo' 47 | .then (found) -> 48 | expect(found).to.be.false 49 | done() 50 | 51 | it "should fail if any other data is received", (done) -> 52 | conn = new MockConnection 53 | cmd = new IsInstalledCommand conn 54 | setImmediate -> 55 | conn.socket.causeRead Protocol.OKAY 56 | conn.socket.causeRead 'open: Permission failed\r\n' 57 | conn.socket.causeEnd() 58 | cmd.execute 'foo' 59 | .catch (err) -> 60 | expect(err).to.be.an.instanceof Error 61 | done() 62 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/local.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | LocalCommand = require '../../../../src/adb/command/host-transport/local' 10 | 11 | describe 'LocalCommand', -> 12 | 13 | it "should send 'localfilesystem:'", (done) -> 14 | conn = new MockConnection 15 | cmd = new LocalCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('localfilesystem:/foo.sock').toString() 19 | setImmediate -> 20 | conn.socket.causeRead Protocol.OKAY 21 | conn.socket.causeEnd() 22 | cmd.execute '/foo.sock' 23 | .then (stream) -> 24 | done() 25 | 26 | it "should send ':' if prefixed with ':'", (done) -> 27 | conn = new MockConnection 28 | cmd = new LocalCommand conn 29 | conn.socket.on 'write', (chunk) -> 30 | expect(chunk.toString()).to.equal \ 31 | Protocol.encodeData('localabstract:/foo.sock').toString() 32 | setImmediate -> 33 | conn.socket.causeRead Protocol.OKAY 34 | conn.socket.causeEnd() 35 | cmd.execute 'localabstract:/foo.sock' 36 | .then (stream) -> 37 | done() 38 | 39 | it "should resolve with the stream", (done) -> 40 | conn = new MockConnection 41 | cmd = new LocalCommand conn 42 | setImmediate -> 43 | conn.socket.causeRead Protocol.OKAY 44 | cmd.execute '/foo.sock' 45 | .then (stream) -> 46 | stream.end() 47 | expect(stream).to.be.an.instanceof Stream.Readable 48 | done() 49 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/log.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | LogCommand = require '../../../../src/adb/command/host-transport/log' 10 | 11 | describe 'LogCommand', -> 12 | 13 | it "should send 'log:'", (done) -> 14 | conn = new MockConnection 15 | cmd = new LogCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('log:main').toString() 19 | setImmediate -> 20 | conn.socket.causeRead Protocol.OKAY 21 | conn.socket.causeEnd() 22 | cmd.execute 'main' 23 | .then (stream) -> 24 | done() 25 | 26 | it "should resolve with the log stream", (done) -> 27 | conn = new MockConnection 28 | cmd = new LogCommand conn 29 | setImmediate -> 30 | conn.socket.causeRead Protocol.OKAY 31 | cmd.execute 'main' 32 | .then (stream) -> 33 | stream.end() 34 | expect(stream).to.be.an.instanceof Stream.Readable 35 | done() 36 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/logcat.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Promise = require 'bluebird' 3 | Sinon = require 'sinon' 4 | Chai = require 'chai' 5 | Chai.use require 'sinon-chai' 6 | {expect} = Chai 7 | 8 | MockConnection = require '../../../mock/connection' 9 | Protocol = require '../../../../src/adb/protocol' 10 | Parser = require '../../../../src/adb/parser' 11 | LogcatCommand = require '../../../../src/adb/command/host-transport/logcat' 12 | 13 | describe 'LogcatCommand', -> 14 | 15 | it "should send 'echo && logcat -B *:I'", (done) -> 16 | conn = new MockConnection 17 | cmd = new LogcatCommand conn 18 | conn.socket.on 'write', (chunk) -> 19 | expect(chunk.toString()).to.equal \ 20 | Protocol.encodeData('shell:echo && 21 | logcat -B *:I 2>/dev/null').toString() 22 | setImmediate -> 23 | conn.socket.causeRead Protocol.OKAY 24 | conn.socket.causeEnd() 25 | cmd.execute() 26 | .then (stream) -> 27 | done() 28 | 29 | it "should send 'echo && logcat -c && logcat -B *:I' if options.clear 30 | is set", (done) -> 31 | conn = new MockConnection 32 | cmd = new LogcatCommand conn 33 | conn.socket.on 'write', (chunk) -> 34 | expect(chunk.toString()).to.equal \ 35 | Protocol.encodeData('shell:echo && logcat -c 2>/dev/null && 36 | logcat -B *:I 2>/dev/null').toString() 37 | setImmediate -> 38 | conn.socket.causeRead Protocol.OKAY 39 | conn.socket.causeEnd() 40 | cmd.execute clear: true 41 | .then (stream) -> 42 | done() 43 | 44 | it "should resolve with the logcat stream", (done) -> 45 | conn = new MockConnection 46 | cmd = new LogcatCommand conn 47 | setImmediate -> 48 | conn.socket.causeRead Protocol.OKAY 49 | cmd.execute() 50 | .then (stream) -> 51 | stream.end() 52 | expect(stream).to.be.an.instanceof Stream.Readable 53 | done() 54 | 55 | it "should perform CRLF transformation by default", (done) -> 56 | conn = new MockConnection 57 | cmd = new LogcatCommand conn 58 | setImmediate -> 59 | conn.socket.causeRead Protocol.OKAY 60 | conn.socket.causeRead '\r\nfoo\r\n' 61 | conn.socket.causeEnd() 62 | cmd.execute() 63 | .then (stream) -> 64 | new Parser(stream).readAll() 65 | .then (out) -> 66 | expect(out.toString()).to.equal 'foo\n' 67 | done() 68 | 69 | it "should not perform CRLF transformation if not needed", (done) -> 70 | conn = new MockConnection 71 | cmd = new LogcatCommand conn 72 | setImmediate -> 73 | conn.socket.causeRead Protocol.OKAY 74 | conn.socket.causeRead '\nfoo\r\n' 75 | conn.socket.causeEnd() 76 | cmd.execute() 77 | .then (stream) -> 78 | new Parser(stream).readAll() 79 | .then (out) -> 80 | expect(out.toString()).to.equal 'foo\r\n' 81 | done() 82 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/monkey.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Promise = require 'bluebird' 3 | Sinon = require 'sinon' 4 | Chai = require 'chai' 5 | Chai.use require 'sinon-chai' 6 | {expect} = Chai 7 | 8 | MockConnection = require '../../../mock/connection' 9 | Protocol = require '../../../../src/adb/protocol' 10 | MonkeyCommand = require '../../../../src/adb/command/host-transport/monkey' 11 | 12 | describe 'MonkeyCommand', -> 13 | 14 | it "should send 'monkey --port -v'", (done) -> 15 | conn = new MockConnection 16 | cmd = new MonkeyCommand conn 17 | conn.socket.on 'write', (chunk) -> 18 | expect(chunk.toString()).to.equal \ 19 | Protocol.encodeData('shell:EXTERNAL_STORAGE=/data/local/tmp monkey 20 | --port 1080 -v').toString() 21 | setImmediate -> 22 | conn.socket.causeRead Protocol.OKAY 23 | conn.socket.causeRead ':Monkey: foo\n' 24 | cmd.execute 1080 25 | .then (stream) -> 26 | done() 27 | 28 | it "should resolve with the output stream", (done) -> 29 | conn = new MockConnection 30 | cmd = new MonkeyCommand conn 31 | setImmediate -> 32 | conn.socket.causeRead Protocol.OKAY 33 | conn.socket.causeRead ':Monkey: foo\n' 34 | cmd.execute 1080 35 | .then (stream) -> 36 | stream.end() 37 | expect(stream).to.be.an.instanceof Stream.Readable 38 | done() 39 | 40 | it "should resolve after a timeout if result can't be judged from 41 | output", (done) -> 42 | conn = new MockConnection 43 | cmd = new MonkeyCommand conn 44 | setImmediate -> 45 | conn.socket.causeRead Protocol.OKAY 46 | cmd.execute 1080 47 | .then (stream) -> 48 | stream.end() 49 | expect(stream).to.be.an.instanceof Stream.Readable 50 | done() 51 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/reboot.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | MockConnection = require '../../../mock/connection' 7 | Protocol = require '../../../../src/adb/protocol' 8 | RebootCommand = require '../../../../src/adb/command/host-transport/reboot' 9 | 10 | describe 'RebootCommand', -> 11 | 12 | it "should send 'reboot:'", (done) -> 13 | conn = new MockConnection 14 | cmd = new RebootCommand conn 15 | conn.socket.on 'write', (chunk) -> 16 | expect(chunk.toString()).to.equal \ 17 | Protocol.encodeData('reboot:').toString() 18 | setImmediate -> 19 | conn.socket.causeRead Protocol.OKAY 20 | conn.socket.causeEnd() 21 | cmd.execute() 22 | .then -> 23 | done() 24 | 25 | it "should send wait for the connection to end", (done) -> 26 | conn = new MockConnection 27 | cmd = new RebootCommand conn 28 | ended = false 29 | conn.socket.on 'write', (chunk) -> 30 | expect(chunk.toString()).to.equal \ 31 | Protocol.encodeData('reboot:').toString() 32 | setImmediate -> 33 | conn.socket.causeRead Protocol.OKAY 34 | setImmediate -> 35 | ended = true 36 | conn.socket.causeEnd() 37 | cmd.execute() 38 | .then -> 39 | expect(ended).to.be.true 40 | done() 41 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/remount.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | MockConnection = require '../../../mock/connection' 7 | Protocol = require '../../../../src/adb/protocol' 8 | RemountCommand = require '../../../../src/adb/command/host-transport/remount' 9 | 10 | describe 'RemountCommand', -> 11 | 12 | it "should send 'remount:'", (done) -> 13 | conn = new MockConnection 14 | cmd = new RemountCommand conn 15 | conn.socket.on 'write', (chunk) -> 16 | expect(chunk.toString()).to.equal \ 17 | Protocol.encodeData('remount:').toString() 18 | conn.socket.causeRead Protocol.OKAY 19 | conn.socket.causeEnd() 20 | cmd.execute() 21 | .then -> 22 | done() 23 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/root.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | RootCommand = require '../../../../src/adb/command/host-transport/root' 10 | 11 | describe 'RootCommand', -> 12 | 13 | it "should send 'root:'", (done) -> 14 | conn = new MockConnection 15 | cmd = new RootCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('root:').toString() 19 | setImmediate -> 20 | conn.socket.causeRead Protocol.OKAY 21 | conn.socket.causeRead "restarting adbd as root\n" 22 | conn.socket.causeEnd() 23 | cmd.execute() 24 | .then (val) -> 25 | expect(val).to.be.true 26 | done() 27 | 28 | it "should reject on unexpected reply", (done) -> 29 | conn = new MockConnection 30 | cmd = new RootCommand conn 31 | setImmediate -> 32 | conn.socket.causeRead Protocol.OKAY 33 | conn.socket.causeRead "adbd cannot run as root in production builds\n" 34 | conn.socket.causeEnd() 35 | cmd.execute() 36 | .catch (err) -> 37 | expect(err.message).to.eql \ 38 | 'adbd cannot run as root in production builds' 39 | done() 40 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/screencap.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | MockConnection = require '../../../mock/connection' 7 | Protocol = require '../../../../src/adb/protocol' 8 | Parser = require '../../../../src/adb/parser' 9 | ScreencapCommand = 10 | require '../../../../src/adb/command/host-transport/screencap' 11 | 12 | describe 'ScreencapCommand', -> 13 | 14 | it "should send 'screencap -p'", (done) -> 15 | conn = new MockConnection 16 | cmd = new ScreencapCommand conn 17 | conn.socket.on 'write', (chunk) -> 18 | expect(chunk.toString()).to.equal \ 19 | Protocol.encodeData('shell:echo && screencap -p 2>/dev/null').toString() 20 | setImmediate -> 21 | conn.socket.causeRead Protocol.OKAY 22 | conn.socket.causeRead '\r\nlegit image' 23 | conn.socket.causeEnd() 24 | cmd.execute() 25 | .then (stream) -> 26 | done() 27 | 28 | it "should resolve with the PNG stream", (done) -> 29 | conn = new MockConnection 30 | cmd = new ScreencapCommand conn 31 | setImmediate -> 32 | conn.socket.causeRead Protocol.OKAY 33 | conn.socket.causeRead '\r\nlegit image' 34 | conn.socket.causeEnd() 35 | cmd.execute() 36 | .then (stream) -> 37 | new Parser(stream).readAll() 38 | .then (out) -> 39 | expect(out.toString()).to.equal 'legit image' 40 | done() 41 | 42 | it "should reject if command not supported", (done) -> 43 | conn = new MockConnection 44 | cmd = new ScreencapCommand conn 45 | setImmediate -> 46 | conn.socket.causeRead Protocol.OKAY 47 | conn.socket.causeEnd() 48 | cmd.execute() 49 | .catch (err) -> 50 | done() 51 | 52 | it "should perform CRLF transformation by default", (done) -> 53 | conn = new MockConnection 54 | cmd = new ScreencapCommand conn 55 | setImmediate -> 56 | conn.socket.causeRead Protocol.OKAY 57 | conn.socket.causeRead '\r\nfoo\r\n' 58 | conn.socket.causeEnd() 59 | cmd.execute() 60 | .then (stream) -> 61 | new Parser(stream).readAll() 62 | .then (out) -> 63 | expect(out.toString()).to.equal 'foo\n' 64 | done() 65 | 66 | it "should not perform CRLF transformation if not needed", (done) -> 67 | conn = new MockConnection 68 | cmd = new ScreencapCommand conn 69 | setImmediate -> 70 | conn.socket.causeRead Protocol.OKAY 71 | conn.socket.causeRead '\nfoo\r\n' 72 | conn.socket.causeEnd() 73 | cmd.execute() 74 | .then (stream) -> 75 | new Parser(stream).readAll() 76 | .then (out) -> 77 | expect(out.toString()).to.equal 'foo\r\n' 78 | done() 79 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/shell.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | Parser = require '../../../../src/adb/parser' 10 | ShellCommand = 11 | require '../../../../src/adb/command/host-transport/shell' 12 | 13 | describe 'ShellCommand', -> 14 | 15 | it "should pass String commands as-is", (done) -> 16 | conn = new MockConnection 17 | cmd = new ShellCommand conn 18 | conn.socket.on 'write', (chunk) -> 19 | expect(chunk.toString()).to.equal \ 20 | Protocol.encodeData('shell:foo \'bar').toString() 21 | setImmediate -> 22 | conn.socket.causeRead Protocol.OKAY 23 | conn.socket.causeEnd() 24 | cmd.execute 'foo \'bar' 25 | .then (out) -> 26 | done() 27 | 28 | it "should escape Array commands", (done) -> 29 | conn = new MockConnection 30 | cmd = new ShellCommand conn 31 | conn.socket.on 'write', (chunk) -> 32 | expect(chunk.toString()).to.equal \ 33 | Protocol.encodeData("""shell:'foo' ''"'"'bar'"'"'' '"'""").toString() 34 | setImmediate -> 35 | conn.socket.causeRead Protocol.OKAY 36 | conn.socket.causeEnd() 37 | cmd.execute ['foo', '\'bar\'', '"'] 38 | .then (out) -> 39 | done() 40 | 41 | it "should not escape numbers in arguments", (done) -> 42 | conn = new MockConnection 43 | cmd = new ShellCommand conn 44 | conn.socket.on 'write', (chunk) -> 45 | expect(chunk.toString()).to.equal \ 46 | Protocol.encodeData("""shell:'foo' 67""").toString() 47 | setImmediate -> 48 | conn.socket.causeRead Protocol.OKAY 49 | conn.socket.causeEnd() 50 | cmd.execute ['foo', 67] 51 | .then (out) -> 52 | done() 53 | 54 | it "should reject with FailError on ADB failure (not command 55 | failure)", (done) -> 56 | conn = new MockConnection 57 | cmd = new ShellCommand conn 58 | conn.socket.on 'write', (chunk) -> 59 | expect(chunk.toString()).to.equal \ 60 | Protocol.encodeData("""shell:'foo'""").toString() 61 | setImmediate -> 62 | conn.socket.causeRead Protocol.FAIL 63 | conn.socket.causeRead Protocol.encodeData 'mystery' 64 | conn.socket.causeEnd() 65 | cmd.execute ['foo'] 66 | .catch Parser.FailError, (err) -> 67 | done() 68 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/startservice.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | StartServiceCommand = require \ 10 | '../../../../src/adb/command/host-transport/startservice' 11 | 12 | describe 'StartServiceCommand', -> 13 | 14 | it "should succeed when 'Success' returned", (done) -> 15 | conn = new MockConnection 16 | cmd = new StartServiceCommand conn 17 | setImmediate -> 18 | conn.socket.causeRead Protocol.OKAY 19 | conn.socket.causeRead 'Success' 20 | conn.socket.causeEnd() 21 | options = 22 | component: 'com.dummy.component/.Main' 23 | cmd.execute options 24 | .then -> 25 | done() 26 | 27 | it "should fail when 'Error' returned", (done) -> 28 | conn = new MockConnection 29 | cmd = new StartServiceCommand conn 30 | setImmediate -> 31 | conn.socket.causeRead Protocol.OKAY 32 | conn.socket.causeRead 'Error: foo\n' 33 | conn.socket.causeEnd() 34 | options = 35 | component: 'com.dummy.component/.Main' 36 | cmd.execute options 37 | .catch (err) -> 38 | expect(err).to.be.be.an.instanceOf Error 39 | done() 40 | 41 | it "should send 'am startservice --user 0 -n '", (done) -> 42 | conn = new MockConnection 43 | cmd = new StartServiceCommand conn 44 | conn.socket.on 'write', (chunk) -> 45 | expect(chunk.toString()).to.equal \ 46 | Protocol.encodeData("shell:am startservice 47 | -n 'com.dummy.component/.Main' 48 | --user 0").toString() 49 | setImmediate -> 50 | conn.socket.causeRead Protocol.OKAY 51 | conn.socket.causeRead 'Success\n' 52 | conn.socket.causeEnd() 53 | options = 54 | component: 'com.dummy.component/.Main' 55 | user: 0 56 | cmd.execute options 57 | .then -> 58 | done() 59 | 60 | it "should not send user option if not set'", (done) -> 61 | conn = new MockConnection 62 | cmd = new StartServiceCommand conn 63 | conn.socket.on 'write', (chunk) -> 64 | expect(chunk.toString()).to.equal \ 65 | Protocol.encodeData("shell:am startservice 66 | -n 'com.dummy.component/.Main'").toString() 67 | setImmediate -> 68 | conn.socket.causeRead Protocol.OKAY 69 | conn.socket.causeRead 'Success\n' 70 | conn.socket.causeEnd() 71 | options = 72 | component: 'com.dummy.component/.Main' 73 | cmd.execute options 74 | .then -> 75 | done() 76 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/sync.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | MockConnection = require '../../../mock/connection' 7 | Protocol = require '../../../../src/adb/protocol' 8 | SyncCommand = require '../../../../src/adb/command/host-transport/sync' 9 | 10 | describe 'SyncCommand', -> 11 | 12 | it "should send 'sync:'", (done) -> 13 | conn = new MockConnection 14 | cmd = new SyncCommand conn 15 | conn.socket.on 'write', (chunk) -> 16 | expect(chunk.toString()).to.equal \ 17 | Protocol.encodeData('sync:').toString() 18 | conn.socket.causeRead Protocol.OKAY 19 | conn.socket.causeEnd() 20 | cmd.execute() 21 | .then -> 22 | done() 23 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/tcp.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | TcpCommand = require '../../../../src/adb/command/host-transport/tcp' 10 | 11 | describe 'TcpCommand', -> 12 | 13 | it "should send 'tcp:' when no host given", (done) -> 14 | conn = new MockConnection 15 | cmd = new TcpCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('tcp:8080').toString() 19 | setImmediate -> 20 | conn.socket.causeRead Protocol.OKAY 21 | conn.socket.causeEnd() 22 | cmd.execute 8080 23 | .then (stream) -> 24 | done() 25 | 26 | it "should send 'tcp::' when host given", (done) -> 27 | conn = new MockConnection 28 | cmd = new TcpCommand conn 29 | conn.socket.on 'write', (chunk) -> 30 | expect(chunk.toString()).to.equal \ 31 | Protocol.encodeData('tcp:8080:127.0.0.1').toString() 32 | setImmediate -> 33 | conn.socket.causeRead Protocol.OKAY 34 | conn.socket.causeEnd() 35 | cmd.execute 8080, '127.0.0.1' 36 | .then (stream) -> 37 | done() 38 | 39 | it "should resolve with the tcp stream", (done) -> 40 | conn = new MockConnection 41 | cmd = new TcpCommand conn 42 | setImmediate -> 43 | conn.socket.causeRead Protocol.OKAY 44 | cmd.execute 8080 45 | .then (stream) -> 46 | stream.end() 47 | expect(stream).to.be.an.instanceof Stream.Readable 48 | done() 49 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/tcpip.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | TcpIpCommand = require '../../../../src/adb/command/host-transport/tcpip' 10 | 11 | describe 'TcpIpCommand', -> 12 | 13 | it "should send 'tcp:'", (done) -> 14 | conn = new MockConnection 15 | cmd = new TcpIpCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('tcpip:5555').toString() 19 | setImmediate -> 20 | conn.socket.causeRead Protocol.OKAY 21 | conn.socket.causeRead "restarting in TCP mode port: 5555\n" 22 | conn.socket.causeEnd() 23 | cmd.execute 5555 24 | .then -> 25 | done() 26 | 27 | it "should resolve with the port", (done) -> 28 | conn = new MockConnection 29 | cmd = new TcpIpCommand conn 30 | setImmediate -> 31 | conn.socket.causeRead Protocol.OKAY 32 | conn.socket.causeRead "restarting in TCP mode port: 5555\n" 33 | conn.socket.causeEnd() 34 | cmd.execute 5555 35 | .then (port) -> 36 | expect(port).to.equal 5555 37 | done() 38 | 39 | it "should reject on unexpected reply", (done) -> 40 | conn = new MockConnection 41 | cmd = new TcpIpCommand conn 42 | setImmediate -> 43 | conn.socket.causeRead Protocol.OKAY 44 | conn.socket.causeRead "not sure what this could be\n" 45 | conn.socket.causeEnd() 46 | cmd.execute 5555 47 | .catch (err) -> 48 | expect(err.message).to.eql 'not sure what this could be' 49 | done() 50 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/uninstall.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | Parser = require '../../../../src/adb/parser' 10 | UninstallCommand = 11 | require '../../../../src/adb/command/host-transport/uninstall' 12 | 13 | describe 'UninstallCommand', -> 14 | 15 | it "should succeed when command responds with 'Success'", (done) -> 16 | conn = new MockConnection 17 | cmd = new UninstallCommand conn 18 | conn.socket.on 'write', (chunk) -> 19 | expect(chunk.toString()).to.equal \ 20 | Protocol.encodeData('shell:pm uninstall foo').toString() 21 | setImmediate -> 22 | conn.socket.causeRead Protocol.OKAY 23 | conn.socket.causeRead 'Success\r\n' 24 | conn.socket.causeEnd() 25 | cmd.execute 'foo' 26 | .then -> 27 | done() 28 | 29 | it "should succeed even if command responds with 'Failure'", (done) -> 30 | conn = new MockConnection 31 | cmd = new UninstallCommand conn 32 | conn.socket.on 'write', (chunk) -> 33 | expect(chunk.toString()).to.equal \ 34 | Protocol.encodeData('shell:pm uninstall foo').toString() 35 | setImmediate -> 36 | conn.socket.causeRead Protocol.OKAY 37 | conn.socket.causeRead 'Failure\r\n' 38 | conn.socket.causeEnd() 39 | cmd.execute 'foo' 40 | .then -> 41 | done() 42 | 43 | it "should succeed even if command responds with 'Failure' 44 | with info in standard format", (done) -> 45 | conn = new MockConnection 46 | cmd = new UninstallCommand conn 47 | conn.socket.on 'write', (chunk) -> 48 | expect(chunk.toString()).to.equal \ 49 | Protocol.encodeData('shell:pm uninstall foo').toString() 50 | setImmediate -> 51 | conn.socket.causeRead Protocol.OKAY 52 | conn.socket.causeRead 'Failure [DELETE_FAILED_INTERNAL_ERROR]\r\n' 53 | conn.socket.causeEnd() 54 | cmd.execute 'foo' 55 | .then -> 56 | done() 57 | 58 | it "should succeed even if command responds with 'Failure' 59 | with info info in weird format", (done) -> 60 | conn = new MockConnection 61 | cmd = new UninstallCommand conn 62 | setImmediate -> 63 | conn.socket.causeRead Protocol.OKAY 64 | conn.socket.causeRead 'Failure - not installed for 0\r\n' 65 | conn.socket.causeEnd() 66 | cmd.execute 'foo' 67 | .then -> 68 | done() 69 | 70 | it "should succeed even if command responds with a buggy exception", (done) -> 71 | conn = new MockConnection 72 | cmd = new UninstallCommand conn 73 | setImmediate -> 74 | conn.socket.causeRead Protocol.OKAY 75 | # coffeelint: disable=max_line_length 76 | conn.socket.causeRead """ 77 | 78 | Exception occurred while dumping: 79 | java.lang.IllegalArgumentException: Unknown package: foo 80 | at com.android.server.pm.Settings.isOrphaned(Settings.java:4134) 81 | at com.android.server.pm.PackageManagerService.isOrphaned(PackageManagerService.java:18066) 82 | at com.android.server.pm.PackageManagerService.deletePackage(PackageManagerService.java:15483) 83 | at com.android.server.pm.PackageInstallerService.uninstall(PackageInstallerService.java:888) 84 | at com.android.server.pm.PackageManagerShellCommand.runUninstall(PackageManagerShellCommand.java:765) 85 | at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:113) 86 | at android.os.ShellCommand.exec(ShellCommand.java:94) 87 | at com.android.server.pm.PackageManagerService.onShellCommand(PackageManagerService.java:18324) 88 | at android.os.Binder.shellCommand(Binder.java:468) 89 | at android.os.Binder.onTransact(Binder.java:367) 90 | at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:2387) 91 | at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:3019) 92 | at android.os.Binder.execTransact(Binder.java:565) 93 | """ 94 | # coffeelint: enable=max_line_length 95 | conn.socket.causeEnd() 96 | cmd.execute 'foo' 97 | .then -> 98 | done() 99 | 100 | it "should reject with Parser.PrematureEOFError if stream ends 101 | before match", (done) -> 102 | conn = new MockConnection 103 | cmd = new UninstallCommand conn 104 | setImmediate -> 105 | conn.socket.causeRead Protocol.OKAY 106 | conn.socket.causeRead 'Hello. Is it me you are looking for?\r\n' 107 | conn.socket.causeEnd() 108 | cmd.execute 'foo' 109 | .catch Parser.PrematureEOFError, (err) -> 110 | done() 111 | 112 | it "should ignore any other data", (done) -> 113 | conn = new MockConnection 114 | cmd = new UninstallCommand conn 115 | conn.socket.on 'write', (chunk) -> 116 | expect(chunk.toString()).to.equal \ 117 | Protocol.encodeData('shell:pm uninstall foo').toString() 118 | setImmediate -> 119 | conn.socket.causeRead Protocol.OKAY 120 | conn.socket.causeRead 'open: Permission failed\r\n' 121 | conn.socket.causeRead 'Failure\r\n' 122 | conn.socket.causeEnd() 123 | cmd.execute 'foo' 124 | .then -> 125 | done() 126 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/usb.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | UsbCommand = require '../../../../src/adb/command/host-transport/usb' 10 | 11 | describe 'UsbCommand', -> 12 | 13 | it "should send 'usb:'", (done) -> 14 | conn = new MockConnection 15 | cmd = new UsbCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('usb:').toString() 19 | setImmediate -> 20 | conn.socket.causeRead Protocol.OKAY 21 | conn.socket.causeRead "restarting in USB mode\n" 22 | conn.socket.causeEnd() 23 | cmd.execute() 24 | .then (val) -> 25 | expect(val).to.be.true 26 | done() 27 | 28 | it "should reject on unexpected reply", (done) -> 29 | conn = new MockConnection 30 | cmd = new UsbCommand conn 31 | setImmediate -> 32 | conn.socket.causeRead Protocol.OKAY 33 | conn.socket.causeRead "invalid port\n" 34 | conn.socket.causeEnd() 35 | cmd.execute() 36 | .catch (err) -> 37 | expect(err.message).to.eql 'invalid port' 38 | done() 39 | -------------------------------------------------------------------------------- /test/adb/command/host-transport/waitbootcomplete.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | Parser = require '../../../../src/adb/parser' 10 | WaitBootCompleteCommand = 11 | require '../../../../src/adb/command/host-transport/waitbootcomplete' 12 | 13 | describe 'WaitBootCompleteCommand', -> 14 | 15 | it "should send a while loop with boot check", (done) -> 16 | conn = new MockConnection 17 | cmd = new WaitBootCompleteCommand conn 18 | want = 19 | 'shell:while getprop sys.boot_completed 2>/dev/null; do sleep 1; done' 20 | conn.socket.on 'write', (chunk) -> 21 | expect(chunk.toString()).to.equal \ 22 | Protocol.encodeData(want).toString() 23 | setImmediate -> 24 | conn.socket.causeRead Protocol.OKAY 25 | conn.socket.causeRead '1\r\n' 26 | conn.socket.causeEnd() 27 | cmd.execute() 28 | .then -> 29 | done() 30 | 31 | it "should reject with Parser.PrematureEOFError if connection cuts 32 | prematurely", (done) -> 33 | conn = new MockConnection 34 | cmd = new WaitBootCompleteCommand conn 35 | setImmediate -> 36 | conn.socket.causeRead Protocol.OKAY 37 | conn.socket.causeEnd() 38 | cmd.execute() 39 | .then -> 40 | done new Error 'Succeeded even though it should not' 41 | .catch Parser.PrematureEOFError, (err) -> 42 | done() 43 | 44 | it "should not return until boot is complete", (done) -> 45 | conn = new MockConnection 46 | cmd = new WaitBootCompleteCommand conn 47 | sent = false 48 | setImmediate -> 49 | conn.socket.causeRead Protocol.OKAY 50 | conn.socket.causeRead '\r\n' 51 | conn.socket.causeRead '\r\n' 52 | conn.socket.causeRead '\r\n' 53 | conn.socket.causeRead '\r\n' 54 | conn.socket.causeRead '\r\n' 55 | conn.socket.causeRead '\r\n' 56 | conn.socket.causeRead '\r\n' 57 | conn.socket.causeRead '\r\n' 58 | conn.socket.causeRead '\r\n' 59 | conn.socket.causeRead '\r\n' 60 | setTimeout -> 61 | sent = true 62 | conn.socket.causeRead '1\r\n' 63 | , 50 64 | cmd.execute() 65 | .then -> 66 | expect(sent).to.be.true 67 | done() 68 | 69 | it "should close connection when done", (done) -> 70 | conn = new MockConnection 71 | cmd = new WaitBootCompleteCommand conn 72 | setImmediate -> 73 | conn.socket.causeRead Protocol.OKAY 74 | conn.socket.causeRead '1\r\n' 75 | conn.socket.on 'end', -> 76 | done() 77 | cmd.execute() 78 | -------------------------------------------------------------------------------- /test/adb/command/host/connect.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | ConnectCommand = require '../../../../src/adb/command/host/connect' 10 | 11 | describe 'ConnectCommand', -> 12 | 13 | it "should send 'host:connect::'", (done) -> 14 | conn = new MockConnection 15 | cmd = new ConnectCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('host:connect:192.168.2.2:5555').toString() 19 | setImmediate -> 20 | conn.socket.causeRead Protocol.OKAY 21 | conn.socket.causeRead Protocol.encodeData('connected to 192.168.2.2:5555') 22 | conn.socket.causeEnd() 23 | cmd.execute '192.168.2.2', 5555 24 | .then -> 25 | done() 26 | 27 | it "should resolve with the new device id if connected", (done) -> 28 | conn = new MockConnection 29 | cmd = new ConnectCommand conn 30 | setImmediate -> 31 | conn.socket.causeRead Protocol.OKAY 32 | conn.socket.causeRead Protocol.encodeData('connected to 192.168.2.2:5555') 33 | conn.socket.causeEnd() 34 | cmd.execute '192.168.2.2', 5555 35 | .then (val) -> 36 | expect(val).to.be.equal '192.168.2.2:5555' 37 | done() 38 | 39 | it "should resolve with the new device id if already connected", (done) -> 40 | conn = new MockConnection 41 | cmd = new ConnectCommand conn 42 | setImmediate -> 43 | conn.socket.causeRead Protocol.OKAY 44 | conn.socket.causeRead \ 45 | Protocol.encodeData('already connected to 192.168.2.2:5555') 46 | conn.socket.causeEnd() 47 | cmd.execute '192.168.2.2', 5555 48 | .then (val) -> 49 | expect(val).to.be.equal '192.168.2.2:5555' 50 | done() 51 | 52 | it "should reject with error if unable to connect", (done) -> 53 | conn = new MockConnection 54 | cmd = new ConnectCommand conn 55 | setImmediate -> 56 | conn.socket.causeRead Protocol.OKAY 57 | conn.socket.causeRead \ 58 | Protocol.encodeData('unable to connect to 192.168.2.2:5555') 59 | conn.socket.causeEnd() 60 | cmd.execute '192.168.2.2', 5555 61 | .catch (err) -> 62 | expect(err.message).to.eql 'unable to connect to 192.168.2.2:5555' 63 | done() 64 | -------------------------------------------------------------------------------- /test/adb/command/host/disconnect.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | MockConnection = require '../../../mock/connection' 8 | Protocol = require '../../../../src/adb/protocol' 9 | DisconnectCommand = require '../../../../src/adb/command/host/disconnect' 10 | 11 | describe 'DisconnectCommand', -> 12 | 13 | it "should send 'host:disconnect::'", (done) -> 14 | conn = new MockConnection 15 | cmd = new DisconnectCommand conn 16 | conn.socket.on 'write', (chunk) -> 17 | expect(chunk.toString()).to.equal \ 18 | Protocol.encodeData('host:disconnect:192.168.2.2:5555').toString() 19 | setImmediate -> 20 | conn.socket.causeRead Protocol.OKAY 21 | conn.socket.causeRead Protocol.encodeData('') 22 | conn.socket.causeEnd() 23 | cmd.execute '192.168.2.2', 5555 24 | .then -> 25 | done() 26 | 27 | it "should resolve with the new device id if disconnected", (done) -> 28 | conn = new MockConnection 29 | cmd = new DisconnectCommand conn 30 | setImmediate -> 31 | conn.socket.causeRead Protocol.OKAY 32 | conn.socket.causeRead Protocol.encodeData('') 33 | conn.socket.causeEnd() 34 | cmd.execute '192.168.2.2', 5555 35 | .then (val) -> 36 | expect(val).to.be.equal '192.168.2.2:5555' 37 | done() 38 | 39 | it "should reject with error if unable to disconnect", (done) -> 40 | conn = new MockConnection 41 | cmd = new DisconnectCommand conn 42 | setImmediate -> 43 | conn.socket.causeRead Protocol.OKAY 44 | conn.socket.causeRead \ 45 | Protocol.encodeData('No such device 192.168.2.2:5555') 46 | conn.socket.causeEnd() 47 | cmd.execute '192.168.2.2', 5555 48 | .catch (err) -> 49 | expect(err.message).to.eql 'No such device 192.168.2.2:5555' 50 | done() 51 | -------------------------------------------------------------------------------- /test/adb/command/host/version.coffee: -------------------------------------------------------------------------------- 1 | Sinon = require 'sinon' 2 | Chai = require 'chai' 3 | Chai.use require 'sinon-chai' 4 | {expect} = Chai 5 | 6 | MockConnection = require '../../../mock/connection' 7 | Protocol = require '../../../../src/adb/protocol' 8 | HostVersionCommand = require '../../../../src/adb/command/host/version' 9 | 10 | describe 'HostVersionCommand', -> 11 | 12 | it "should send 'host:version'", (done) -> 13 | conn = new MockConnection 14 | cmd = new HostVersionCommand conn 15 | conn.socket.on 'write', (chunk) -> 16 | expect(chunk.toString()).to.equal \ 17 | Protocol.encodeData('host:version').toString() 18 | setImmediate -> 19 | conn.socket.causeRead Protocol.OKAY 20 | conn.socket.causeRead Protocol.encodeData '0000' 21 | conn.socket.causeEnd() 22 | cmd.execute() 23 | .then (version) -> 24 | done() 25 | 26 | it "should resolve with version", (done) -> 27 | conn = new MockConnection 28 | cmd = new HostVersionCommand conn 29 | setImmediate -> 30 | conn.socket.causeRead Protocol.OKAY 31 | conn.socket.causeRead Protocol.encodeData (0x1234).toString 16 32 | conn.socket.causeEnd() 33 | cmd.execute() 34 | .then (version) -> 35 | expect(version).to.equal 0x1234 36 | done() 37 | 38 | it "should handle old-style version", (done) -> 39 | conn = new MockConnection 40 | cmd = new HostVersionCommand conn 41 | setImmediate -> 42 | conn.socket.causeRead (0x1234).toString 16 43 | conn.socket.causeEnd() 44 | cmd.execute() 45 | .then (version) -> 46 | expect(version).to.equal 0x1234 47 | done() 48 | -------------------------------------------------------------------------------- /test/adb/framebuffer/rgbtransform.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | RgbTransform = require '../../../src/adb/framebuffer/rgbtransform' 8 | 9 | describe 'RgbTransform', -> 10 | 11 | it "should transform BGRA into RGB", (done) -> 12 | meta = 13 | bpp: 32 14 | red_offset: 16 15 | red_length: 8 16 | green_offset: 8 17 | green_length: 8 18 | blue_offset: 0 19 | blue_length: 8 20 | alpha_offset: 24 21 | alpha_length: 8 22 | pixel = new Buffer 4 23 | pixel.writeUInt8 50, 0 24 | pixel.writeUInt8 100, 1 25 | pixel.writeUInt8 150, 2 26 | pixel.writeUInt8 200, 3 27 | stream = new Stream.PassThrough 28 | transform = new RgbTransform meta 29 | stream.pipe transform 30 | transform.on 'data', (chunk) -> 31 | expect(chunk).to.have.length 3 32 | expect(chunk.readUInt8 0).to.equal 150 33 | expect(chunk.readUInt8 1).to.equal 100 34 | expect(chunk.readUInt8 2).to.equal 50 35 | done() 36 | stream.write pixel 37 | stream.end() 38 | 39 | it "should transform BGR into RGB", (done) -> 40 | meta = 41 | bpp: 32 42 | red_offset: 16 43 | red_length: 8 44 | green_offset: 8 45 | green_length: 8 46 | blue_offset: 0 47 | blue_length: 8 48 | alpha_offset: 0 49 | alpha_length: 0 50 | pixel = new Buffer 4 51 | pixel.writeUInt8 50, 0 52 | pixel.writeUInt8 100, 1 53 | pixel.writeUInt8 150, 2 54 | stream = new Stream.PassThrough 55 | transform = new RgbTransform meta 56 | stream.pipe transform 57 | transform.on 'data', (chunk) -> 58 | expect(chunk).to.have.length 3 59 | expect(chunk.readUInt8 0).to.equal 150 60 | expect(chunk.readUInt8 1).to.equal 100 61 | expect(chunk.readUInt8 2).to.equal 50 62 | done() 63 | stream.write pixel 64 | stream.end() 65 | 66 | it "should transform RGB into RGB", (done) -> 67 | meta = 68 | bpp: 24 69 | red_offset: 0 70 | red_length: 8 71 | green_offset: 8 72 | green_length: 8 73 | blue_offset: 16 74 | blue_length: 8 75 | alpha_offset: 0 76 | alpha_length: 0 77 | pixel = new Buffer 3 78 | pixel.writeUInt8 50, 0 79 | pixel.writeUInt8 100, 1 80 | pixel.writeUInt8 150, 2 81 | stream = new Stream.PassThrough 82 | transform = new RgbTransform meta 83 | stream.pipe transform 84 | transform.on 'data', (chunk) -> 85 | expect(chunk).to.have.length 3 86 | expect(chunk.readUInt8 0).to.equal 50 87 | expect(chunk.readUInt8 1).to.equal 100 88 | expect(chunk.readUInt8 2).to.equal 150 89 | done() 90 | stream.write pixel 91 | stream.end() 92 | 93 | it "should transform RGBA into RGB", (done) -> 94 | meta = 95 | bpp: 32 96 | red_offset: 0 97 | red_length: 8 98 | green_offset: 8 99 | green_length: 8 100 | blue_offset: 16 101 | blue_length: 8 102 | alpha_offset: 24 103 | alpha_length: 8 104 | pixel = new Buffer 4 105 | pixel.writeUInt8 50, 0 106 | pixel.writeUInt8 100, 1 107 | pixel.writeUInt8 150, 2 108 | pixel.writeUInt8 200, 3 109 | stream = new Stream.PassThrough 110 | transform = new RgbTransform meta 111 | stream.pipe transform 112 | transform.on 'data', (chunk) -> 113 | expect(chunk).to.have.length 3 114 | expect(chunk.readUInt8 0).to.equal 50 115 | expect(chunk.readUInt8 1).to.equal 100 116 | expect(chunk.readUInt8 2).to.equal 150 117 | done() 118 | stream.write pixel 119 | stream.end() 120 | 121 | it "should wait for a complete pixel before transforming", (done) -> 122 | meta = 123 | bpp: 32 124 | red_offset: 0 125 | red_length: 8 126 | green_offset: 8 127 | green_length: 8 128 | blue_offset: 16 129 | blue_length: 8 130 | alpha_offset: 24 131 | alpha_length: 8 132 | pixel = new Buffer 4 133 | pixel.writeUInt8 50, 0 134 | pixel.writeUInt8 100, 1 135 | pixel.writeUInt8 150, 2 136 | pixel.writeUInt8 200, 3 137 | stream = new Stream.PassThrough 138 | transform = new RgbTransform meta 139 | stream.pipe transform 140 | transform.on 'data', (chunk) -> 141 | expect(chunk).to.have.length 3 142 | expect(chunk.readUInt8 0).to.equal 50 143 | expect(chunk.readUInt8 1).to.equal 100 144 | expect(chunk.readUInt8 2).to.equal 150 145 | done() 146 | stream.write pixel.slice 0, 2 147 | stream.write pixel.slice 2, 3 148 | stream.write pixel.slice 3, 4 149 | stream.end() 150 | 151 | it "should transform a stream of multiple pixels", (done) -> 152 | meta = 153 | bpp: 32 154 | red_offset: 16 155 | red_length: 8 156 | green_offset: 8 157 | green_length: 8 158 | blue_offset: 0 159 | blue_length: 8 160 | alpha_offset: 24 161 | alpha_length: 8 162 | pixel1 = new Buffer 4 163 | pixel1.writeUInt8 50, 0 164 | pixel1.writeUInt8 100, 1 165 | pixel1.writeUInt8 150, 2 166 | pixel1.writeUInt8 200, 3 167 | pixel2 = new Buffer 4 168 | pixel2.writeUInt8 51, 0 169 | pixel2.writeUInt8 101, 1 170 | pixel2.writeUInt8 151, 2 171 | pixel2.writeUInt8 201, 3 172 | stream = new Stream.PassThrough 173 | transform = new RgbTransform meta 174 | stream.pipe transform 175 | all = new Buffer '' 176 | transform.on 'data', (chunk) -> 177 | all = Buffer.concat [all, chunk] 178 | transform.on 'end', -> 179 | expect(all).to.have.length 15 180 | expect(all.readUInt8 0).to.equal 150 181 | expect(all.readUInt8 1).to.equal 100 182 | expect(all.readUInt8 2).to.equal 50 183 | expect(all.readUInt8 3).to.equal 151 184 | expect(all.readUInt8 4).to.equal 101 185 | expect(all.readUInt8 5).to.equal 51 186 | done() 187 | stream.write pixel1 188 | stream.write pixel2 189 | stream.write pixel1 190 | stream.write pixel2 191 | stream.write pixel1 192 | stream.end() 193 | -------------------------------------------------------------------------------- /test/adb/linetransform.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = Chai 6 | 7 | LineTransform = require '../../src/adb/linetransform' 8 | MockDuplex = require '../mock/duplex' 9 | 10 | describe 'LineTransform', -> 11 | 12 | it "should implement stream.Transform", (done) -> 13 | expect(new LineTransform).to.be.an.instanceOf Stream.Transform 14 | done() 15 | 16 | describe 'with autoDetect', -> 17 | it "should not modify data if first byte is 0x0a", (done) -> 18 | duplex = new MockDuplex 19 | transform = new LineTransform autoDetect: true 20 | transform.on 'data', (data) -> 21 | expect(data.toString()).to.equal 'bar\r\n' 22 | done() 23 | duplex.pipe transform 24 | duplex.causeRead '\nbar\r\n' 25 | duplex.causeEnd() 26 | 27 | it "should not include initial 0x0a", (done) -> 28 | duplex = new MockDuplex 29 | transform = new LineTransform autoDetect: true 30 | buffer = new Buffer '' 31 | transform.on 'data', (data) -> 32 | buffer = Buffer.concat [buffer, data] 33 | transform.on 'end', -> 34 | expect(buffer.toString()).to.equal 'bar\r\n' 35 | done() 36 | duplex.pipe transform 37 | duplex.causeRead '\nbar\r\n' 38 | duplex.causeEnd() 39 | 40 | it "should not include initial 0x0d 0x0a", (done) -> 41 | duplex = new MockDuplex 42 | transform = new LineTransform autoDetect: true 43 | buffer = new Buffer '' 44 | transform.on 'data', (data) -> 45 | buffer = Buffer.concat [buffer, data] 46 | transform.on 'end', -> 47 | expect(buffer.toString()).to.equal 'bar\n' 48 | done() 49 | duplex.pipe transform 50 | duplex.causeRead '\r\nbar\r\n' 51 | duplex.causeEnd() 52 | 53 | it "should not include initial 0x0d 0x0a even if in separate 54 | chunks", (done) -> 55 | duplex = new MockDuplex 56 | transform = new LineTransform autoDetect: true 57 | buffer = new Buffer '' 58 | transform.on 'data', (data) -> 59 | buffer = Buffer.concat [buffer, data] 60 | transform.on 'end', -> 61 | expect(buffer.toString()).to.equal 'bar\n' 62 | done() 63 | duplex.pipe transform 64 | duplex.causeRead '\r' 65 | duplex.causeRead '\nbar\r\n' 66 | duplex.causeEnd() 67 | 68 | it "should transform as usual if first byte is not 0x0a", (done) -> 69 | duplex = new MockDuplex 70 | transform = new LineTransform autoDetect: true 71 | buffer = new Buffer '' 72 | transform.on 'data', (data) -> 73 | buffer = Buffer.concat [buffer, data] 74 | transform.on 'end', -> 75 | expect(buffer.toString()).to.equal 'bar\nfoo' 76 | done() 77 | duplex.pipe transform 78 | duplex.causeRead '\r\nbar\r\nfoo' 79 | duplex.causeEnd() 80 | 81 | describe 'without autoDetect', -> 82 | it "should transform as usual even if first byte is 0x0a", (done) -> 83 | duplex = new MockDuplex 84 | transform = new LineTransform 85 | buffer = new Buffer '' 86 | transform.on 'data', (data) -> 87 | buffer = Buffer.concat [buffer, data] 88 | transform.on 'end', -> 89 | expect(buffer.toString()).to.equal '\n\nbar\nfoo' 90 | done() 91 | duplex.pipe transform 92 | duplex.causeRead '\n\r\nbar\r\nfoo' 93 | duplex.causeEnd() 94 | 95 | it "should not modify data that does not have 0x0d 0x0a in it", (done) -> 96 | duplex = new MockDuplex 97 | transform = new LineTransform 98 | transform.on 'data', (data) -> 99 | expect(data.toString()).to.equal 'foo' 100 | done() 101 | duplex.pipe transform 102 | duplex.causeRead 'foo' 103 | duplex.causeEnd() 104 | 105 | it "should not remove 0x0d if not followed by 0x0a", (done) -> 106 | duplex = new MockDuplex 107 | transform = new LineTransform 108 | transform.on 'data', (data) -> 109 | expect(data.length).to.equal 2 110 | expect(data[0]).to.equal 0x0d 111 | expect(data[1]).to.equal 0x05 112 | done() 113 | duplex.pipe transform 114 | duplex.causeRead new Buffer [0x0d, 0x05] 115 | duplex.causeEnd() 116 | 117 | it "should remove 0x0d if followed by 0x0a", (done) -> 118 | duplex = new MockDuplex 119 | transform = new LineTransform 120 | transform.on 'data', (data) -> 121 | expect(data.length).to.equal 2 122 | expect(data[0]).to.equal 0x0a 123 | expect(data[1]).to.equal 0x97 124 | done() 125 | duplex.pipe transform 126 | duplex.causeRead new Buffer [0x0d, 0x0a, 0x97] 127 | duplex.causeEnd() 128 | 129 | it "should push 0x0d without 0x0a if last in stream", (done) -> 130 | duplex = new MockDuplex 131 | transform = new LineTransform 132 | transform.on 'data', (data) -> 133 | expect(data.length).to.equal 1 134 | expect(data[0]).to.equal 0x0d 135 | done() 136 | duplex.pipe transform 137 | duplex.causeRead new Buffer [0x0d] 138 | duplex.causeEnd() 139 | 140 | it "should push saved 0x0d if next chunk does not start with 0x0a", (done) -> 141 | duplex = new MockDuplex 142 | transform = new LineTransform 143 | buffer = new Buffer '' 144 | transform.on 'data', (data) -> 145 | buffer = Buffer.concat [buffer, data] 146 | transform.on 'end', -> 147 | expect(buffer).to.have.length 3 148 | expect(buffer[0]).to.equal 0x62 149 | expect(buffer[1]).to.equal 0x0d 150 | expect(buffer[2]).to.equal 0x37 151 | done() 152 | duplex.pipe transform 153 | duplex.causeRead new Buffer [0x62, 0x0d] 154 | duplex.causeRead new Buffer [0x37] 155 | duplex.causeEnd() 156 | duplex.end() 157 | 158 | it "should remove saved 0x0d if next chunk starts with 0x0a", (done) -> 159 | duplex = new MockDuplex 160 | transform = new LineTransform 161 | buffer = new Buffer '' 162 | transform.on 'data', (data) -> 163 | buffer = Buffer.concat [buffer, data] 164 | transform.on 'end', -> 165 | expect(buffer).to.have.length 2 166 | expect(buffer[0]).to.equal 0x62 167 | expect(buffer[1]).to.equal 0x0a 168 | done() 169 | duplex.pipe transform 170 | duplex.causeRead new Buffer [0x62, 0x0d] 171 | duplex.causeRead new Buffer [0x0a] 172 | duplex.causeEnd() 173 | duplex.end() 174 | -------------------------------------------------------------------------------- /test/adb/parser.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Promise = require 'bluebird' 3 | Sinon = require 'sinon' 4 | Chai = require 'chai' 5 | Chai.use require 'sinon-chai' 6 | {expect} = require 'chai' 7 | 8 | Parser = require '../../src/adb/parser' 9 | 10 | describe 'Parser', -> 11 | 12 | describe 'end()', -> 13 | 14 | it "should end the stream and consume all remaining data", (done) -> 15 | stream = new Stream.PassThrough 16 | parser = new Parser stream 17 | stream.write 'F' 18 | stream.write 'O' 19 | stream.write 'O' 20 | parser.end() 21 | .then -> 22 | done() 23 | 24 | describe 'readAll()', -> 25 | 26 | it "should return a cancellable Promise", (done) -> 27 | stream = new Stream.PassThrough 28 | parser = new Parser stream 29 | promise = parser.readAll() 30 | expect(promise).to.be.an.instanceOf Promise 31 | expect(promise.isCancellable()).to.be.true 32 | promise.catch Promise.CancellationError, (err) -> 33 | done() 34 | promise.cancel() 35 | 36 | it "should read all remaining content until the stream ends", (done) -> 37 | stream = new Stream.PassThrough 38 | parser = new Parser stream 39 | parser.readAll() 40 | .then (buf) -> 41 | expect(buf.length).to.equal 3 42 | expect(buf.toString()).to.equal 'FOO' 43 | done() 44 | stream.write 'F' 45 | stream.write 'O' 46 | stream.write 'O' 47 | stream.end() 48 | 49 | it "should resolve with an empty Buffer if the stream has already ended 50 | and there's nothing more to read", (done) -> 51 | stream = new Stream.PassThrough 52 | parser = new Parser stream 53 | parser.readAll() 54 | .then (buf) -> 55 | expect(buf.length).to.equal 0 56 | done() 57 | stream.end() 58 | 59 | describe 'readBytes(howMany)', -> 60 | 61 | it "should return a cancellable Promise", (done) -> 62 | stream = new Stream.PassThrough 63 | parser = new Parser stream 64 | promise = parser.readBytes 1 65 | expect(promise).to.be.an.instanceOf Promise 66 | expect(promise.isCancellable()).to.be.true 67 | promise.catch Promise.CancellationError, (err) -> 68 | done() 69 | promise.cancel() 70 | 71 | it "should read as many bytes as requested", (done) -> 72 | stream = new Stream.PassThrough 73 | parser = new Parser stream 74 | parser.readBytes 4 75 | .then (buf) -> 76 | expect(buf.length).to.equal 4 77 | expect(buf.toString()).to.equal 'OKAY' 78 | parser.readBytes 2 79 | .then (buf) -> 80 | expect(buf).to.have.length 2 81 | expect(buf.toString()).to.equal 'FA' 82 | done() 83 | stream.write 'OKAYFAIL' 84 | 85 | it "should wait for enough data to appear", (done) -> 86 | stream = new Stream.PassThrough 87 | parser = new Parser stream 88 | parser.readBytes 5 89 | .then (buf) -> 90 | expect(buf.toString()).to.equal 'BYTES' 91 | done() 92 | Promise.delay 50 93 | .then -> 94 | stream.write 'BYTES' 95 | 96 | it "should keep data waiting even when nothing has been 97 | requested", (done) -> 98 | stream = new Stream.PassThrough 99 | parser = new Parser stream 100 | stream.write 'FOO' 101 | Promise.delay 50 102 | .then -> 103 | parser.readBytes 2 104 | .then (buf) -> 105 | expect(buf.length).to.equal 2 106 | expect(buf.toString()).to.equal 'FO' 107 | done() 108 | 109 | it "should reject with Parser.PrematureEOFError if stream ends 110 | before enough bytes can be read", (done) -> 111 | stream = new Stream.PassThrough 112 | parser = new Parser stream 113 | stream.write 'F' 114 | parser.readBytes 10 115 | .catch Parser.PrematureEOFError, (err) -> 116 | expect(err.missingBytes).to.equal 9 117 | done() 118 | stream.end() 119 | 120 | describe 'readByteFlow(maxHowMany, targetStream)', -> 121 | 122 | it "should return a cancellable Promise", (done) -> 123 | stream = new Stream.PassThrough 124 | parser = new Parser stream 125 | target = new Stream.PassThrough 126 | promise = parser.readByteFlow 1, target 127 | expect(promise).to.be.an.instanceOf Promise 128 | expect(promise.isCancellable()).to.be.true 129 | promise.catch Promise.CancellationError, (err) -> 130 | done() 131 | promise.cancel() 132 | 133 | it "should read as many bytes as requested", (done) -> 134 | stream = new Stream.PassThrough 135 | parser = new Parser stream 136 | target = new Stream.PassThrough 137 | parser.readByteFlow 4, target 138 | .then -> 139 | expect(target.read()).to.eql new Buffer('OKAY') 140 | parser.readByteFlow 2, target 141 | .then -> 142 | expect(target.read()).to.eql new Buffer('FA') 143 | done() 144 | .catch done 145 | stream.write 'OKAYFAIL' 146 | 147 | it "should progress with new/partial chunk until maxHowMany", (done) -> 148 | stream = new Stream.PassThrough 149 | parser = new Parser stream 150 | target = new Stream.PassThrough 151 | parser.readByteFlow 3, target 152 | .then -> 153 | expect(target.read()).to.eql new Buffer('PIE') 154 | done() 155 | .catch done 156 | b1 = new Buffer 'P' 157 | b2 = new Buffer 'I' 158 | b3 = new Buffer 'ES' 159 | b4 = new Buffer 'R' 160 | stream.write b1 161 | stream.write b2 162 | stream.write b3 163 | stream.write b4 164 | 165 | describe 'readAscii(howMany)', -> 166 | 167 | it "should return a cancellable Promise", (done) -> 168 | stream = new Stream.PassThrough 169 | parser = new Parser stream 170 | promise = parser.readAscii 1 171 | expect(promise).to.be.an.instanceOf Promise 172 | expect(promise.isCancellable()).to.be.true 173 | promise.catch Promise.CancellationError, (err) -> 174 | done() 175 | promise.cancel() 176 | 177 | it "should read as many ascii characters as requested", (done) -> 178 | stream = new Stream.PassThrough 179 | parser = new Parser stream 180 | parser.readAscii 4 181 | .then (str) -> 182 | expect(str.length).to.equal 4 183 | expect(str).to.equal 'OKAY' 184 | done() 185 | stream.write 'OKAYFAIL' 186 | 187 | it "should reject with Parser.PrematureEOFError if stream ends 188 | before enough bytes can be read", (done) -> 189 | stream = new Stream.PassThrough 190 | parser = new Parser stream 191 | stream.write 'FOO' 192 | parser.readAscii 7 193 | .catch Parser.PrematureEOFError, (err) -> 194 | expect(err.missingBytes).to.equal 4 195 | done() 196 | stream.end() 197 | 198 | describe 'readValue()', -> 199 | 200 | it "should return a cancellable Promise", (done) -> 201 | stream = new Stream.PassThrough 202 | parser = new Parser stream 203 | promise = parser.readValue() 204 | expect(promise).to.be.an.instanceOf Promise 205 | expect(promise.isCancellable()).to.be.true 206 | promise.catch Promise.CancellationError, (err) -> 207 | done() 208 | promise.cancel() 209 | 210 | it "should read a protocol value as a Buffer", (done) -> 211 | stream = new Stream.PassThrough 212 | parser = new Parser stream 213 | parser.readValue() 214 | .then (value) -> 215 | expect(value).to.be.an.instanceOf Buffer 216 | expect(value).to.have.length 4 217 | expect(value.toString()).to.equal '001f' 218 | done() 219 | stream.write '0004001f' 220 | 221 | it "should return an empty value", (done) -> 222 | stream = new Stream.PassThrough 223 | parser = new Parser stream 224 | parser.readValue() 225 | .then (value) -> 226 | expect(value).to.be.an.instanceOf Buffer 227 | expect(value).to.have.length 0 228 | done() 229 | stream.write '0000' 230 | 231 | it "should reject with Parser.PrematureEOFError if stream ends 232 | before the value can be read", (done) -> 233 | stream = new Stream.PassThrough 234 | parser = new Parser stream 235 | parser.readValue() 236 | .catch Parser.PrematureEOFError, (err) -> 237 | done() 238 | stream.write '00ffabc' 239 | stream.end() 240 | 241 | describe 'readError()', -> 242 | 243 | it "should return a cancellable Promise", (done) -> 244 | stream = new Stream.PassThrough 245 | parser = new Parser stream 246 | promise = parser.readError() 247 | expect(promise).to.be.an.instanceOf Promise 248 | expect(promise.isCancellable()).to.be.true 249 | promise.catch Promise.CancellationError, (err) -> 250 | done() 251 | promise.cancel() 252 | 253 | it "should reject with Parser.FailError using the value", (done) -> 254 | stream = new Stream.PassThrough 255 | parser = new Parser stream 256 | parser.readError() 257 | .catch Parser.FailError, (err) -> 258 | expect(err.message).to.equal "Failure: 'epic failure'" 259 | done() 260 | stream.write '000cepic failure' 261 | 262 | it "should reject with Parser.PrematureEOFError if stream ends 263 | before the error can be read", (done) -> 264 | stream = new Stream.PassThrough 265 | parser = new Parser stream 266 | parser.readError() 267 | .catch Parser.PrematureEOFError, (err) -> 268 | done() 269 | stream.write '000cepic' 270 | stream.end() 271 | 272 | describe 'searchLine(re)', -> 273 | 274 | it "should return a cancellable Promise", (done) -> 275 | stream = new Stream.PassThrough 276 | parser = new Parser stream 277 | promise = parser.searchLine /foo/ 278 | expect(promise).to.be.an.instanceOf Promise 279 | expect(promise.isCancellable()).to.be.true 280 | promise.catch Promise.CancellationError, (err) -> 281 | done() 282 | promise.cancel() 283 | 284 | it "should return the re.exec match of the matching line", (done) -> 285 | stream = new Stream.PassThrough 286 | parser = new Parser stream 287 | parser.searchLine /za(p)/ 288 | .then (line) -> 289 | expect(line[0]).to.equal 'zap' 290 | expect(line[1]).to.equal 'p' 291 | expect(line.input).to.equal 'zip zap' 292 | done() 293 | stream.write 'foo bar\nzip zap\npip pop\n' 294 | 295 | it "should reject with Parser.PrematureEOFError if stream ends 296 | before a line is found", (done) -> 297 | stream = new Stream.PassThrough 298 | parser = new Parser stream 299 | parser.searchLine /nope/ 300 | .catch Parser.PrematureEOFError, (err) -> 301 | done() 302 | stream.write 'foo bar' 303 | stream.end() 304 | 305 | describe 'readLine()', -> 306 | 307 | it "should return a cancellable Promise", (done) -> 308 | stream = new Stream.PassThrough 309 | parser = new Parser stream 310 | promise = parser.readLine() 311 | expect(promise).to.be.an.instanceOf Promise 312 | expect(promise.isCancellable()).to.be.true 313 | promise.catch Promise.CancellationError, (err) -> 314 | done() 315 | promise.cancel() 316 | 317 | it "should skip a line terminated by \\n", (done) -> 318 | stream = new Stream.PassThrough 319 | parser = new Parser stream 320 | parser.readLine() 321 | .then -> 322 | parser.readBytes 7 323 | .then (buf) -> 324 | expect(buf.toString()).to.equal 'zip zap' 325 | done() 326 | stream.write 'foo bar\nzip zap\npip pop' 327 | 328 | it "should return skipped line", (done) -> 329 | stream = new Stream.PassThrough 330 | parser = new Parser stream 331 | parser.readLine() 332 | .then (buf) -> 333 | expect(buf.toString()).to.equal 'foo bar' 334 | done() 335 | stream.write 'foo bar\nzip zap\npip pop' 336 | 337 | it "should strip trailing \\r", (done) -> 338 | stream = new Stream.PassThrough 339 | parser = new Parser stream 340 | parser.readLine() 341 | .then (buf) -> 342 | expect(buf.toString()).to.equal 'foo bar' 343 | done() 344 | stream.write 'foo bar\r\n' 345 | 346 | it "should reject with Parser.PrematureEOFError if stream ends 347 | before a line is found", (done) -> 348 | stream = new Stream.PassThrough 349 | parser = new Parser stream 350 | parser.readLine() 351 | .catch Parser.PrematureEOFError, (err) -> 352 | done() 353 | stream.write 'foo bar' 354 | stream.end() 355 | 356 | describe 'readUntil(code)', -> 357 | 358 | it "should return a cancellable Promise", (done) -> 359 | stream = new Stream.PassThrough 360 | parser = new Parser stream 361 | promise = parser.readUntil 0xa0 362 | expect(promise).to.be.an.instanceOf Promise 363 | expect(promise.isCancellable()).to.be.true 364 | promise.catch Promise.CancellationError, (err) -> 365 | done() 366 | promise.cancel() 367 | 368 | it "should return any characters before given value", (done) -> 369 | stream = new Stream.PassThrough 370 | parser = new Parser stream 371 | parser.readUntil 'p'.charCodeAt 0 372 | .then (buf) -> 373 | expect(buf.toString()).to.equal 'foo bar\nzi' 374 | done() 375 | stream.write 'foo bar\nzip zap\npip pop' 376 | 377 | it "should reject with Parser.PrematureEOFError if stream ends 378 | before a line is found", (done) -> 379 | stream = new Stream.PassThrough 380 | parser = new Parser stream 381 | parser.readUntil 'z'.charCodeAt 0 382 | .catch Parser.PrematureEOFError, (err) -> 383 | done() 384 | stream.write 'ho ho' 385 | stream.end() 386 | 387 | describe 'raw()', -> 388 | 389 | it "should return the resumed raw stream", (done) -> 390 | stream = new Stream.PassThrough 391 | parser = new Parser stream 392 | raw = parser.raw() 393 | expect(raw).to.equal stream 394 | raw.on 'data', -> 395 | done() 396 | raw.write 'foo' 397 | 398 | describe 'unexpected(data, expected)', -> 399 | 400 | it "should reject with Parser.UnexpectedDataError", (done) -> 401 | stream = new Stream.PassThrough 402 | parser = new Parser stream 403 | parser.unexpected('foo', "'bar' or end of stream") 404 | .catch Parser.UnexpectedDataError, (err) -> 405 | expect(err.message).to.equal "Unexpected 'foo', was expecting 'bar' 406 | or end of stream" 407 | expect(err.unexpected).to.equal 'foo' 408 | expect(err.expected).to.equal "'bar' or end of stream" 409 | done() 410 | -------------------------------------------------------------------------------- /test/adb/protocol.coffee: -------------------------------------------------------------------------------- 1 | {expect} = require 'chai' 2 | 3 | Protocol = require '../../src/adb/protocol' 4 | 5 | describe 'Protocol', -> 6 | 7 | it "should expose a 'FAIL' property", (done) -> 8 | expect(Protocol).to.have.property 'FAIL' 9 | expect(Protocol.FAIL).to.equal 'FAIL' 10 | done() 11 | 12 | it "should expose an 'OKAY' property", (done) -> 13 | expect(Protocol).to.have.property 'OKAY' 14 | expect(Protocol.OKAY).to.equal 'OKAY' 15 | done() 16 | 17 | describe '@decodeLength(length)', -> 18 | 19 | it "should return a Number", (done) -> 20 | expect(Protocol.decodeLength '0x0046').to.be.a 'number' 21 | done() 22 | 23 | it "should accept a hexadecimal string", (done) -> 24 | expect(Protocol.decodeLength '0x5887').to.equal 0x5887 25 | done() 26 | 27 | describe '@encodeLength(length)', -> 28 | 29 | it "should return a String", (done) -> 30 | expect(Protocol.encodeLength 27).to.be.a 'string' 31 | done() 32 | 33 | it "should return a valid hexadecimal number", (done) -> 34 | expect(parseInt Protocol.encodeLength(32), 16).to.equal 32 35 | expect(parseInt Protocol.encodeLength(9999), 16).to.equal 9999 36 | done() 37 | 38 | it "should return uppercase hexadecimal digits", (done) -> 39 | expect(Protocol.encodeLength(0x0abc)).to.equal '0ABC' 40 | done() 41 | 42 | it "should pad short values with zeroes for a 4-byte size", (done) -> 43 | expect(Protocol.encodeLength 1).to.have.length 4 44 | expect(Protocol.encodeLength 2).to.have.length 4 45 | expect(Protocol.encodeLength 57).to.have.length 4 46 | done() 47 | 48 | it "should return 0000 for 0 length", (done) -> 49 | expect(Protocol.encodeLength 0).to.equal '0000' 50 | done() 51 | 52 | describe '@encodeData(data)', -> 53 | 54 | it "should return a Buffer", (done) -> 55 | expect(Protocol.encodeData new Buffer '').to.be.an.instanceOf Buffer 56 | done() 57 | 58 | it "should accept a string or a Buffer", (done) -> 59 | expect(Protocol.encodeData '').to.be.an.instanceOf Buffer 60 | expect(Protocol.encodeData new Buffer '').to.be.an.instanceOf Buffer 61 | done() 62 | 63 | it "should prefix data with length", (done) -> 64 | data = Protocol.encodeData new Buffer 0x270F 65 | expect(data).to.have.length 0x270F + 4 66 | expect(data.toString 'ascii', 0, 4).to.equal '270F' 67 | done() 68 | -------------------------------------------------------------------------------- /test/adb/sync.coffee: -------------------------------------------------------------------------------- 1 | Fs = require 'fs' 2 | Stream = require 'stream' 3 | Promise = require 'bluebird' 4 | Sinon = require 'sinon' 5 | Chai = require 'chai' 6 | Chai.use require 'sinon-chai' 7 | {expect, assert} = Chai 8 | 9 | Adb = require '../../src/adb' 10 | Sync = require '../../src/adb/sync' 11 | Stats = require '../../src/adb/sync/stats' 12 | Entry = require '../../src/adb/sync/entry' 13 | PushTransfer = require '../../src/adb/sync/pushtransfer' 14 | PullTransfer = require '../../src/adb/sync/pulltransfer' 15 | MockConnection = require '../mock/connection' 16 | 17 | # This test suite is a bit special in that it requires a connected Android 18 | # device (or many devices). All will be tested. 19 | describe 'Sync', -> 20 | # By default, skip tests that require a device. 21 | dt = xit 22 | dt = it if process.env.RUN_DEVICE_TESTS 23 | 24 | SURELY_EXISTING_FILE = '/system/build.prop' 25 | SURELY_EXISTING_PATH = '/' 26 | SURELY_NONEXISTING_PATH = '/non-existing-path' 27 | SURELY_WRITABLE_FILE = '/data/local/tmp/_sync.test' 28 | 29 | client = null 30 | deviceList = null 31 | 32 | forEachSyncDevice = (iterator, done) -> 33 | assert deviceList.length > 0, 34 | 'At least one connected Android device is required' 35 | 36 | promises = deviceList.map (device) -> 37 | client.syncService device.id 38 | .then (sync) -> 39 | expect(sync).to.be.an.instanceof Sync 40 | Promise.cast iterator sync 41 | .finally -> 42 | sync.end() 43 | 44 | Promise.all promises 45 | .then -> 46 | done() 47 | .catch done 48 | 49 | before (done) -> 50 | client = Adb.createClient() 51 | client.listDevices() 52 | .then (devices) -> 53 | deviceList = devices 54 | done() 55 | 56 | describe 'end()', -> 57 | 58 | it "should end the sync connection", -> 59 | conn = new MockConnection 60 | sync = new Sync conn 61 | Sinon.stub conn, 'end' 62 | sync.end() 63 | expect(conn.end).to.have.been.called 64 | 65 | describe 'push(contents, path[, mode])', -> 66 | 67 | it "should call pushStream when contents is a Stream", -> 68 | conn = new MockConnection 69 | sync = new Sync conn 70 | stream = new Stream.PassThrough 71 | Sinon.stub sync, 'pushStream' 72 | sync.push stream, 'foo' 73 | expect(sync.pushStream).to.have.been.called 74 | 75 | it "should call pushFile when contents is a String", -> 76 | conn = new MockConnection 77 | sync = new Sync conn 78 | stream = new Stream.PassThrough 79 | Sinon.stub sync, 'pushFile' 80 | sync.push __filename, 'foo' 81 | expect(sync.pushFile).to.have.been.called 82 | 83 | it "should return a PushTransfer instance", -> 84 | conn = new MockConnection 85 | sync = new Sync conn 86 | stream = new Stream.PassThrough 87 | transfer = sync.push stream, 'foo' 88 | expect(transfer).to.be.an.instanceof PushTransfer 89 | transfer.cancel() 90 | 91 | describe 'pushStream(stream, path[, mode])', -> 92 | 93 | it "should return a PushTransfer instance", -> 94 | conn = new MockConnection 95 | sync = new Sync conn 96 | stream = new Stream.PassThrough 97 | transfer = sync.pushStream stream, 'foo' 98 | expect(transfer).to.be.an.instanceof PushTransfer 99 | transfer.cancel() 100 | 101 | dt "should be able to push >65536 byte chunks without error", (done) -> 102 | forEachSyncDevice (sync) -> 103 | new Promise (resolve, reject) -> 104 | stream = new Stream.PassThrough 105 | content = new Buffer 1000000 106 | transfer = sync.pushStream stream, SURELY_WRITABLE_FILE 107 | transfer.on 'error', reject 108 | transfer.on 'end', resolve 109 | stream.write content 110 | stream.end() 111 | , done 112 | 113 | describe 'pull(path)', -> 114 | 115 | dt "should retrieve the same content pushStream() pushed", (done) -> 116 | forEachSyncDevice (sync) -> 117 | new Promise (resolve, reject) -> 118 | stream = new Stream.PassThrough 119 | content = 'ABCDEFGHI' + Date.now() 120 | transfer = sync.pushStream stream, SURELY_WRITABLE_FILE 121 | expect(transfer).to.be.an.instanceof PushTransfer 122 | transfer.on 'error', reject 123 | transfer.on 'end', -> 124 | transfer = sync.pull SURELY_WRITABLE_FILE 125 | expect(transfer).to.be.an.instanceof PullTransfer 126 | transfer.on 'error', reject 127 | transfer.on 'readable', -> 128 | while chunk = transfer.read() 129 | expect(chunk).to.not.be.null 130 | expect(chunk.toString()).to.equal content 131 | return resolve() 132 | stream.write content 133 | stream.end() 134 | , done 135 | 136 | dt "should emit error for non-existing files", (done) -> 137 | forEachSyncDevice (sync) -> 138 | new Promise (resolve, reject) -> 139 | transfer = sync.pull SURELY_NONEXISTING_PATH 140 | transfer.on 'error', resolve 141 | , done 142 | 143 | dt "should return a PullTransfer instance", (done) -> 144 | forEachSyncDevice (sync) -> 145 | rval = sync.pull SURELY_EXISTING_FILE 146 | expect(rval).to.be.an.instanceof PullTransfer 147 | rval.cancel() 148 | , done 149 | 150 | describe 'Stream', -> 151 | 152 | dt "should emit 'end' when pull is done", (done) -> 153 | forEachSyncDevice (sync) -> 154 | new Promise (resolve, reject) -> 155 | transfer = sync.pull SURELY_EXISTING_FILE 156 | transfer.on 'error', reject 157 | transfer.on 'end', resolve 158 | transfer.resume() 159 | , done 160 | 161 | describe 'stat(path)', -> 162 | 163 | dt "should return a Promise", (done) -> 164 | forEachSyncDevice (sync) -> 165 | rval = sync.stat SURELY_EXISTING_PATH 166 | expect(rval).to.be.an.instanceof Promise 167 | rval 168 | , done 169 | 170 | dt "should call with an ENOENT error if the path does not exist", (done) -> 171 | forEachSyncDevice (sync) -> 172 | sync.stat SURELY_NONEXISTING_PATH 173 | .then (stats) -> 174 | throw new Error 'Should not reach success branch' 175 | .catch (err) -> 176 | expect(err).to.be.an.instanceof Error 177 | expect(err.code).to.equal 'ENOENT' 178 | expect(err.errno).to.equal 34 179 | expect(err.path).to.equal SURELY_NONEXISTING_PATH 180 | , done 181 | 182 | dt "should call with an fs.Stats instance for an existing path", (done) -> 183 | forEachSyncDevice (sync) -> 184 | sync.stat SURELY_EXISTING_PATH 185 | .then (stats) -> 186 | expect(stats).to.be.an.instanceof Fs.Stats 187 | , done 188 | 189 | describe 'Stats', -> 190 | 191 | it "should implement Fs.Stats", (done) -> 192 | expect(new Stats 0, 0, 0).to.be.an.instanceof Fs.Stats 193 | done() 194 | 195 | dt "should set the `.mode` property for isFile() etc", (done) -> 196 | forEachSyncDevice (sync) -> 197 | sync.stat SURELY_EXISTING_FILE 198 | .then (stats) -> 199 | expect(stats).to.be.an.instanceof Fs.Stats 200 | expect(stats.mode).to.be.above 0 201 | expect(stats.isFile()).to.be.true 202 | expect(stats.isDirectory()).to.be.false 203 | , done 204 | 205 | dt "should set the `.size` property", (done) -> 206 | forEachSyncDevice (sync) -> 207 | sync.stat SURELY_EXISTING_FILE 208 | .then (stats) -> 209 | expect(stats).to.be.an.instanceof Fs.Stats 210 | expect(stats.isFile()).to.be.true 211 | expect(stats.size).to.be.above 0 212 | , done 213 | 214 | dt "should set the `.mtime` property", (done) -> 215 | forEachSyncDevice (sync) -> 216 | sync.stat SURELY_EXISTING_FILE 217 | .then (stats) -> 218 | expect(stats).to.be.an.instanceof Fs.Stats 219 | expect(stats.mtime).to.be.an.instanceof Date 220 | , done 221 | 222 | describe 'Entry', -> 223 | 224 | it "should implement Stats", (done) -> 225 | expect(new Entry 'foo', 0, 0, 0).to.be.an.instanceof Stats 226 | done() 227 | 228 | dt "should set the `.name` property", (done) -> 229 | forEachSyncDevice (sync) -> 230 | sync.readdir SURELY_EXISTING_PATH 231 | .then (files) -> 232 | expect(files).to.be.an 'Array' 233 | files.forEach (file) -> 234 | expect(file.name).to.not.be.null 235 | expect(file).to.be.an.instanceof Entry 236 | , done 237 | 238 | dt "should set the Stats properties", (done) -> 239 | forEachSyncDevice (sync) -> 240 | sync.readdir SURELY_EXISTING_PATH 241 | .then (files) -> 242 | expect(files).to.be.an 'Array' 243 | files.forEach (file) -> 244 | expect(file.mode).to.not.be.null 245 | expect(file.size).to.not.be.null 246 | expect(file.mtime).to.not.be.null 247 | , done 248 | -------------------------------------------------------------------------------- /test/adb/tracker.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Sinon = require 'sinon' 3 | Chai = require 'chai' 4 | Chai.use require 'sinon-chai' 5 | {expect} = require 'chai' 6 | 7 | Parser = require '../../src/adb/parser' 8 | Tracker = require '../../src/adb/tracker' 9 | Protocol = require '../../src/adb/protocol' 10 | HostTrackDevicesCommand = require '../../src/adb/command/host/trackdevices' 11 | 12 | describe 'Tracker', -> 13 | 14 | beforeEach -> 15 | @writer = new Stream.PassThrough 16 | @conn = 17 | parser: new Parser @writer 18 | end: -> 19 | @cmd = new HostTrackDevicesCommand @conn 20 | @tracker = new Tracker @cmd 21 | 22 | it "should emit 'add' when a device is added", (done) -> 23 | spy = Sinon.spy() 24 | @tracker.on 'add', spy 25 | device1 = 26 | id: 'a', 27 | type: 'device' 28 | device2 = 29 | id: 'b', 30 | type: 'device' 31 | @tracker.update [device1, device2] 32 | expect(spy).to.have.been.calledTwice 33 | expect(spy).to.have.been.calledWith device1 34 | expect(spy).to.have.been.calledWith device2 35 | done() 36 | 37 | it "should emit 'remove' when a device is removed", (done) -> 38 | spy = Sinon.spy() 39 | @tracker.on 'remove', spy 40 | device1 = 41 | id: 'a', 42 | type: 'device' 43 | device2 = 44 | id: 'b', 45 | type: 'device' 46 | @tracker.update [device1, device2] 47 | @tracker.update [device1] 48 | expect(spy).to.have.been.calledOnce 49 | expect(spy).to.have.been.calledWith device2 50 | done() 51 | 52 | it "should emit 'change' when a device changes", (done) -> 53 | spy = Sinon.spy() 54 | @tracker.on 'change', spy 55 | deviceOld = 56 | id: 'a', 57 | type: 'device' 58 | deviceNew = 59 | id: 'a', 60 | type: 'offline' 61 | @tracker.update [deviceOld] 62 | @tracker.update [deviceNew] 63 | expect(spy).to.have.been.calledOnce 64 | expect(spy).to.have.been.calledWith deviceNew, deviceOld 65 | done() 66 | 67 | it "should emit 'changeSet' with all changes", (done) -> 68 | spy = Sinon.spy() 69 | @tracker.on 'changeSet', spy 70 | device1 = 71 | id: 'a', 72 | type: 'device' 73 | device2 = 74 | id: 'b', 75 | type: 'device' 76 | device3 = 77 | id: 'c', 78 | type: 'device' 79 | device3New = 80 | id: 'c', 81 | type: 'offline' 82 | device4 = 83 | id: 'd', 84 | type: 'offline' 85 | @tracker.update [device1, device2, device3] 86 | @tracker.update [device1, device3New, device4] 87 | expect(spy).to.have.been.calledTwice 88 | expect(spy).to.have.been.calledWith 89 | added: [device1, device2, device3] 90 | changed: [] 91 | removed: [] 92 | expect(spy).to.have.been.calledWith 93 | added: [device4] 94 | changed: [device3New] 95 | removed: [device2] 96 | done() 97 | 98 | it "should emit 'error' and 'end' when connection ends", (done) -> 99 | @tracker.on 'error', => 100 | @tracker.on 'end', -> 101 | done() 102 | @writer.end() 103 | 104 | it "should read devices from socket", (done) -> 105 | spy = Sinon.spy() 106 | @tracker.on 'changeSet', spy 107 | device1 = 108 | id: 'a', 109 | type: 'device' 110 | device2 = 111 | id: 'b', 112 | type: 'device' 113 | device3 = 114 | id: 'c', 115 | type: 'device' 116 | device3New = 117 | id: 'c', 118 | type: 'offline' 119 | device4 = 120 | id: 'd', 121 | type: 'offline' 122 | @writer.write Protocol.encodeData """ 123 | a\tdevice 124 | b\tdevice 125 | c\tdevice 126 | """ 127 | @writer.write Protocol.encodeData """ 128 | a\tdevice 129 | c\toffline 130 | d\toffline 131 | """ 132 | setTimeout -> 133 | expect(spy).to.have.been.calledTwice 134 | expect(spy).to.have.been.calledWith 135 | added: [device1, device2, device3] 136 | changed: [] 137 | removed: [] 138 | expect(spy).to.have.been.calledWith 139 | added: [device4] 140 | changed: [device3New] 141 | removed: [device2] 142 | done() 143 | , 10 144 | 145 | describe 'end()', -> 146 | 147 | it "should close the connection", (done) -> 148 | Sinon.spy @conn.parser, 'end' 149 | @tracker.on 'end', => 150 | expect(@conn.parser.end).to.have.been.calledOnce 151 | done() 152 | @tracker.end() 153 | 154 | it "should not cause an error to be emit", (done) -> 155 | spy = Sinon.spy() 156 | @tracker.on 'error', spy 157 | @tracker.on 'end', -> 158 | expect(spy).to.not.have.been.called 159 | done() 160 | @tracker.end() 161 | -------------------------------------------------------------------------------- /test/adb/util.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | Promise = require 'bluebird' 3 | Sinon = require 'sinon' 4 | Chai = require 'chai' 5 | Chai.use require 'sinon-chai' 6 | {expect} = require 'chai' 7 | 8 | util = require '../../src/adb/util' 9 | 10 | describe 'util', -> 11 | 12 | describe 'readAll(stream)', -> 13 | 14 | it "should return a cancellable Promise", (done) -> 15 | stream = new Stream.PassThrough 16 | promise = util.readAll(stream) 17 | expect(promise).to.be.an.instanceOf Promise 18 | expect(promise.isCancellable()).to.be.true 19 | promise.catch Promise.CancellationError, (err) -> 20 | done() 21 | promise.cancel() 22 | 23 | it "should read all remaining content until the stream ends", (done) -> 24 | stream = new Stream.PassThrough 25 | util.readAll(stream) 26 | .then (buf) -> 27 | expect(buf.length).to.equal 3 28 | expect(buf.toString()).to.equal 'FOO' 29 | done() 30 | stream.write 'F' 31 | stream.write 'O' 32 | stream.write 'O' 33 | stream.end() 34 | -------------------------------------------------------------------------------- /test/mock/connection.coffee: -------------------------------------------------------------------------------- 1 | Parser = require '../../src/adb/parser' 2 | MockDuplex = require './duplex' 3 | 4 | class MockConnection 5 | constructor: -> 6 | @socket = new MockDuplex 7 | @parser = new Parser @socket 8 | 9 | end: -> 10 | @socket.causeEnd() 11 | return this 12 | 13 | write: -> 14 | @socket.write.apply @socket, arguments 15 | return this 16 | 17 | module.exports = MockConnection 18 | -------------------------------------------------------------------------------- /test/mock/duplex.coffee: -------------------------------------------------------------------------------- 1 | Stream = require 'stream' 2 | 3 | class MockDuplex extends Stream.Duplex 4 | _read: (size) -> 5 | 6 | _write: (chunk, encoding, callback) -> 7 | @emit 'write', chunk, encoding, callback 8 | callback null 9 | return 10 | 11 | causeRead: (chunk) -> 12 | unless Buffer.isBuffer chunk 13 | chunk = new Buffer chunk 14 | this.push chunk 15 | return 16 | 17 | causeEnd: -> 18 | this.push null 19 | return 20 | 21 | end: -> 22 | this.causeEnd() # In order to better emulate socket streams 23 | Stream.Duplex.prototype.end.apply this, arguments 24 | 25 | module.exports = MockDuplex 26 | --------------------------------------------------------------------------------