diff --git a/.vscode/launch.json b/.vscode/launch.json index a8ed713..adff66a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,10 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], - "stopOnEntry": false + "stopOnEntry": false, + "skipFiles": [ + "/**" + ] }, { "name": "Server", @@ -16,7 +19,10 @@ "request": "launch", "cwd": "${workspaceRoot}", "program": "${workspaceRoot}/src/debugMain.js", - "args": [ "--server=4711" ] + "args": [ "--server=4711" ], + "skipFiles": [ + "/**" + ] }, { "name": "Launch Tests", diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c64cf..81de563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +### version 1.0.0 +* Update extension to support minimum version of node v10 +* refactoring and improvement of type-checking using jsdocs + ### version 0.8.0 * Try to extract Android manifest directly from APK * Added `manifestFile` launch configuration property diff --git a/extension.js b/extension.js index 24ef41f..fb54fc0 100644 --- a/extension.js +++ b/extension.js @@ -3,15 +3,6 @@ const vscode = require('vscode'); const { AndroidContentProvider } = require('./src/contentprovider'); const { openLogcatWindow } = require('./src/logcat'); -const state = require('./src/state'); - -function getADBPort() { - var defaultPort = 5037; - var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort); - if (typeof adbPort === 'number' && adbPort === (adbPort|0)) - return adbPort; - return defaultPort; -} // this method is called when your extension is activated // your extension is activated the very first time the command is executed @@ -20,29 +11,20 @@ function activate(context) { /* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */ AndroidContentProvider.register(context, vscode.workspace); - // logcat connections require the (fake) websocket proxy to be up - // - take the ADB port from launch.json - const wsproxyserver = require('./src/wsproxy').proxy.Server(6037, getADBPort()); - // The commandId parameter must match the command field in package.json - var disposables = [ + const disposables = [ // add the view logcat handler vscode.commands.registerCommand('android-dev-ext.view_logcat', () => { openLogcatWindow(vscode); }), - // watch for changes in the launch config - vscode.workspace.onDidChangeConfiguration(e => { - wsproxyserver.setADBPort(getADBPort()); - }) ]; - var spliceparams = [context.subscriptions.length,0].concat(disposables); - Array.prototype.splice.apply(context.subscriptions,spliceparams); + context.subscriptions.splice(context.subscriptions.length, 0, ...disposables); } -exports.activate = activate; - // this method is called when your extension is deactivated function deactivate() { } -exports.deactivate = deactivate; \ No newline at end of file + +exports.activate = activate; +exports.deactivate = deactivate; diff --git a/package-lock.json b/package-lock.json index 0a566d8..8a096cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,89 +1,83 @@ { "name": "android-dev-ext", - "version": "0.8.0", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", "dev": true, "requires": { - "@babel/highlight": "^7.0.0" + "@babel/highlight": "^7.8.3" } }, + "@babel/helper-validator-identifier": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", + "dev": true + }, "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, "@types/mocha": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", - "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", "dev": true }, "@types/node": { - "version": "10.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.11.tgz", - "integrity": "sha512-3iIOhNiPGTdcUNVCv9e5G7GotfvJJe2pc9w2UgDXlUwnxSZ3RgcUocIU+xYm+rTU54jIKih998QE4dMOyMN1NQ==", + "version": "10.17.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.20.tgz", + "integrity": "sha512-XgDgo6W10SeGEAM0k7FosJpvLCynOTYns4Xk3J5HGrA+UI/bKZ30PGMzOP5Lh2zs4259I71FSYLAtjnx3qhObw==", + "dev": true + }, + "@types/vscode": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.24.0.tgz", + "integrity": "sha512-Zu4LCYJXkMcae01zGf8IFcMeDHZo509MgK/OW7qo/Zw/ZoeYSHpchrsSVPHrXxNeFC5CPAZ210c7tM7EnImz+w==", "dev": true }, "acorn": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", - "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true }, "acorn-jsx": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", - "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "ajv": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", - "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, - "ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, "ansi-escapes": { - "version": "3.1.0", - "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", "dev": true }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -99,21 +93,6 @@ "color-convert": "^1.9.0" } }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true - }, - "append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", - "dev": true, - "requires": { - "buffer-equal": "^1.0.0" - } - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -123,123 +102,21 @@ "sprintf-js": "~1.0.2" } }, - "arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", - "dev": true - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", - "dev": true - }, - "array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, "big-integer": { - "version": "1.6.44", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.44.tgz", - "integrity": "sha512-7MzElZPTyJ2fNvBkPxtFQ2fWIkVmuzw41+BZHSzpEq3ymB2MfeKp1+yXl/tS75xCx+WnyV+yb0kp+K1C3UNwmQ==" + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" }, "binary": { "version": "0.3.0", @@ -250,15 +127,6 @@ "chainsaw": "~0.1.0" } }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, "bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -279,24 +147,6 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, - "buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, "buffer-indexof-polyfill": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz", @@ -307,25 +157,10 @@ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, "callsites": { - "version": "0.2.0", - "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, "chainsaw": { @@ -337,9 +172,9 @@ } }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -353,12 +188,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -369,40 +198,11 @@ } }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "dev": true }, - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "dev": true - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", - "dev": true - }, - "cloneable-readable": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", - "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -418,15 +218,6 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", @@ -438,15 +229,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -465,54 +247,21 @@ "which": "^1.2.9" } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, - "deep-assign": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz", - "integrity": "sha1-sJJ0O+hCfcYh6gBnzex+cN0Z83s=", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -520,20 +269,14 @@ "dev": true }, "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { "esutils": "^2.0.2" } }, - "duplexer": { - "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -542,36 +285,11 @@ "readable-stream": "^2.0.2" } }, - "duplexify": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", - "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true }, "escape-string-regexp": { "version": "1.0.5", @@ -580,55 +298,53 @@ "dev": true }, "eslint": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.9.0.tgz", - "integrity": "sha512-g4KWpPdqN0nth+goDNICNXGfJF7nNnepthp46CAlJoJtC5K/cLu3NgCM3AHu1CkJ5Hzt9V0Y0PBAO6Ay/gGb+w==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "ajv": "^6.5.3", + "ajv": "^6.9.1", "chalk": "^2.1.0", "cross-spawn": "^6.0.5", "debug": "^4.0.1", - "doctrine": "^2.1.0", - "eslint-scope": "^4.0.0", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", "eslint-utils": "^1.3.1", "eslint-visitor-keys": "^1.0.0", - "espree": "^4.0.0", + "espree": "^5.0.1", "esquery": "^1.0.1", "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", + "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob": "^7.1.2", "globals": "^11.7.0", "ignore": "^4.0.6", + "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.1.0", - "is-resolvable": "^1.1.0", - "js-yaml": "^3.12.0", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.5", + "lodash": "^4.17.11", "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", "progress": "^2.0.0", "regexpp": "^2.0.1", - "require-uncached": "^1.0.3", "semver": "^5.5.1", "strip-ansi": "^4.0.0", "strip-json-comments": "^2.0.1", - "table": "^5.0.2", + "table": "^5.2.3", "text-table": "^0.2.0" } }, "eslint-scope": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", - "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -636,27 +352,27 @@ } }, "eslint-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", - "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.0.0" + "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", "dev": true }, "espree": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-4.1.0.tgz", - "integrity": "sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", "dev": true, "requires": { - "acorn": "^6.0.2", + "acorn": "^6.0.7", "acorn-jsx": "^5.0.0", "eslint-visitor-keys": "^1.0.0" } @@ -668,12 +384,20 @@ "dev": true }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } } }, "esrecurse": { @@ -686,51 +410,21 @@ } }, "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, - "event-stream": { - "version": "3.3.4", - "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", - "dev": true, - "requires": { - "kind-of": "^1.1.0" - } - }, "external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, "requires": { "chardet": "^0.7.0", @@ -738,22 +432,16 @@ "tmp": "^0.0.33" } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "fast-levenshtein": { @@ -762,15 +450,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -781,70 +460,42 @@ } }, "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", "dev": true, "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" + "flat-cache": "^2.0.1" } }, "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", "dev": true, "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, - "flush-write-stream": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true - }, - "fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -861,31 +512,16 @@ "rimraf": "2" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -895,44 +531,16 @@ "path-is-absolute": "^1.0.0" } }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - } - }, "globals": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", - "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, "growl": { "version": "1.10.5", @@ -940,250 +548,18 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, - "gulp-chmod": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-chmod/-/gulp-chmod-2.0.0.tgz", - "integrity": "sha1-AMOQuSigeZslGsz2MaoJ4BzGKZw=", - "dev": true, - "requires": { - "deep-assign": "^1.0.0", - "stat-mode": "^0.2.0", - "through2": "^2.0.0" - } - }, - "gulp-filter": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.1.0.tgz", - "integrity": "sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM=", - "dev": true, - "requires": { - "multimatch": "^2.0.0", - "plugin-error": "^0.1.2", - "streamfilter": "^1.0.5" - } - }, - "gulp-gunzip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.0.0.tgz", - "integrity": "sha1-FbdBFF6Dqcb1CIYkG1fMWHHxUak=", - "dev": true, - "requires": { - "through2": "~0.6.5", - "vinyl": "~0.4.6" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true, - "requires": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - } - } - }, - "gulp-remote-src-vscode": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/gulp-remote-src-vscode/-/gulp-remote-src-vscode-0.5.1.tgz", - "integrity": "sha512-mw4OGjtC/jlCWJFhbcAlel4YPvccChlpsl3JceNiB/DLJi24/UPxXt53/N26lgI3dknEqd4ErfdHrO8sJ5bATQ==", - "dev": true, - "requires": { - "event-stream": "3.3.4", - "node.extend": "^1.1.2", - "request": "^2.79.0", - "through2": "^2.0.3", - "vinyl": "^2.0.1" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "gulp-untar": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.7.tgz", - "integrity": "sha512-0QfbCH2a1k2qkTLWPqTX+QO4qNsHn3kC546YhAP3/n0h+nvtyGITDuDrYBMDZeW4WnFijmkOvBWa5HshTic1tw==", - "dev": true, - "requires": { - "event-stream": "~3.3.4", - "streamifier": "~0.1.1", - "tar": "^2.2.1", - "through2": "~2.0.3", - "vinyl": "^1.2.0" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", - "dev": true - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "dev": true, - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "gulp-vinyl-zip": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz", - "integrity": "sha512-wJn09jsb8PyvUeyFF7y7ImEJqJwYy40BqL9GKfJs6UGpaGW9A+N68Q+ajsIpb9AeR6lAdjMbIdDPclIGo1/b7Q==", - "dev": true, - "requires": { - "event-stream": "3.3.4", - "queue": "^4.2.1", - "through2": "^2.0.3", - "vinyl": "^2.0.2", - "vinyl-fs": "^3.0.3", - "yauzl": "^2.2.1", - "yazl": "^2.2.1" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, "he": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1199,6 +575,16 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1215,157 +601,60 @@ } }, "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "inquirer": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", - "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", - "external-editor": "^3.0.0", + "external-editor": "^3.0.3", "figures": "^2.0.0", - "lodash": "^4.17.10", + "lodash": "^4.17.12", "mute-stream": "0.0.7", "run-async": "^2.2.0", - "rxjs": "^6.1.0", + "rxjs": "^6.4.0", "string-width": "^2.1.0", - "strip-ansi": "^5.0.0", + "strip-ansi": "^5.1.0", "through": "^2.3.6" }, "dependencies": { "ansi-regex": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", - "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "strip-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", - "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^4.0.0" + "ansi-regex": "^4.1.0" } } } }, - "is": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", - "dev": true - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - }, - "is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", - "dev": true - }, - "is-obj": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "requires": { - "is-unc-path": "^1.0.0" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1377,12 +666,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1399,87 +682,18 @@ "esprima": "^4.0.0" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", - "dev": true - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "dev": true, - "requires": { - "readable-stream": "^2.0.5" - } - }, - "lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", - "dev": true, - "requires": { - "flush-write-stream": "^1.0.2" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -1496,9 +710,9 @@ "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" }, "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "long": { @@ -1506,27 +720,6 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, - "map-stream": { - "version": "0.1.0", - "resolved": "http://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", - "dev": true - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", - "dev": true - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "dev": true, - "requires": { - "mime-db": "~1.37.0" - } - }, "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -1542,16 +735,16 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { - "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "mocha": { @@ -1596,6 +789,21 @@ "path-is-absolute": "^1.0.0" } }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1614,23 +822,11 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -1649,64 +845,6 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "node.extend": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.8.tgz", - "integrity": "sha512-L/dvEBwyg3UowwqOUTyDsGBU6kjBQOpOhshio9V3i3BMPv5YUb9+mWNN8MK0IbWqT0AqaTSONZf0aTuMMahWgA==", - "dev": true, - "requires": { - "has": "^1.0.3", - "is": "^3.2.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "now-and-later": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.0.tgz", - "integrity": "sha1-vGHLtFbXnLMiB85HygUTb/Ln1u4=", - "dev": true, - "requires": { - "once": "^1.3.2" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1725,43 +863,37 @@ } }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" + "word-wrap": "~1.2.3" } }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { @@ -1776,46 +908,6 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, - "pause-stream": { - "version": "0.0.11", - "resolved": "http://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, - "requires": { - "through": "~2.3" - } - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", - "dev": true, - "requires": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" - } - }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -1823,74 +915,26 @@ "dev": true }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.2.tgz", - "integrity": "sha512-/OLz5F9beZUWwSHZDreXgap1XShX6W+DCHQCqwCF7uZ88s6uTlD2cR3JBE77SegCmNtb1Idst+NfmwcdU6KVhw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", - "dev": true - }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "querystringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz", - "integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==", - "dev": true - }, - "queue": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/queue/-/queue-4.5.1.tgz", - "integrity": "sha512-AMD7w5hRXcFSb8s9u38acBZ+309u6GsiibP4/0YacJeaurRshogB7v/ZcVPxP5gD5+zIw6ixRHdutiYUJfwKHw==", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1907,98 +951,12 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, - "remove-bom-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - } - }, - "remove-bom-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", - "dev": true, - "requires": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", - "dev": true, - "requires": { - "value-or-function": "^3.0.0" - } - }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -2010,26 +968,26 @@ } }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", "dev": true, "requires": { "is-promise": "^2.1.0" } }, "rxjs": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", - "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -2047,9 +1005,9 @@ "dev": true }, "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, "setimmediate": { @@ -2073,15 +1031,15 @@ "dev": true }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, "slice-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.0.0.tgz", - "integrity": "sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", "dev": true, "requires": { "ansi-styles": "^3.2.0", @@ -2089,90 +1047,12 @@ "is-fullwidth-code-point": "^2.0.0" } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "split": { - "version": "0.3.3", - "resolved": "http://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", - "dev": true, - "requires": { - "through": "2" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sshpk": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", - "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stat-mode": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", - "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", - "dev": true - }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", - "dev": true, - "requires": { - "duplexer": "~0.1.1" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true - }, - "streamfilter": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-1.0.7.tgz", - "integrity": "sha512-Gk6KZM+yNA1JpW0KzlZIhjo3EaBJDkYfXtYSbOwNIQ7Zd6006E6+sCFlW1NDvFG/vnXhKmw6TJJgiEQg/8lXfQ==", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "streamifier": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", - "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", - "dev": true - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -2185,7 +1065,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -2216,26 +1096,43 @@ } }, "table": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/table/-/table-5.1.1.tgz", - "integrity": "sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", "dev": true, "requires": { - "ajv": "^6.6.1", - "lodash": "^4.17.11", - "slice-ansi": "2.0.0", - "string-width": "^2.1.1" - } - }, - "tar": { - "version": "2.2.2", - "resolved": "http://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } } }, "text-table": { @@ -2246,30 +1143,10 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "through2-filter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", - "dev": true, - "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -2279,67 +1156,15 @@ "os-tmpdir": "~1.0.2" } }, - "to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - } - }, - "to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", - "dev": true, - "requires": { - "through2": "^2.0.3" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" }, "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", "dev": true }, "type-check": { @@ -2352,31 +1177,15 @@ } }, "typescript": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.1.tgz", - "integrity": "sha512-jw7P2z/h6aPT4AENXDGjcfHTu5CSqzsbZc6YlUIebTyBAq8XaKp78x7VcSh30xwSCcsu5irZkYZUSFP1MrAMbg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true - }, - "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", - "dev": true, - "requires": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" - } - }, "unzipper": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.4.tgz", - "integrity": "sha512-7fmeZe+1J5D5fRMl4Zl4Chn+y+Os0bFgBrzc0Iuv3Xj+xFY0mrRUxzbrj8ZUO/lkyErJxtZJ5a13pLXc+KxvRg==", + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", + "integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==", "requires": { "big-integer": "^1.6.17", "binary": "~0.3.0", @@ -2384,6 +1193,7 @@ "buffer-indexof-polyfill": "~1.0.0", "duplexer2": "~0.1.4", "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", "listenercount": "~1.0.1", "readable-stream": "~2.3.6", "setimmediate": "~1.0.4" @@ -2398,284 +1208,29 @@ "punycode": "^2.1.0" } }, - "url-parse": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.4.tgz", - "integrity": "sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==", - "dev": true, - "requires": { - "querystringify": "^2.0.0", - "requires-port": "^1.0.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "value-or-function": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vinyl": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "dev": true, - "requires": { - "clone": "^0.2.0", - "clone-stats": "^0.0.1" - } - }, - "vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "requires": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "vinyl-source-stream": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vinyl-source-stream/-/vinyl-source-stream-1.1.2.tgz", - "integrity": "sha1-YrU6E1YQqJbpjKlr7jqH8Aio54A=", - "dev": true, - "requires": { - "through2": "^2.0.3", - "vinyl": "^0.4.3" - } - }, - "vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", - "dev": true, - "requires": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "vscode": { - "version": "1.1.26", - "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.26.tgz", - "integrity": "sha512-z1Nf5J38gjUFbuDCbJHPN6OJ//5EG+e/yHlh6ERxj/U9B2Qc3aiHaFr38/fee/GGnxvRw/XegLMOG+UJwKi/Qg==", - "dev": true, - "requires": { - "glob": "^7.1.2", - "gulp-chmod": "^2.0.0", - "gulp-filter": "^5.0.1", - "gulp-gunzip": "1.0.0", - "gulp-remote-src-vscode": "^0.5.1", - "gulp-untar": "^0.0.7", - "gulp-vinyl-zip": "^2.1.2", - "mocha": "^4.0.1", - "request": "^2.88.0", - "semver": "^5.4.1", - "source-map-support": "^0.5.0", - "url-parse": "^1.4.3", - "vinyl-fs": "^3.0.3", - "vinyl-source-stream": "^1.1.0" - }, - "dependencies": { - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", - "dev": true - }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", - "dev": true - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", - "dev": true, - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", - "mkdirp": "0.5.1", - "supports-color": "4.4.0" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", - "dev": true, - "requires": { - "has-flag": "^2.0.0" - } - } - } + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "vscode-debugadapter": { - "version": "1.32.1", - "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.32.1.tgz", - "integrity": "sha512-D7hpwmh4mPNJXLavginjGYbeIpSJj7L/FhfB4EqEnJH2om8jTvnMq6NvOyuHONn6YzarS/L3oAye7RNro9iviw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.40.0.tgz", + "integrity": "sha512-cudm9ROtFRxiBgcM+B8cQXA1DfsRKaOfEYDMh9upxbYxN3v0c40SHCPmNivIYp7LDzcG60UGaIYD1vsUfC1Qcg==", "requires": { "mkdirp": "^0.5.1", - "vscode-debugprotocol": "1.32.0" + "vscode-debugprotocol": "1.40.0" } }, "vscode-debugprotocol": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.32.0.tgz", - "integrity": "sha512-x3+HV+BkLqfl1ZuDJEILAv1sT5mDceuPThDXD12hwXAEjAdfc6MLQFvaoVPuO6C6gb+lHQTd0R9FNkCflEJHbA==" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.40.0.tgz", + "integrity": "sha512-Fwze+9qbLDPuQUhtITJSu/Vk6zIuakNM1iR2ZiZRgRaMEgBpMs2JSKaT0chrhJHCOy6/UbpsUbUBIseF6msV+g==" }, "which": { "version": "1.3.1", @@ -2686,10 +1241,10 @@ "isexe": "^2.0.0" } }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, "wrappy": { @@ -2698,56 +1253,28 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", "dev": true, "requires": { "mkdirp": "^0.5.1" } }, "ws": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", - "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", - "requires": { - "async-limiter": "^1.0.0" - } + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz", + "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==" }, "xmldom": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", - "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==" }, "xpath": { "version": "0.0.27", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3" - } } } } diff --git a/package.json b/package.json index 1d74e3d..f0cfb7d 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,12 @@ "name": "android-dev-ext", "displayName": "Android", "description": "Android debugging support for VS Code", - "version": "0.8.0", + "version": "1.0.0", "publisher": "adelphes", "preview": true, "license": "MIT", "engines": { - "vscode": "^1.8.0" + "vscode": "^1.24.0" }, "categories": [ "Debuggers" @@ -148,15 +148,14 @@ ] }, "scripts": { - "prepare": "node ./node_modules/vscode/bin/install", "test": "node ./node_modules/vscode/bin/test" }, "dependencies": { "long": "^4.0.0", - "unzipper": "^0.10.4", + "unzipper": "^0.10.10", "uuid": "^3.3.2", - "vscode-debugadapter": "^1.32.0", - "vscode-debugprotocol": "^1.32.0", + "vscode-debugadapter": "^1.40.0", + "vscode-debugprotocol": "^1.40.0", "ws": "^7.1.2", "xmldom": "^0.1.27", "xpath": "^0.0.27" @@ -164,9 +163,9 @@ "devDependencies": { "@types/mocha": "^5.2.5", "@types/node": "^10.12.5", + "@types/vscode": "1.24.0", "eslint": "^5.9.0", "mocha": "^5.2.0", - "typescript": "^3.1.6", - "vscode": "^1.1.26" + "typescript": "^3.8.3" } } diff --git a/src/adbclient.js b/src/adbclient.js index 54fc5ea..5794fe4 100644 --- a/src/adbclient.js +++ b/src/adbclient.js @@ -1,826 +1,223 @@ /* - ADBClient: class to manage connection and commands to adb (via the Dex plugin) running on the local machine. + ADBClient: class to manage commands to ADB */ -const _JDWP = require('./jdwp')._JDWP; -const $ = require('./jq-promise'); -const WebSocket = require('./minwebsocket').WebSocketClient; -const { atob,btoa,D } = require('./util'); +const JDWPSocket = require('./sockets/jdwpsocket'); +const ADBSocket = require('./sockets/adbsocket'); -function ADBClient(deviceid) { - this.deviceid = deviceid; - this.status = 'notinit'; - this.reset(); - this.JDWP = new _JDWP(); +/** + * + * @param {string} data + * @param {boolean} [extended] + */ +function parse_device_list(data, extended = false) { + var lines = data.trim().split(/\r\n?|\n/); + lines.sort(); + const devicelist = []; + if (extended) { + for (let i = 0, m; i < lines.length; i++) { + try { + m = JSON.parse(lines[i]); + } catch (e) { continue; } + if (!m) continue; + m.num = i; + devicelist.push(m); + } + } else { + for (let i = 0; i < lines.length; i++) { + const m = lines[i].match(/([^\t]+)\t([^\t]+)/); + if (!m) continue; + devicelist.push({ + serial: m[1], + status: m[2], + num: i, + }); + } + } + return devicelist; } -ADBClient.prototype = { +class ADBClient { - reset : function() { - this.ws = null; - this.activepromise={}; - this.authdone=false; - this.fd=-1; - this.disconnect_reject_reason=null; - }, + /** + * @param {string} [deviceid] + * @param {number} [adbPort] the port number to connect to ADB + */ + constructor(deviceid, adbPort = ADBSocket.ADBPort) { + this.deviceid = deviceid; + this.adbsocket = null; + this.jdwp_socket = null; + this.adbPort = adbPort; + } - _parse_device_list:function(data, extended) { - var lines = atob(data).trim().split(/\r\n?|\n/); - lines.sort(); - var devicelist = []; - var i=0; - if (extended) { - for (var i=0; i < lines.length; i++) { - try { - var m = JSON.parse(lines[i]); - if (!m) continue; - m.num = i; - } catch(e) {continue;} - devicelist.push(m); - } - } else { - for (var i=0; i < lines.length; i++) { - var m = lines[i].match(/([^\t]+)\t([^\t]+)/); - if (!m) continue; - devicelist.push({ - serial: m[1], - status: m[2], - num:i, - }); - } + async test_adb_connection() { + try { + await this.connect_to_adb(); + await this.disconnect_from_adb(); + } catch(err) { + // if we fail, still resolve the promise, passing the error + return err; } + } + + async list_devices() { + await this.connect_to_adb() + const data = await this.adbsocket.cmd_and_reply('host:devices'); + const devicelist = parse_device_list(data); + await this.disconnect_from_adb(); return devicelist; - }, + } - track_devices_extended : function(o) { - var x = {o:o||{},deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd_read_status('track_devices', 'wa', this.fd, 'host:track-devices-extended'); - }) - .then(function(data) { - return this.dexcmd('ra', this.fd); - }) - .then(function(data) { - function nextdeviceinfo(data) { - this.dexcmd('ra', this.fd, null, {notimeout:true}) - .then(nextdeviceinfo); - var devicelist = this._parse_device_list(data, true); - x.o.ondevices(devicelist, this); - } - nextdeviceinfo.call(this, data); - x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, + /** + * Return a list of debuggable pids from the device + */ + async jdwp_list() { + await this.connect_to_adb(); + await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`); + const stdout = await this.adbsocket.cmd_and_read_stdout('jdwp'); + await this.disconnect_from_adb(); + return stdout.trim().split(/\r?\n|\r/); + } - finish_track_devices : function() { - return this.dexcmd('dc', this.fd) - .then(function() { - return this.proxy_disconnect(); - }); - }, + /** + * Setup ADB port-forwarding from a local port to a JDWP process + * @param {{localport:number, jdwp:number}} o + */ + async jdwp_forward(o) { + await this.connect_to_adb(); + await this.adbsocket.cmd_and_status(`host-serial:${this.deviceid}:forward:tcp:${o.localport};jdwp:${o.jdwp}`); + await this.disconnect_from_adb(); + return true; + } - test_adb_connection : function(o) { - var x = {o:o||{},deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [null, x.o.extra]); - }) - .fail(function(err) { - // if we fail, still resolve the deferred, passing the error - x.deferred.resolveWith(x.o.ths||this, [err, x.o.extra]); - }); - return x.deferred; - }, + /** + * remove all port-forwarding configs + */ + async forward_remove_all() { + await this.connect_to_adb(); + await this.adbsocket.cmd_and_status('host:killforward-all'); + await this.disconnect_from_adb(); + return true; + } - list_devices : function(o) { - var x = {o:o||{},deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd_read_status('list_devices', 'wa', this.fd, 'host:devices'); - }) - .then(function(data) { - return this.dexcmd('ra', this.fd); - }) - .then(function(data) { - x.devicelist = this._parse_device_list(data); - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [x.devicelist, x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, + /** + * Connect to the JDWP debugging client and perform the handshake + * @param {{localport:number, onreply:()=>void, ondisconnect:()=>void}} o + */ + async jdwp_connect(o) { + // note that upon success, this method does not close the connection (it must be left open for + // future commands to be sent over the jdwp socket) + this.jdwp_socket = new JDWPSocket(o.onreply, o.ondisconnect); + await this.jdwp_socket.connect(o.localport) + await this.jdwp_socket.start(); + return true; + } - jdwp_list : function(o) { - var x = {o:o||{},deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid); - }) - .then(function(data) { - return this.dexcmd_read_status('jdwp', 'wa', this.fd, 'jdwp'); - }) - .then(function(data) { - return this.dexcmd_read_stdout(this.fd); - }) - .then(function(data) { - this.stdout = data; - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [this.stdout.trim().split(/\r?\n|\r/g), x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, + /** + * Send a JDWP command to the device + * @param {{cmd}} o + */ + async jdwp_command(o) { + // send the raw command over the socket - the reply is received via the JDWP monitor + const reply = await this.jdwp_socket.cmd_and_reply(o.cmd); + return reply.decoded; + } - jdwp_forward : function(o) { - // localport:1234 - // jdwp:1234 - var x = {o:o,deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd_read_status('forward', 'wa', this.fd, 'host-serial:'+this.deviceid+':forward:tcp:'+x.o.localport+';jdwp:'+x.o.jdwp) - }) - .then(function(data) { - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, + /** + * Disconnect the JDWP socket + */ + async jdwp_disconnect() { + await this.jdwp_socket.disconnect(); + return true; + } - forward_remove_all : function(o) { - var x = {o:o||{},deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd_read_status('forward_remove_all', 'wa', this.fd, 'host:killforward-all'); - }) - .then(function(data) { - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, + /** + * Run a shell command on the connected device + * @param {{command:string}} o + */ + async shell_cmd(o) { + await this.connect_to_adb(); + await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`); + const stdout = await this.adbsocket.cmd_and_read_stdout(`shell:${o.command}`); + await this.disconnect_from_adb(); + return stdout; + } - jdwp_connect : function(o) { - // {localport:1234, onreply:fn()} - // note that upon success, this method does not close the connection - var x = {o:o,deferred:$.Deferred()}; - this.jdwpinfo = { - o: o, - localport: o.localport, - onreply: o.onreply, - received: [], - }; - this.proxy_connect() - .then(function() { - return this.dexcmd('cp', o.localport); - }) - .then(function(data) { - this.jdwpfd = data; - return this.dexcmd('wx', this.jdwpfd, 'JDWP-Handshake'); - }) - .then(function(data) { - return this.dexcmd_read_stdout(this.jdwpfd); - }) - .then(function(data) { - if (data!=='JDWP-Handshake') { - // disconnect and fail - return this.dexcmd('dc', this.jdwpfd) - .then(function() { - return this.proxy_disconnect_with_fail({cat:'jdwp', msg:'Invalid handshake response'}); - }); - } - // start the monitor - we don't want it terminated on timeout - return this.logsend('rj', 'rj '+this.jdwpfd, {notimeout:true}); - }) - .then(function() { - // the first rj reply is a blank ok message indicating the monitor - // has started - x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, - - jdwp_command : function(o) { - // cmd: JDWP.Command - // resolveonreply: true/false - - // send the raw command over the socket - the reply - // is received via the JDWP monitor - var x = {o:o,deferred:$.Deferred()}; - this.dexcmd('wx', this.jdwpfd, o.cmd.toRawString()) - .fail(function(err) { - o.cmd.deferred.rejectWith(o.ths||this, [err]); - }); - - o.cmd.deferred - .then(function(decoded,reply,command) { - x.deferred.resolveWith(x.o.ths||this, [decoded,x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - - return x.deferred; - }, - - jdwp_disconnect : function(o) { - var x = {o:o,deferred:$.Deferred()}; - this.dexcmd('dc', this.jdwpfd) - .then(function() { - delete this.jdwpfd; - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, - - readwritesocket : function(o) { - var x = {o:o,deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd('qs', this.fd, ''+o.port+':'+o.readlen+':'+o.data); - }) - .then(function(data) { - this.socket_reply = data; - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [this.socket_reply, x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, - - shell_cmd : function(o) { - // command='ls /' - // untilclosed=true - var x = {o:o,deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid); - }) - .then(function(data) { - return this.dexcmd_read_status('shell_cmd', 'wa', this.fd, 'shell:'+x.o.command); - }) - .then(function(data) { - return this.dexcmd_read_stdout(this.fd, !!x.o.untilclosed); - }) - .then(function(data) { - this.stdout = data; - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [this.stdout, x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, - - logcat : function(o) { + /** + * Starts the Logcat monitor. + * Logcat lines are passed back via onlog callback. If the device disconnects, onclose is called. + * @param {{onlog:(e)=>void, onclose:()=>void}} o + */ + async startLogcatMonitor(o) { // onlog:function(e) // onclose:function(e) - // data:anything - var x = {o:o,deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid); - }) - .then(function(data) { - return this.dexcmd_read_status('shell_cmd', 'wa', this.fd, 'shell:logcat -v time'); - }) - .then(function(data) { - // if there's no handler, just read the complete log and finish - if (!o.onlog) { - return this.dexcmd_read_stdout(this.fd) - .then(function(data) { - this.logcatbuffer = data; - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [this.logcatbuffer, x.o.extra]); - }); - } - - // start the logcat monitor - return this.dexcmd('so', this.fd) - .then(function() { - this.logcatinfo = { - deferred: x.deferred, - buffer: '', - onlog: o.onlog||(()=>{}), - onlogdata: o.data, - onclose: o.onclose||(()=>{}), - fd: this.fd, - waitfn:_waitfornextlogcat, - } - this.logcatinfo.waitfn.call(this); - function _waitfornextlogcat() { - // create a new promise for when the next message is received - this.activepromise.so = $.Deferred(); - this.activepromise.so - .then(function(data) { - var decodeddata = atob(data); - if (decodeddata === 'eoso:d10d9798-1351-11e5-bdd9-5b316631f026') { - this.logcatinfo.fd=0; - this.proxy_disconnect().always(function() { - var e = {adbclient:this, data:this.logcatinfo.onlogdata}; - this.logcatinfo.onclose.call(this, e); - if (this.logcatinfo.end) { - var x = this.logcatinfo.end; - x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - } - }); - return; - } - var s = this.logcatinfo.buffer + atob(data); - var sp = s.split(/\r\n?|\n/); - if (/[\r\n]$/.test(s)) { - this.logcatinfo.buffer = '' - } else { - this.logcatinfo.buffer = sp.pop(); - } - var e = {adbclient:this, data:this.logcatinfo.onlogdata, logs:sp}; - this.logcatinfo.onlog.call(this, e); - this.logcatinfo.waitfn.call(this); - }); - } - // resolve the promise to indicate that logging has started - return x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - }); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, - - endlogcat : function(o) { - var x = {o:o||{},deferred:$.Deferred()}; - var logcatfd = this.logcatinfo && this.logcatinfo.fd; - if (!logcatfd) - return x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - this.logcatinfo.fd = 0; - this.logcatinfo.end = x; - - // close the connection - the monitor callback will resolve the promise - this.dexcmd('dc', logcatfd); - return x.deferred; - }, - - push_file : function(o) { - // filepathname='/data/local/tmp/fname' - // filedata: - // filemtime:12345678 - this.push_file_info = o; - var x = {o:o,deferred:$.Deferred()}; - this.proxy_connect() - .then(function() { - return this.dexcmd('cn'); - }) - .then(function(data) { - this.fd = data; - return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid); - }) - .then(function(data) { - return this.dexcmd_read_status('sync', 'wa', this.fd, 'sync:'); - }) - .then(function() { - var perms = '33204'; - var cmddata = this.push_file_info.filepathname+','+perms; - var cmd='SEND'+String.fromCharCode(cmddata.length)+'\0\0\0'+cmddata; - return this.dexcmd('wx', this.fd, cmd) - }) - .then(function(data) { - return this.dexcmd_write_data(this.push_file_info.filedata); - }) - .then(function(data) { - var cmd='DONE'; - var mtime = this.push_file_info.filemtime; - for(var i=0;i < 4; i++) - cmd+= String.fromCharCode((mtime>>(i*8))&255); - return this.dexcmd_read_sync_response('done', 'wx', this.fd, cmd); - }) - .then(function(data) { - this.progress = 'quit'; - var cmd='QUIT\0\0\0\0'; - return this.dexcmd('wx', this.fd, cmd); - }) - .then(function(data) { - return this.dexcmd('dc', this.fd); - }) - .then(function() { - return this.proxy_disconnect(); - }) - .then(function() { - x.deferred.resolveWith(x.o.ths||this, [x.o.extra]); - }) - .fail(function(err) { - x.deferred.rejectWith(x.o.ths||this, [err]); - }); - return x.deferred; - }, - - do_auth : function(msg) { - var m = msg.match(/^vscadb proxy version 1/); - if (m) { - this.authdone = true; - this.status='connected'; - return this.activepromise.auth.resolveWith(this, []); + await this.connect_to_adb(); + await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`); + await this.adbsocket.cmd_and_status('shell:logcat -v time'); + // if there's no handler, just read the complete log and finish + if (!o.onlog) { + const logcatbuffer = await this.adbsocket.read_stdout(); + await this.disconnect_from_adb(); + return logcatbuffer; } - return this.proxy_disconnect_with_fail({cat:"Authentication", msg:"Proxy handshake failed"}); - }, - proxy_disconnect_with_fail : function(reason) { - this.disconnect_reject_reason = reason; - return this.proxy_disconnect(); - }, - - proxy_disconnect : function() { - this.ws&&this.ws.close(); - return this.activepromise.disconnect; - }, - - proxy_onopen : function() { - this.status='handshake'; - this.logsend('auth','vscadb client version 1') - .then(function(){ - this.activepromise.connected.resolveWith(this, []); - }); - }, - - proxy_onerror : function() { - var reason; - if (this.status!=='connecting') { - reason= {cat:"Protocol", msg:"Connection fault"}; - } else { - reason = {cat:"Connection", msg:"A connection to the Dex debugger could not be established.", nodbgr:true}; - } - this.proxy_disconnect_with_fail(reason); - }, - - proxy_onmessage : function(e) { - if (!this.authdone) - return this.do_auth(e.data); - var cmd = e.data.substring(0, 2); - var msgresult = e.data.substring(3, 5); - if (cmd === 'rj' && this.jdwpinfo) { - // rj is the receive-jdwp reply - it is handled separately - if (this.jdwpinfo.started) { - this.jdwpinfo.received.push(e.data.substring(6)); - if (this.jdwpinfo.received.length > 1) return; - process.nextTick(function() { - while (this.jdwpinfo.received.length) { - var nextdata = this.jdwpinfo.received.shift(); - this.jdwpinfo.onreply.call(this.jdwpinfo.o.ths||this, atob(nextdata)); - } - }.bind(this)); + // start the logcat monitor + let logcatbuffer = Buffer.alloc(0); + const next_logcat_lines = async () => { + // read the next data from ADB + let next_data; + try{ + next_data = await this.adbsocket.read_stdout(null); + } catch(e) { + o.onclose(); return; } - if (e.data === 'rj ok') - this.jdwpinfo.started = new Date(); - } - var err; - var ap = this.activepromise[cmd], p = ap; - if (Array.isArray(p)) - p = p.shift(); - if (msgresult === "ok") { - if (p) { - if (!ap.length) - this.activepromise[cmd] = null; - p.resolveWith(this, [e.data.substring(6)]); + logcatbuffer = Buffer.concat([logcatbuffer, next_data]); + const last_newline_index = logcatbuffer.lastIndexOf(10) + 1; + if (last_newline_index === 0) { + // wait for a whole line + next_logcat_lines(); return; } - err = {cat:"Command", msg:'Missing response message: ' + cmd}; - } else if (e.data==='cn error connection failed') { - // this is commonly expected, so remap the error to something nice - err = {cat:"Connection", msg:'ADB server is not running or cannot be contacted'}; - } else { - err = {cat:"Command", msg:e.data}; + // split into lines + const logs = logcatbuffer.slice(0, last_newline_index).toString().split(/\r\n?|\n/); + logcatbuffer = logcatbuffer.slice(last_newline_index); + + const e = { + adbclient: this, + logs, + }; + o.onlog(e); + next_logcat_lines(); } - this.proxy_disconnect_with_fail(err); - }, + next_logcat_lines(); + } - proxy_onclose : function(e) { - // when disconnecting, reject any pending promises first - var pending = []; - for (var cmd in this.activepromise) { - do { - var p = this.activepromise[cmd]; - if (!p) break; - if (Array.isArray(p)) - p = p.shift(); - if (p !== this.activepromise.disconnect) - if (p.state()==='pending') - pending.push(p); - } while(this.activepromise[cmd].length); - } - if (pending.length) { - var reject_reason = this.disconnect_reject_reason || {cat:'Connection', msg:'Proxy disconnection'}; - for (var i=0; i < pending.length; i++) - pending[i].rejectWith(this, [reject_reason]); - } + endLogcatMonitor() { + return this.adbsocket.disconnect(); + } - // reset the object so it can be reused - var dcinfo = { - client: this, - deferred: this.activepromise.disconnect, - reason: this.disconnect_reject_reason - }; - this.status='closed'; - this.reset(); + /** + * @param {ADBFileTransferParams} o + */ + async push_file(o) { + await this.connect_to_adb(); + await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`); + await this.adbsocket.transfer_file(o); + await this.adbsocket.disconnect(); + return true; + } - // resolve the disconnect promise after all others - pending.unshift(dcinfo); - $.when.apply($, pending) - .then(function(dcinfo) { - if (dcinfo.reason) - dcinfo.deferred.rejectWith(dcinfo.client, [dcinfo.reason]); - else - dcinfo.deferred.resolveWith(dcinfo.client); - }); - }, - - proxy_connect : function(o) { - var ws, port=(o&&o.port)||6037; - try { - ws = new WebSocket('ws://127.0.0.1:'+port); - } catch(e) { - ws=null; - return $.Deferred().rejectWith(this, [new Error('A connection to the ADB proxy could not be established.')]); - }; - - this.ws = ws; - this.ws.adbclient = this; - this.status='connecting'; - // connected is resolved after auth has completed - this.activepromise.connected = $.Deferred(); - // disconnect is resolved when the websocket is closed - this.activepromise.disconnect = $.Deferred(); - - ws.onopen = function(e) { - this.adbclient.proxy_onopen(e); - } - ws.onerror = function(e) { - clearTimeout(this.commandTimeout); - this.adbclient.proxy_onerror(e); - }; - ws.onmessage = function(e) { - clearTimeout(this.commandTimeout); - this.adbclient.proxy_onmessage(e); - }; - ws.onclose = function(e) { - clearTimeout(this.commandTimeout); - // safari doesn't call onerror for connection failures - if (this.adbclient.status==='connecting' && !this.adbclient.disconnect_reject_reason) - this.adbclient.proxy_onerror(e); - this.adbclient.proxy_onclose(e); - }; - - // the first promise is always connected, resolved after auth has completed - return this.activepromise.connected.promise(); - }, - - logsend : function(cmd, msg, opts) { - var def = $.Deferred(); - if (this.activepromise[cmd]) { - if (Array.isArray(this.activepromise[cmd])) { - // already a queue - just add it - this.activepromise[cmd].push(def); - } else { - // one pending - turn this into a queue - this.activepromise[cmd] = [this.activepromise[cmd], def]; - } - } else { - // no active entry - this.activepromise[cmd] = def; - } - if (!this.ws) { - this.proxy_disconnect_with_fail({cat:'Connection', msg:'Proxy disconnected'}); - return def; - } - clearTimeout(this.ws.commandTimeout); - try { - this.ws.send(msg); - } catch (e){ - this.proxy_disconnect_with_fail({cat:'Connection', msg:e.toString()}); - return def; - } - var docmdtimeout = 0;// !(opts&&opts.notimeout); - // if adb is not active, Windows takes at least 1 second to fail - // the socket connect... - this.ws.commandTimeout = docmdtimeout ? - setTimeout(function(adbclient) { - adbclient.proxy_disconnect_with_fail({cat:'Connection', msg:'Command timeout'}); - }, 300*1000, this) - : -1; - - return def; - }, - - dexcmd : function(cmd, fd, data, opts) { - var msg = cmd; - if (fd) - msg = msg + " " + fd; - if (data) - msg = msg + " " + btoa(data); - return this.logsend(cmd, msg, opts); - }, - - dexcmd_read_status : function(cmdname, cmd, fd, data) { - return this.dexcmd(cmd, fd, data) - .then(function() { - return this.dexcmd('rs', this.fd); - }) - .then(function(data) { - if (data !== 'OKAY') { - return this.proxy_disconnect_with_fail({cat:"cmd", msg:"Command "+ cmdname +" failed"}); - } - return data; - }); - }, - - dexcmd_read_sync_response : function(cmdname, cmd, fd, data) { - return this.dexcmd(cmd, fd, data) - .then(function() { - return this.dexcmd('rs', this.fd, '4'); - }) - .then(function(data) { - if (data.slice(0,4) !== 'OKAY') { - return this.proxy_disconnect_with_fail({cat:"cmd", msg:"Command "+ cmdname +" failed"}); - } - return data; - }); - }, - - dexcmd_read_stdout : function(fd, untilclosed) { - this.stdoutinfo = { - fd: fd, - result:'', - untilclosed:untilclosed||false, - deferred: $.Deferred(), - } - function readchunk() { - this.dexcmd('rx', this.stdoutinfo.fd) - .then(function(data) { - var eod = data==='nomore'; - if (data && data.length && !eod) { - this.stdoutinfo.result += atob(data); - } - if (this.stdoutinfo.untilclosed && !eod) { - readchunk.call(this); - return; - } - var info = this.stdoutinfo; - delete this.stdoutinfo; - info.deferred.resolveWith(this, [info.result]); - }) - .fail(function(err) { - var info = this.stdoutinfo; - delete this.stdoutinfo; - info.deferred.rejectWith(this, [err]); - }); - } - readchunk.call(this); - return this.stdoutinfo.deferred.promise(); - }, - - dexcmd_write_data : function(data) { - this.dtinfo = { - transferred: 0, - transferring: 0, - data: data, - deferred: $.Deferred(), - } - - function writechunk() { - this.dtinfo.transferred += this.dtinfo.transferring; - var remaining = this.dtinfo.data.byteLength-this.dtinfo.transferred; - if (remaining <= 0 || isNaN(remaining)) { - var info = this.dtinfo; - delete this.dtinfo; - info.deferred.resolveWith(this, [info.transferred]); - return; - } - var datalen=remaining; - if (datalen > 4000) datalen=4000; - var cmd='DATA'; - for(var i=0;i < 4; i++) - cmd+= String.fromCharCode((datalen>>(i*8))&255); - var bytes = new Uint8Array(this.dtinfo.data.slice(this.dtinfo.transferred, this.dtinfo.transferred+datalen)); - for(var i=0;i < bytes.length; i++) - cmd+= String.fromCharCode(bytes[i]); - bytes = null; - this.dtinfo.transferring = datalen; - this.dexcmd('wx', this.fd, cmd) - .then(function(data) { - writechunk.call(this); - }) - .fail(function(err) { - var info = this.dtinfo; - delete this.dtinfo; - info.deferred.rejectWith(this, [err]); - }); - } - writechunk.call(this); - return this.dtinfo.deferred.promise(); - }, + /** + * @param {string} [hostname] + */ + connect_to_adb(hostname = '127.0.0.1') { + this.adbsocket = new ADBSocket(); + return this.adbsocket.connect(this.adbPort, hostname); + } + disconnect_from_adb () { + return this.adbsocket.disconnect(); + } }; exports.ADBClient = ADBClient; diff --git a/src/apkdecoder.js b/src/apk-decoder.js similarity index 99% rename from src/apkdecoder.js rename to src/apk-decoder.js index de2bd4c..e8582fe 100644 --- a/src/apkdecoder.js +++ b/src/apk-decoder.js @@ -193,13 +193,14 @@ function decode_binary_xml(buf) { } case 0x0102: { // begin node - const node = {}; + const node = { + nodes: [], + }; idx += decode_spec(buf, BEGIN_NODE_SPEC, node, node, idx); node.namespaces = namespaces.slice(); node.namespaces.forEach(ns => { if (!ns.node) ns.node = node; }); - node.nodes = []; node_stack[0].nodes.push(node); node_stack.unshift(node); break; diff --git a/src/apk-file-info.js b/src/apk-file-info.js new file mode 100644 index 0000000..70d92c5 --- /dev/null +++ b/src/apk-file-info.js @@ -0,0 +1,145 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +const { extractManifestFromAPK, parseManifest } = require('./manifest'); +const { D } = require('./utils/print'); + +class APKFileInfo { + /** + * the full file path to the APK file + */ + fpn = ''; + + /** + * The APK file data + * @type {Buffer} + */ + file_data = null; + + /** + * last modified time of the APK file (in ms) + */ + app_modified = 0; + + /** + * SHA-1 (hex) digest of the APK file + */ + content_hash = ''; + + /** + * Contents of Android Manifest XML file + */ + manifestXml = ''; + + /** + * Extracted data from the manifest + */ + manifest = { + /** + * Package name of the app + */ + package: '', + + /** + * List of all named Activities + * @type {string[]} + */ + activities: [], + + /** + * The launcher Activity + */ + launcher: '', + }; + + constructor(apk_fpn) { + this.fpn = apk_fpn; + } + + /** + * Build a new APKFileInfo instance + * @param {*} args + */ + static async from(args) { + const result = new APKFileInfo(args.apkFile); + + // read the APK file contents + try { + result.file_data = await readFile(args.apkFile); + } catch(err) { + throw new Error(`APK read error. ${err.message}`); + } + // save the last modification time of the app + result.app_modified = fs.statSync(result.fpn).mtime.getTime(); + + // create a SHA-1 hash as a simple way to see if we need to install/update the app + const h = crypto.createHash('SHA1'); + h.update(result.file_data); + result.content_hash = h.digest('hex'); + + // read the manifest + try { + result.manifestXml = await getAndroidManifestXml(args); + } catch (err) { + throw new Error(`Manifest read error. ${err.message}`); + } + // extract the parts we need from the manifest + try { + result.manifest = parseManifest(result.manifestXml); + } catch(err) { + throw new Error(`Manifest parse failed. ${err.message}`); + } + return result; + } +} + +/** + * Retrieve the AndroidManifest.xml file content + * + * Because of manifest merging and build-injected properties, the manifest compiled inside + * the APK is frequently different from the AndroidManifest.xml source file. + * We try to extract the manifest from 3 sources (in priority order): + * 1. The 'manifestFile' launch configuration property + * 2. The decoded manifest from the APK + * 3. The AndroidManifest.xml file from the root of the source tree. + */ +async function getAndroidManifestXml(args) { + const {manifestFile, apkFile, appSrcRoot} = args; + let manifest; + + // a value from the manifestFile overrides the default manifest extraction + // note: there's no validation that the file is a valid AndroidManifest.xml file + if (manifestFile) { + D(`Reading manifest from ${manifestFile}`); + manifest = await readFile(manifestFile, 'utf8'); + return manifest; + } + + try { + D(`Reading APK Manifest`); + manifest = await extractManifestFromAPK(apkFile); + } catch(err) { + // if we fail to get manifest from the APK, revert to the source file version + D(`Reading source manifest from ${appSrcRoot}`); + manifest = await readFile(path.join(appSrcRoot, 'AndroidManifest.xml'), 'utf8'); + } + return manifest; +} + +/** + * Promisified fs.readFile() + * @param {string} path + * @param {*} [options] + */ +function readFile(path, options) { + return new Promise((res, rej) => { + fs.readFile(path, options || {}, (err, data) => { + err ? rej(err) : res(data); + }) + }) +} + +module.exports = { + APKFileInfo, +} diff --git a/src/chrome-polyfill.js b/src/chrome-polyfill.js deleted file mode 100644 index c4e48aa..0000000 --- a/src/chrome-polyfill.js +++ /dev/null @@ -1,198 +0,0 @@ -const net = require('net'); -const D = require('./util').D; - -var sockets_by_id = {}; -var last_socket_id = 0; - -const chrome = { - storage: { - local: { - q:{}, - get(o, cb) { - for (var key in o) { - var x = this.q[key]; - if (typeof(x) !== 'undefined') o[key] = x; - } - process.nextTick(cb, o); - }, - set(obj, cb) { - for (var key in obj) - this.q[key] = obj[key]; - process.nextTick(cb); - } - } - }, - runtime: { - lastError:null, - _noError() { this.lastError = null } - }, - permissions: { - request(usbPermissions, cb) { - process.nextTick(cb, true); - } - }, - socket: { - listen(socketId, host, port, max_connections, cb) { - var s = sockets_by_id[socketId]; - s._raw.listen(port, host, max_connections); - process.nextTick(cb => { - chrome.runtime._noError(); - cb(0); - }, cb); - }, - connect(socketId, host, port, cb) { - var s = sockets_by_id[socketId]; - s._raw.connect({port:port,host:host}, function(){ - chrome.runtime._noError(); - this.s.onerror = null; - this.cb.call(null,0); - }.bind({s:s,cb:cb})); - s.onerror = function(e) { - this.s.onerror = null; - this.cb.call(null,-1); - }.bind({s:s,cb:cb}); - }, - disconnect(socketId) { - var s = sockets_by_id[socketId]; - s._raw.end(); - }, - setNoDelay(socketId, state, cb) { - var s = sockets_by_id[socketId]; - s._raw.setNoDelay(state); - process.nextTick(cb => { - chrome.runtime._noError(); - cb(1); - }, cb); - }, - read(socketId, bufferSize, onRead) { - if (!onRead && typeof(bufferSize) === 'function') - onRead = bufferSize, bufferSize=-1; - if (!onRead) return; - var s = sockets_by_id[socketId]; - if (bufferSize === 0) { - process.nextTick(function(onRead) { - chrome.runtime._noError(); - onRead.call(null, {resultCode:1,data:Buffer.alloc(0)}); - }, onRead); - return; - } - s.read_requests.push({onRead:onRead, bufferSize:bufferSize}); - if (s.read_requests.length > 1) { - return; - } - !s.ondata && s._raw.on('data', s.ondata = function(data) { - this.readbuffer = Buffer.concat([this.readbuffer, data]); - while(this.read_requests.length) { - var amount = this.read_requests[0].bufferSize; - if (amount <= 0) amount = this.readbuffer.length; - if (amount > this.readbuffer.length || this.readbuffer.length === 0) - return; // wait for more data - var readInfo = { - resultCode:1, - data:Buffer.from(this.readbuffer.slice(0,amount)), - }; - this.readbuffer = this.readbuffer.slice(amount); - chrome.runtime._noError(); - this.read_requests.shift().onRead.call(null,readInfo); - } - this.onerror = this.onclose = null; - }.bind(s)); - var on_read_terminated = function(e) { - this.readbuffer = Buffer.alloc(0); - while(this.read_requests.length) { - var readInfo = { - resultCode:-1, // <=0 for error - }; - this.read_requests.shift().onRead.call(null,readInfo); - } - this.onerror = this.onclose = null; - }.bind(s); - !s.onerror && (s.onerror = on_read_terminated); - !s.onclose && (s.onclose = on_read_terminated); - if (s.readbuffer.length || bufferSize < 0) { - process.nextTick(s.ondata, Buffer.alloc(0)); - } - }, - write(socketId, data, cb) { - var s = sockets_by_id[socketId]; - if (!(data instanceof Buffer)) - data = Buffer.from(data); - s._raw.write(data, function(e,f,g) { - if (this.s.write_cbs.length === 1) - this.s.onerror = null; - var writeInfo = { - bytesWritten: this.len, - }; - chrome.runtime._noError(); - this.s.write_cbs.shift().call(null, writeInfo); - }.bind({s:s,len:data.length,cb:cb})); - s.write_cbs.push(cb); - if (!s.onerror) { - s.onerror = function(e) { - this.s.onerror = null; - while (this.s.write_cbs.length) { - var writeInfo = { - bytesWritten: 0, - }; - this.s.write_cbs.shift().call(null, writeInfo); - } - }.bind({s:s}); - } - }, - }, - - create_socket:function(id, type, cb) { - if (!cb && typeof(type) === 'function') { - cb = type, type = null; - } - var socket = type === 'server' ? new net.Server() : new net.Socket(); - var socketInfo = { - id: id, - socketId: ++last_socket_id, - _raw: socket, - onerror:null, - onclose:null, - write_cbs:[], - read_requests:[], - readbuffer:Buffer.alloc(0), - }; - socketInfo._raw.on('error', function(e) { - chrome.runtime.lastError = e; - this.onerror && this.onerror(e); - }.bind(socketInfo)); - socketInfo._raw.on('close', function(e) { - this.onclose && this.onclose(e); - }.bind(socketInfo)); - sockets_by_id[socketInfo.socketId] = socketInfo; - process.nextTick(cb, socketInfo); - }, - create_chrome_socket(id, type, cb) { return chrome.create_socket(id, type, cb) }, - - accept_socket:function(id, socketId, cb) { - var s = sockets_by_id[socketId]; - if (s.onconnection) { - s.onconnection = cb; - } else { - s.onconnection = cb; - s._raw.on('connection', function(client_socket) { - var acceptInfo = { - socketId: ++last_socket_id, - _raw: client_socket, - } - sockets_by_id[acceptInfo.socketId] = acceptInfo; - this.onconnection(acceptInfo); - }.bind(s)); - } - }, - accept_chrome_socket(id, socketId, cb) { return chrome.accept_socket(id, socketId, cb) }, - - destroy_socket:function(socketId) { - var s = sockets_by_id[socketId]; - if (!s) return; - s._raw.end(); - sockets_by_id[socketId] = null; - }, - destroy_chrome_socket(socketId) { return chrome.destroy_socket(socketId) }, -} - -exports.chrome = chrome; diff --git a/src/contentprovider.js b/src/contentprovider.js index 67574e0..9f17baa 100644 --- a/src/contentprovider.js +++ b/src/contentprovider.js @@ -1,11 +1,11 @@ -'use strict' -// vscode stuff -const { workspace, EventEmitter, Uri } = require('vscode'); +const vscode = require('vscode'); +const { workspace, EventEmitter, Uri } = vscode; -class AndroidContentProvider /*extends TextDocumentContentProvider*/ { +class AndroidContentProvider { constructor() { - this._docs = {}; // hashmap + /** @type {Map} */ + this._docs = new Map(); // Map this._onDidChange = new EventEmitter(); } @@ -27,13 +27,15 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ { * [document](TextDocument). Resources allocated should be released when * the corresponding document has been [closed](#workspace.onDidCloseTextDocument). * - * @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for. - * @param token A cancellation token. - * @return A string or a thenable that resolves to such. + * @param {Uri} uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for. + * @param {vscode.CancellationToken} token A cancellation token. + * @return {string|Thenable} A string or a thenable that resolves to such. */ - provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable;*/ { - var doc = this._docs[uri]; - if (doc) return this._docs[uri].content; + provideTextDocumentContent(uri, token) { + const doc = this._docs.get(uri); + if (doc) { + return doc.content(); + } switch (uri.authority) { // android-dev-ext://logcat/read? case 'logcat': return this.provideLogcatDocumentContent(uri); @@ -41,38 +43,51 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ { throw new Error('Document Uri not recognised'); } + /** + * @param {Uri} uri + */ provideLogcatDocumentContent(uri) { // LogcatContent depends upon AndroidContentProvider, so we must delay-load this const { LogcatContent } = require('./logcat'); - var doc = this._docs[uri] = new LogcatContent(uri.query); - return doc.content; + const doc = new LogcatContent(uri.query); + this._docs.set(uri, doc); + return doc.content(); } } -// the statics AndroidContentProvider.SCHEME = 'android-dev-ext'; + AndroidContentProvider.register = (ctx, workspace) => { - var provider = new AndroidContentProvider(); - var registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider); - ctx.subscriptions.push(registration); - ctx.subscriptions.push(provider); + const provider = new AndroidContentProvider(); + const registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider); + ctx.subscriptions.push(registration, provider); } + AndroidContentProvider.getReadLogcatUri = (deviceId) => { - var uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`); + const uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`); return uri.with({ query: deviceId }); } + AndroidContentProvider.getLaunchConfigSetting = (name, defvalue) => { // there's surely got to be a better way than this... - var configs = workspace.getConfiguration('launch.configurations'); - for (var i=0,config; config=configs.get(''+i); i++) { - if (config.type!=='android') continue; - if (config.request!=='launch') continue; - if (config[name]) return config[name]; + const configs = workspace.getConfiguration('launch.configurations'); + for (let i = 0, config; config = configs.get(`${i}`); i++) { + if (config.type!=='android') { + continue; + } + if (config.request!=='launch') { + continue; + } + if (Object.prototype.hasOwnProperty.call(config, name)) { + return config[name]; + } break; } return defvalue; } -exports.AndroidContentProvider = AndroidContentProvider; +module.exports = { + AndroidContentProvider, +} diff --git a/src/debugMain.js b/src/debugMain.js index d123d30..86f43f2 100644 --- a/src/debugMain.js +++ b/src/debugMain.js @@ -1,29 +1,25 @@ -'use strict' const { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent, - Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter'); + Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter'); // node and external modules -const crypto = require('crypto'); -const dom = require('xmldom').DOMParser; const fs = require('fs'); const os = require('os'); const path = require('path'); -const unzipper = require('unzipper'); -const xpath = require('xpath'); // our stuff const { ADBClient } = require('./adbclient'); -const { decode_binary_xml } = require('./apkdecoder'); +const { APKFileInfo } = require('./apk-file-info'); const { Debugger } = require('./debugger'); -const $ = require('./jq-promise'); +const { BreakpointOptions, BuildInfo, DebuggerException, DebuggerValue, JavaBreakpointEvent, JavaClassType, JavaExceptionEvent, SourceLocation } = require('./debugger-types'); +const { evaluate } = require('./expression/evaluate'); +const { PackageInfo } = require('./package-searcher'); +const ADBSocket = require('./sockets/adbsocket'); const { AndroidThread } = require('./threads'); -const { D, onMessagePrint, isEmptyObject } = require('./util'); -const { AndroidVariables } = require('./variables'); -const { evaluate } = require('./expressions'); -const ws_proxy = require('./wsproxy').proxy.Server(6037, 5037); -const { exmsg_var_name, signatureToFullyQualifiedType, ensure_path_end_slash,is_subpath_of,variableRefToThreadId } = require('./globals'); +const { D, onMessagePrint } = require('./utils/print'); +const { hasValidSourceFileExtension } = require('./utils/source-file'); +const { VariableManager } = require('./variable-manager'); class AndroidDebugSession extends DebugSession { @@ -39,40 +35,46 @@ class AndroidDebugSession extends DebugSession { this.app_src_root = ''; // the filepathname of the built apk this.apk_fpn = ''; - // the apk file content - this._apk_file_data = null; - // the file info, hash and manifest data of the apk - this.apk_file_info = {}; - // hashmap of packages we found in the source tree - this.src_packages = {}; + /** + * the file info, hash and manifest data of the apk + * @type {APKFileInfo} + */ + this.apk_file_info = null; + // packages we found in the source tree + this.src_packages = { + last_src_modified: 0, + /** @type {Map} */ + packages: new Map(), + }; // the device we are debugging this._device = null; + // the API level of the device we are debugging + this.device_api_level = ''; // the full file path name of the AndroidManifest.xml, taken from the manifestFile launch property this.manifest_fpn = ''; - // the threads (we know about from the last refreshThreads call) - // this is implemented as both a hashmap and an array of AndroidThread objects - this._threads = { - array:[], - } + /** + * the threads (from the last refreshThreads() call) + * @type {AndroidThread[]} + */ + this._threads = [] + // path to the the ANDROID_HOME/sources/ (only set if it's a valid path) this._android_sources_path = ''; // number of call stack entries to display above the project source this.callStackDisplaySize = 1; - // the set of variables used for evalution outside of any thread/frame context - this._globals = new AndroidVariables(this, 10000); - // the fifo queue of evaluations (watches, hover, etc) this._evals_queue = []; // since we want to send breakpoint events, we will assign an id to every event // so that the frontend can match events with breakpoints. this._breakpointId = 1000; + // the fifo queue of breakpoints to enable + this._set_breakpoints_queue = []; this._sourceRefs = { all:[null] }; // hashmap + array of (non-zero) source references - this._nextVSCodeThreadId = 0; // vscode doesn't like thread id reuse (the Android runtime is OK with it) // flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests this._isDisconnecting = false; @@ -112,135 +114,151 @@ class AndroidDebugSession extends DebugSession { this.sendResponse(response); } + /** + * @param {string} msg + */ LOG(msg) { if (!this.trace) { D(msg); } // VSCode no longer auto-newlines output - this.sendEvent(new OutputEvent(msg + os.EOL)); + this.sendEvent(new OutputEvent(`${msg}${os.EOL}`)); } + /** + * @param {string} msg + */ WARN(msg) { - D(msg = 'Warning: '+msg); + D(msg = `Warning: ${msg}`); // the message will already be sent if trace is enabled - if (!this.trace) { - this.sendEvent(new OutputEvent(msg + os.EOL)); + if (this.trace) { + return; } + this.sendEvent(new OutputEvent(`${msg}${os.EOL}`)); } - failRequest(msg, response) { + /** + * @param {string} msg + * @param {boolean} silent + */ + failRequest(msg, response, silent = false) { // yeah, it can happen sometimes... - msg && this.WARN(msg); - if (response) { - response.success = false; - this.sendResponse(response); - } - } - - cancelRequest(msg, response) { - D(msg); // just log it in debug - don't output it to the client + if (silent) { + D(msg); // just log it in debug - don't output it to the client + } else if (msg) { + this.WARN(msg); + } if (response) { response.success = false; this.sendResponse(response); } } + /** + * @param {string} requestName + * @param {number} threadId + * @param {*} response + */ failRequestNoThread(requestName, threadId, response) { this.failRequest(`${requestName} failed. Thread ${threadId} not found`, response); } + /** + * @param {string} requestName + * @param {number} threadId + * @param {*} response + */ failRequestThreadNotSuspended(requestName, threadId, response) { this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response); } + /** + * @param {string} requestName + * @param {number} threadId + * @param {*} response + */ cancelRequestThreadNotSuspended(requestName, threadId, response) { // now that vscode can resume threads before the locals,callstack,etc are retrieved, we only need to cancel the request - this.cancelRequest(`${requestName} cancelled. Thread ${threadId} is not suspended`, response); + this.failRequest(`${requestName} cancelled. Thread ${threadId} is not suspended`, response, true); } - getThread(id) { - var t; + /** + * @param {JavaThreadID|VSCThreadID} id + * @param {string} [name] + */ + getThread(id, name) { + let thread; switch(typeof id) { case 'string': - t = this._threads[id]; - if (!t) { - t = new AndroidThread(this, id, ++this._nextVSCodeThreadId); - this._threads[id] = this._threads.array[t.vscode_threadid] = t; + thread = this._threads.find(t => t && t.threadid === id); + if (!thread) { + thread = new AndroidThread(this.dbgr, name, id); + this._threads[thread.vscode_threadid] = thread; } break; case 'number': - t = this._threads.array[id]; + thread = this._threads[id]; break; } - return t; + return thread; } - reportStoppedEvent(reason, location, last_exception) { - var thread = this.getThread(location.threadid); - if (thread.stepTimeout) { - clearTimeout(thread.stepTimeout); - thread.stepTimeout = null; - } + /** + * + * @param {'breakpoint'|'step'|'exception'} reason + * @param {SourceLocation} location + * @param {DebuggerException} [last_exception] + */ + reportStoppedEvent(reason, location, last_exception = null) { + const thread = this.getThread(location.threadid); if (thread.paused) { // this thread is already in the paused state - ignore the notification thread.paused.reasons.push(reason); - if (last_exception) + if (last_exception) { thread.paused.last_exception = last_exception; + } return; } - thread.paused = { - when: Date.now(), // when - reasons: [reason], // why - location: Object.assign({},location), // where - last_exception: last_exception || null, - locals_done: {}, // promise to wait on for the stack variables to be evaluated - stack_frame_vars: {}, // hashmap for the stack frame locals - stoppedEvent:null, // event we (eventually) send to vscode - } + thread.setPaused(reason, location, last_exception); this.checkPendingThreadBreaks(); } - refreshThreads(extra) { - return this.dbgr.allthreads(extra) - .then((thread_ids, extra) => this.dbgr.threadinfos(thread_ids, extra)) - .then((threadinfos, extra) => { + async refreshThreads() { + const thread_ids = await this.dbgr.getJavaThreadIDs(); + const threadinfos = await this.dbgr.getJavaThreadInfos(thread_ids); - for (var i=0; i < threadinfos.length; i++) { - var ti = threadinfos[i]; - var thread = this.getThread(ti.threadid); - if (thread.name === null) { - thread.name = ti.name; - } else if (thread.name !== ti.name) { - // give the thread a new id for VS code - delete this._threads.array[thread.vscode_threadid]; - thread.vscode_threadid = ++this._nextVSCodeThreadId; - this._threads.array[thread.vscode_threadid] = thread; - thread.name = ti.name; - } + // configure the thread names + threadinfos.forEach(threadinfo => { + const thread = this.getThread(threadinfo.threadid); + if (thread.name === null) { + thread.name = threadinfo.name; + } else if (thread.name !== threadinfo.name) { + // give the thread a new id for VS code + delete this._threads[thread.vscode_threadid]; + thread.allocateNewThreadID(); + this._threads[thread.vscode_threadid] = thread; + thread.name = threadinfo.name; + } + }); + + // remove any threads that are no longer in the system + this._threads.slice().forEach(thread => { + if (thread) { + const exists = threadinfos.find(ti => ti.threadid === thread.threadid); + if (!exists) { + delete this._threads[thread.vscode_threadid]; } - - // remove any threads that are no longer in the system - this._threads.array.reduceRight((threadinfos,t) => { - if (!t) return threadinfos; - var exists = threadinfos.find(ti => ti.threadid === t.threadid); - if (!exists) { - delete this._threads[t.threadid]; - delete this._threads.array[t.vscode_threadid]; - } - return threadinfos; - },threadinfos); - - return extra; - }) + } + }) } - launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) { + async launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) { if (args && args.trace) { this.trace = args.trace; onMessagePrint(this.LOG.bind(this)); } - try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {} + D(`Launching: ${JSON.stringify(args)}`); // app_src_root must end in a path-separator for correct validation of sub-paths this.app_src_root = ensure_path_end_slash(args.appSrcRoot); this.apk_fpn = args.apkFile; @@ -249,15 +267,16 @@ class AndroidDebugSession extends DebugSession { if (typeof args.callStackDisplaySize === 'number' && args.callStackDisplaySize >= 0) this.callStackDisplaySize = args.callStackDisplaySize|0; - // configure the ADB port - if it's undefined, it will set the default value. - // if it's not a valid port number, any connection request should neatly fail. - ws_proxy.setADBPort(args.adbPort); + // set the custom ADB port - this should be changed to pass it to each ADBClient instance + if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) { + ADBSocket.ADBPort = args.adbPort; + } try { // start by scanning the source folder for stuff we need to know about (packages, manifest, etc) - this.src_packages = this.scanSourceSync(this.app_src_root); + this.src_packages = PackageInfo.scanSourceSync(this.app_src_root); // warn if we couldn't find any packages (-> no source -> cannot debug anything) - if (isEmptyObject(this.src_packages.packages)) + if (this.src_packages.packages.size === 0) this.WARN('No source files found. Check the "appSrcRoot" setting in launch.json'); } catch(err) { @@ -268,349 +287,217 @@ class AndroidDebugSession extends DebugSession { return; } - var fail_launch = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]); + try { + this.LOG('Checking build') + this.apk_file_info = await APKFileInfo.from(args); + this.checkBuildIsUpToDate(args.staleBuild); - this.LOG('Checking build') - this.getAPKFileInfo() - .then(apk_file_info => { - this.apk_file_info = apk_file_info; - // check if any source file was modified after the apk - if (this.src_packages.last_src_modified >= this.apk_file_info.app_modified) { - switch (args.staleBuild) { - case 'ignore': break; - case 'stop': return fail_launch('Build is not up-to-date'); - case 'warn': - default: this.WARN('Build is not up-to-date. Source files may not match execution when debugging.'); break; - } - } - // check we have something to launch - we do this again later, but it's a bit better to do it before we start device comms - var launchActivity = args.launchActivity; - if (!launchActivity) - if (!(launchActivity = this.apk_file_info.launcher)) - return fail_launch('No valid launch activity found in AndroidManifest.xml or launch.json'); + // check we have something to launch - we do this again later, but it's a bit better to do it before we start device comms + let launchActivity = args.launchActivity; + if (!launchActivity) + if (!(launchActivity = this.apk_file_info.manifest.launcher)) + throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json'); - return new ADBClient().test_adb_connection() - .then(err => { - // if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number) - var adbport = ws_proxy.adbport; - if (err && args.autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) { - var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb'); - var adbargs = ['-P',''+adbport,'start-server']; - try { - this.LOG([adbpath, ...adbargs].join(' ')); - var stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'}); - this.LOG(stdout); - } catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves - } - }) - }) - .then(() => this.findSuitableDevice(args.targetDevice)) - .then(device => { - this._device = device; - this._device.adbclient = new ADBClient(this._device.serial); - // we've got our device - retrieve the hash of the installed app (or sha1 utility itself if the app is not installed) - const query_app_hash = `/system/bin/sha1sum $(pm path ${this.apk_file_info.package}|grep -o -e '/.*' || echo '/system/bin/sha1sum')`; - return this._device.adbclient.shell_cmd({command: query_app_hash}); - }) - .then(sha1sum_output => { - const installed_hash = sha1sum_output.match(/^[0-9a-fA-F]*/)[0].toLowerCase(); - // does the installed apk hash match the content hash? if, so we don't need to install the app - if (installed_hash === this.apk_file_info.content_hash) { - this.LOG('Current build already installed'); - return; - } - return this.copyAndInstallAPK(); - }) - .then(() => { - // when we reach here, the app should be installed and ready to be launched - // - before we continue, splunk the apk file data because node *still* hangs when evaluating large arrays - this._apk_file_data = null; + // make sure ADB exists and is started and look for a device to install on + await this.checkADBStarted(args.autoStartADB !== false); + this._device = await this.findSuitableDevice(args.targetDevice); + this._device.adbclient = new ADBClient(this._device.serial); - // get the API level of the device - return this._device.adbclient.shell_cmd({command:'getprop ro.build.version.sdk'}); - }) - .then(apilevel => { - apilevel = apilevel.trim(); + // install the APK we are going to debug + await this.ensureAPKInstalled(); - // look for the android sources folder appropriate for this device - if (process.env.ANDROID_HOME && apilevel) { - var sources_path = path.join(process.env.ANDROID_HOME,'sources','android-'+apilevel); - fs.stat(sources_path, (err,stat) => { - if (!err && stat && stat.isDirectory()) - this._android_sources_path = sources_path; - }); - } + // when we reach here, the app should be installed and ready to be launched + // - we no longer need the APK file data + this.apk_file_info.file_data = null; - // start the launch - var launchActivity = args.launchActivity; - if (!launchActivity) - if (!(launchActivity = this.apk_file_info.launcher)) - return fail_launch('No valid launch activity found in AndroidManifest.xml or launch.json'); - var build = { - pkgname:this.apk_file_info.package, - packages:Object.assign({}, this.src_packages.packages), - launchActivity: launchActivity, - }; - this.LOG(`Launching ${build.pkgname+'/'+launchActivity} on device ${this._device.serial} [API:${apilevel||'?'}]`); - return this.dbgr.startDebugSession(build, this._device.serial, launchActivity); - }) - .then(() => { - // if we get this far, the debugger is connected and waiting for the resume command - // - set up some events - this.dbgr.on('bpstatechange', this, this.onBreakpointStateChange) - .on('bphit', this, this.onBreakpointHit) - .on('step', this, this.onStep) - .on('exception', this, this.onException) - .on('threadchange', this, this.onThreadChange) - .on('disconnect', this, this.onDebuggerDisconnect); - this.waitForConfigurationDone = $.Deferred(); - // - tell the client we're initialised and ready for breakpoint info, etc - this.sendEvent(new InitializedEvent()); - return this.waitForConfigurationDone; - }) - .then(() => { - // get the debugger to tell us about any thread creations/terminations - return this.dbgr.setThreadNotify(); - }) - .then(() => { - // config is done - we're all set and ready to go! - D('Continuing app start'); - this.sendResponse(response); - return this.dbgr.resume(); - }) - .then(() => { - this.LOG('Application started'); - }) - .fail(e => { - // exceptions use message, adbclient uses msg - this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available')); - // more info for adb connect errors - if (/^ADB server is not running/.test(e.msg)) { - this.LOG('Make sure the Android SDK Platform Tools are installed and run:'); - this.LOG(' adb start-server'); - this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.'); - } - // tell the client we're done - this.sendEvent(new TerminatedEvent(false)); + // try and determine the relevant path for the API sources (based upon the API level of the connected device) + await this.configureAPISourcePath(); + + // launch the app + await this.startLaunchActivity(args.launchActivity); + + // if we get this far, the debugger is connected and waiting for the resume command + // - set up some events... + this.dbgr.on('bpstatechange', e => this.onBreakpointStateChange(e)) + .on('bphit', e => this.onBreakpointHit(e)) + .on('step', e => this.onStep(e)) + .on('exception', e => this.onException(e)) + .on('threadchange', e => this.onThreadChange(e)) + .on('disconnect', () => this.onDebuggerDisconnect()); + + // - tell the client we're initialised and ready for breakpoint info, etc + this.sendEvent(new InitializedEvent()); + await new Promise(resolve => this.waitForConfigurationDone = resolve); + + // get the debugger to tell us about any thread creations/terminations + await this.dbgr.setThreadNotify(); + + // config is done - we're all set and ready to go! + D('Continuing app start'); + this.sendResponse(response); + await this.dbgr.resume(); + + this.LOG('Application started'); + } catch(e) { + // exceptions use message, adbclient uses msg + this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available')); + // more info for adb connect errors + if (/^ADB server is not running/.test(e.msg)) { + this.LOG('Make sure the Android SDK Platform Tools are installed and run:'); + this.LOG(' adb start-server'); + this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.'); + } + // tell the client we're done + this.sendEvent(new TerminatedEvent(false)); + } + } + + async checkADBStarted(autoStartADB) { + const err = await new ADBClient().test_adb_connection(); + // if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number) + if (err && autoStartADB && process.env.ANDROID_HOME) { + const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb'); + const adbargs = ['-P',`${ADBSocket.ADBPort}`,'start-server']; + try { + this.LOG([adbpath, ...adbargs].join(' ')); + const stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'}); + this.LOG(stdout); + } catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves + } + } + + checkBuildIsUpToDate(staleBuild) { + // check if any source file was modified after the apk + if (this.src_packages.last_src_modified >= this.apk_file_info.app_modified) { + switch (staleBuild) { + case 'ignore': break; + case 'stop': throw new Error('Build is not up-to-date'); + case 'warn': + default: this.WARN('Build is not up-to-date. Source files may not match execution when debugging.'); break; + } + } + } + + startLaunchActivity(launchActivity) { + if (!launchActivity) { + if (!(launchActivity = this.apk_file_info.manifest.launcher)) { + throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json'); + } + } + + const build = new BuildInfo(this.apk_file_info.manifest.package, new Map(this.src_packages.packages), launchActivity); + this.LOG(`Launching ${build.pkgname}/${launchActivity} on device ${this._device.serial} [API:${this.device_api_level||'?'}]`); + return this.dbgr.startDebugSession(build, this._device.serial); + } + + async configureAPISourcePath() { + const apilevel = await this.getDeviceAPILevel(); + + // look for the android sources folder appropriate for this device + if (process.env.ANDROID_HOME && apilevel) { + const sources_path = path.join(process.env.ANDROID_HOME,'sources',`android-${apilevel}`); + fs.stat(sources_path, (err,stat) => { + if (!err && stat && stat.isDirectory()) + this._android_sources_path = sources_path; }); - } + } + } - copyAndInstallAPK() { + async getDeviceAPILevel() { + const apilevel = await this._device.adbclient.shell_cmd({command:'getprop ro.build.version.sdk'}); + this.device_api_level = apilevel.trim(); + return this.device_api_level; + } + + async ensureAPKInstalled() { + const installed = await this.isAPKInstalled(); + if (installed) { + this.LOG('Current build already installed'); + return; + } + await this.copyAndInstallAPK(); + } + + async isAPKInstalled() { + // retrieve the hash of the installed app (or sha1 utility itself if the app is not installed) + const query_app_hash = `/system/bin/sha1sum $(pm path ${this.apk_file_info.manifest.package}|grep -o -e '/.*' || echo '/system/bin/sha1sum')`; + const sha1sum_output = await this._device.adbclient.shell_cmd({command: query_app_hash}); + const installed_hash = sha1sum_output.match(/^[0-9a-fA-F]*/)[0].toLowerCase(); + + // does the installed apk hash match the content hash? if, so we don't need to install the app + return installed_hash === this.apk_file_info.content_hash; + } + + async copyAndInstallAPK() { // copy the file to the device this.LOG('Deploying current build...'); const device_apk_fpn = '/data/local/tmp/debug.apk'; - return this._device.adbclient.push_file({ - filepathname:device_apk_fpn, - filedata:this._apk_file_data, - filemtime:new Date().getTime(), + await this._device.adbclient.push_file({ + pathname: device_apk_fpn, + data: this.apk_file_info.file_data, + mtime: (Date.now() / 1000) | 0, + perms: 0o100664, }) - .then(() => { - // send the install command - this.LOG('Installing...'); - const command = `pm install ${Array.isArray(this.pmInstallArgs) ? this.pmInstallArgs.join(' ') : '-r'} ${device_apk_fpn}`; - D(command); - return this._device.adbclient.shell_cmd({ - command, - untilclosed:true, - }) + // send the install command + this.LOG('Installing...'); + const command = `pm install ${Array.isArray(this.pmInstallArgs) ? this.pmInstallArgs.join(' ') : '-r'} ${device_apk_fpn}`; + D(command); + const stdout = await this._device.adbclient.shell_cmd({ + command, }) - .then((stdout) => { - // failures: - // pkg: x-y-z.apk - // Failure [INSTALL_FAILED_OLDER_SDK] - var m = stdout.match(/Failure\s+\[([^\]]+)\]/g); - if (m) { - return $.Deferred().rejectWith(this, [new Error('Installation failed. ' + m[0])]); - } - // now the 'pm install' command can have user-defined arguments, we must check that the command - // is not rejected because of bad values - m = stdout.match(/^java.lang.IllegalArgumentException:.+/m); - if (m) { - return $.Deferred().rejectWith(this, [new Error('Installation failed. ' + m[0])]); - } - }) - } - - getAPKFileInfo() { - var done = $.Deferred(); - done.result = { fpn:this.apk_fpn, app_modified:0, content_hash:'', manifest:'', package:'', activities:[], launcher:'' }; - // read the APK - fs.readFile(this.apk_fpn, (err,apk_file_data) => { - if (err) return done.rejectWith(this, [new Error('APK read error. ' + err.message)]); - // debugging is painful when the APK file content is large, so keep the data in a separate field so node - // doesn't have to evaluate it when we're looking at the apk info - this._apk_file_data = apk_file_data; - // save the last modification time of the app - done.result.app_modified = fs.statSync(done.result.fpn).mtime.getTime(); - // create a SHA-1 hash as a simple way to see if we need to install/update the app - const h = crypto.createHash('SHA1'); - h.update(apk_file_data); - done.result.content_hash = h.digest('hex'); - // read the manifest - this.readAndroidManifest((err, manifest) => { - if (err) return done.rejectWith(this, [new Error('Manifest read error. ' + err.message)]); - done.result.manifest = manifest; - try { - const doc = new dom().parseFromString(manifest); - // extract the package name from the manifest - const pkg_xpath = '/manifest/@package'; - done.result.package = xpath.select1(pkg_xpath, doc).value; - const android_select = xpath.useNamespaces({"android": "http://schemas.android.com/apk/res/android"}); - // extract a list of all the (named) activities declared in the manifest - const activity_xpath='/manifest/application/activity/@android:name'; - var nodes = android_select(activity_xpath, doc); - nodes && (done.result.activities = nodes.map(n => n.value)); - - // extract the default launcher activity - const launcher_xpath='/manifest/application/activity[intent-filter/action[@android:name="android.intent.action.MAIN"] and intent-filter/category[@android:name="android.intent.category.LAUNCHER"]]/@android:name'; - var nodes = android_select(launcher_xpath, doc); - // should we warn if there's more than one? - if (nodes && nodes.length >= 1) - done.result.launcher = nodes[0].value - } catch(err) { - return done.rejectWith(this, [new Error('Manifest parse failed. ' + err.message)]); - } - done.resolveWith(this, [done.result]); - }); - }); - return done; - } - - readAndroidManifest(cb) { - // Because of manifest merging and build-injected properties, the manifest compiled inside - // the APK is frequently different from the AndroidManifest.xml source file. - // We try to extract the manifest from 3 sources (in priority order): - // 1. The 'manifestFile' launch configuration property - // 2. The decoded manifest from the APK - // 3. The AndroidManifest.xml file from the root of the source tree. - - const readAPKManifest = (cb) => { - D(`Reading APK Manifest`); - const apk_manifest_chunks = []; - function cb_once(err, manifest) { - cb && cb(err, manifest); - cb = null; - } - fs.createReadStream(this.apk_fpn) - .pipe(unzipper.ParseOne(/^AndroidManifest\.xml$/)) - .on('data', chunk => { - apk_manifest_chunks.push(chunk); - }) - .once('error', err => { - cb_once(err); - }) - .once('end', () => { - try { - const manifest = decode_binary_xml(Buffer.concat(apk_manifest_chunks)); - D(`APK manifest read complete`); - cb_once(null, manifest); - } catch (err) { - D(`APK manifest decode failed: ${err.message}`); - cb_once(err); - } - }); + // failures: + // pkg: x-y-z.apk + // Failure [INSTALL_FAILED_OLDER_SDK] + const failure_match = stdout.match(/Failure\s+\[([^\]]+)\]/g); + if (failure_match) { + throw new Error('Installation failed. ' + failure_match[0]); } - - const readSourceManifest = (cb) => { - D(`Reading source manifest from ${this.app_src_root}`); - fs.readFile(path.join(this.app_src_root, 'AndroidManifest.xml'), 'utf8', cb); - } - - // a value from the manifestFile overrides the default manifest extraction - // note: there's no validation that the file is a valid AndroidManifest.xml file - if (this.manifest_fpn) { - D(`Reading manifest from ${this.manifest_fpn}`); - fs.readFile(this.manifest_fpn, 'utf8', cb); - return; - } - - readAPKManifest((err, manifest) => { - if (err) { - // if we fail to read the APK manifest, revert to the source manifest - readSourceManifest(cb) - return; - } - cb(err, manifest); - }); - } - - scanSourceSync(app_root) { - try { - // scan known app folders looking for file changes and package folders - var p, paths = fs.readdirSync(app_root,'utf8'), done=[]; - var src_packages = { - last_src_modified: 0, - packages: {}, - }; - while (paths.length) { - p = paths.shift(); - // just in case someone has some crazy circular links going on - if (done.indexOf(p)>=0) continue; - done.push(p); - var subfiles = [], stat, fpn = path.join(app_root,p); - try { - stat = fs.statSync(fpn); - src_packages.last_src_modified = Math.max(src_packages.last_src_modified, stat.mtime.getTime()); - if (!stat.isDirectory()) continue; - subfiles = fs.readdirSync(fpn, 'utf8'); - } - catch (err) { continue } - // ignore folders not starting with a known top-level Android folder - if (!/^(assets|res|src|main|java|kotlin)([\\/]|$)/.test(p)) continue; - // is this a package folder - var pkgmatch = p.match(/^(src|main|java|kotlin)[\\/](.+)/); - if (pkgmatch && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(pkgmatch[2].split(/[\\/]/).pop())) { - // looks good - add it to the list - const src_folder = pkgmatch[1]; // src, main, java or kotlin - const pkgname = pkgmatch[2].replace(/[\\/]/g,'.'); - src_packages.packages[pkgname] = { - package: pkgname, - package_path: fpn, - srcroot: path.join(app_root,src_folder), - public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.(?:java|kt)$/.test(sf)).map(sf => sf.match(/^(.*)\.(?:java|kt)$/)[1]) - } - } - // add the subfiles to the list to process - paths = subfiles.map(sf => path.join(p,sf)).concat(paths); - } - return src_packages; - } catch(err) { - throw new Error('Source path error: ' + err.message); + // now the 'pm install' command can have user-defined arguments, we must check that the command + // is not rejected because of bad values + const m = stdout.match(/^java.lang.IllegalArgumentException:.+/m); + if (m) { + throw new Error('Installation failed. ' + m[0]); } } - findSuitableDevice(target_deviceid) { + /** + * @param {string} target_deviceid + */ + async findSuitableDevice(target_deviceid) { this.LOG('Searching for devices...'); - return this.dbgr.list_devices() - .then(devices => { - this.LOG(`Found ${devices.length} device${devices.length===1?'':'s'}`); - var reject; - if (devices.length === 0) { - reject = 'No devices are connected'; - } else if (target_deviceid) { - // check (only one of) the requested device is present - var matching_devices = devices.filter(d => d.serial === target_deviceid); - switch(matching_devices.length) { - case 0: reject = `Target device: '${target_deviceid}' is not connected. Connect it or specify an alternate target device in launch.json`; break; - case 1: return matching_devices[0]; - default: reject = `Target device: '${target_deviceid}' has multiple candidates. Connect a single device or specify an alternate target device in launch.json`; break; - } - } else if (devices.length === 1) { - // no specific target device and only one device is connected to adb - use it - return devices[0]; - } else { - // more than one device and no specific target - fail the launch - reject = `Multiple devices are connected and no target device is specified in launch.json`; - // be nice and list the devices so the user can easily configure - devices.forEach(d => this.LOG(`\t${d.serial}\t${d.status}`)); - } - return $.Deferred().rejectWith(this, [new Error(reject)]); - }) + const devices = await this.dbgr.listConnectedDevices() + this.LOG(`Found ${devices.length} device${devices.length===1?'':'s'}`); + let reject; + if (devices.length === 0) { + reject = 'No devices are connected'; + } else if (target_deviceid) { + // check (only one of) the requested device is present + const matching_devices = devices.filter(d => d.serial === target_deviceid); + switch(matching_devices.length) { + case 0: + reject = `Target device: '${target_deviceid}' is not connected. Connect it or specify an alternate target device in launch.json`; + break; + case 1: + return matching_devices[0]; + default: + reject = `Target device: '${target_deviceid}' has multiple candidates. Connect a single device or specify an alternate target device in launch.json`; + break; + } + } else if (devices.length === 1) { + // no specific target device and only one device is connected to adb - use it + return devices[0]; + } else { + // more than one device and no specific target - fail the launch + reject = `Multiple devices are connected and no target device is specified in launch.json`; + // be nice and list the devices so the user can easily configure + devices.forEach(d => this.LOG(`\t${d.serial}\t${d.status}`)); + } + throw new Error(reject); } configurationDoneRequest(response/*, args*/) { D('configurationDoneRequest'); - this.waitForConfigurationDone.resolve(); + this.waitForConfigurationDone(); this.sendResponse(response); } @@ -624,19 +511,16 @@ class AndroidDebugSession extends DebugSession { } } - disconnectRequest(response/*, args*/) { + async disconnectRequest(response/*, args*/) { D('disconnectRequest'); this._isDisconnecting = true; - // if we're connected, ask ADB to terminate the app - if (this.dbgr.status() === 'connected') - this.dbgr.forcestop(); - return this.dbgr.disconnect(response) - .then((state, response) => { - if (/^connect/.test(state)) - this.LOG(`Debugger disconnected`); - this.sendResponse(response); - //this.sendEvent(new ExitedEvent(0)); - }) + try { + await this.dbgr.forceStop(); + await this.dbgr.disconnect(); + this.LOG(`Debugger stopped`); + } catch (e) { + } + this.sendResponse(response); } onBreakpointStateChange(e) { @@ -644,31 +528,35 @@ class AndroidDebugSession extends DebugSession { e.breakpoints.forEach(javabp => { // if there's no associated vsbp we're deleting it, so just ignore the update if (!javabp.vsbp) return; - var verified = !!javabp.state.match(/set|enabled/); + const verified = !!javabp.state.match(/set|enabled/); javabp.vsbp.verified = verified; javabp.vsbp.message = null; this.sendEvent(new BreakpointEvent('changed', javabp.vsbp)); }); } + /** + * + * @param {JavaBreakpointEvent} e + */ onBreakpointHit(e) { // if we step into a breakpoint, both onBreakpointHit and onStep will be called - D('Breakpoint hit: ' + JSON.stringify(e.stoppedlocation)); - this.reportStoppedEvent("breakpoint", e.stoppedlocation); + D(`Breakpoint hit: ${e.stoppedLocation}`); + this.reportStoppedEvent("breakpoint", e.stoppedLocation); } /** * Called when the user requests a change to breakpoints in a source file * Note: all breakpoints in a file are always sent in args, even if they are not changing */ - setBreakPointsRequest(response/*: DebugProtocol.SetBreakpointsResponse*/, args/*: DebugProtocol.SetBreakpointsArguments*/) { - var srcfpn = args.source && args.source.path; - D('setBreakPointsRequest: ' + srcfpn); + async setBreakPointsRequest(response/*: DebugProtocol.SetBreakpointsResponse*/, args/*: DebugProtocol.SetBreakpointsArguments*/) { + const source_filename = args.source && args.source.path; + D('setBreakPointsRequest: ' + source_filename); const unverified_breakpoint = (src_bp,reason) => { - var bp = new Breakpoint(false,src_bp.line); - bp.id = ++this._breakpointId; - bp.message = reason; + const bp = new Breakpoint(false,src_bp.line); + bp['id'] = ++this._breakpointId; + bp['message'] = reason; return bp; } @@ -681,26 +569,21 @@ class AndroidDebugSession extends DebugSession { } // the file must lie inside one of the source packages we found (and it must be have a .java extension) - var srcfolder = path.dirname(srcfpn); - var pkginfo; - for (var pkg in this.src_packages.packages) { - if ((pkginfo = this.src_packages.packages[pkg]).package_path === srcfolder) break; - pkginfo = null; - } + const srcfolder = path.dirname(source_filename); + const package_infos = [...this.src_packages.packages.values()]; + let pkginfo = package_infos.find(pi => pi.package_path === srcfolder); + // if we didn't find an exact path match, look for a case-insensitive match if (!pkginfo) { - for (var pkg in this.src_packages.packages) { - if ((pkginfo = this.src_packages.packages[pkg]).package_path.localeCompare(srcfolder, undefined, { sensitivity: 'base' }) === 0) break; - pkginfo = null; - } + pkginfo = package_infos.find(pi => pi.package_path.localeCompare(srcfolder, undefined, { sensitivity: 'base' }) === 0); } // if it's not in our source packages, check if it's in the Android source file cache - if (!pkginfo && is_subpath_of(srcfpn, this._android_sources_path)) { + if (!pkginfo && is_subpath_of(source_filename, this._android_sources_path)) { // create a fake pkginfo to use to construct the bp - pkginfo = { srcroot:this._android_sources_path } + pkginfo = new PackageInfo(this._android_sources_path, '', [], '', ''); } - if (!pkginfo || !/\.(java|kt)$/i.test(srcfpn)) { + if (!pkginfo || !hasValidSourceFileExtension(source_filename)) { // source file is not a java file or is outside of the known source packages // just send back a list of unverified breakpoints sendBPResponse(response, args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid'))); @@ -709,242 +592,221 @@ class AndroidDebugSession extends DebugSession { // our debugger requires a relative fpn beginning with / , rooted at the java source base folder // - it should look like: /some/package/name/abc.java - var relative_fpn = srcfpn.slice(pkginfo.srcroot.match(/^(.*?)[\\/]?$/)[1].length).replace(/\\/g,'/'); + const relative_fpn = source_filename.slice(pkginfo.srcroot.match(/^(.*?)[\\/]?$/)[1].length).replace(/\\/g,'/'); // delete any existing breakpoints not in the list - var src_line_nums = args.breakpoints.map(bp => bp.line); - this.dbgr.clearbreakpoints(javabp => { - var remove = javabp.srcfpn===relative_fpn && !src_line_nums.includes(javabp.linenum); - if (remove) javabp.vsbp = null; - return remove; - }); + const src_line_nums = args.breakpoints.map(bp => bp.line); + const deleted_breakpoints = this.dbgr.findBreakpoints( + javabp => (javabp.srcfpn === relative_fpn) && !src_line_nums.includes(javabp.linenum) + ); + deleted_breakpoints.forEach(bp => bp.vsbp = null); + this.dbgr.removeBreakpoints(deleted_breakpoints); - // return the list of new and existing breakpoints - // - setting a debugger bp is now asynchronous, so we do this as an orderly queue - const _setup_breakpoints = (o, idx, javabp_arr) => { - javabp_arr = javabp_arr || []; - var src_bp = o.args.breakpoints[idx|=0]; - if (!src_bp) { - // done - return $.Deferred().resolveWith(this, [javabp_arr]); - } - var dbgline = this.convertClientLineToDebugger(src_bp.line); - var options = {}; - if (src_bp.hitCondition) { - // the hit condition is an expression that requires evaluation - // until we get more comprehensive evaluation support, just allow integer literals - var m = src_bp.hitCondition.match(/^\s*(?:0x([0-9a-f]+)|0b([01]+)|0*(\d+([e]\+?\d+)?))\s*$/i); - var hitcount = m && (m[3] ? parseFloat(m[3]) : m[2] ? parseInt(m[2],2) : parseInt(m[1],16)); - if (!m || hitcount < 0 || hitcount > 0x7fffffff) return unverified_breakpoint(src_bp, 'The breakpoint is configured with an invalid hit count value'); - options.hitcount = hitcount; - } - return this.dbgr.setbreakpoint(o.relative_fpn, dbgline, options) - .then(javabp => { - if (!javabp.vsbp) { - // state is one of: set,notloaded,enabled,removed - var verified = !!javabp.state.match(/set|enabled/); - const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(dbgline)); - // the breakpoint *must* have an id field or it won't update properly - bp.id = ++this._breakpointId; - if (javabp.state === 'notloaded') - bp.message = 'The runtime hasn\'t loaded this code location'; - javabp.vsbp = bp; - } - javabp.vsbp.order = idx; - javabp_arr.push(javabp); - }). - then((/*javabp*/) => _setup_breakpoints(o, ++idx, javabp_arr)); - }; - - if (!this._set_breakpoints_queue) { - this._set_breakpoints_queue = { - _dbgr:this, - _queue:[], - add(item) { - if (this._queue.push(item) > 1) return; - this._next(); - }, - _setup_breakpoints: _setup_breakpoints, - _next() { - if (!this._queue.length) return; // done - this._setup_breakpoints(this._queue[0]).then(javabp_arr => { - // send back the VS Breakpoint instances - var response = this._queue[0].response; - sendBPResponse(response, javabp_arr.map(javabp => javabp.vsbp)); - // .. and do the next one - this._queue.shift(); - this._next(); - }); - }, - }; + // setting a debugger bp is now asynchronous, so we do this as an orderly queue + const bp_queue_len = this._set_breakpoints_queue.push({args,response,relative_fpn}); + if (bp_queue_len === 1) { + do { + const next_bp = this._set_breakpoints_queue[0]; + const javabp_arr = await this._setup_breakpoints(next_bp); + // send back the VS Breakpoint instances + sendBPResponse(next_bp.response, javabp_arr.map(javabp => javabp.vsbp)); + // .. and do the next one + this._set_breakpoints_queue.shift(); + } while (this._set_breakpoints_queue.length); } - - this._set_breakpoints_queue.add({args,response,relative_fpn}); } - setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) { - this.dbgr.clearBreakOnExceptions({response,args}) - .then(x => { - if (x.args.filters.includes('all')) { - x.set = this.dbgr.setBreakOnExceptions('both', x); - } else if (x.args.filters.includes('uncaught')) { - x.set = this.dbgr.setBreakOnExceptions('uncaught', x); - } else { - x.set = $.Deferred().resolveWith(this, [x]); + /** + * @param {*} o + * @param {number} idx + * @param {*[]} javabp_arr + */ + async _setup_breakpoints(o, idx = 0, javabp_arr = []) { + const src_bp = o.args.breakpoints[idx]; + if (!src_bp) { + // end of list + return javabp_arr; + } + const dbgline = this.convertClientLineToDebugger(src_bp.line); + const options = new BreakpointOptions(); + if (src_bp.hitCondition) { + // the hit condition is an expression that requires evaluation + // until we get more comprehensive evaluation support, just allow integer literals + const m = src_bp.hitCondition.match(/^\s*(?:0x([0-9a-f]+)|0b([01]+)|0*(\d+([e]\+?\d+)?))\s*$/i); + if (m) { + const hitcount = m[3] ? parseFloat(m[3]) : m[2] ? parseInt(m[2],2) : parseInt(m[1],16); + if ((hitcount > 0) && (hitcount <= 0x7fffffff)) { + options.hitcount = hitcount; } - x.set.then(x => this.sendResponse(x.response)); - }); + } + } + const javabp = await this.dbgr.setBreakpoint(o.relative_fpn, dbgline, options); + if (!javabp.vsbp) { + // state is one of: set,notloaded,enabled,removed + const verified = !!javabp.state.match(/set|enabled/); + const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(dbgline)); + // the breakpoint *must* have an id field or it won't update properly + bp['id'] = ++this._breakpointId; + if (javabp.state === 'notloaded') + bp['message'] = 'The runtime hasn\'t loaded this code location'; + javabp.vsbp = bp; + } + javabp.vsbp.order = idx; + javabp_arr.push(javabp); + return this._setup_breakpoints(o, ++idx, javabp_arr); + }; + + async setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) { + await this.dbgr.clearBreakOnExceptions(); + switch(true) { + case args.filters.includes('all'): + await this.dbgr.setBreakOnExceptions('both'); + break; + case args.filters.includes('uncaught'): + await this.dbgr.setBreakOnExceptions('uncaught'); + break; + } + this.sendResponse(response); } - threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) { - if (this._threads.array.length) { - D('threadsRequest: ' + this._threads.array.length); - response.body = { - threads: this._threads.array.filter(x=>x).map(t => { - var javaid = parseInt(t.threadid, 16); - return new Thread(t.vscode_threadid, `Thread (id:${javaid}) ${t.name||''}`); - }) - }; - this.sendResponse(response); - return; - } - - this.refreshThreads(response) - .then(response => { - response.body = { - threads: this._threads.array.filter(x=>x).map(t => { - var javaid = parseInt(t.threadid, 16); - return new Thread(t.vscode_threadid, `Thread (id:${javaid}) ${t.name}`); - }) - }; - this.sendResponse(response); - }) - .fail(() => { + async threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) { + if (!this._threads.length) { + try { + await this.refreshThreads(); + } catch (e) { response.success = false; this.sendResponse(response); - }); - } + return; + } + } + D('threadsRequest: ' + this._threads.length); + response.body = { + threads: this._threads + .filter(x => x) + .map(t => { + const javaid = parseInt(t.threadid, 16); + return new Thread(t.vscode_threadid, `Thread (id:${javaid}) ${t.name||''}`); + }) + }; + this.sendResponse(response); + } /** * Returns a stack trace for the given threadId */ - stackTraceRequest(response/*: DebugProtocol.StackTraceResponse*/, args/*: DebugProtocol.StackTraceArguments*/) { - - // debugger threadid's are a padded 64bit hex string - var thread = this.getThread(args.threadId); + async stackTraceRequest(response/*: DebugProtocol.StackTraceResponse*/, args/*: DebugProtocol.StackTraceArguments*/) { + D(`stackTraceRequest thread:${args.threadId}`); + // only retrieve the stack if the thread is paused + const thread = this.getThread(args.threadId); if (!thread) return this.failRequestNoThread('Stack trace', args.threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Stack trace', args.threadId, response); - // retrieve the (stack) frames from the debugger - this.dbgr.getframes(thread.threadid, {response, args, thread}) - .then((frames, x) => { - // first ensure that the line-tables for all the methods are loaded - var defs = frames.map(f => this.dbgr._ensuremethodlines(f.method)); - defs.unshift(frames,x); - return $.when.apply($,defs); - }) - .then((frames, x) => { - const startFrame = typeof x.args.startFrame === 'number' ? x.args.startFrame : 0; - const maxLevels = typeof x.args.levels === 'number' ? x.args.levels : frames.length-startFrame; - const endFrame = Math.min(startFrame + maxLevels, frames.length); - var stack = [], totalFrames = frames.length, highest_known_source=0; - const android_src_path = this._android_sources_path || '{Android SDK}'; - for (var i = startFrame; (i < endFrame) && x.thread.paused; i++) { - // the stack_frame_id must be unique across all threads - const stack_frame_id = x.thread.addStackFrameVariable(frames[i], i).frameId; - const name = `${frames[i].method.owningclass.name}.${frames[i].method.name}`; - const pkginfo = this.src_packages.packages[frames[i].method.owningclass.type.package]; - const srcloc = this.dbgr.line_idx_to_source_location(frames[i].method, frames[i].location.idx); - if (!srcloc && !pkginfo) { - totalFrames--; - continue; // ignore frames which have no location (they're probably synthetic) - } - const linenum = srcloc && this.convertDebuggerLineToClient(srcloc.linenum); - const sourcefile = frames[i].method.owningclass.src.sourcefile || (frames[i].method.owningclass.type.signature.match(/([^\/$]+)[;$]/)[1]+'.java'); - var srcRefId = 0; - if (!pkginfo) { - var sig = frames[i].method.owningclass.type.signature, srcInfo = this._sourceRefs[sig]; - if (!srcInfo) { - this._sourceRefs.all.push(srcInfo = { - id: this._sourceRefs.all.length, - signature:sig, - filepath:path.join(android_src_path,frames[i].method.owningclass.type.package.replace(/[.]/g,path.sep), sourcefile), - content:null - }); - this._sourceRefs[sig] = srcInfo; - } - srcRefId = srcInfo.id; - } - // if this is not a known package, check if android sources is valid - // - if it is, return the expected path - VSCode will auto-load it - // - if not, set the path to null and a sourceRequest will be made. - const srcpath = pkginfo ? path.join(pkginfo.package_path,sourcefile) - : this._android_sources_path ? srcInfo.filepath - : null; - const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId); - pkginfo && (highest_known_source=i); - // we don't support column number when reporting source locations (because JDWP only supports line-granularity) - // but in order to get the Exception UI to show, we must have a non-zero column - const colnum = (!i && x.thread.paused.last_exception && x.thread.paused.reasons[0]==='exception') ? 1 : 0; - stack.push(new StackFrame(stack_frame_id, name, src, linenum, colnum)); + try { + // retrieve the (stack) frames from the debugger + const frames = await this.dbgr.getFrames(thread.threadid); + // ensure that the line-tables for all the methods are loaded + await Promise.all(frames.map(f => this.dbgr._ensureMethodLines(f.method))); + + const startFrame = typeof args.startFrame === 'number' ? args.startFrame : 0; + const maxLevels = typeof args.levels === 'number' ? args.levels : frames.length-startFrame; + const endFrame = Math.min(startFrame + maxLevels, frames.length); + let stack = []; + let totalFrames = frames.length; + let highest_known_source = 0; + const android_src_path = this._android_sources_path || '{Android SDK}'; + for (let i = startFrame; (i < endFrame) && thread.paused; i++) { + // the stack_frame_id must be unique across all threads + const stack_frame = thread.createStackFrameVariable(frames[i], i); + const name = `${frames[i].method.owningclass.name}.${frames[i].method.name}`; + const type = frames[i].method.owningclass.type; + if (!(type instanceof JavaClassType)) { + totalFrames--; + continue; // sanity check - the call stack must be in a class type } - // trim the stack to exclude calls above the known sources - if (this.callStackDisplaySize > 0) { - stack = stack.slice(0,highest_known_source+this.callStackDisplaySize); - totalFrames = stack.length; + const pkginfo = this.src_packages.packages.get(type.package); + const srcloc = this.dbgr.frameToSourceLocation(frames[i]); + if (!srcloc && !pkginfo) { + totalFrames--; + continue; // ignore frames which have no location (they're probably synthetic) } - // return the frames - response.body = { - stackFrames: stack, - totalFrames: totalFrames, - }; - this.sendResponse(response); - }) - .fail(() => { - this.failRequest('No call stack is available', response); - }); + const linenum = srcloc && this.convertDebuggerLineToClient(srcloc.linenum); + const sourcefile = frames[i].method.owningclass.src.sourcefile || (type.signature.match(/([^\/$]+)[;$]/)[1]+'.java'); + let srcRefId = 0; + let srcInfo; + if (!pkginfo) { + const sig = type.signature; + srcInfo = this._sourceRefs[sig]; + if (!srcInfo) { + this._sourceRefs.all.push(srcInfo = { + id: this._sourceRefs.all.length, + signature:sig, + filepath:path.join(android_src_path,type.package.replace(/[.]/g,path.sep), sourcefile), + content:null + }); + this._sourceRefs[sig] = srcInfo; + } + srcRefId = srcInfo.id; + } + // if this is not a known package, check if android sources is valid + // - if it is, return the expected path - VSCode will auto-load it + // - if not, set the path to null and a sourceRequest will be made. + const srcpath = pkginfo ? path.join(pkginfo.package_path,sourcefile) + : this._android_sources_path ? srcInfo.filepath + : null; + const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId); + pkginfo && (highest_known_source=i); + // we don't support column number when reporting source locations (because JDWP only supports line-granularity) + // but in order to get the Exception UI to show, we must have a non-zero column + const colnum = (!i && thread.paused.last_exception && thread.paused.reasons[0]==='exception') ? 1 : 0; + stack.push(new StackFrame(stack_frame.variableReference, name, src, linenum, colnum)); + } + // trim the stack to exclude calls above the known sources + if (this.callStackDisplaySize > 0) { + stack = stack.slice(0,highest_known_source+this.callStackDisplaySize); + totalFrames = stack.length; + } + // return the frames + response.body = { + stackFrames: stack, + totalFrames: totalFrames, + }; + this.sendResponse(response); + } catch(e) { + return this.failRequest('No call stack is available', response); + } } - scopesRequest(response/*: DebugProtocol.ScopesResponse*/, args/*: DebugProtocol.ScopesArguments*/) { - var threadId = variableRefToThreadId(args.frameId); - var thread = this.getThread(threadId); + async scopesRequest(response/*: DebugProtocol.ScopesResponse*/, args/*: DebugProtocol.ScopesArguments*/) { + D(`scopesRequest frame:${args.frameId}`); + const threadId = AndroidThread.variableRefToThreadId(args.frameId); + const thread = this.getThread(threadId); if (!thread) return this.failRequestNoThread('Scopes',threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Scopes', threadId, response); - var scopes = [new Scope("Local", args.frameId, false)]; + const scopes = [new Scope("Local", args.frameId, false)]; response.body = { - scopes: scopes + scopes, }; - var last_exception = thread.paused.last_exception; - if (last_exception && !last_exception.objvar) { - // retrieve the exception object - thread.allocateExceptionScopeReference(args.frameId); - this.dbgr.getExceptionLocal(last_exception.exception, {thread,response,scopes,last_exception}) - .then((ex_local,x) => { - x.last_exception.objvar = ex_local; - return $.when(x, x.thread.getVariables(x.last_exception.scopeRef)); - }) - .then((x, vars) => { - var {response,scopes,last_exception} = x; - // put the exception first - otherwise it can get lost if there's a lot of locals - scopes.unshift(new Scope("Exception: " + last_exception.objvar.type.typename, last_exception.scopeRef, false)); - this.sendResponse(response); - // notify the exceptionInfo who may be waiting on us - if (last_exception.waitForExObject) { - var def = last_exception.waitForExObject; - last_exception.waitForExObject = null; - def.resolveWith(this, []); - } - }) - .fail((/*e*/) => { this.sendResponse(response); }); + const last_exception = thread.paused.last_exception; + if (!last_exception) { + this.sendResponse(response); return; } - this.sendResponse(response); - } - sourceRequest(response/*: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments*/) { - var content = + try { + last_exception.scopeRef = args.frameId + 1; + const scope = new Scope(`Exception: ${last_exception.exceptionValue.type.typename}`, last_exception.scopeRef, false); + // put the exception first - otherwise it can get lost if there's a lot of locals + scopes.unshift(scope); + } catch(e) { + } + this.sendResponse(response); +} + + sourceRequest(response/*: DebugProtocol.SourceResponse*/, args/*: DebugProtocol.SourceArguments*/) { + D(`sourceRequest: ${args.sourceId}`); + const content = `/* The source for this class is unavailable. @@ -956,8 +818,8 @@ class AndroidDebugSession extends DebugSession { `; // don't actually attempt to load the file here - just recheck to see if the sources // path is valid yet. - if (process.env.ANDROID_HOME && this.dbgr.session.apilevel) { - var sources_path = path.join(process.env.ANDROID_HOME,'sources','android-'+this.dbgr.session.apilevel); + if (process.env.ANDROID_HOME && this.device_api_level) { + const sources_path = path.join(process.env.ANDROID_HOME,'sources','android-'+this.device_api_level); fs.stat(sources_path, (err,stat) => { if (!err && stat && stat.isDirectory()) this._android_sources_path = sources_path; @@ -968,66 +830,104 @@ class AndroidDebugSession extends DebugSession { this.sendResponse(response); } - variablesRequest(response/*: DebugProtocol.VariablesResponse*/, args/*: DebugProtocol.VariablesArguments*/) { - var threadId = variableRefToThreadId(args.variablesReference); - var thread = this.getThread(threadId); + /** + * + * @param {*} response + * @param {{variablesReference:VSCVariableReference}} args + */ + async variablesRequest(response/*: DebugProtocol.VariablesResponse*/, args/*: DebugProtocol.VariablesArguments*/) { + D(`variablesRequest variablesReference:${args.variablesReference}`); + const threadId = AndroidThread.variableRefToThreadId(args.variablesReference); + const thread = this.getThread(threadId); if (!thread) return this.failRequestNoThread('Variables',threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Variables',threadId, response); - thread.getVariables(args.variablesReference) - .then(vars => { - response.body = { - variables: vars, - }; - this.sendResponse(response); - }); + let variables = []; + const stack_frame = thread.findStackFrame(args.variablesReference); + const vref = args.variablesReference % 1e6; + switch(vref) { + case 0: // frame scope reference + case 1: // exception scope reference + variables = await stack_frame.getLocalVariables(); + if (vref === 1) { + variables = [stack_frame.makeVariableValue(thread.paused.last_exception.exceptionValue)]; + variables = await stack_frame.getExpandableValues(variables[0].variablesReference); + } + break; + default: { + // variable reference for an expandable entry + variables = await stack_frame.getExpandableValues(args.variablesReference); + break; + } + } + response.body = { + variables, + }; + this.sendResponse(response); } + /** + * Choose a stopped thread to show in VSCode. + * This function prioritises multiple stopped threads. + */ checkPendingThreadBreaks() { - var stepping_thread = this._threads.array.find(t => t && t.stepTimeout); - var paused_threads = this._threads.array.filter(t => t && t.paused); - var stopped_thread = paused_threads.find(t => t.paused.stoppedEvent); - if (!stopped_thread && !stepping_thread && paused_threads.length) { - // prioritise any stepped thread (if it's stopped) or whichever other thread stopped first - var thread; - var paused_step_thread = paused_threads.find(t => t.paused.reasons.includes("step")); - if (paused_step_thread) { - thread = paused_step_thread; - } else { - paused_threads.sort((a,b) => a.paused.when - b.paused.when); - thread = paused_threads[0]; - } - // if the break was due to a breakpoint and it has since been removed, just resume the thread - if (thread.paused.reasons.length === 1 && thread.paused.reasons[0] === 'breakpoint') { - var bp = this.dbgr.breakpoints.bysrcloc[thread.paused.location.qtype + ':' + thread.paused.location.linenum]; - if (!bp) { - this.doContinue(thread); - return; - } - } - var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid, thread.paused.last_exception && "Exception thrown"); - thread.paused.stoppedEvent = event; - this.sendEvent(event); + // threads that are currently mid-step + const stepping_thread = this._threads.find(t => t && t.stepTimeout); + // threads that are currently paused + const paused_threads = this._threads.filter(t => t && t.paused); + // paused threads that we've notified VSCode about + const stopped_thread = paused_threads.find(t => t.paused.stoppedEvent); + if (stopped_thread || stepping_thread || !paused_threads.length) { + // we already have a stopped thread, or + // we are waiting for the stepping thread to complete its step, or + // there are no paused threads (i.e all threads are currently running) + return; } + + // prioritise any stepped thread (if it's stopped) - this allows the user to step through + // code without bouncing between different threads + let thread; + const paused_step_thread = paused_threads.find(t => t.paused.reasons.includes("step")); + if (paused_step_thread) { + thread = paused_step_thread; + } else { + // if there's no paused step thread, choose the earliest paused thread + paused_threads.sort((a,b) => a.paused.when - b.paused.when); + thread = paused_threads[0]; + } + // if the break was due to a breakpoint and it has since been removed, just silently resume the thread + if (thread.paused.reasons.length === 1 && thread.paused.reasons[0] === 'breakpoint') { + const { linenum, qtype} = thread.paused.location; + const bp = this.dbgr.breakpoints.byID.get(`${linenum}:${qtype}`); + if (!bp) { + this.continueThread(thread); + return; + } + } + // tell VSCode about the stopped thread + const event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid, thread.paused.last_exception && "Exception thrown"); + thread.paused.stoppedEvent = event; + this.sendEvent(event); } - doContinue(thread) { + /** + * @param {AndroidThread} thread + */ + async continueThread(thread) { thread.paused = null; - this.checkPendingThreadBreaks(); - this.dbgr.resumethread(thread.threadid); - console.log(''); + await this.dbgr.resumeThread(thread.threadid); } continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) { - D('Continue'); + D(`Continue thread:${args.threadId}`); - var t = this.getThread(args.threadId); - if (!t) return this.failRequestNoThread('Continue', args.threadId, response); - if (!t.paused) return this.failRequestThreadNotSuspended('Continue', args.threadId, response); + const thread = this.getThread(args.threadId); + if (!thread) return this.failRequestNoThread('Continue', args.threadId, response); + if (!thread.paused) return this.failRequestThreadNotSuspended('Continue', args.threadId, response); this.sendResponse(response); - this.doContinue(t); + this.continueThread(thread); } /** @@ -1035,32 +935,35 @@ class AndroidDebugSession extends DebugSession { */ onStep(e) { // if we step into a breakpoint, both onBreakpointHit and onStep will be called - D('step hit: ' + JSON.stringify(e.stoppedlocation)); - this.reportStoppedEvent("step", e.stoppedlocation); + D(`step hit: ${e.stoppedLocation}`); + this.reportStoppedEvent("step", e.stoppedLocation); } /** * Called by the user to start a step operation + * @param {DebuggerStepType} which + * @param {*} response + * @param {*} args */ doStep(which, response, args) { - D('step '+which); + D(`step ${which}`); - var t = this.getThread(args.threadId); - if (!t) return this.failRequestNoThread('Step', args.threadId, response); - if (!t.paused) return this.failRequestThreadNotSuspended('Step', args.threadId, response); + const thread = this.getThread(args.threadId); + if (!thread) return this.failRequestNoThread('Step', args.threadId, response); + if (!thread.paused) return this.failRequestThreadNotSuspended('Step', args.threadId, response); - t.paused = null; + thread.paused = null; this.sendResponse(response); - // we time the step - if it takes more than 2 seconds, we switch to any other threads that are waiting - t.stepTimeout = setTimeout(t => { - D('Step timeout on thread:'+t.threadid); - t.stepTimeout = null; + + // we time the step - if it takes too long to complete, we switch to any other threads that are waiting + thread.stepTimeout = setTimeout(() => { + D(`Step timeout on thread: ${thread.threadid}`); + thread.stepTimeout = null; this.checkPendingThreadBreaks(); - }, 2000, t); - t.stepTimeout._begun = process.hrtime(); - this.dbgr.step(which, t.threadid); - console.log(''); + }, 2000); + + this.dbgr.step(which, thread.threadid); } stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) { @@ -1077,183 +980,45 @@ class AndroidDebugSession extends DebugSession { /** * Called by the debugger if an exception event is triggered + * @param {JavaExceptionEvent} e */ - onException(e) { + async onException(e) { // it's possible for the debugger to send multiple exception notifications for the same thread, depending on the package filters - D('exception hit: ' + JSON.stringify(e.throwlocation)); - var last_exception = { - exception: e.event.exception, - threadid: e.throwlocation.threadid, - frameId: null, // allocated during scopesRequest - scopeRef: null, // allocated during scopesRequest - }; + D(`exception hit: ${e.throwlocation}`); + const thread_id = e.throwlocation.threadid; + // retrieve the exception object + const ex_value = await this.dbgr.getExceptionValue(thread_id, e.event.exception) + const last_exception = new DebuggerException(ex_value, thread_id); this.reportStoppedEvent("exception", e.throwlocation, last_exception); } - /** - * Called by the debugger if a thread start/end event is triggered - */ - onThreadChange(e) { - D(`thread ${e.state}: ${e.threadid}(${parseInt(e.threadid,16)})`); - switch(e.state) { - case 'start': - this.dbgr.threadinfos([e.threadid]) - .then((threadinfos) => { - var ti = threadinfos[0], t = this.getThread(ti.threadid), event = new ThreadEvent(); - t.name = ti.name; - event.body = { reason:'started', threadId: t.vscode_threadid }; - this.sendEvent(event); - }) - .always(() => this.dbgr.resumethread(e.threadid)); - return; - case 'end': - var t = this._threads[e.threadid]; - if (t) { - t.stepTimeout && clearTimeout(t.stepTimeout) && (t.stepTimeout = null); - delete this._threads[e.threadid]; - delete this._threads.array[t.vscode_threadid]; - var event = new ThreadEvent(); - event.body = { reason:'exited', threadId: t.vscode_threadid }; - this.sendEvent(event); - this.checkPendingThreadBreaks(); // in case we were stepping this thread - } - break; - } - this.dbgr.resumethread(e.threadid); - } - - setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) { - - var threadId = variableRefToThreadId(args.variablesReference); - var t = this.getThread(threadId); - if (!t) return this.failRequestNoThread('Set variable', threadId, response); - if (!t.paused) return this.failRequestThreadNotSuspended('Set variable', threadId, response); - - t.setVariableValue(args) - .then(function(response,vsvar) { - response.body = { - value: vsvar.value, - type: vsvar.type, - variablesReference: vsvar.variablesReference, - }; - this.sendResponse(response); - }.bind(this,response)) - .fail(function(response,e) { - response.success = false; - response.message = e.message; - this.sendResponse(response); - }.bind(this,response)); - } - - /** - * Called by VSCode to perform watch, console and hover evaluations - */ - evaluateRequest(response/*: DebugProtocol.EvaluateResponse*/, args/*: DebugProtocol.EvaluateArguments*/) { - - // Some notes to remember: - // annoyingly, during stepping, the step can complete before the resume has called evaluateRequest on watches. - // The order can go: doStep(running=true),onStep(running=false),evaluateRequest(),evaluateRequest() - // so we end up evaluating twice... - // also annoyingly, this method is called before the locals in the current stack frame are evaluated - // and even more annoyingly, Android (or JDWP) seems to get confused on the first request when we're retrieving multiple values, fields, etc - // so we have to queue them or we end up with strange results - - // look for a matching entry in the list (other than at index:0) - var previdx = this._evals_queue.findIndex(e => e.args.expression === args.expression); - if (previdx > 0) { - // if we find a match, immediately fail the old one and queue the new one - var prev = this._evals_queue.splice(previdx,1)[0]; - prev.response.success = false; - prev.response.message = '(evaluating)'; - this.sendResponse(prev.response); - } - // if there's no frameId, we are being asked to evaluate the value in the 'global' context - var getvars; - if (args.frameId) { - var threadId = variableRefToThreadId(args.frameId); - var thread = this.getThread(threadId); - if (!thread) return this.failRequestNoThread('Evaluate',threadId, response); - if (!thread.paused) return this.failRequestThreadNotSuspended('Evaluate',threadId, response); - getvars = thread._ensureLocals(args.frameId).then(frameId => { - var locals = thread.paused.stack_frame_vars[frameId].locals; - return $.Deferred().resolve(thread, locals.variableHandles[frameId].cached, locals); - }) - } else { - // global context - no locals - getvars = $.Deferred().resolve(null, [], this._globals); - } - - this._evals_queue.push({response,args,getvars,thread}); - - // if we're currently processing, just wait - if (this._evals_queue.length > 1) { - return; - } - - // begin processing - this.doNextEvaluateRequest(); - } - - doNextEvaluateRequest() { - if (!this._evals_queue.length) { - return; - } - var {response, args, getvars} = this._evals_queue[0]; - - // wait for any locals in the given context to be retrieved - getvars.then((thread, locals, vars) => { - return evaluate(args.expression, thread, locals, vars, this.dbgr); - }) - .then((value,variablesReference) => { - response.body = { result:value, variablesReference:variablesReference|0 }; - }) - .fail(e => { - response.success = false; - response.message = e.message; - }) - .always(() => { - this.sendResponse(response); - this._evals_queue.shift(); - this.doNextEvaluateRequest(); - }) - } - - exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) { - var thread = this.getThread(args.threadId); + async exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) { + D(`exceptionInfoRequest: ${args.threadId}`); + const thread = this.getThread(args.threadId); if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Exception info', args.threadId, response); if (!thread.paused.last_exception) return this.failRequest('No exception available', response); - if (!thread.paused.last_exception.objvar || !thread.paused.last_exception.cached) { - // we must wait for the exception object to be retreived as a local (along with the message field) - if (!thread.paused.last_exception.waitForExObject) { - thread.paused.last_exception.waitForExObject = $.Deferred().then(() => { - // redo the request - this.exceptionInfoRequest(response, args); - }); - } - return; - } - var exobj = thread.paused.last_exception.objvar; - var exmsg = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name); - exmsg = (exmsg && exmsg.string) || ''; + // we must wait for the exception object to be retreived as a local (along with the message field) + const ex_value = thread.paused.last_exception.exceptionValue; + const message = ex_value.data.msg.string; response.body = { /** ID of the exception that was thrown. */ - exceptionId: exobj.type.typename, + exceptionId: ex_value.type.typename, /** Descriptive text for the exception provided by the debug adapter. */ - description: exmsg, + description: `${os.EOL}${message}`, /** Mode that caused the exception notification to be raised. */ //'never' | 'always' | 'unhandled' | 'userUnhandled'; breakMode: 'always', /** Detailed information about the exception. */ details: { /** Message contained in the exception. */ - message: exmsg, + message, /** Short type name of the exception object. */ - typeName: exobj.type.typename, + typeName: ex_value.type.typename, /** Fully-qualified type name of the exception object. */ - fullTypeName: signatureToFullyQualifiedType(exobj.type.signature), + fullTypeName: ex_value.type.fullyQualifiedName(), /** Optional expression that can be evaluated in the current scope to obtain the exception object. */ //evaluateName: "evaluateName", /** Stack trace at the time the exception was thrown. */ @@ -1264,7 +1029,170 @@ class AndroidDebugSession extends DebugSession { } this.sendResponse(response); } + + /** + * Called by the debugger if a thread start/end event is triggered + */ + async onThreadChange(e) { + D(`thread ${e.state}: ${e.threadid}(${parseInt(e.threadid,16)})`); + switch(e.state) { + case 'start': { + try { + const threadinfos = await this.dbgr.getJavaThreadInfos([e.threadid]); + const t = this.getThread(threadinfos[0].threadid, threadinfos[0].name); + this.sendEvent(new ThreadEvent('started', t.vscode_threadid)); + } catch(e) { + } + break; + } + case 'end': + const t = this._threads.find(t => t && t.threadid === e.threadid); + if (t) { + if (t.stepTimeout) { + clearTimeout(t.stepTimeout); + t.stepTimeout = null; + } + delete this._threads[t.vscode_threadid]; + this.sendEvent(new ThreadEvent('exited', t.vscode_threadid)); + this.checkPendingThreadBreaks(); // in case we were stepping this thread + } + break; + } + this.dbgr.resumeThread(e.threadid); + } + + /** + * @typedef SetVariableArgs + * @property {string} name + * @property {string} value + * @property {number} variablesReference + * + * @param {*} response + * @param {SetVariableArgs} args + */ + async setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) { + + const threadId = AndroidThread.variableRefToThreadId(args.variablesReference); + const thread = this.getThread(threadId); + if (!thread) return this.failRequestNoThread('Set variable', threadId, response); + if (!thread.paused) return this.failRequestThreadNotSuspended('Set variable', threadId, response); + + try { + // retrieve the stack frame the variable belongs to + const stack_frame = thread.findStackFrame(args.variablesReference); + // evaluate the expression + const locals = await stack_frame.getLocals(); + const value = await evaluate(args.value, thread, locals, this.dbgr); + // update the variable + const vsvar = await stack_frame.setVariableValue(args.variablesReference, args.name, value); + response.body = { + value: vsvar.value, + type: vsvar.type, + variablesReference: vsvar.variablesReference, + }; + } catch (e) { + response.success = false; + response.message = e.message; + } + this.sendResponse(response); + } + + /** + * Called by VSCode to perform watch, console and hover evaluations + */ + async evaluateRequest(response/*: DebugProtocol.EvaluateResponse*/, args/*: DebugProtocol.EvaluateArguments*/) { + + // Some notes to remember: + // annoyingly, during stepping, the step can complete before the resume has called evaluateRequest on watches. + // The order can go: doStep(running=true),onStep(running=false),evaluateRequest(),evaluateRequest() + // so we end up evaluating twice... + // also annoyingly, this method is called before the locals in the current stack frame are evaluated + // and even more annoyingly, Android (or JDWP) seems to get confused on the first request when we're retrieving multiple values, fields, etc + // so we have to queue them or we end up with strange results + + // look for a matching entry in the list (other than at index:0) + const previdx = this._evals_queue.findIndex(e => e.expression === args.expression); + if (previdx > 0) { + // if we find a match, immediately fail the old one and queue the new one + const prev = this._evals_queue.splice(previdx,1)[0]; + prev.response.success = false; + prev.response.message = '(evaluating)'; + this.sendResponse(prev.response); + } + + const eval_info = { + expression: args.expression, + response, + /** @type {DebuggerValue[]} */ + locals: null, + /** @type {VariableManager} */ + var_manager: null, + /** @type {AndroidThread} */ + thread: null, + } + if (args.frameId) { + const threadId = AndroidThread.variableRefToThreadId(args.frameId); + const thread = this.getThread(threadId); + if (!thread) return this.failRequestNoThread('Evaluate',threadId, response); + if (!thread.paused) return this.failRequestThreadNotSuspended('Evaluate',threadId, response); + eval_info.thread = thread; + const stack_frame = thread.findStackFrame(args.frameId); + eval_info.var_manager = stack_frame; + eval_info.locals = await stack_frame.getLocals(); + } else { + // if there's no frameId, we are being asked to evaluate the value in the 'global' context. + // This is a problem because there's no associated stack frame, so we include any locals in the evaluation. + // We still want the user to be able to call into the runtime to create new objects, evaluate static fields, etc so + // we choose an arbitrary paused thread to execute on (without this, the only evaluations we could perform + // would require primitive literals) + const thread = this._threads.find(t => t && t.paused); + if (!thread) return this.failRequest(`No threads are paused`, response); + eval_info.thread = thread; + eval_info.var_manager = thread.getGlobalVariableManager(); + eval_info.locals = []; + } + + const queue_len = this._evals_queue.push(eval_info); + if (queue_len > 1) { + return; + } + + while (this._evals_queue.length > 0) { + const { expression, response, locals, var_manager, thread } = this._evals_queue[0]; + try { + const value = await evaluate(expression, thread, locals, this.dbgr); + const v = var_manager.makeVariableValue(value); + response.body = { + result: v.value, + variablesReference: v.variablesReference|0 + }; + } catch (e) { + response.success = false; + response.message = e.message; + } + this.sendResponse(response); + this._evals_queue.shift(); + } + } } +/** + * @param {string} p + */ +function ensure_path_end_slash(p) { + return p + (/[\\/]$/.test(p) ? '' : path.sep); +} -DebugSession.run(AndroidDebugSession); \ No newline at end of file +/** + * @param {string} fullpath + * @param {string} subpath + */ +function is_subpath_of(fullpath, subpath) { + if (!subpath || !fullpath) { + return false; + } + subpath = ensure_path_end_slash(subpath); + return fullpath.slice(0,subpath.length) === subpath; +} + +DebugSession.run(AndroidDebugSession); diff --git a/src/debugger-types.js b/src/debugger-types.js new file mode 100644 index 0000000..359160b --- /dev/null +++ b/src/debugger-types.js @@ -0,0 +1,772 @@ +const { ADBClient } = require('./adbclient'); +const { PackageInfo } = require('./package-searcher'); +//const { JavaType } = require('./util'); +const { splitSourcePath } = require('./utils/source-file'); + +class BuildInfo { + + /** + * @param {string} pkgname + * @param {Map} packages + * @param {string} launchActivity + */ + constructor(pkgname, packages, launchActivity) { + this.pkgname = pkgname; + this.packages = packages; + this.launchActivity = launchActivity; + /** the arguments passed to `am start` */ + this.startCommandArgs = [ + '-D', // enable debugging + '--activity-brought-to-front', + '-a android.intent.action.MAIN', + '-c android.intent.category.LAUNCHER', + `-n ${pkgname}/${launchActivity}`, + ]; + /** + * the amount of time to wait after 'am start ...' is invoked. + * We need this because invoking JDWP too soon causes a hang. + */ + this.postLaunchPause = 1000; + } +} + +/** + * A single debugger session + */ +class DebugSession { + + /** + * @param {BuildInfo} build + * @param {string} deviceid + */ + constructor(build, deviceid) { + /** + * Build information for this session + */ + this.build = build; + + /** + * The device ID of the device being debugged + */ + this.deviceid = deviceid; + + /** + * The ADB connection to the device being debugged + * @type {ADBClient} + */ + this.adbclient = null; + + /** + * Location of the last stop event (breakpoint, exception, step) + * @type {SourceLocation} + */ + this.stoppedLocation = null; + + /** + * The entire list of retrieved types during the debug session + * @type {DebuggerTypeInfo[]} + */ + this.classList = []; + + /** + * Map of type signatures to cached types + * @type {Map>} + */ + this.classCache = new Map(); + + /** + * The class-prepare filters set up on the device + * @type {Set} + */ + this.classPrepareFilters = new Set(); + + /** + * The set of class signatures already prepared + * @type {Set} + */ + this.preparedClasses = new Set(); + + /** + * Enabled step JDWP IDs for each thread + * @type {Map} + */ + this.stepIDs = new Map(); + + /** + * The counts of thread-suspend calls. A thread is only resumed when the + * all suspend calls are matched with resume calls. + * @type {Map} + */ + this.threadSuspends = new Map(); + + /** + * The queue of pending method invoke expressions to be called for each thread. + * Method invokes can only be called sequentially on a per-thread basis. + * @type {Map} + */ + this.methodInvokeQueues = new Map(); + } +} + +class JavaTaggedValue { + /** + * + * @param {string|number|boolean} value + * @param {JavaValueType} valuetype + */ + constructor(value, valuetype) { + this.value = value; + this.valuetype = valuetype; + } + + static signatureToJavaValueType(s) { + return { + B: 'byte',C:'char',D:'double',F:'float',I:'int',J:'long','S':'short',V:'void',Z:'boolean' + }[s[0]] || 'oref'; + } + + /** + * + * @param {DebuggerValue} v + * @param {string} [signature] + */ + static from(v, signature) { + return new JavaTaggedValue(v.value, JavaTaggedValue.signatureToJavaValueType(signature || v.type.signature)); + } +} + +/** + * Base class of Java types + */ +class JavaType { + + /** + * @param {string} signature JRE type signature + * @param {string} typename human-readable type name + * @param {boolean} [invalid] true if the type could not be parsed from the signature + */ + constructor(signature, typename, invalid) { + this.signature = signature; + this.typename = typename; + if (invalid) { + this.invalid = invalid; + } + } + + fullyQualifiedName() { + return this.typename; + } + + /** @type {Map} */ + static _cache = new Map(); + + /** + * @param {string} signature + * @returns {JavaType} + */ + static from(signature) { + let type = JavaType._cache.get(signature); + if (!type) { + type = JavaClassType.from(signature) + || JavaArrayType.from(signature) + || JavaPrimitiveType.from(signature) + || new JavaType(signature, signature, true); + JavaType._cache.set(signature, type); + } + return type; + } + + static get Object() { + return JavaType.from('Ljava/lang/Object;'); + } + + static get String() { + return JavaType.from('Ljava/lang/String;'); + } + + static get byte() { + return JavaType.from('B'); + } + static get short() { + return JavaType.from('S'); + } + static get int() { + return JavaType.from('I'); + } + static get long() { + return JavaType.from('J'); + } + static get float() { + return JavaType.from('F'); + } + static get double() { + return JavaType.from('D'); + } + static get char() { + return JavaType.from('C'); + } + static get boolean() { + return JavaType.from('Z'); + } + static null = new JavaType('Lnull;', 'null'); // null has no type really, but we need something for literals + + /** + * @param {JavaType} t + */ + static isArray(t) { return /^\[/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isByte(t) { return /^B$/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isClass(t) { return /^L/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isReference(t) { return /^[L[]/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isPrimitive(t) { return /^[BCIJSFDZ]$/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isInteger(t) { return /^[BIS]$/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isLong(t) { return /^J$/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isFloat(t) { return /^[FD]$/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isArrayIndex(t) { return /^[BCIJS]$/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) } + + /** + * @param {JavaType} t + */ + static isString(t) { return t.signature === this.String.signature } + + /** + * @param {JavaType} t + */ + static isChar(t) { return t.signature === this.char.signature } + + /** + * @param {JavaType} t + */ + static isBoolean(t) { return t.signature === this.boolean.signature } +} + +class JavaClassType extends JavaType { + + /** + * + * @param {string} signature + * @param {string} package_name + * @param {string} typename + * @param {boolean} anonymous + */ + constructor(signature, package_name, typename, anonymous) { + super(signature, typename); + this.package = package_name; + this.anonymous = anonymous; + } + + fullyQualifiedName() { + return this.package ? `${this.package}.${this.typename}` : this.typename; + } + + /** + * @param {string} signature + */ + static from(signature) { + const class_match = signature.match(/^L([^$]+)\/([^$\/]+)(\$.+)?;$/); + if (!class_match) { + return null; + } + const package_name = class_match[1].replace(/\//g,'.'); + const typename = (class_match[2]+(class_match[3]||'')).replace(/\$(?=[^\d])/g,'.'); + const anonymous = /\$\d/.test(class_match[3]); + return new JavaClassType(signature, package_name, typename, anonymous); + } +} + +class JavaArrayType extends JavaType { + + /** + * @param {string} signature JRE type signature + * @param {number} arraydims number of array dimensions + * @param {JavaType} elementType array element type + */ + constructor(signature, arraydims, elementType) { + super(signature, `${elementType.typename}[]`); + this.arraydims = arraydims; + this.elementType = elementType; + } + + fullyQualifiedName() { + return `${this.elementType.fullyQualifiedName()}[]`; + } + + static from(signature) { + const array_match = signature.match(/^(\[+)(.+)$/); + if (!array_match) { + return null; + } + const elementType = JavaType.from(array_match[1].slice(0,-1) + array_match[2]); + return new JavaArrayType(signature, array_match[1].length, elementType); + } +} + +class JavaPrimitiveType extends JavaType { + + /** + * @param {string} signature + * @param {string} typename + */ + constructor(signature, typename) { + super(signature, typename); + } + + /** + * @param {string} signature + */ + static from(signature) { + return Object.prototype.hasOwnProperty.call(JavaPrimitiveType.bySignature, signature) + ? JavaPrimitiveType.bySignature[signature] + : null; + } + + static bySignature = { + B: new JavaPrimitiveType('B', 'byte'), + C: new JavaPrimitiveType('C', 'char'), + F: new JavaPrimitiveType('F', 'float'), + D: new JavaPrimitiveType('D', 'double'), + I: new JavaPrimitiveType('I', 'int'), + J: new JavaPrimitiveType('J', 'long'), + S: new JavaPrimitiveType('S', 'short'), + V: new JavaPrimitiveType('V', 'void'), + Z: new JavaPrimitiveType('Z', 'boolean'), + } +} + +class DebuggerValue { + + /** + * @param {DebuggerValueType} vtype + * @param {JavaType} type + * @param {*} value + * @param {boolean} valid + * @param {boolean} hasnullvalue + * @param {string} name + * @param {*} data + */ + constructor(vtype, type, value, valid, hasnullvalue, name, data) { + this.vtype = vtype; + this.hasnullvalue = hasnullvalue; + this.name = name; + this.type = type; + this.valid = valid; + this.value = value; + this.data = data; + + /** @type {string} */ + this.string = null; + /** @type {number} */ + this.biglen = null; + /** @type {number} */ + this.arraylen = null; + /** @type {string} */ + this.fqname = null; + } +} + +class LiteralValue extends DebuggerValue { + /** + * @param {JavaType} type + * @param {*} value + * @param {boolean} [hasnullvalue] + * @param {*} [data] + */ + constructor(type, value, hasnullvalue = false, data = null) { + super('literal', type, value, true, hasnullvalue, '', data); + } + + static Null = new LiteralValue(JavaType.null, '0000000000000000', true); +} + +/** + * The base class of all debugger events invoked by JDWP + */ +class DebuggerEvent { + constructor(event) { + this.event = event; + } +} + +class JavaBreakpointEvent extends DebuggerEvent { + /** + * + * @param {*} event + * @param {SourceLocation} stoppedLocation + * @param {DebuggerBreakpoint} breakpoint + */ + constructor(event, stoppedLocation, breakpoint) { + super(event) + this.stoppedLocation = stoppedLocation; + this.bp = breakpoint; + } +} + +class JavaExceptionEvent extends DebuggerEvent { + /** + * @param {JavaObjectID} event + * @param {SourceLocation} throwlocation + * @param {SourceLocation} catchlocation + */ + constructor(event, throwlocation, catchlocation) { + super(event); + this.throwlocation = throwlocation; + this.catchlocation = catchlocation; + }; +} + +class DebuggerException { + /** + * @param {DebuggerValue} exceptionValue + * @param {JavaThreadID} threadid + */ + constructor(exceptionValue, threadid) { + this.exceptionValue = exceptionValue; + this.threadid = threadid; + /** @type {VSCVariableReference} */ + this.scopeRef = null; + /** @type {VSCVariableReference} */ + this.frameId = null; + } +} + +class BreakpointLocation { + /** + * @param {DebuggerBreakpoint} bp + * @param {DebuggerTypeInfo} c + * @param {DebuggerMethodInfo} m + * @param {hex64} l + */ + constructor(bp, c, m, l) { + this.bp = bp; + this.c = c; + this.m = m; + this.l = l; + } +} + +class SourceLocation { + + /** + * @param {string} qtype + * @param {number} linenum + * @param {boolean} exact + * @param {JavaThreadID} threadid + */ + constructor(qtype, linenum, exact, threadid) { + this.qtype = qtype; + this.linenum = linenum; + this.exact = exact; + this.threadid = threadid; + } + + toString() { + return JSON.stringify(this); + } +} + +class DebuggerMethodInfo { + + /** + * @param {JavaMethod} m + * @param {DebuggerTypeInfo} owningclass + */ + constructor(m, owningclass) { + this._method = m; + this.owningclass = owningclass; + /** @type {JavaVarTable} */ + this.vartable = null; + /** @type {JavaLineTable} */ + this.linetable = null; + } + + get genericsig() { return this._method.genericsig } + + get methodid() { return this._method.methodid } + + /** + * https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6-200-A.1 + */ + get modbits() { return this._method.modbits } + + get name() { return this._method.name } + + get sig() { return this._method.sig } + + get isStatic() { + return (this._method.modbits & 0x0008) !== 0; + } + + /** + * @param {JavaLineTable} linetable + */ + setLineTable(linetable) { + return this.linetable = linetable; + } + + /** + * @param {JavaVarTable} vartable + */ + setVarTable(vartable) { + return this.vartable = vartable; + } + + get returnTypeSignature() { + return (this._method.genericsig || this._method.sig).match(/\)(.+)$/)[1]; + } + + static NullLineTable = { + start: '0000000000000000', + end: '0000000000000000', + lines: [], + }; +} + +class DebuggerFrameInfo { + /** + * + * @param {JavaFrame} frame + * @param {DebuggerMethodInfo} method + * @param {JavaThreadID} threadid + */ + constructor(frame, method, threadid) { + this._frame = frame; + this.method = method; + this.threadid = threadid; + } + + get frameid() { + return this._frame.frameid; + } + + get location() { + return this._frame.location; + } +} + +class DebuggerBreakpoint { + + /** + * @param {string} srcfpn + * @param {number} linenum + * @param {BreakpointOptions} options + * @param {BreakpointState} initialState + */ + constructor(srcfpn, linenum, options, initialState = 'set') { + const cls = splitSourcePath(srcfpn); + this.id = DebuggerBreakpoint.makeBreakpointID(srcfpn, linenum); + this.srcfpn = srcfpn; + this.qtype = cls.qtype; + this.pkg = cls.pkg; + this.type = cls.type; + this.linenum = linenum; + this.options = options; + this.sigpattern = new RegExp(`^L${cls.qtype}([$][$a-zA-Z0-9_]+)?;$`), + this.state = initialState; // set,notloaded,enabled,removed + this.hitcount = 0; // number of times this bp was hit during execution + this.stopcount = 0; // number of times this bp caused a break into the debugger + this.vsbp = null; + this.enabled = null; + } + + /** + * @param {BreakpointLocation} bploc + * @param {number} requestid JDWP request ID for the breakpoint + */ + setEnabled(bploc, requestid) { + this.enabled = { + /** @type {CMLKey} */ + cml: `${bploc.c.info.typeid}:${bploc.m.methodid}:${bploc.l}`, + bp: this, + bploc: { + c: bploc.c, + m: bploc.m, + l: bploc.l, + }, + requestid, + } + } + + setDisabled() { + this.enabled = null; + } + + /** + * Constructs a unique breakpoint ID from the source path and line number + * @param {string} srcfpn + * @param {number} line + * @returns {BreakpointID} + */ + static makeBreakpointID(srcfpn, line) { + const cls = splitSourcePath(srcfpn); + return `${line}:${cls.qtype}`; + } +} + +class BreakpointOptions { + /** + * Hit-count used for conditional breakpoints + * @type {number|null} + */ + hitcount = null; +} + +class DebuggerTypeInfo { + + /** + * @param {JavaClassInfo} info + * @param {JavaType} type + */ + constructor(info, type) { + this.info = info; + this.type = type; + + /** @type {JavaField[]} */ + this.fields = null; + + /** @type {DebuggerMethodInfo[]} */ + this.methods = null; + + /** @type {JavaSource} */ + this.src = null; + + // if it's not a class type, set super to null + // otherwise, leave super undefined to be updated later + if (info.reftype.string !== 'class' || type.signature[0] !== 'L' || type.signature === JavaType.Object.signature) { + if (info.reftype.string !== 'array') { + /** @type {JavaType} */ + this.super = null; + } + } + } + + get name() { + return this.type.typename; + } +} + +/** + * Dummy type info for when the Java runtime hasn't loaded the class. + */ +class TypeNotAvailable extends DebuggerTypeInfo { + /** @type {JavaClassInfo} */ + static info = { + reftype: 0, + status: null, + type: null, + typeid: '', + } + + constructor(type) { + super(TypeNotAvailable.info, type); + super.fields = []; + super.methods = []; + } +} + +class JavaThreadInfo { + /** + * @param {JavaThreadID} threadid + * @param {string} name + * @param {*} status + */ + constructor(threadid, name, status) { + this.threadid = threadid; + this.name = name; + this.status = status; + } +} + +class MethodInvokeArgs { + /** + * @param {JavaObjectID} objectid + * @param {JavaThreadID} threadid + * @param {DebuggerMethodInfo} method + * @param {DebuggerValue[]} args + */ + constructor(objectid, threadid, method, args) { + this.objectid = objectid; + this.threadid = threadid; + this.method = method; + this.args = args; + this.promise = null; + } +} + +class VariableValue { + /** + * @param {string} name + * @param {string} value + * @param {string} [type] + * @param {number} [variablesReference] + * @param {string} [evaluateName] + */ + constructor(name, value, type = '', variablesReference = 0, evaluateName = '') { + this.name = name; + this.value = value; + this.type = type; + this.variablesReference = variablesReference; + this.evaluateName = evaluateName; + } +} + +module.exports = { + BreakpointLocation, + BreakpointOptions, + BuildInfo, + DebuggerBreakpoint, + DebuggerException, + DebuggerFrameInfo, + DebuggerMethodInfo, + DebuggerTypeInfo, + DebugSession, + DebuggerValue, + LiteralValue, + JavaBreakpointEvent, + JavaExceptionEvent, + JavaTaggedValue, + JavaType, + JavaArrayType, + JavaClassType, + JavaPrimitiveType, + JavaThreadInfo, + MethodInvokeArgs, + SourceLocation, + TypeNotAvailable, + VariableValue, +} diff --git a/src/debugger.js b/src/debugger.js index 585bbbd..e0b0cd8 100644 --- a/src/debugger.js +++ b/src/debugger.js @@ -1,607 +1,525 @@ -'use strict' /* Debugger: thin wrapper around other classes to manage debug connections */ -const _JDWP = require('./jdwp')._JDWP; +const { EventEmitter }= require('events'); +const { JDWP } = require('./jdwp'); const { ADBClient } = require('./adbclient'); -const $ = require('./jq-promise'); -const { D } = require('./util'); +const { D } = require('./utils/print'); +const { sleep } = require('./utils/thread'); +const { decodeJavaStringLiteral } = require('./utils/char-decode'); +const { + BreakpointLocation, + BreakpointOptions, + BuildInfo, + DebuggerBreakpoint, + DebuggerFrameInfo, + DebuggerMethodInfo, + DebuggerTypeInfo, + DebuggerValue, + DebugSession, + JavaArrayType, + JavaBreakpointEvent, + JavaClassType, + JavaExceptionEvent, + JavaTaggedValue, + JavaThreadInfo, + JavaType, + MethodInvokeArgs, + SourceLocation, + TypeNotAvailable, +} = require('./debugger-types'); -function Debugger() { - this.connection = null; - this.ons = {}; - this.breakpoints = { all: [], enabled: {}, bysrcloc: {} }; - this.exception_ids = []; - this.JDWP = new _JDWP(); - this.session = null; - this.globals = Debugger.globals; -} +class Debugger extends EventEmitter { -Debugger.globals = { - portrange: { lowest: 31000, highest: 31099 }, - inuseports: [], - debuggers: {}, - reserveport: function () { - // choose a random port to use each time - for (var i = 0; i < 10000; i++) { - var portidx = ((Math.random() * 100) | 0); - if (this.inuseports.includes(portidx)) - continue; // try again - this.inuseports.push(portidx); - return this.portrange.lowest + portidx; - } - }, - freeport: function (port) { - var iuidx = this.inuseports.indexOf(port - this.portrange.lowest); - if (iuidx >= 0) this.inuseports.splice(iuidx, 1); + constructor () { + super(); + this.connection = null; + + this.breakpoints = { + /** @type {DebuggerBreakpoint[]} */ + all: [], + /** @type {Map} */ + byID: new Map(), + }; + + /** @type {JDWPRequestID[]} */ + this.exception_ids = []; + + /** @type {DebugSession} */ + this.session = null; } -}; -Debugger.prototype = { - - on: function (which, context, data, fn) { - if (!fn && !data && typeof (context) === 'function') { - fn = context; context = data = null; - } - else if (!fn && typeof (data) === 'function') { - fn = data; data = null; - } - if (!this.ons[which]) this.ons[which] = []; - this.ons[which].push({ - context: context, data: data, fn: fn - }); - return this; - }, - - _trigger: function (which, e) { - var k = this.ons[which]; - if (!k || !k.length) return this; - k = k.slice(); - e = e || {}; - e.dbgr = this; - for (var i = 0; i < k.length; i++) { - e.data = k[i].data; - try { k[i].fn.call(k[i].context, e) } - catch (ex) { - D('Exception in event trigger: ' + ex.message); + static portManager = { + portrange: { lowest: 31000, highest: 31099 }, + inuseports: new Set(), + debuggers: {}, + reserveport: function () { + // choose a random port to use each time + for (let i = 0; i < 10000; i++) { + const portidx = this.portrange.lowest + ((Math.random() * 100) | 0); + if (this.inuseports.has(portidx)) { + continue; // try again + } + this.inuseports.add(portidx); + return portidx; } + throw new Error('Failed to reserve debugger port'); + }, + freeport: function (port) { + this.inuseports.delete(port); } - return this; - }, + }; - startDebugSession(build, deviceid, launcherActivity) { - return this.newSession(build, deviceid) - .runapp('debug', launcherActivity, this) - .then(function (deviceid) { - return this.getDebuggablePIDs(this.session.deviceid, this); - }) - .then(function (pids, dbgr) { - // choose the last pid in the list - var pid = pids[pids.length - 1]; - // after connect(), the caller must call resume() to begin - return dbgr.connect(pid, dbgr); - }) - }, + /** + * @param {BuildInfo} build + * @param {string} deviceid + */ + async startDebugSession(build, deviceid) { + this.session = new DebugSession(build, deviceid); + await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause); - runapp(action, launcherActivity) { + // retrieve the list of debuggable processes + const pids = await this.getDebuggablePIDs(this.session.deviceid); + // choose the last pid in the list + const pid = pids[pids.length - 1]; + // after connect(), the caller must call resume() to begin + await this.connect(pid); + } + + /** + * @param {string} deviceid Device ID to connect to + * @param {string[]} launch_cmd_args Array of arguments to pass to 'am start' + * @param {number} [post_launch_pause] amount to time to wait after each launch attempt + */ + static async runApp(deviceid, launch_cmd_args, post_launch_pause = 1000) { // older (<3) versions of Android only allow target components to be specified with -n - var launchcmdparams = ['--activity-brought-to-front', '-a android.intent.action.MAIN', '-c android.intent.category.LAUNCHER', '-n ' + this.session.build.pkgname + '/' + launcherActivity]; - if (action === 'debug') { - launchcmdparams.splice(0, 0, '-D'); - } - var x = { - dbgr: this, - shell_cmd: { - command: 'am start ' + launchcmdparams.join(' '), - untilclosed: true, - }, - retries: { - count: 10, pause: 1000, - }, - deviceid: this.session.deviceid, - deferred: $.Deferred(), + const shell_cmd = { + command: 'am start ' + launch_cmd_args.join(' '), }; - tryrunapp(x); - function tryrunapp(x) { - var adb = new ADBClient(x.deviceid); - adb.shell_cmd(x.shell_cmd) - .then(function (stdout) { - // failures: - // Error: Activity not started... - var m = stdout.match(/Error:.*/g); - if (m) { - if (--x.retries.count) { - setTimeout(function (o) { - tryrunapp(o); - }, x.retries.pause, x); - return; - } - return x.deferred.reject({ cat: 'cmd', msg: m[0] }); - } - // running the JDWP command so soon after launching hangs, so give it a breather before continuing - setTimeout(x => { - x.deferred.resolveWith(x.dbgr, [x.deviceid]) - }, 1000, x); - }) - .fail(function (err) { - }); + let retries = 10 + for (;;) { + D(shell_cmd.command); + const stdout = await new ADBClient(deviceid).shell_cmd(shell_cmd); + // running the JDWP command so soon after launching hangs, so give it a breather before continuing + await sleep(post_launch_pause); + // failures: + // Error: Activity not started... + const m = stdout.match(/Error:.*/g); + if (!m) { + break; + } + else if (retries <= 0){ + throw new Error(m[0]); + } + retries -= 1; } - return x.deferred; - }, + } - newSession: function (build, deviceid) { - this.session = { - build: build, - deviceid: deviceid, - apilevel: 0, - adbclient: null, - stoppedlocation: null, - classes: {}, - // classprepare filters - cpfilters: [], - preparedclasses: [], - stepids: {}, // hashmap - threadsuspends: [], // hashmap - invokes: {}, // hashmap - } - return this; - }, + /** + * return a list of deviceids available for debugging + */ + listConnectedDevices() { + return new ADBClient().list_devices(); + } - /* return a list of deviceids available for debugging */ - list_devices: function (extra) { - return new ADBClient().list_devices(extra); - }, + /** + * Retrieve a list of debuggable process IDs from a device + */ + getDebuggablePIDs(deviceid) { + return new ADBClient(deviceid).jdwp_list(); + } - getDebuggablePIDs: function (deviceid, extra) { - return new ADBClient(deviceid).jdwp_list({ - ths: this, - extra: extra, - }) - }, - - getDebuggableProcesses: function (deviceid, extra) { - var info = { + async getDebuggableProcesses(deviceid) { + const adbclient = new ADBClient(deviceid); + const info = { debugger: this, - adbclient: new ADBClient(deviceid), - extra: extra, + jdwps: null, }; - return info.adbclient.jdwp_list({ - ths: this, - extra: info, - }) - .then(function (jdwps, info) { - if (!jdwps.length) - return $.Deferred().resolveWith(this, [[], info.extra]); - info.jdwps = jdwps; - // retrieve the ps list from the device - return info.adbclient.shell_cmd({ - ths: this, - extra: info, - command: 'ps', - untilclosed: true, - }).then(function (stdout, info) { - // output should look something like... - // USER PID PPID VSIZE RSS WCHAN PC NAME - // u0_a153 32721 1452 1506500 37916 ffffffff 00000000 S com.example.somepkg - // but we cope with variations so long as PID and NAME exist - var lines = stdout.split(/\r?\n|\r/g); - var hdrs = (lines.shift() || '').trim().toUpperCase().split(/\s+/); - var pidindex = hdrs.indexOf('PID'); - var nameindex = hdrs.indexOf('NAME'); - var result = { deviceid: info.adbclient.deviceid, name: {}, jdwp: {}, all: [] }; - if (pidindex < 0 || nameindex < 0) - return $.Deferred().resolveWith(null, [[], info.extra]); - // scan the list looking for matching pids... - for (var i = 0; i < lines.length; i++) { - var entries = lines[i].trim().replace(/ [S] /, ' ').split(/\s+/); - if (entries.length != hdrs.length) continue; - var jdwpidx = info.jdwps.indexOf(entries[pidindex]); - if (jdwpidx < 0) continue; - // we found a match - var entry = { - jdwp: entries[pidindex], - name: entries[nameindex], - }; - result.all.push(entry); - result.name[entry.name] = entry; - result.jdwp[entry.jdwp] = entry; - } - return $.Deferred().resolveWith(this, [result, info.extra]); - }) - }); - }, + const jdwps = await info.adbclient.jdwp_list(); + if (!jdwps.length) + return null; + info.jdwps = jdwps; + // retrieve the ps list from the device + const stdout = await adbclient.shell_cmd({ + command: 'ps', + }); + // output should look something like... + // USER PID PPID VSIZE RSS WCHAN PC NAME + // u0_a153 32721 1452 1506500 37916 ffffffff 00000000 S com.example.somepkg + // but we cope with variations so long as PID and NAME exist + const lines = stdout.split(/\r?\n|\r/g); + const hdrs = (lines.shift() || '').trim().toUpperCase().split(/\s+/); + const pidindex = hdrs.indexOf('PID'); + const nameindex = hdrs.indexOf('NAME'); + if (pidindex < 0 || nameindex < 0) + return []; + const result = []; + // scan the list looking for matching pids... + for (let i = 0; i < lines.length; i++) { + const entries = lines[i].trim().replace(/ [S] /, ' ').split(/\s+/); + if (entries.length !== hdrs.length) { + continue; + } + const jdwpidx = info.jdwps.indexOf(entries[pidindex]); + if (jdwpidx < 0) { + continue; + } + // we found a match + const entry = { + jdwp: entries[pidindex], + name: entries[nameindex], + }; + result.push(entry); + } + return result; + } - /* attach to the debuggable pid - Quite a lot happens in this - we setup port forwarding, complete the JDWP handshake, - setup class loader notifications and call anyone waiting for us. - If anything fails, we call disconnect() to return to a sense of normality. + /** + * Attach to the debuggable pid + * Quite a lot happens in this - we setup port forwarding, complete the JDWP handshake, + * setup class loader notifications and call anyone waiting for us. + * If anything fails, we call disconnect() to return to a sense of normality. + * @param {number|null} jdwpid */ - connect: function (jdwpid, extra) { + async connect(jdwpid) { switch (this.status()) { case 'connected': - // already connected - just resolve - return $.Deferred().resolveWith(this, [extra]); + // already connected + return; case 'connecting': // wait for the connection to complete (or fail) - var x = { deferred: $.Deferred(), extra: extra }; - this.connection.connectingpromises.push(x); - return x.deferred; + return this.connection.connectingpromise; default: if (!jdwpid) - return $.Deferred().rejectWith(this, [new Error('Debugger not connected')]); + throw new Error('Debugger not connected'); break; } - var info = { - dbgr: this, - extra: extra, - }; - // from this point on, we are in the "connecting" state until the JDWP handshake is complete // (and we mark as connected) or we fail and return to the disconnected state this.connection = { + /** pid of the debuggable process to connect to (on the device) */ jdwp: jdwpid, - localport: this.globals.reserveport(), + /** the local port number to use for ADB port-forwarding */ + localport: Debugger.portManager.reserveport(), + /** set to true once ADB port-forwarding is completed */ portforwarding: false, + /** set to true after the JDWP handshake has completed */ connected: false, - connectingpromises: [], + /** @type {Promise} fulfilled once the connection tasks have completed */ + connectingpromise: null, }; + try { + await (this.connection.connectingpromise = this.performConnectionTasks()); + // at this point, we are ready to go - all the caller needs to do is call resume(). + this.emit('connected'); + } catch(err) { + this.connection.err = err; + // force a return to the disconnected state + this.disconnect(); + throw err; + } + } + + async performConnectionTasks() { // setup port forwarding - return new ADBClient(this.session.deviceid).jdwp_forward({ - ths: this, - extra: info, + await new ADBClient(this.session.deviceid).jdwp_forward({ localport: this.connection.localport, jdwp: this.connection.jdwp, - }) - .then(function (info) { - this.connection.portforwarding = true; - // after this, the client keeps an open connection until - // jdwp_disconnect() is called - this.session.adbclient = new ADBClient(this.session.deviceid); - return this.session.adbclient.jdwp_connect({ - ths: this, - extra: info, - localport: this.connection.localport, - onreply: this._onjdwpmessage, - }); - }) - .then(function (info) { - // handshake has completed - this.connection.connected = true; - // call suspend first - we shouldn't really need to do this (as the debugger - // is already suspended and will not resume until we tell it), but if we - // don't do this, it logs a complaint... - return this.suspend(); - }) - .then(function () { - return this.session.adbclient.jdwp_command({ - ths: this, - cmd: this.JDWP.Commands.idsizes(), - }); - }) - .then(function (idsizes) { - // set the class loader event notifier so we can set breakpoints... - this.JDWP.setIDSizes(idsizes); - return this._initbreakpoints(); - }) - .then(function () { - return new ADBClient(this.session.deviceid).shell_cmd({ - ths: this, - command: 'getprop ro.build.version.sdk', - }); - }) - .then(function (apilevel) { - this.session.apilevel = apilevel.trim(); - // at this point, we are ready to go - all the caller needs to do is call resume(). - // resolve all the connection promises for those waiting on us (usually none) - var cp = this.connection.connectingpromises; - var deferreds = [this, info]; - delete this.connection.connectingpromises; - for (var i = 0; i < cp.length; i++) { - deferreds.push(cp[i].deferred); - cp[i].deferred.resolveWith(this, [cp[i].extra]); - } - return $.when.apply($, deferreds).then(function (dbgr, info) { - return $.Deferred().resolveWith(dbgr, [info.extra]); - }) - }) - .then(function () { - this._trigger('connected'); - }) - .fail(function (err) { - this.connection.err = err; - // force a return to the disconnected state - this.disconnect(); - }) - }, + }); + this.connection.portforwarding = true; - _onjdwpmessage: function (data) { - // decodereply will resolve the promise associated with + // after this, the client keeps an open connection until + // jdwp_disconnect() is called + this.session.adbclient = new ADBClient(this.session.deviceid); + await this.session.adbclient.jdwp_connect({ + localport: this.connection.localport, + onreply: data => this._onJDWPMessage(data), + ondisconnect: () => this._onJDWPDisconnect(), + }); + // handshake has completed + this.connection.connected = true; + // call suspend first - we shouldn't really need to do this (as the debugger + // is already suspended and will not resume until we tell it), but if we + // don't do this, it logs a complaint... + await this.suspend(); + + // retrieve the JRE reference ID sizes, so we can decode JDWP messages + const idsizes = await this.session.adbclient + .jdwp_command({ + cmd: JDWP.Commands.idsizes(), + }); + JDWP.initDataCoder(idsizes); + + // set the class loader event notifier so we can enable breakpoints when the + // runtime loads the classes + await this.initClassPrepareForBreakpoints(); + } + + /** + * @param {Buffer} data + */ + _onJDWPMessage(data) { + // decodeReply will resolve the promise associated with // any command this reply is in response to. - var reply = this.JDWP.decodereply(this, data); - if (reply.isevent) { - if (reply.decoded.events && reply.decoded.events.length) { - switch (reply.decoded.events[0].kind.value) { - case 100: - // vm disconnected - sent by plugin - this.disconnect(); - break; - } - } - } - }, + return JDWP.decodeReply(data); + } - ensureconnected: function (extra) { + _onJDWPDisconnect() { + // the JDWP socket has disconnected - terminate the debugger + this.disconnect(); + } + + /** + * Returns a resolved Promise if (and when) a debugger connection is established. + * The promise is rejected if the device has disconnected. + */ + ensureConnected() { // passing null as the jdwpid will cause a fail if the client is not connected (or connecting) - return this.connect(null, extra); - }, + return this.connect(null); + } - status: function () { + /** + * @returns {'connected'|'connecting'|'disconnected'} + */ + status() { if (!this.connection) return "disconnected"; if (this.connection.connected) return "connected"; return "connecting"; - }, + } - forcestop: function (extra) { - return this.ensureconnected() - .then(function () { - return new ADBClient(this.session.deviceid).shell_cmd({ - command: 'am force-stop ' + this.session.build.pkgname, - }); - }) - }, + /** + * Force stop the app running in the current session + */ + async forceStop() { + if (!this.session) { + return; + } + return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname); + } - disconnect: function (extra) { + /** + * Sends a 'am force-stop' command to the given device + * @param {string} deviceid + * @param {string} pkgname + * @param {boolean} [throw_on_error] + */ + static async forceStopApp(deviceid, pkgname, throw_on_error = false) { + try { + await new ADBClient(deviceid).shell_cmd({ + command: 'am force-stop ' + pkgname, + }); + } catch(e) { + if (throw_on_error) { + throw e; + } + } + } + + /** + * Perform disconnect tasks and cleanup + * @return previous state + */ + async disconnect() { // disconnect is called from a variety of failure scenarios // so it must be fairly robust in how it undoes stuff - const current_state = this.status(); - if (!this.connection) - return $.Deferred().resolveWith(this, [current_state, extra]); + const previous_state = this.status(); + const connection = this.connection; + if (!connection) + return previous_state; - var info = { - connection: this.connection, - current_state: current_state, - extra: extra, - }; // from here on in, this instance is in the disconnected state this.connection = null; - // fail any waiting for the connection to complete - var cp = info.connection.connectingpromises; - if (cp) { - for (var i = 0; i < cp.length; i++) { - cp[i].deferred.rejectWith(this, [info.connection.err]); - } - } - // reset the breakpoint states - this._finitbreakpoints(); - - this._trigger('disconnect'); + this.resetBreakpoints(); + this.emit('disconnect'); // perform the JDWP disconnect - info.jdwpdisconnect = info.connection.connected - ? this.session.adbclient.jdwp_disconnect({ ths: this, extra: info }) - : $.Deferred().resolveWith(this, [info]); - - return info.jdwpdisconnect - .then(function (info) { - this.session.adbclient = null; - // undo the portforwarding - // todo: replace remove_all with remove_port - info.pfremove = info.connection.portforwarding - ? new ADBClient(this.session.deviceid).forward_remove_all({ ths: this, extra: info }) - : $.Deferred().resolveWith(this, [info]); - - return info.pfremove; - }) - .then(function (info) { - // mark the port as freed - if (info.connection.portforwarding) { - this.globals.freeport(info.connection.localport) - } - this.session = null; - return $.Deferred().resolveWith(this, [info.current_state, info.extra]); - }); - }, - - allthreads: function (extra) { - return this.ensureconnected(extra) - .then(function (extra) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: extra, - cmd: this.JDWP.Commands.allthreads(), - }); - }); - }, - - threadinfos: function(thread_ids, extra) { - if (!Array.isArray(thread_ids)) - thread_ids = [thread_ids]; - var o = { - dbgr: this, thread_ids, extra, threadinfos:[], idx:0, - next() { - var thread_id = this.thread_ids[this.idx]; - if (typeof(thread_id) === 'undefined') - return $.Deferred().resolveWith(this.dbgr, [this.threadinfos, this.extra]); - var info = { - threadid: thread_id, - name:'', - status:null, - }; - return this.dbgr.session.adbclient.jdwp_command({ ths:this.dbgr, extra:info, cmd:this.dbgr.JDWP.Commands.threadname(info.threadid) }) - .then((name,info) => { - info.name = name; - return this.dbgr.session.adbclient.jdwp_command({ ths:this.dbgr, extra:info, cmd:this.dbgr.JDWP.Commands.threadstatus(info.threadid) }) - }) - .then((status, info) => { - info.status = status; - this.threadinfos.push(info); - }) - .always(() => (this.idx++,this.next())) - } - }; - return this.ensureconnected(o).then(o => o.next()); - }, - - suspend: function (extra) { - return this.ensureconnected(extra) - .then(function (extra) { - this._trigger('suspending'); - return this.session.adbclient.jdwp_command({ - ths: this, - extra: extra, - cmd: this.JDWP.Commands.suspend(), - }); - }) - .then(function () { - this._trigger('suspended'); - }); - }, - - suspendthread: function (threadid, extra) { - return this.ensureconnected({threadid,extra}) - .then(function (x) { - this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) + 1; - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x.extra, - cmd: this.JDWP.Commands.suspendthread(x.threadid), - }); - }) - .then((res,extra) => extra); - }, - - _resume:function(triggers, extra) { - return this.ensureconnected(extra) - .then(function (extra) { - if (triggers) this._trigger('resuming'); - this.session.stoppedlocation = null; - return this.session.adbclient.jdwp_command({ - ths: this, - extra: extra, - cmd: this.JDWP.Commands.resume(), - }); - }) - .then(function (decoded, extra) { - if (triggers) this._trigger('resumed'); - return extra; - }); - }, - - resume: function (extra) { - return this._resume(true, extra); - }, - - _resumesilent: function () { - return this._resume(false); - }, - - resumethread: function (threadid, extra) { - return this.ensureconnected({threadid,extra}) - .then(function (x) { - this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) - 1; - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x.extra, - cmd: this.JDWP.Commands.resumethread(x.threadid), - }); - }) - .then((res,extra) => extra); - }, - - step: function (steptype, threadid, extra) { - var x = { steptype, threadid, extra }; - return this.ensureconnected(x) - .then(function (x) { - this._trigger('stepping'); - return this._setupstepevent(x.steptype, x.threadid, x); - }) - .then(x => { - return this.resumethread(x.threadid, x.extra); - }); - }, - - _splitsrcfpn: function (srcfpn) { - var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.(java|kt)$/); - return { - pkg: m[1].replace(/\/+/g, '.'), - type: m[2], - qtype: m[1] + '/' + m[2], + if (connection.connected) { + await this.session.adbclient.jdwp_disconnect(); } - }, - getbreakpoint: function (srcfpn, line) { - var cls = this._splitsrcfpn(srcfpn); - var bp = this.breakpoints.bysrcloc[cls.qtype + ':' + line]; - return bp; - }, + // undo the portforwarding + // todo: replace remove_all with remove_port + if (connection.portforwarding) { + await new ADBClient(this.session.deviceid).forward_remove_all(); + } - getbreakpoints: function (filterfn) { - var x = this.breakpoints.all.reduce(function (x, bp) { - if (x.filterfn(bp)) - x.res.push(bp); - return x; - }, { filterfn: filterfn, res: [] }); - return x.res; - }, + // mark the port as freed + if (connection.portforwarding) { + Debugger.portManager.freeport(connection.localport); + } - getallbreakpoints: function () { - return this.breakpoints.all.slice(); - }, + // clear the session + this.session = null; + return previous_state; + } - setbreakpoint: function (srcfpn, line, conditions) { - var cls = this._splitsrcfpn(srcfpn); - var bid = cls.qtype + ':' + line; - var newbp = this.breakpoints.bysrcloc[bid]; - if (newbp) return $.Deferred().resolveWith(this, [newbp]); - newbp = { - id: bid, - srcfpn: srcfpn, - qtype: cls.qtype, - pkg: cls.pkg, - type: cls.type, - linenum: line, - conditions: Object.assign({},conditions), - sigpattern: new RegExp('^L' + cls.qtype + '([$][$a-zA-Z0-9_]+)?;$'), - state: 'set', // set,notloaded,enabled,removed - hitcount: 0, // number of times this bp was hit during execution - stopcount: 0. // number of times this bp caused a break into the debugger - }; + /** + * Retrieve all the thread IDs from the running app. + */ + async getJavaThreadIDs() { + await this.ensureConnected(); + /** @type {JavaThreadID[]} */ + const threads = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.allthreads(), + }); + return threads; + } + + /** + * + * @param {JavaThreadID[]} thread_ids + */ + async getJavaThreadInfos(thread_ids) { + const threadinfos = []; + for (let i=0; i < thread_ids.length; i++) { + const threadid = thread_ids[i]; + try { + const name = await this.session.adbclient.jdwp_command({ cmd: JDWP.Commands.threadname(threadid) }); + const status = await this.session.adbclient.jdwp_command({ cmd: JDWP.Commands.threadstatus(threadid) }) + threadinfos.push(new JavaThreadInfo(threadid, name, status)); + } catch(e) {} + } + return threadinfos; + } + + /** + * Increments or decrements the suspend count for a given thread + * @param {JavaThreadID} threadid + * @param {number} inc + */ + updateThreadSuspendCount(threadid, inc) { + const count = this.session.threadSuspends.get(threadid); + this.session.threadSuspends.set(threadid, (count | 0) + inc); + } + + /** + * Sends a JDWP command to suspend execution + */ + async suspend() { + await this.ensureConnected() + this.emit('suspending'); + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.suspend(), + }); + this.emit('suspended'); + } + + /** + * Sends a JDWP command to suspend execution of a single thread + * @param {JavaThreadID} threadid + */ + async suspendThread(threadid) { + await this.ensureConnected(); + try { + this.updateThreadSuspendCount(threadid, +1); + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.suspendthread(threadid), + }); + } catch(e) { + this.updateThreadSuspendCount(threadid, -1); + throw e; + } + } + + /** + * Sends a JDWP command to resume execution + * @param {boolean} triggers true if 'resuming' and 'resumed' events should be invoked, false if this is a silent resume + */ + async _resume(triggers) { + await this.ensureConnected(); + if (triggers) { + this.emit('resuming'); + } + this.session.stoppedLocation = null; + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.resume(), + }); + if (triggers) { + this.emit('resumed'); + } + } + + /** + * Resume execution of a suspended app + */ + resume() { + return this._resume(true); + } + + /** + * Resume execution of a suspended app without triggering resume events + */ + _resumesilent() { + return this._resume(false); + } + + /** + * Sends a JDWP command to resume execution of a single thread + * @param {JavaThreadID} thread_id + */ + async resumeThread(thread_id) { + await this.ensureConnected(); + this.updateThreadSuspendCount(thread_id, -1); + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.resumethread(thread_id), + }); + } + + /** + * Performs a single step of the given type + * @param {DebuggerStepType} step_type + * @param {JavaThreadID} thread_id + */ + async step(step_type, thread_id) { + await this.ensureConnected(); + this.emit('stepping'); + await this._setupStepEvent(step_type, thread_id); + await this.resumeThread(thread_id); + } + + /** + * Returns the DebuggerBreakpoint at the given location, or null if none exists + * @param {string} srcfpn + * @param {number} line + */ + getBreakpointAt(srcfpn, line) { + const bp_id = DebuggerBreakpoint.makeBreakpointID(srcfpn, line); + return this.breakpoints.byID.get(bp_id); + } + + /** + * Returns the breakpoints that meet the condition specified in a callback function. + * @param {(value:DebuggerBreakpoint,idx:number,array:DebuggerBreakpoint[]) => boolean} filter_fn + */ + findBreakpoints(filter_fn) { + return this.breakpoints.all.filter(filter_fn); + } + + /** + * Sets a breakpoint at the given location + * @param {string} srcfpn + * @param {number} line + * @param {BreakpointOptions} options + */ + async setBreakpoint(srcfpn, line, options) { + const existing_bp = this.getBreakpointAt(srcfpn, line); + if (existing_bp) { + return existing_bp; + } + const newbp = new DebuggerBreakpoint(srcfpn, line, options, 'set'); this.breakpoints.all.push(newbp); - this.breakpoints.bysrcloc[bid] = newbp; + this.breakpoints.byID.set(newbp.id, newbp); // what happens next depends upon what state we are in switch (this.status()) { case 'connected': newbp.state = 'notloaded'; - // try and load the class - if the runtime hasn't loaded it yet, this will just return an empty classes object - return this._loadclzinfo('L'+newbp.qtype+';') - .then(classes => { - var bploc = this._findbplocation(classes, newbp); - if (!bploc) { - // the required location may be inside a nested class (anonymous or named) - // Since Android doesn't support the NestedTypes JDWP call (ffs), all we can do here - // is look for existing (cached) loaded types matching inner type signatures - for (var sig in this.session.classes) { - if (newbp.sigpattern.test(sig)) - classes[sig] = this.session.classes[sig]; - } - // try again - bploc = this._findbplocation(classes, newbp); - } - if (!bploc) { - // we couldn't identify a matching location - either the class is not yet loaded or the - // location doesn't correspond to any code. In case it's the former, make sure we are notified - // when classes in this package are loaded - return this._ensureClassPrepareForPackage(newbp.pkg); - } - // we found a matching location - set the breakpoint event - return this._setupbreakpointsevent([bploc]); - }) - .then(() => newbp) + await this.initialiseBreakpoint(newbp); + break; case 'connecting': case 'disconnected': default: @@ -609,1242 +527,1138 @@ Debugger.prototype = { break; } - return $.Deferred().resolveWith(this, [newbp]); - }, + return newbp; + } - clearbreakpoint: function (srcfpn, line) { - var cls = this._splitsrcfpn(srcfpn); - var bp = this.breakpoints.bysrcloc[cls.qtype + ':' + line]; - if (!bp) return null; - return this._clearbreakpoints([bp])[0]; - }, - - clearbreakpoints: function (bps) { - if (typeof (bps) === 'function') { - // argument is a filter function - return this.clearbreakpoints(this.getbreakpoints(bps)); + /** + * + * @param {DebuggerBreakpoint} bp + */ + async initialiseBreakpoint(bp) { + // try and load the class - if the runtime hasn't loaded it yet, this will just return a TypeNotAvailable instance + let classes = [await this.loadClassInfo(`L${bp.qtype};`)]; + let bploc = Debugger.findBreakpointLocation(classes, bp); + if (!bploc) { + // the required location may be inside a nested class (anonymous or named) + // Since Android doesn't support the NestedTypes JDWP call (ffs), all we can do here + // is look for existing (cached) loaded types matching inner type signatures + classes = this.session.classList + .filter(c => bp.sigpattern.test(c.type.signature)); + // try again + bploc = Debugger.findBreakpointLocation(classes, bp); } - // sanitise first to remove duplicates, non-existants, nulls, etc - var bpstoclear = []; - var bpkeys = {}; - (bps || []).forEach(function (bp) { - if (!bp) return; - if (this.breakpoints.all.indexOf(bp) < 0) return; - var bpkey = bp.cls + ':' + bp.linenum; - if (bpkeys[bpkey]) return; - bpkeys[bpkey] = 1; - bpstoclear.push(bp); - }, this); - return this._clearbreakpoints(bpstoclear); - }, + if (!bploc) { + // we couldn't identify a matching location - either the class is not yet loaded or the + // location doesn't correspond to any code. In case it's the former, make sure we are notified + // when classes in this package are loaded + await this._ensureClassPrepareForPackage(bp.pkg); + return; + } + // we found a matching location - set the breakpoint event + await this._setupBreakpointsEvent([bploc]); + } - _clearbreakpoints: function (bpstoclear) { - if (!bpstoclear || !bpstoclear.length) return []; - bpstoclear.forEach(function (bp) { - delete this.breakpoints.bysrcloc[bp.qtype + ':' + bp.linenum]; + /** + * Deletes a set of breakpoints. + * @param {DebuggerBreakpoint[]} breakpoints + */ + removeBreakpoints(breakpoints) { + // sanitise first to remove duplicates, non-existants, nulls, etc + const bps_to_clear = [...new Set(breakpoints)].filter(bp => bp && this.breakpoints.all.includes(bp)); + + bps_to_clear.forEach(bp => { + this.breakpoints.byID.delete(bp.id); this.breakpoints.all.splice(this.breakpoints.all.indexOf(bp), 1); - }, this); + }); switch (this.status()) { case 'connected': - var bpcleareddefs = [{ dbgr: this, bpstoclear: bpstoclear }]; - for (var cmlkey in this.breakpoints.enabled) { - var enabledbp = this.breakpoints.enabled[cmlkey].bp; - if (bpstoclear.indexOf(enabledbp) >= 0) { - bpcleareddefs.push(this._clearbreakpointsevent([cmlkey], enabledbp)); - } - } - $.when.apply($, bpcleareddefs) - .then(function (x) { - x.dbgr._changebpstate(x.bpstoclear, 'removed'); - }); + this.disableBreakpoints(bps_to_clear, 'removed'); break; case 'connecting': case 'disconnected': default: - this._changebpstate(bpstoclear, 'removed'); + this._changeBPState(bps_to_clear, 'removed'); break; } - return bpstoclear; - }, + return bps_to_clear; + } - getframes: function (threadid, extra) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: extra, - cmd: this.JDWP.Commands.Frames(threadid), - }).then(function (frames, extra) { - var deferreds = [{ dbgr: this, frames: frames, threadid: threadid, extra: extra }]; - for (var i = 0; i < frames.length; i++) { - deferreds.push(this._findmethodasync(this.session.classes, frames[i].location)); - } - return $.when.apply($, deferreds) - .then(function (x) { - for (var i = 0; i < x.frames.length; i++) { - x.frames[i].method = arguments[i + 1][0]; - x.frames[i].threadid = x.threadid; - } - return $.Deferred().resolveWith(x.dbgr, [x.frames, x.extra]); - }); + /** + * Retrieve call-stack frames for a thread + * @param {JavaThreadID} threadid + */ + async getFrames(threadid) { + /** @type {JavaFrame[]} */ + const frames = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.Frames(threadid), }) - }, + const methods = await Promise.all( + frames.map(frame => this._findMethodAsync(this.session.classList, frame.location)) + ); + return frames.map((frame,i) => new DebuggerFrameInfo(frame, methods[i], threadid)); + } - getlocals: function (threadid, frame, extra) { - var method = this._findmethod(this.session.classes, frame.location.cid, frame.location.mid); - if (!method) - return $.Deferred().resolveWith(this); - - return this._ensuremethodvars(method) - .then(function (method) { - - function withincodebounds(low, length, idx) { - var i = parseInt(low, 16), j = parseInt(idx, 16); - return (j >= i) && (j < (i + length)); - } - - var slots = []; - var validslots = []; - var tags = { '[': 76, B: 66, C: 67, L: 76, F: 70, D: 68, I: 73, J: 74, S: 83, V: 86, Z: 90 }; - for (var i = 0, k = method.vartable.vars; i < k.length; i++) { - var tag = tags[k[i].type.signature[0]]; - if (!tag) continue; - var p = { - slot: k[i].slot, - tag: tag, - valid: withincodebounds(k[i].codeidx, k[i].length, frame.location.idx) - }; - slots.push(p); - if (p.valid) validslots.push(p); - } - - var x = { method: method, extra: extra, slots: slots }; - - if (!validslots.length) { - return $.Deferred().resolveWith(this, [[], x]); - } - - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetStackValues(threadid, frame.frameid, validslots), - }); - }) - .then(function (values, x) { - var sv2 = []; - for (var i = 0; i < x.slots.length; i++) { - sv2.push(x.slots[i].valid ? values.shift() : null); - } - return this._mapvalues( - 'local', - x.method.vartable.vars, - sv2, - { frame: frame, slotinfo: null }, - x - ); - }) - .then(function (res, x) { - for (var i = 0; i < res.length; i++) - res[i].data.slotinfo = x.slots[i]; - return $.Deferred().resolveWith(this, [res, x.extra]); - }); - }, - - setlocalvalue: function (localvar, data, extra) { - return this.ensureconnected({ localvar: localvar, data: data, extra: extra }) - .then(function (x) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.SetStackValue(x.localvar.data.frame.threadid, x.localvar.data.frame.frameid, x.localvar.data.slotinfo.slot, x.data), - }); - }) - .then(function (success, x) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetStackValues(x.localvar.data.frame.threadid, x.localvar.data.frame.frameid, [x.localvar.data.slotinfo]), - }); - }) - .then(function (stackvalues, x) { - return this._mapvalues( - 'local', - [x.localvar], - stackvalues, - x.localvar.data, - x - ); - }) - .then(function (res, x) { - return $.Deferred().resolveWith(this, [res[0], x.extra]); - }); - }, - - getsupertype: function (local, extra) { - if (local.type.signature==='Ljava/lang/Object;') - return $.Deferred().rejectWith(this,[new Error('java.lang.Object has no super type')]); - return this.gettypedebuginfo(local.type.signature, { local: local, extra: extra }) - .then(function (dbgtype, x) { - return this._ensuresuper(dbgtype[x.local.type.signature]) - }) - .then(function (typeinfo) { - return $.Deferred().resolveWith(this, [typeinfo.super, extra]); - }); - }, - - getsuperinstance: function (local, extra) { - return this.getsupertype(local, {local,extra}) - .then(function (supertypeinfo, x) { - var castobj = Object.assign({}, x.local); - castobj.type = supertypeinfo; - return $.Deferred().resolveWith(this, [castobj, x.extra]); - }); - }, - - createstring: function (string, extra) { - return this.ensureconnected({ string: string, extra: extra }) - .then(function (x) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.CreateStringObject(string), - }); - }) - .then(function (strobjref, x) { - var keys = [{ name: '', type: this.JDWP.signaturetotype('Ljava/lang/String;') }]; - return this._mapvalues('literal', keys, [strobjref], null, x); - }) - .then(function (vars, x) { - return $.Deferred().resolveWith(this, [vars[0], x.extra]); - }); - }, - - setstringvalue: function (variable, string, extra) { - return this.createstring(string, { variable: variable, extra: extra }) - .then(function (string_variable, x) { - var value = { - value: string_variable.value, - valuetype: 'oref', - }; - return this.setvalue(x.variable, value, x.extra); - }) - }, - - setvalue: function (variable, data, extra) { - if (data.stringliteral) { - return this.setstringvalue(variable, data.value, extra); + /** + * Retrieve the list of local variables for a given fram + * @param {DebuggerFrameInfo} frame + */ + async getLocals(frame) { + const method = this.findMethod(this.session.classList, frame.location.cid, frame.location.mid); + if (!method) { + D(`getLocals: No method in frame location: ${JSON.stringify(frame.location)}`) + return []; } - switch (variable.vtype) { - case 'field': return this.setfieldvalue(variable, data, extra); - case 'local': return this.setlocalvalue(variable, data, extra); - case 'arrelem': - return this.setarrayvalues(variable.data.arrobj, parseInt(variable.name), 1, data, extra) - .then(function (res, extra) { - // setarrayvalues returns an array of updated elements - just return the one - return $.Deferred().resolveWith(this, [res[0], extra]); - }); + await this._ensureMethodVars(method); + + const location_idx = parseInt(frame.location.idx, 16); + const tags = { '[': 76, B: 66, C: 67, L: 76, F: 70, D: 68, I: 73, J: 74, S: 83, V: 86, Z: 90 }; + const slots = method.vartable.vars.map(v => { + const tag = tags[v.type.signature[0]]; + if (!tag) { + return null; + } + const code_idx = parseInt(v.codeidx, 16); + const withincodebounds = (location_idx >= code_idx) && (location_idx < (code_idx + v.length)); + return { + v, + slot: v.slot, + tag, + valid: withincodebounds, + }; + }); + + const validslots = slots.filter(s => s && s.valid); + if (!validslots.length) { + return []; } - }, - setfieldvalue: function (fieldvar, data, extra) { - return this.ensureconnected({ fieldvar: fieldvar, data: data, extra: extra }) - .then(function (x) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.SetFieldValue(x.fieldvar.data.objvar.value, x.fieldvar.data.field, x.data), - }); - }) - .then(function (success, x) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetFieldValues(x.fieldvar.data.objvar.value, [x.fieldvar.data.field]), - }); - }) - .then(function (fieldvalues, x) { - return this._mapvalues('field', [x.fieldvar.data.field], fieldvalues, x.fieldvar.data, x); - }) - .then(function (data, x) { - return $.Deferred().resolveWith(this, [data[0], x.extra]); + return this._getStackValues(frame, validslots); + } + + /** + * @param {DebuggerFrameInfo} frame + * @param {*} slotinfo + * @param {JavaTaggedValue} data + */ + async setLocalVariableValue(frame, slotinfo, data) { + await this.ensureConnected(); + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.SetStackValue(frame.threadid, frame.frameid, slotinfo.slot, data), + }); + + const res = await this._getStackValues(frame, [slotinfo]); + return res[0]; + } + + /** + * + * @param {DebuggerFrameInfo} frame + * @param {*[]} validslots + */ + async _getStackValues(frame, validslots) { + try { + const values = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.GetStackValues(frame.threadid, frame.frameid, validslots), }); - }, + const res = await this._makeValues( + 'local', + validslots.map(x => x.v), + values, + { frame, slotinfo: null } + ); - getfieldvalues: function (objvar, extra) { - return this.gettypedebuginfo(objvar.type.signature, { objvar: objvar, extra: extra }) - .then(function (dbgtype, x) { - return this._ensurefields(dbgtype[x.objvar.type.signature], x); - }) - .then(function (typeinfo, x) { - x.typeinfo = typeinfo; - // the Android runtime now pointlessly barfs into logcat if an instance value is used - // to retrieve a static field. So, we now split into two calls... - x.splitfields = typeinfo.fields.reduce((z,f) => { - if (f.modbits & 8) z.static.push(f); else z.instance.push(f); - return z; - }, {instance:[],static:[]}); - // if there are no instance fields, just resolve with an empty array - if (!x.splitfields.instance.length) - return $.Deferred().resolveWith(this,[[], x]); - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetFieldValues(x.objvar.value, x.splitfields.instance), - }); - }) - .then(function (instance_fieldvalues, x) { - x.instance_fieldvalues = instance_fieldvalues; - // and now the statics (with a type reference) - if (!x.splitfields.static.length) - return $.Deferred().resolveWith(this,[[], x]); - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetStaticFieldValues(x.splitfields.static[0].typeid, x.splitfields.static), - }); - }) - .then(function (static_fieldvalues, x) { - x.static_fieldvalues = static_fieldvalues; - // make sure the fields and values match up... - var fields = x.splitfields.instance.concat(x.splitfields.static); - var values = x.instance_fieldvalues.concat(x.static_fieldvalues); - return this._mapvalues('field', fields, values, { objvar: x.objvar }, x); - }) - .then(function (res, x) { - for (var i = 0; i < res.length; i++) { - res[i].data.field = x.typeinfo.fields[i]; - } - return $.Deferred().resolveWith(this, [res, x.extra]); - }); - }, + for (let i = 0; i < res.length; i++) + res[i].data.slotinfo = validslots[i];// slots[slots.indexOf(validslots[i])]; - getFieldValue: function(objvar, fieldname, includeInherited, extra) { - const findfield = x => { - return this.getfieldvalues(x.objvar, x) - .then((fields, x) => { - var field = fields.find(f => f.name === x.fieldname); - if (field) return $.Deferred().resolveWith(this,[field,x.extra]); - if (!x.includeInherited || x.objvar.type.signature==='Ljava/lang/Object;') { - var fqtname = [x.reqtype.package,x.reqtype.typename].join('.'); - return $.Deferred().rejectWith(this,[new Error(`No such field '${x.fieldname}' in type ${fqtname}`), x.extra]); - } - // search supertype - return this.getsuperinstance(x.objvar, x) - .then((superobjvar,x) => { - x.objvar = superobjvar; - return x.findfield(x); - }); - }); + return res; + } catch (e) { + D(`_getStackValues: failed to retrieve stack values: ${e.message}`); + return []; } - return findfield({findfield, objvar, fieldname, includeInherited, extra, reqtype:objvar.type}); - }, + } - getExceptionLocal: function (ex_ref_value, extra) { - var x = { - ex_ref_value: ex_ref_value, - extra: extra - }; - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetObjectType(ex_ref_value), - }) - .then((typeref, x) => this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.signature(typeref) - })) - .then((type, x) => { - x.type = type; - return this.gettypedebuginfo(type.signature, x) - }) - .then((dbgtype, x) => { - return this._ensurefields(dbgtype[x.type.signature], x) - }) - .then((typeinfo, x) => { - return this._mapvalues('exception', [{ name: '{ex}', type: x.type }], [x.ex_ref_value], {}, x); - }) - .then((res, x) => { - return $.Deferred().resolveWith(this, [res[0], x.extra]) + /** + * @param {DebuggerValue} value + */ + async getSuperType(value) { + if (value.type.signature === JavaType.Object.signature) + throw new Error('java.lang.Object has no super type'); + + const typeinfo = await this.getTypeInfo(value.type.signature); + await this._ensureSuperType(typeinfo); + return typeinfo.super; + } + + /** + * @param {DebuggerValue} value + */ + async getSuperInstance(value) { + const supertype = await this.getSuperType(value); + if (value.vtype === 'class') { + return this.getTypeValue(supertype.signature); + } + return new DebuggerValue(value.vtype, supertype, value.value, value.valid, value.hasnullvalue, value.name, value.data); + } + + async getTypeValue(signature) { + const typeinfo = await this.getTypeInfo(signature); + const valid = !(typeinfo instanceof TypeNotAvailable); + return new DebuggerValue('class', typeinfo.type, typeinfo.info.typeid, valid, false, typeinfo.type.typename, null); + } + + /** + * + * @param {string} s Java quoted or literal (raw) string + * @param {{israw:boolean}} [opts] + */ + async createJavaStringLiteral(s, opts) { + const string = (opts && opts.israw) ? s : decodeJavaStringLiteral(s); + await this.ensureConnected(); + const string_ref = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.CreateStringObject(string), + }); + const keys = [{ + name: '', + type: JavaType.String, + }]; + const vars = await this._makeValues('literal', keys, [string_ref], null); + return vars[0]; + } + + /** + * @param {DebuggerValue} instance + * @param {JavaField} field + * @param {JavaTaggedValue} new_value + */ + async setFieldValue(instance, field, new_value) { + await this.ensureConnected(); + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.SetFieldValue(instance.value, field, new_value), + }); + return this.getFieldValue(instance, field.name, true); + } + + /** + * + * @param {DebuggerValue} object_value + */ + async getFieldValues(object_value) { + const type = await this.getTypeInfo(object_value.type.signature); + await this._ensureFields(type); + // the Android runtime now pointlessly barfs into logcat if an instance value is used + // to retrieve a static field. So, we now split into two calls... + const splitfields = type.fields.reduce((z, f) => { + if (f.modbits & 8) { + z.static.push(f); + } else { + z.instance.push(f); + } + return z; + }, { instance: [], static: [] }); + + // we cannot retrieve instance fields with a class type + if (object_value.vtype === 'class') { + splitfields.instance = []; + } + + // first, the instance values... + let instance_fieldvalues = []; + if (splitfields.instance.length) { + instance_fieldvalues = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.GetFieldValues(object_value.value, splitfields.instance), }); - }, + } + // and now the statics (with a type reference) + let static_fieldvalues = []; + if (splitfields.static.length) { + static_fieldvalues = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.GetStaticFieldValues(type.info.typeid, splitfields.static), + }); + } + // make sure the fields and values match up... + const fields = [...splitfields.instance, ...splitfields.static]; + const values = [...instance_fieldvalues, ...static_fieldvalues]; + const res = await this._makeValues('field', fields, values, { objvar: object_value }); + res.forEach((value,i) => { + value.data.field = fields[i]; + value.fqname = `${object_value.fqname || object_value.name}.${value.name}`; + }) + return res; + } - invokeMethod: function (objectid, threadid, type_signature, method_name, method_sig, args, extra) { - var x = { - objectid, threadid, type_signature, method_name, method_sig, args, extra, - return_type_signature: method_sig.match(/\)(.*)/)[1], - def: $.Deferred() - }; - // we must wait until any previous invokes on the same thread have completed - var invokes = this.session.invokes[threadid] = (this.session.invokes[threadid] || []); - if (invokes.push(x) === 1) - this._doInvokeMethod(x); - return x.def; - }, + /** + * @param {DebuggerValue} object_value + * @param {string} fieldname + * @param {boolean} includeInherited true if we should search up the super instances, false to only search the current instance + */ + async getFieldValue(object_value, fieldname, includeInherited) { + if (!(object_value.type instanceof JavaClassType)) { + return null; + } + let instance = object_value; + for (;;) { + // retrieve all the fields for this instance + const fields = await this.getFieldValues(instance); + const field = fields.find(f => f.name === fieldname); + if (field) { + return field; + } + // if there's no matching field in this instance, check the super + if (!includeInherited || instance.type.signature === JavaType.Object.signature) { + const fully_qualified_typename = `${object_value.type.package}.${object_value.type.typename}`; + throw new Error(`No such field '${fieldname}' in type ${fully_qualified_typename}`); + } + instance = await this.getSuperInstance(instance); + } + } - _doInvokeMethod: function (x) { - this.gettypedebuginfo(x.return_type_signature) - .then(dbgtypes => { - x.return_type = dbgtypes[x.return_type_signature].type; - return this.gettypedebuginfo(x.type_signature); - }) - .then(dbgtype => this._ensuremethods(dbgtype[x.type_signature])) - .then(typeinfo => { - // resolving the methods only resolves the non-inherited methods - // if we can't find a matching method, we need to search the super types - var o = { - dbgr:this, - def:$.Deferred(), - x: x, - find_method(typeinfo) { - for (var mid in typeinfo.methods) { - var m = typeinfo.methods[mid]; - if ((m.name === this.x.method_name) && ((m.genericsig||m.sig) === this.x.method_sig)) { - this.def.resolveWith(this, [typeinfo, m, this.x]); - return; - } - } - // search the supertype - if (typeinfo.type.signature==='Ljava/lang/Object;') { - this.def.rejectWith(this, [new Error('No such method: ' + this.x.method_name + ' ' + this.x.method_sig)]); - return; - } - - this.dbgr._ensuresuper(typeinfo) - .then(typeinfo => { - return this.dbgr.gettypedebuginfo(typeinfo.super.signature, typeinfo.super.signature) - }) - .then((dbgtype, sig) => { - return this.dbgr._ensuremethods(dbgtype[sig]) - }) - .then(typeinfo => { - this.find_method(typeinfo) - }); - } + /** + * Retrieve a list of signatures for all classes making up the inheritence tree for the given type. + * The last entry is always `"Ljava/lang/Object;"` + * @param {string} signature + */ + async getClassInheritanceList(signature) { + const signatures = []; + for (;;) { + const typeinfo = await this.getTypeInfo(signature); + signatures.push(typeinfo.type.signature); + await this._ensureSuperType(typeinfo); + if (typeinfo.super === null) { + return signatures; + } + signature = typeinfo.super.signature; + } + } + + /** + * @param {JavaThreadID} thread_id + * @param {JavaObjectID} exception_object_id + */ + async getExceptionValue(thread_id, exception_object_id) { + const typeref = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.GetObjectType(exception_object_id), + }); + /** @type {JavaType} */ + const type = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.signature(typeref) + }); + const typeinfo = await this.getTypeInfo(type.signature); + await this._ensureFields(typeinfo); + const msg = await this.invokeToString(exception_object_id, thread_id, type.signature); + const res = await this._makeValues('exception', [{ name: '{ex}', type }], [exception_object_id], { msg }); + return res[0]; + } + + /** + * @param {JavaObjectID} objectid + * @param {JavaThreadID} threadid + * @param {DebuggerMethodInfo} method + * @param {DebuggerValue[]} args + * @returns {Promise} + */ + async invokeMethod(objectid, threadid, method, args) { + const x = new MethodInvokeArgs(objectid, threadid, method, args); + // method invokes must be handled sequentially on a per-thread basis, so we add the info + // to a list and execute them one at a time + let list = this.session.methodInvokeQueues.get(threadid); + if (!list) { + this.session.methodInvokeQueues.set(threadid, list = []); + } + // create a new promise to be fulfilled with the result of the invoke + const result_promise = new Promise( + (resolve, reject) => x.promise = {resolve, reject} + ); + // if this is the only item, start the loop to perform the invokes + if (list.push(x) === 1) { + while (list.length) { + try { + const result = await this.performMethodInvoke(list[0]); + list[0].promise.resolve(result); + } catch (e) { + list[0].promise.reject(e); } - o.find_method(typeinfo); - return o.def; - }) - .then((typeinfo, method, x) => { - x.typeinfo = typeinfo; - x.method = method; - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, x.typeinfo.info.typeid, x.method.methodid, x.args), - }) - }) - .then((res, x) => { - // res = {return_value, exception} - if (/^0+$/.test(res.exception)) - return this._mapvalues('return', [{ name:'{return}', type:x.return_type }], [res.return_value], {}, x); - // todo - handle reutrn exceptions - }) - .then((res, x) => { - x.def.resolveWith(this, [res[0], x.extra]); - }) - .always(function(invokes) { - invokes.shift(); - if (invokes.length) - this._doInvokeMethod(invokes[0]); - }.bind(this,this.session.invokes[x.threadid])); - }, + list.shift(); + } + } + return result_promise; + } - invokeToString(objectid, threadid, type_signature, extra) { - return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra); - }, + /** + * @param {MethodInvokeArgs} x + */ + async performMethodInvoke({ objectid, threadid, method, args }) { - findNamedMethods(type_signature, name, method_signature) { - var x = { type_signature, name, method_signature } - const ismatch = function(x, y) { + // convert the arguments to JDWP-compatible values + const jdwp_args = args.map(arg => JavaTaggedValue.from(arg)); + + // invoke the method + const res = await this.session.adbclient.jdwp_command({ + cmd: method.isStatic + ? JDWP.Commands.InvokeStaticMethod(threadid, method.owningclass.info.typeid, method.methodid, jdwp_args) + : JDWP.Commands.InvokeMethod(objectid, threadid, method.owningclass.info.typeid, method.methodid, jdwp_args) + }) + // res = {return_value, exception} + if (!/^0+$/.test(res.exception)) { + // todo - handle reutrn exceptions + throw new Error('Exception thrown from method invoke'); + } + const return_typeinfo = await this.getTypeInfo(method.returnTypeSignature); + const values = await this._makeValues('return', [{ name: '{return}', type: return_typeinfo.type }], [res.return_value], {}); + return values[0]; + } + + /** + * @param {JavaObjectID} objectid + * @param {JavaThreadID} threadid + * @param {string} type_signature + */ + async invokeToString(objectid, threadid, type_signature) { + const methods = await this.findNamedMethods(type_signature, 'toString', '()Ljava/lang/String;', true); + return this.invokeMethod(objectid, threadid, methods[0], []); + } + + /** + * @param {string} type_signature + * @param {string|RegExp} method_name + * @param {string|RegExp} method_signature + * @param {boolean} first + */ + async findNamedMethods(type_signature, method_name, method_signature, first) { + function ismatch (x, y) { if (!x || (x === y)) return true; return (x instanceof RegExp) && x.test(y); } - return this.gettypedebuginfo(x.type_signature) - .then(dbgtype => this._ensuremethods(dbgtype[x.type_signature])) - .then(typeinfo => ({ - // resolving the methods only resolves the non-inherited methods - // if we can't find a matching method, we need to search the super types - dbgr: this, - def: $.Deferred(), - matches:[], - find_methods(typeinfo) { - for (var mid in typeinfo.methods) { - var m = typeinfo.methods[mid]; - // does the name match - if (!ismatch(x.name, m.name)) continue; - // does the signature match - if (!ismatch(x.method_signature, m.genericsig || m.sig)) continue; - // add it to the results - this.matches.push(m); - } - // search the supertype - if (typeinfo.type.signature === 'Ljava/lang/Object;') { - this.def.resolveWith(this.dbgr, [this.matches]); - return this; - } - this.dbgr._ensuresuper(typeinfo) - .then(typeinfo => { - return this.dbgr.gettypedebuginfo(typeinfo.super.signature, typeinfo.super.signature) - }) - .then((dbgtype, sig) => { - return this.dbgr._ensuremethods(dbgtype[sig]) - }) - .then(typeinfo => { - this.find_methods(typeinfo) - }); - return this; - } - }).find_methods(typeinfo).def) - }, + let typeinfo = await this.getTypeInfo(type_signature); - getstringchars: function (stringref, extra) { + // resolving the methods only resolves the non-inherited methods + // if we can't find a matching method, we need to search the super types + /** @type {DebuggerMethodInfo[]} */ + let matches = []; + for (;;) { + await this._ensureMethods(typeinfo); + matches = [ + ...matches, + ...typeinfo.methods.filter( + m => ismatch(method_name, m.name) && ismatch(method_signature, m.genericsig || m.sig) + ) + ] + if (first && matches.length) { + return [matches[0]]; + } + if (typeinfo.super === null) { + return matches; + } + // search the supertype + await this._ensureSuperType(typeinfo); + typeinfo = await this.getTypeInfo(typeinfo.super.signature); + } + } + + /** + * @param {string} type_signature + * @param {string|RegExp} field_name + * @param {boolean} first + */ + async findNamedFields(type_signature, field_name, first) { + function ismatch (x, y) { + if (!x || (x === y)) return true; + return (x instanceof RegExp) && x.test(y); + } + let typeinfo = await this.getTypeInfo(type_signature); + + // resolving the methods only resolves the non-inherited methods + // if we can't find a matching method, we need to search the super types + /** @type {JavaField[]} */ + let matches = []; + for (;;) { + await this._ensureFields(typeinfo); + matches = [ + ...matches, + ...typeinfo.fields.filter(f => ismatch(field_name, f.name)) + ] + if (first && matches.length) { + return [matches[0]]; + } + if (typeinfo.super === null) { + return matches; + } + // search the supertype + await this._ensureSuperType(typeinfo); + typeinfo = await this.getTypeInfo(typeinfo.super.signature); + } + } + + /** + * Retrieve the UTF8 text of a String object + * @param {JavaObjectID} string_ref + * @returns {Promise} + */ + getStringText(string_ref) { return this.session.adbclient.jdwp_command({ - ths: this, - extra: extra, - cmd: this.JDWP.Commands.GetStringValue(stringref), + cmd: JDWP.Commands.GetStringValue(string_ref), }); - }, + } - _getstringlen: function (stringref, extra) { - return this.gettypedebuginfo('Ljava/lang/String;', { stringref: stringref, extra: extra }) - .then(function (dbgtype, x) { - return this._ensurefields(dbgtype['Ljava/lang/String;'], x); - }) - .then(function (typeinfo, x) { - var countfields = typeinfo.fields.filter(f => f.name === 'count'); - if (!countfields.length) return -1; - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetFieldValues(x.stringref, countfields), - }); - }) - .then(function (countfields, x) { - var len = (countfields && countfields.length === 1) ? countfields[0] : -1; - return $.Deferred().resolveWith(this, [len, x.extra]); - }); - }, + /** + * Retrieve the text length of a String object + * @param {JavaObjectID} stringref + */ + async getStringLength(stringref) { + const typeinfo = await this.getTypeInfo(JavaType.String.signature); + await this._ensureFields(typeinfo); + const countfield = typeinfo.fields.find(f => f.name === 'count'); + const count_values = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.GetFieldValues(stringref, [countfield]), + }); + return count_values[0]; + } - getarrayvalues: function (local, start, count, extra) { - return this.gettypedebuginfo(local.type.elementtype.signature, { local: local, start: start, count: count, extra: extra }) - .then(function (dbgtype, x) { - x.type = dbgtype[x.local.type.elementtype.signature].type; - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetArrayValues(x.local.value, x.start, x.count), - }); - }) - .then(function (values, x) { - // generate some dummy keys to map against - var keys = []; - for (var i = 0; i < x.count; i++) { - keys.push({ name: '' + (x.start + i), type: x.type }); - } - return this._mapvalues('arrelem', keys, values, { arrobj: x.local }, x.extra); - }); - }, - - setarrayvalues: function (arrvar, start, count, data, extra) { - return this.ensureconnected({ arrvar: arrvar, start: start, count: count, data: data, extra: extra }) - .then(function (x) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.SetArrayElements(x.arrvar.value, x.start, x.count, x.data), - }); - }) - .then(function (success, x) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: x, - cmd: this.JDWP.Commands.GetArrayValues(x.arrvar.value, x.start, x.count), - }); - }) - .then(function (values, x) { - // generate some dummy keys to map against - var keys = []; - for (var i = 0; i < count; i++) { - keys.push({ name: '' + (x.start + i), type: x.arrvar.type.elementtype }); - } - return this._mapvalues('arrelem', keys, values, { arrobj: x.arrvar }, x.extra); - }); - }, - - _mapvalues: function (vtype, keys, values, data, extra) { - var res = []; - var arrayfields = []; - var stringfields = []; - - if (values && Array.isArray(values)) { - var v = values.slice(0), i = 0; - while (v.length) { - var info = { - vtype: vtype, - name: keys[i].name, - value: v.shift(), - type: keys[i].type, - hasnullvalue: false, - valid: true, - data: Object.assign({}, data), - }; - info.hasnullvalue = /^0+$/.test(info.value); - info.valid = info.value !== null; - res.push(info); - if (keys[i].type.arraydims) - arrayfields.push(info); - else if (keys[i].type.signature === 'Ljava/lang/String;') - stringfields.push(info); - else if (keys[i].type.signature === 'C') - info.char = info.valid ? String.fromCodePoint(info.value) : ''; - i++; - } + /** + * Retrieve a range of array element values + * @param {DebuggerValue} array + * @param {number} start first element index + * @param {number} count number of elements to retrieve + */ + async getArrayElementValues(array, start, count) { + if (!(array.type instanceof JavaArrayType)) { + throw new Error(`getArrayElementValues: object is not an array type`); } - var defs = [{ dbgr: this, res: res, extra: extra }]; + const values = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.GetArrayValues(array.value, start, count), + }); + const typeinfo = await this.getTypeInfo(array.type.elementType.signature); + // generate some dummy keys to map against + const keys = values.map((_,i) => + ({ + name: `${start + i}`, + type: typeinfo.type, + }) + ); + const elements = await this._makeValues('arrelem', keys, values, { array }); + // assign fully qualified names for the elements + elements.forEach(element => element.fqname = `${array.fqname||array.name}[${element.name}]`); + return elements; + } + + /** + * Set (fill) an array range with the specified value + * @param {DebuggerValue} array + * @param {number} start + * @param {number} count + * @param {JavaTaggedValue} value + */ + async setArrayElements(array, start, count, value) { + if (!Number.isInteger(start)) { + throw new Error('setArrayElementValues: Array start index is not an integer'); + } + if (!Number.isInteger(count)) { + throw new Error('setArrayElementValues: Array element count is not an integer'); + } + await this.ensureConnected(); + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.SetArrayElements(array.value, start, count, value), + }) + return this.getArrayElementValues(array, start, count); + } + + /** + * Create a new array of DebuggerValues from a set of keys and values + * @param {DebuggerValueType} vtype + * @param {{name:string,type:JavaType}[]} keys + * @param {*[]} values + * @param {*} data + */ + async _makeValues(vtype, keys, values, data) { + if (!values || !Array.isArray(values)) { + return []; + } + let res = values.map((v,i) => + new DebuggerValue( + vtype, + keys[i].type, + v, + v !== null, + /^0+$/.test(v), + keys[i].name, + {...data} + )); + + const fetch_values = []; // for those fields that are (non-null) arrays, retrieve the length - for (var i in arrayfields) { - if (arrayfields[i].hasnullvalue || !arrayfields[i].valid) continue; - var def = this.session.adbclient.jdwp_command({ - ths: this, - extra: arrayfields[i], - cmd: this.JDWP.Commands.GetArrayLength(arrayfields[i].value), - }) - .then(function (arrlen, arrfield) { - arrfield.arraylen = arrlen; - }); - defs.push(def); - } - // for those fields that are strings, retrieve the text - for (var i in stringfields) { - if (stringfields[i].hasnullvalue || !stringfields[i].valid) continue; - var def = this._getstringlen(stringfields[i].value, stringfields[i]) - .then(function (len, strfield) { - if (len > 10000) - return $.Deferred().resolveWith(this, [len, strfield]); - // retrieve the actual chars - return this.getstringchars(strfield.value, strfield); + res.filter(v => JavaType.isArray(v.type)) + .forEach(f => { + if (f.hasnullvalue || !f.valid) { + return; + } + const promise = this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.GetArrayLength(f.value), }) - .then(function (str, strfield) { - if (typeof (str) === 'number') { - strfield.string = '{string exceeds maximum display length}'; - strfield.biglen = str; - } else { - strfield.string = str; - } - }); - defs.push(def); - } - - return $.when.apply($, defs) - .then(function (x) { - return $.Deferred().resolveWith(x.dbgr, [x.res, x.extra]); + .then(arrlen => f.arraylen = arrlen); + fetch_values.push(promise); }); - }, - gettypedebuginfo: function (signature, extra) { - - var info = { - signature: signature, - classes: {}, - ci: { type: this.JDWP.signaturetotype(signature), }, - extra: extra, - deferred: $.Deferred(), - }; - - if (this.session) { - // see if we've already retrieved the type for this session - var cached = this.session.classes[signature]; - if (cached) { - // are we still retrieving it... - if (cached.promise) { - return cached.promise(); + // for those fields that are strings, retrieve the string text + res.filter(v => JavaType.isString(v.type)) + .forEach(f => { + if (f.hasnullvalue || !f.valid) { + return; } - // return the cached entry - var res = {}; res[signature] = cached; - return $.Deferred().resolveWith(this, [res, extra]); - } - // while we're retrieving it, set a deferred in it's place - this.session.classes[signature] = info.deferred; - } - - this.ensureconnected(info) - .then(function (info) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: info, - cmd: this.JDWP.Commands.classinfo(info.ci), - }); - }) - .then(function (classinfoarr, info) { - if (!classinfoarr || !classinfoarr.length) { - if (this.session) - delete this.session.classes[info.signature]; - return info.deferred.resolveWith(this, [{}, info.extra]); - } - info.ci.info = classinfoarr[0]; - info.ci.name = info.ci.type.typename; - info.classes[info.ci.type.signature] = info.ci; - - // querying the source file for array or primitive types causes the app to crash - return (info.ci.type.signature[0] !== 'L' - ? $.Deferred().resolveWith(this, [[null], info]) - : this.session.adbclient.jdwp_command({ - ths: this, - extra: info, - cmd: this.JDWP.Commands.sourcefile(info.ci), - })) - .then(function (srcinfoarr, info) { - info.ci.src = srcinfoarr[0]; - if (this.session) { - Object.assign(this.session.classes, info.classes); + const promise = this.getStringLength(f.value) + .then(async len => { + if (len > 10000) { + f.string = '{string exceeds maximum display length}'; + f.biglen = len; + } else { + f.string = await this.getStringText(f.value); } - return info.deferred.resolveWith(this, [info.classes, info.extra]); // done }); - }); + fetch_values.push(promise); + }); - return info.deferred; - }, + await Promise.all(fetch_values); + return res; + } - _ensuresuper: function (typeinfo) { - if (typeinfo.super || typeinfo.super === null) { - if (typeinfo.super && typeinfo.super.promise) - return typeinfo.super.promise(); - return $.Deferred().resolveWith(this, [typeinfo]); + /** + * Convert a JRE signature to a DebuggerTypeInfo instance + * @param {string} signature + */ + getTypeInfo(signature) { + // see if we've already retrieved the type for this session + const cached = this.session.classCache.get(signature); + if (cached) { + // return the cached entry + // - this will either be the DebuggerTypeInfo instance or a promise resolving with the DebuggerTypeInfo instance + return cached; } - if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') { - if (typeinfo.info.reftype.string !== 'array') { - typeinfo.super = null; - return $.Deferred().resolveWith(this, [typeinfo]); + + // while we're retrieving it, set a promise in it's place + // - this prevents multiple requests from being forwarded over JDWP + const promise = this.fetchTypeInfo(signature); + this.session.classCache.set(signature, promise); + return promise; + } + + /** + * @param {string} signature + */ + async fetchTypeInfo(signature) { + await this.ensureConnected(); + /** @type {JavaClassInfo[]} */ + const class_infos = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.classinfo(signature), + }); + + // if the runtime has not loaded the type yet, return a dummy class + if (!class_infos || !class_infos.length) { + if (this.session) { + // delete the entry in the cache so that any future requests will + // perform a new fetch. + this.session.classCache.delete(signature); } + return new TypeNotAvailable(JavaType.from(signature)); } - typeinfo.super = $.Deferred(); - this.session.adbclient.jdwp_command({ - ths: this, - extra: typeinfo, - cmd: this.JDWP.Commands.superclass(typeinfo), - }) - .then(function (superclassref, typeinfo) { - return this.session.adbclient.jdwp_command({ - ths: this, - extra: typeinfo, - cmd: this.JDWP.Commands.signature(superclassref), - }); - }) - .then(function (supertype, typeinfo) { - var def = typeinfo.super; - typeinfo.super = supertype; - def.resolveWith(this, [typeinfo]); + const typeinfo = new DebuggerTypeInfo(class_infos[0], JavaType.from(signature)); + + /** @type {JavaSource[]} */ + let srcinfoarr = [null]; + // querying the source file for array or primitive types causes the app to crash + if (/^L/.test(signature)) { + srcinfoarr = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.sourcefile(typeinfo), }); + } + typeinfo.src = srcinfoarr[0]; + if (this.session) { + this.session.classList.push(typeinfo); + this.session.classCache.set(signature, typeinfo); + } + return typeinfo; + } - return typeinfo.super.promise(); - }, + /** + * Ensure any 'super' type information is retrieved + * @param {DebuggerTypeInfo} typeinfo + */ + async _ensureSuperType(typeinfo) { + // a null value implies no super type is valid (eg. Object) + if (typeinfo.super === null) { + return null; + } + if (typeinfo.super) { + return typeinfo.super; + } + const fetchSuperType = async (typeinfo) => { + const supertyperef = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.superclass(typeinfo), + }); + typeinfo.super = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.signature(supertyperef), + }); + return typeinfo.super; + } + // to ensure we don't perform multiple redundant JDWP requests, set the field to the Promise + // @ts-ignore + return typeinfo.super = fetchSuperType(typeinfo); + } - _ensurefields: function (typeinfo, extra) { + /** + * Ensure any type fields information is retrieved + * @param {DebuggerTypeInfo} typeinfo + */ + _ensureFields(typeinfo) { if (typeinfo.fields) { - if (typeinfo.fields.promise) - return typeinfo.fields.promise(); - return $.Deferred().resolveWith(this, [typeinfo, extra]); + return typeinfo.fields; } - typeinfo.fields = $.Deferred(); + const fetchFields = async (typeinfo) => { + /** @type {JavaField[]} */ + const fields = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.fieldsWithGeneric(typeinfo), + }) + return typeinfo.fields = fields; + } + // to ensure we don't perform multiple redundant JDWP requests, set the field to the Promise + // @ts-ignore + return typeinfo.fields = fetchFields(typeinfo); + } - this.session.adbclient.jdwp_command({ - ths: this, - extra: { typeinfo: typeinfo, extra: extra }, - cmd: this.JDWP.Commands.fieldsWithGeneric(typeinfo), - }) - .then(function (fields, x) { - var def = x.typeinfo.fields; - x.typeinfo.fields = fields; - def.resolveWith(this, [x.typeinfo, x.extra]); - }); - - return typeinfo.fields.promise(); - }, - - _ensuremethods: function (typeinfo) { + /** + * Ensure any type methods information is retrieved + * @param {DebuggerTypeInfo} typeinfo + */ + async _ensureMethods(typeinfo) { if (typeinfo.methods) { - if (typeinfo.methods.promise) - return typeinfo.methods.promise(); - return $.Deferred().resolveWith(this, [typeinfo]); + return typeinfo.methods; } - typeinfo.methods = $.Deferred(); + const fetchMethods = async (typeinfo) => { + const methods = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.methodsWithGeneric(typeinfo), + }) + return typeinfo.methods = methods.map(m => new DebuggerMethodInfo(m, typeinfo)); + } + // to ensure we don't perform multiple redundant JDWP requests, set the field to the Promise + // @ts-ignore + return typeinfo.methods = fetchMethods(typeinfo); + } - this.session.adbclient.jdwp_command({ - ths: this, - extra: typeinfo, - cmd: this.JDWP.Commands.methodsWithGeneric(typeinfo), - }) - .then(function (methods, typeinfo) { - var def = typeinfo.methods; - typeinfo.methods = {}; - for (var i in methods) { - methods[i].owningclass = typeinfo; - typeinfo.methods[methods[i].methodid] = methods[i]; - } - def.resolveWith(this, [typeinfo]); - }); - - return typeinfo.methods.promise(); - }, - - _ensuremethodvars: function (methodinfo) { + /** + * Ensure any method variables information is retrieved + * @param {DebuggerMethodInfo} methodinfo + */ + _ensureMethodVars(methodinfo) { if (methodinfo.vartable) { - if (methodinfo.vartable.promise) - return methodinfo.vartable.promise(); - return $.Deferred().resolveWith(this, [methodinfo]); + return methodinfo.vartable; } - methodinfo.vartable = $.Deferred(); + const fetchMethodVarTable = async (methodinfo) => { + /** @type {JavaVarTable} */ + const vartable = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.VariableTableWithGeneric(methodinfo.owningclass, methodinfo), + }) + return methodinfo.setVarTable(vartable); + } + // to ensure we don't perform multiple redundant JDWP requests, set the field to the Promise + // @ts-ignore + return methodinfo.vartable = fetchMethodVarTable(methodinfo); + } - this.session.adbclient.jdwp_command({ - ths: this, - extra: methodinfo, - cmd: this.JDWP.Commands.VariableTableWithGeneric(methodinfo.owningclass, methodinfo), - }) - .then(function (vartable, methodinfo) { - var def = methodinfo.vartable; - methodinfo.vartable = vartable; - def.resolveWith(this, [methodinfo]); - }); - - return methodinfo.vartable.promise(); - }, - - _ensuremethodlines: function (methodinfo) { + /** + * Ensure any method code lines information is retrieved + * @param {DebuggerMethodInfo} methodinfo + */ + async _ensureMethodLines(methodinfo) { if (methodinfo.linetable) { - if (methodinfo.linetable.promise) - return methodinfo.linetable.promise(); - return $.Deferred().resolveWith(this, [methodinfo]); + return methodinfo.linetable; } - methodinfo.linetable = $.Deferred(); - - this.session.adbclient.jdwp_command({ - ths: this, - extra: methodinfo, - cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo), - }) - .then(function (linetable, methodinfo) { + const fetchMethodLines = async (methodinfo) => { + /** @type {JavaLineTable} */ + const linetable = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo), + }) // if the request failed, just return a blank table - if (linetable.errorcode) { - linetable = { - errorcode: linetable.errorcode, - start: '00000000000000000000000000000000', - end: '00000000000000000000000000000000', - lines:[], - } - } + .catch(() => DebuggerMethodInfo.NullLineTable); + // the linetable does not correlate code indexes with line numbers // - location searching relies on the table being ordered by code indexes linetable.lines.sort(function (a, b) { return (a.linecodeidx === b.linecodeidx) ? 0 : ((a.linecodeidx < b.linecodeidx) ? -1 : +1); }); - var def = methodinfo.linetable; - methodinfo.linetable = linetable; - def.resolveWith(this, [methodinfo]); - }); + return methodinfo.setLineTable(linetable); + } + // to ensure we don't perform multiple redundant JDWP requests, set the field to the Promise + // @ts-ignore + return methodinfo.linetable = fetchMethodLines(methodinfo); + } - return methodinfo.linetable.promise(); - }, - - _setupclassprepareevent: function (filter, onprepare) { - var onevent = { - data: { - dbgr: this, - onprepare: onprepare, - }, - fn: function (e) { - var x = e.data; - x.onprepare.apply(x.dbgr, [e.event]); + /** + * Sends a JDWP command to register for class-prepare events + * @param {string} pattern signature pattern to match against prepared classes. Only those matching the pattern will cause an event trigger. + * @param {(event) => void} onprepare + */ + _setupClassPrepareEvent(pattern, onprepare) { + const onevent = { + fn: (e) => { + onprepare(e.event); } }; - var cmd = this.session.adbclient.jdwp_command({ - cmd: this.JDWP.Commands.OnClassPrepare(filter, onevent), + return this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.OnClassPrepare(pattern, onevent), }); + } - return cmd.promise(); - }, + /** + * Sends a JDWP command to clear any outstanding step requests for the given thread + * @param {JavaThreadID} threadid + */ + async clearLastStepRequest(threadid) { + if (!this.session || !this.session.stepIDs.has(threadid)) + return; - _clearLastStepRequest: function (threadid, extra) { - if (!this.session || !this.session.stepids[threadid]) - return $.Deferred().resolveWith(this,[extra]); + const stepid = this.session.stepIDs.get(threadid); + this.session.stepIDs.set(threadid, 0); - var clearStepCommand = this.session.adbclient.jdwp_command({ - cmd: this.JDWP.Commands.ClearStep(this.session.stepids[threadid]), - extra: extra, - }).then((decoded, extra) => extra); - this.session.stepids[threadid] = 0; - return clearStepCommand; - }, - - _setupstepevent: function (steptype, threadid, extra) { - var onevent = { - data: { - dbgr: this, - }, - fn: function (e) { - e.data.dbgr._clearLastStepRequest(e.event.threadid, e) - .then(function (e) { - var x = e.data; - var loc = e.event.location; - - // search the cached classes for a matching source location - x.dbgr._findcmllocation(x.dbgr.session.classes, loc) - .then(function (sloc) { - var stoppedloc = sloc || { qtype: null, linenum: null }; - stoppedloc.threadid = e.event.threadid; - - var eventdata = { - event: e.event, - stoppedlocation: stoppedloc, - }; - x.dbgr.session.stoppedlocation = stoppedloc; - x.dbgr._trigger('step', eventdata); - }); - }); - } - }; - var cmd = this.session.adbclient.jdwp_command({ - cmd: this.JDWP.Commands.SetSingleStep(steptype, threadid, onevent), - extra: extra, - }).then((res,extra) => { - // save the step id so we can manually clear it if an exception break occurs - if (this.session && res && res.id) - this.session.stepids[threadid] = res.id; - return extra; + return this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.ClearStep(stepid), }); + } - return cmd.promise(); - }, - - _setupbreakpointsevent: function (locations) { - var onevent = { - data: { - dbgr: this, - }, - fn: function (e) { - var x = e.data; - var loc = e.event.location; - var cmlkey = loc.cid + ':' + loc.mid + ':' + loc.idx; - var bp = x.dbgr.breakpoints.enabled[cmlkey].bp; - var stoppedloc = { - qtype: bp.qtype, - linenum: bp.linenum, - threadid: e.event.threadid - }; - var eventdata = { + /** + * + * @param {DebuggerStepType} steptype + * @param {JavaThreadID} threadid + */ + async _setupStepEvent(steptype, threadid) { + const onevent = { + fn: async (e) => { + await this.clearLastStepRequest(e.event.threadid); + // search the cached classes for a matching source location + const sloc = await this.javaLocationToSourceLocation(e.event.location, e.event.threadid); + const stoppedLocation = sloc || new SourceLocation(null, null, false, e.event.threadid); + const eventdata = { event: e.event, - stoppedlocation: stoppedloc, - bp: x.dbgr.breakpoints.enabled[cmlkey].bp, + stoppedLocation, }; - x.dbgr.session.stoppedlocation = stoppedloc; + this.session.stoppedLocation = stoppedLocation; + this.emit('step', eventdata); + } + }; + + const res = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.SetSingleStep(steptype, threadid, onevent), + }); + // save the step id so we can manually clear it if an exception break occurs + if (this.session && res && res.id) { + this.session.stepIDs.set(threadid, res.id); + } + } + + /** + * Send SetBreakpoint command to the connected device and register a handler for the event + * @param {BreakpointLocation[]} locations + */ + async _setupBreakpointsEvent(locations) { + const onevent = { + data: { + dbgr: this, + }, + fn: async (e) => { + const loc = e.event.location; + const cmlkey = `${loc.cid}:${loc.mid}:${loc.idx}`; + // find the DebuggerBreakpoint matching the location + const bp = this.breakpoints.all.find(bp => bp.enabled && bp.enabled.cml === cmlkey); + const stoppedLocation = new SourceLocation(bp.qtype, bp.linenum, true, e.event.threadid); + this.session.stoppedLocation = stoppedLocation; + const eventdata = new JavaBreakpointEvent(e.event, stoppedLocation, bp); // if this was a conditional breakpoint, it will have been automatically cleared // - set a new (unconditional) breakpoint in it's place - if (bp.conditions.hitcount) { - bp.hitcount += bp.conditions.hitcount; - delete bp.conditions.hitcount; - var bploc = x.dbgr.breakpoints.enabled[cmlkey].bploc; - x.dbgr.session.adbclient.jdwp_command({ - cmd: x.dbgr.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, null, onevent), + if (bp.options.hitcount) { + bp.hitcount += bp.options.hitcount; + bp.options.hitcount = null; + const { bploc } = bp.enabled; + const res = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, null, onevent), }); + bp.enabled.requestid = res.id; } else { bp.hitcount++; } bp.stopcount++; - x.dbgr._trigger('bphit', eventdata); + this.emit('bphit', eventdata); } }; - var bparr = []; - var cmlkeys = []; - var setbpcmds = [{ dbgr: this, bparr: bparr, cmlkeys: cmlkeys }]; - for (var i in locations) { - var bploc = locations[i]; - // associate, so we can find it when the bp hits... - var cmlkey = bploc.c.info.typeid + ':' + bploc.m.methodid + ':' + bploc.l; - cmlkeys.push(cmlkey); - this.breakpoints.enabled[cmlkey] = { - bp: bploc.bp, - bploc: {c:bploc.c,m:bploc.m,l:bploc.l}, - requestid: null, - }; - bparr.push(bploc.bp); - var cmd = this.session.adbclient.jdwp_command({ - cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, bploc.bp.conditions.hitcount, onevent), + const enabled_breakpoints = []; + for (let bploc of locations) { + const { bp } = bploc; + const res = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, bp.options.hitcount, onevent), }); - setbpcmds.push(cmd); + // save the JDWP request IDs from the SetBreakpoint command so we can disable the breakpoint later + bp.setEnabled(bploc, res.id); + enabled_breakpoints.push(bp); } - return $.when.apply($, setbpcmds) - .then(function (x) { - // save the request ids from the SetBreakpoint commands so we can disable them later - for (var i = 0; i < x.cmlkeys.length; i++) { - x.dbgr.breakpoints.enabled[x.cmlkeys[i]].requestid = arguments[i + 1][0].id; - } - x.dbgr._changebpstate(x.bparr, 'enabled'); - return $.Deferred().resolveWith(x.dbgr); - }); - }, + this._changeBPState(enabled_breakpoints, 'enabled'); + } - _clearbreakpointsevent: function (cmlarr, extra) { - var bparr = []; - var clearbpcmds = [{ dbgr: this, extra: extra, bparr: bparr }]; - - for (var i in cmlarr) { - var enabled = this.breakpoints.enabled[cmlarr[i]]; - delete this.breakpoints.enabled[cmlarr[i]]; - bparr.push(enabled.bp); - var cmd = this.session.adbclient.jdwp_command({ - cmd: this.JDWP.Commands.ClearBreakpoint(enabled.requestid), + /** + * @param {DebuggerBreakpoint[]} breakpoints + * @param {BreakpointState} [new_state] + */ + async disableBreakpoints(breakpoints, new_state = 'notloaded') { + const enabled_bps = breakpoints.filter(bp => bp.enabled); + for (let bp of enabled_bps) { + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.ClearBreakpoint(bp.enabled.requestid), }); - clearbpcmds.push(cmd); + bp.setDisabled(); } + this._changeBPState(enabled_bps, new_state); + } - return $.when.apply($, clearbpcmds) - .then(function (x) { - x.dbgr._changebpstate(x.bparr, 'notloaded'); - return $.Deferred().resolveWith(x.dbgr, [x.extra]); - }); - }, - - _changebpstate: function (bparr, newstate) { - if (!bparr || !bparr.length || !newstate) return; - for (var i in bparr) { - bparr[i].state = newstate; + /** + * Set the internal state of the breakpoints and trigger the 'bpstatechange' event + * @param {DebuggerBreakpoint[]} breakpoints + * @param {BreakpointState} new_state + */ + _changeBPState(breakpoints, new_state) { + if (!breakpoints || !breakpoints.length || !new_state) { + return; } - this._trigger('bpstatechange', { breakpoints: bparr.slice(), newstate: newstate }); - }, + breakpoints.forEach(bp => bp.state = new_state); + this.emit('bpstatechange', { + breakpoints: breakpoints.slice(), + newstate: new_state, + }); + } - _initbreakpoints: function () { - var deferreds = [{ dbgr: this }]; - // reset any current associations - this.breakpoints.enabled = {}; + /** + * Setup class-prepare events for the classes we have breakpoints set for. + */ + initClassPrepareForBreakpoints() { // set all the breakpoints to the notloaded state - this._changebpstate(this.breakpoints.all, 'notloaded'); + this._changeBPState(this.breakpoints.all, 'notloaded'); // setup class prepare notifications for all the packages associated with breakpoints - // when each class is prepared, we initialise any breakpoints for it - var cpdefs = this.breakpoints.all.map(bp => this._ensureClassPrepareForPackage(bp.pkg)); - deferreds = deferreds.concat(cpdefs); + // when each class is prepared (loaded by the runtime), we initialise any breakpoints for it + const class_prepare_promises = this.breakpoints.all.map( + bp => this._ensureClassPrepareForPackage(bp.pkg) + ); - return $.when.apply($, deferreds).then(function (x) { - return $.Deferred().resolveWith(x.dbgr); - }); - }, + return Promise.all(class_prepare_promises); + } - _ensureClassPrepareForPackage: function(pkg) { - var filter = pkg + '.*'; - if (this.session.cpfilters.includes(filter)) - return $.Deferred().resolveWith(this,[]); // already setup + /** + * Reset all breakpoints back to disabled set + */ + resetBreakpoints() { + this._changeBPState(this.breakpoints.all, 'set'); + this.breakpoints.all.forEach(bp => bp.setDisabled()); + } - this.session.cpfilters.push(filter); - return this._setupclassprepareevent(filter, preppedclass => { - // if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get - // multiple notifications (which duplicates breakpoints, etc) - if (this.session.preparedclasses.includes(preppedclass.type.signature)) { - return; // we already know about this - } - this.session.preparedclasses.push(preppedclass.type.signature); - D('Prepared: ' + preppedclass.type.signature); - var m = preppedclass.type.signature.match(/^L(.*);$/); - if (!m) { - // unrecognised type - just resume - this._resumesilent(); - return; - } - this._loadclzinfo(preppedclass.type.signature) - .then(function (classes) { - var bplocs = []; - for (var idx in this.breakpoints.all) { - var bp = this.breakpoints.all[idx]; - var bploc = this._findbplocation(classes, bp); - if (bploc) { - bplocs.push(bploc); - } - } - if (!bplocs.length) return; - // set all the breakpoints in one go... - return this._setupbreakpointsevent(bplocs); - }) - .then(function () { - // when all the breakpoints for the newly-prepared type have been set... - this._resumesilent(); - }); - }); - }, - - clearBreakOnExceptions: function(extra) { - var o = { - dbgr: this, - def: $.Deferred(), - extra: extra, - next() { - if (!this.dbgr.exception_ids.length) { - return this.def.resolveWith(this.dbgr, [this.extra]); // done + /** + * Setup a class-prepare event for the given package name + * @param {string} package_name + */ + _ensureClassPrepareForPackage(package_name) { + const filter = `${package_name}.*`; + if (this.session.classPrepareFilters.has(filter)) { + return; // already setup + } + this.session.classPrepareFilters.add(filter); + return this._setupClassPrepareEvent(filter, + async clz => { + try { + await this._onClassPrepared(clz); + } catch (e) { + D(`_onClassPrepared failed. ${e.message}`) } - // clear next pattern - this.dbgr.session.adbclient.jdwp_command({ - cmd: this.dbgr.JDWP.Commands.ClearExceptionBreak(this.dbgr.exception_ids.pop()) - }) - .then(() => this.next()) - .fail(e => this.def.rejectWith(this, [e])) - } - }; - o.next(); - return o.def; - }, + // when the class-prepare event triggers, JDWP automatically suspends the app + // - we must always manually resume to continue... + this._resumesilent(); + }); + } - setBreakOnExceptions: function(which, extra) { - var onevent = { + /** + * Callback when the JDWP class-prepare event triggers + * @param {JavaClassInfo} prepared_class + */ + async _onClassPrepared(prepared_class) { + // if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get + // multiple notifications (which duplicates breakpoints, etc) + const signature = prepared_class.type.signature; + if (this.session.preparedClasses.has(signature)) { + return; // we already know about this + } + this.session.preparedClasses.add(signature); + D('Prepared: ' + signature); + if (!/^L(.*);$/.test(signature)) { + // unrecognised type signature - ignore it + return; + } + + const classes = [await this.loadClassInfo(signature)]; + const bplocs = this.breakpoints.all + .map(bp => Debugger.findBreakpointLocation(classes, bp)) + .filter(x => x); + + if (bplocs.length) { + // set all the breakpoints in one go... + await this._setupBreakpointsEvent(bplocs); + } + } + + /** + * Send JDWP commands to clear break-on-exception options + */ + async clearBreakOnExceptions() { + while (this.exception_ids.length) { + // clear next pattern + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.ClearExceptionBreak(this.exception_ids.pop()) + }) + } + } + + /** + * Enable break-on-exceptions. JDWP will send an event when an exception is thrown. + * @param {ExceptionBreakMode} which + */ + async setBreakOnExceptions(which) { + const onevent = { data: { - dbgr: this, }, - fn: function (e) { + fn: async e => { // if this exception break occurred during a step request, we must manually clear the event // or the (device-side) debugger will crash on next step - this._clearLastStepRequest(e.event.threadid, e).then(e => { - this._findcmllocation(this.session.classes, e.event.throwlocation) - .then(tloc => { - this._findcmllocation(this.session.classes, e.event.catchlocation) - .then(cloc => { - var eventdata = { - event: e.event, - throwlocation: Object.assign({ threadid: e.event.threadid }, tloc), - catchlocation: Object.assign({ threadid: e.event.threadid }, cloc), - }; - this.session.stoppedlocation = Object.assign({}, eventdata.throwlocation); - this._trigger('exception', eventdata); - }) - }) - }); - }.bind(this) + await this.clearLastStepRequest(e.event.threadid); + // retrieve the catch and throw locations + const tloc = await this.javaLocationToSourceLocation(e.event.throwlocation, e.event.threadid); + const cloc = await this.javaLocationToSourceLocation(e.event.catchlocation, e.event.threadid); + const eventdata = new JavaExceptionEvent(e.event, tloc, cloc); + this.session.stoppedLocation = eventdata.throwlocation; + this.emit('exception', eventdata); + } }; - var c = false, u = false; + let caught = false, uncaught = false; switch (which) { - case 'caught': c = true; break; - case 'uncaught': u = true; break; - case 'both': c = u = true; break; - default: throw new Error('Invalid exception option'); + case 'caught': caught = true; break; + case 'uncaught': uncaught = true; break; + case 'both': caught = uncaught = true; break; + default: throw new Error(`Invalid exception option: ${which}`); } // when setting up the exceptions, we filter by packages containing public classes in the current session // - each filter needs a separate call (I think), so we do this as an asynchronous list - var pkgs = this.session.build.packages; - var pkgs_to_monitor = c ? Object.keys(pkgs).filter(pkgname => pkgs[pkgname].public_classes.length) : []; - var o = { - dbgr: this, - filters: pkgs_to_monitor.map(pkg=>pkg+'.*'), - caught: c, - uncaught: u, - onevent: onevent, - cmds:[], - def: $.Deferred(), - extra: extra, - next() { - var uncaught = false; - if (!this.filters.length) { - if (!this.uncaught) { - this.def.resolveWith(this.dbgr, [this.extra]); // done - return; - } - // setup the uncaught exception break - with no filter - uncaught = true; - this.filters.push(null); - this.caught = this.uncaught = false; - } - // setup next pattern - this.dbgr.session.adbclient.jdwp_command({ - cmd: this.dbgr.JDWP.Commands.SetExceptionBreak(this.filters.shift(), this.caught, uncaught, this.onevent), - }) - .then(x => { - this.dbgr.exception_ids.push(x.id); - this.next(); - }) - .fail(e => this.def.rejectWith(this, [e])) - } - }; - o.next(); - return o.def; - }, + const pkgs = this.session.build.packages; + const pkgs_to_monitor = caught + ? [...pkgs.keys()].filter(name => pkgs.get(name).public_classes.length) + : []; - setThreadNotify: function(extra) { - var onevent = { - data: { - dbgr: this, - }, - fn: function (e) { - // the thread notifiers don't give any location information - //this.session.stoppedlocation = ... - this._trigger('threadchange', {state:e.event.state, threadid:e.event.threadid}); - }.bind(this) - }; - - return this.ensureconnected(extra) - .then((extra) => this.session.adbclient.jdwp_command({ - cmd: this.JDWP.Commands.ThreadStartNotify(onevent), - extra:extra, - })) - .then((res,extra) => this.session.adbclient.jdwp_command({ - cmd: this.JDWP.Commands.ThreadEndNotify(onevent), - extra:extra, - })) - .then((res, extra) => extra); - }, - - _loadclzinfo: function (signature) { - return this.gettypedebuginfo(signature) - .then(function (classes) { - var defs = [{ dbgr: this, classes: classes }]; - for (var clz in classes) { - defs.push(this._ensuremethods(classes[clz])); - } - return $.when.apply($, defs).then(function (x) { - return $.Deferred().resolveWith(x.dbgr, [x.classes]); - }) - }) - .then(function (classes) { - var defs = [{ dbgr: this, classes: classes }]; - for (var clz in classes) { - for (var m in classes[clz].methods) { - defs.push(this._ensuremethodlines(classes[clz].methods[m])); - } - } - return $.when.apply($, defs).then(function (x) { - return $.Deferred().resolveWith(x.dbgr, [x.classes]); - }) - }); - }, - - _findbplocation: function (classes, bp) { - // search the classes for a method containing the line - for (var i in classes) { - if (!bp.sigpattern.test(classes[i].type.signature)) - continue; - for (var j in classes[i].methods) { - var lines = classes[i].methods[j].linetable.lines; - for (var k in lines) { - if (lines[k].linenum === bp.linenum) { - // match - save the info for the command later - var bploc = { - c: classes[i], m: classes[i].methods[j], l: lines[k].linecodeidx, - bp: bp, - }; - return bploc; - } - } - } + let filters = pkgs_to_monitor.map(pkg => `${pkg}.*`); + if (uncaught) { + // setup the uncaught exception break - with no filter + filters.push(null); } - return null; - }, + for (let filter of filters) { + // we only enable 'caught' with a package filter + // - otherwise we end up stopping on every exception in the Android framework + // (and there are a lot of exceptions thrown) + const c = !!filter && caught; + const u = filter !== null; + const res = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.SetExceptionBreak(filter, c, u, onevent), + }) + this.exception_ids.push(res.id); + } + } - line_idx_to_source_location: function (method, idx) { - if (!method || !method.linetable || !method.linetable.lines || !method.linetable.lines.length) + /** + * Setup notifiers for thread start and ends + */ + async setThreadNotify() { + const onevent = { + data: {}, + fn: (e) => { + // the thread notifiers don't give any location information + this.emit('threadchange', { + state: e.event.state, + threadid: e.event.threadid, + }); + }, + }; + await this.ensureConnected(); + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.ThreadStartNotify(onevent), + }); + await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.ThreadEndNotify(onevent), + }); + } + + /** + * Return a DebuggerTypeInfo for the given type signature with methods and code lines retrieved + * @param {string} signature + */ + async loadClassInfo(signature) { + const typeinfo = await this.getTypeInfo(signature); + // load the methods + await this._ensureMethods(typeinfo); + // load the method lines + await Promise.all(typeinfo.methods.map(m => this._ensureMethodLines(m))); + return typeinfo; + } + + /** + * Search the list of classes for a location matching the breakpoint + * @param {DebuggerTypeInfo[]} classes + * @param {DebuggerBreakpoint} bp + * @returns {BreakpointLocation} + */ + static findBreakpointLocation(classes, bp) { + // search the classes for a method containing the line + let bploc = null; + classes.find(c => + bp.sigpattern.test(c.type.signature) + && c.methods.find(m => { + const line = m.linetable.lines.find(line => line.linenum === bp.linenum); + if (line) { + bploc = new BreakpointLocation(bp, c, m, line.linecodeidx); + return true; + } + }) + ) + return bploc; + } + + /** + * Returns a SourceLocation instance for the given frame or null if the location cannot be determined + * @param {DebuggerFrameInfo} frame + */ + frameToSourceLocation(frame) { + return this.lineIndexToSourceLocation(frame.method, frame.location.idx, frame.threadid); + } + + /** + * Converts the specified method and code index to a SourceLocation + * @param {DebuggerMethodInfo} method + * @param {hex64} idx + * @param {JavaThreadID} threadid + */ + lineIndexToSourceLocation(method, idx, threadid) { + if (!method || !method.linetable || !method.linetable.lines || !method.linetable.lines.length) { return null; - var m = method.owningclass.type.signature.match(/^L([^;$]+)[$a-zA-Z0-9_]*;$/); - if (!m) + } + const m = method.owningclass.type.signature.match(/^L([^;$]+)[$a-zA-Z0-9_]*;$/); + if (!m) { return null; - var lines = method.linetable.lines, prevk = 0; - for (var k in lines) { + } + const qualified_type_name = m[1]; + const lines = method.linetable.lines; + let prevk = 0; + for (let k=0; k < lines.length; k++) { if (lines[k].linecodeidx < idx) { prevk = k; continue; @@ -1853,84 +1667,84 @@ Debugger.prototype = { // - if the idx is not an exact match, use the previous value if (lines[k].linecodeidx > idx) k = prevk; - // convert the class signature to a file location - return { - qtype: m[1], - linenum: lines[k].linenum, - exact: lines[k].linecodeidx === idx, - }; + // convert to a file location + return new SourceLocation(qualified_type_name, lines[k].linenum, lines[k].linecodeidx === idx, threadid); } // just return the last location in the list - return { - qtype: m[1], - linenum: lines[lines.length-1].linenum, - exact: false, - }; - }, + return new SourceLocation(qualified_type_name, lines[lines.length - 1].linenum, false, threadid); + } - _findcmllocation: function (classes, loc) { + /** + * + * @param {JavaLocation} location + * @param {JavaThreadID} threadid + */ + async javaLocationToSourceLocation(location, threadid) { // search the classes for a method containing the line - return this._findmethodasync(classes, loc) - .then(function (method) { - if (!method) - return $.Deferred().resolveWith(this, [null]); - return this._ensuremethodlines(method) - .then(function (method) { - var srcloc = this.line_idx_to_source_location(method, loc.idx); - return $.Deferred().resolveWith(this, [srcloc]); - }); - }); - }, + const method = await this._findMethodAsync(this.session.classList, location); + if (!method) + return null; + await this._ensureMethodLines(method); + return this.lineIndexToSourceLocation(method, location.idx, threadid); + } - _findmethodasync: function (classes, location) { + /** + * @param {DebuggerTypeInfo[]} classes + * @param {JavaLocation} location + */ + async _findMethodAsync(classes, location) { // some locations are null (which causes the jdwp command to fail) - if (/^0+$/.test(location.cid)) return $.Deferred().resolveWith(this, [null]); - var m = this._findmethod(classes, location.cid, location.mid); - if (m) return $.Deferred().resolveWith(this, [m]); - // convert the classid to a type signature - return this.session.adbclient.jdwp_command({ - ths: this, - extra: { location: location }, - cmd: this.JDWP.Commands.signature(location.cid), - }) - .then(function (type, x) { - return this.gettypedebuginfo(type.signature, x); - }) - .then(function (classes, x) { - var defs = [{ dbgr: this, classes: classes, x: x }]; - for (var clz in classes) { - defs.push(this._ensuremethods(classes[clz])); - } - return $.when.apply($, defs).then(function (x) { - return $.Deferred().resolveWith(x.dbgr, [x.classes, x.x]); - }) - }) - .then(function (classes, x) { - var m = this._findmethod(classes, x.location.cid, x.location.mid); - return $.Deferred().resolveWith(this, [m]); - }); - }, - - _findmethod: function (classes, classid, methodid) { - for (var i in classes) { - if (classes[i]._isdeferred) - continue; - if (classes[i].info.typeid !== classid) - continue; - for (var j in classes[i].methods) { - if (classes[i].methods[j].methodid !== methodid) - continue; - return classes[i].methods[j]; - } + if (/^0+$/.test(location.cid)) { + return null; } - return null; - }, + const m = this.findMethod(classes, location.cid, location.mid); + if (m) { + return m; + } + return this._findMethodFromLocation(location); + } - _finitbreakpoints: function () { - this._changebpstate(this.breakpoints.all, 'set'); - this.breakpoints.enabled = {}; - }, + /** + * Sends a JDWP command to retrieve the method at the given location + * @param {JavaLocation} location + */ + async _findMethodFromLocation(location) { + // convert the location classid to a type signature + /** @type {JavaType} */ + const type = await this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.signature(location.cid), + }); + // retrieve the type info with methods + const typeinfo = await this.getTypeInfo(type.signature); + await this._ensureMethods(typeinfo); + // search for the method matching the method ID + return this.findMethod([typeinfo], location.cid, location.mid); + } -}; + /** + * Search the list of classes for a particular method in a class + * @param {DebuggerTypeInfo[]} classes + * @param {JavaClassID} classid + * @param {JavaMethodID} methodid + */ + findMethod(classes, classid, methodid) { + const clz = classes.find(c => c.info.typeid === classid); + const method = clz && clz.methods.find(m => m.methodid === methodid); + return method || null; + } -exports.Debugger = Debugger; + /** + * Retrieve a list of class signatures loaded by the runtime. + * (note that this method is slow - there are usually thousands of classes in the list) + */ + getAllClasses() { + return this.session.adbclient.jdwp_command({ + cmd: JDWP.Commands.AllClassesWithGeneric(), + }); + + } +} + +module.exports = { + Debugger, +} diff --git a/src/expression/assign.js b/src/expression/assign.js new file mode 100644 index 0000000..c004adb --- /dev/null +++ b/src/expression/assign.js @@ -0,0 +1,109 @@ +const { Debugger } = require('../debugger'); +const { DebuggerValue, JavaTaggedValue, JavaType } = require('../debugger-types'); +const { NumberBaseConverter } = require('../utils/nbc'); + +const validmap = { + B: 'BC', // char might not fit into a byte - we special-case this + S: 'BSC', + I: 'BSIC', + J: 'BSIJC', + F: 'BSIJCF', + D: 'BSIJCFD', + C: 'BSC', + Z: 'Z', + isCharInRangeForByte: c => c.charCodeAt(0) < 256, +}; + +/** + * Checks if the value will fit into a variable with given type + * @param {JavaType} variable_type + * @param {DebuggerValue} value + */ +function checkPrimitiveSize(variable_type, value) { + // variable_type_signature must be a primitive + if (!Object.prototype.hasOwnProperty.call(validmap, variable_type.signature)) { + return false; + } + + let value_type_signature = value.type.signature; + if (value.vtype === 'literal' && /[BSI]/.test(value_type_signature)) { + // for integer literals, find the minimum type the value will fit into + if (value.value >= -128 && value.value <= 127) value_type_signature = 'B'; + else if (value.value >= -32768 && value.value <= 32767) value_type_signature = 'S'; + else if (value.value >= -2147483648 && value.value <= 2147483647) value_type_signature = 'I'; + } + + let is_in_range = validmap[variable_type.signature].indexOf(value_type_signature) >= 0; + + // special check to see if a char value fits into a single byte + if (JavaType.isByte(variable_type) && JavaType.isChar(value.type)) { + is_in_range = validmap.isCharInRangeForByte(value.value); + } + + return is_in_range; +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue} destvar + * @param {string} name + * @param {DebuggerValue} result + */ +async function assignVariable(dbgr, destvar, name, result) { + if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) { + throw new Error(`The value is read-only and cannot be updated.`); + } + + // non-string reference types can only set to null + if (JavaType.isReference(destvar.type) && !JavaType.isString(destvar.type)) { + if (!result.hasnullvalue) { + throw new Error('Object references can only be set to null'); + } + } + + // as a nicety, if the destination is a string, stringify any primitive value + if (JavaType.isPrimitive(result.type) && JavaType.isString(destvar.type)) { + result = await dbgr.createJavaStringLiteral(result.value.toString(), { israw:true }); + } + + if (JavaType.isPrimitive(destvar.type)) { + // if the destination is a primitive, we need to range-check it here + // Neither our debugger nor the JDWP endpoint validates primitives, so we end up with + // weirdness if we allow primitives to be set with out-of-range values + const is_in_range = checkPrimitiveSize(destvar.type, result); + if (!is_in_range) { + throw new Error(`'${result.value}' is not compatible with variable type: ${destvar.type.typename}`); + } + } + + const data = JavaTaggedValue.from(result, destvar.type.signature); + + if (JavaType.isLong(destvar.type) && typeof data.value === 'number') { + // convert ints to hex-string longs + data.value = NumberBaseConverter.decToHex(data.value.toString(),16); + } + + // convert the debugger value to a JavaTaggedValue + let newlocalvar; + // setxxxvalue sets the new value and then returns a new local for the variable + switch(destvar.vtype) { + case 'field': + newlocalvar = await dbgr.setFieldValue(destvar.data.objvar, destvar.data.field, data); + break; + case 'local': + newlocalvar = await dbgr.setLocalVariableValue(destvar.data.frame, destvar.data.slotinfo, data); + break; + case 'arrelem': + newlocalvar = await dbgr.setArrayElements(destvar.data.array, parseInt(name, 10), 1, data); + newlocalvar = newlocalvar[0]; + break; + default: + throw new Error('Unsupported variable type'); + } + + return newlocalvar; +} + +module.exports = { + assignVariable, +} diff --git a/src/expression/evaluate.js b/src/expression/evaluate.js new file mode 100644 index 0000000..f47d52f --- /dev/null +++ b/src/expression/evaluate.js @@ -0,0 +1,983 @@ +const Long = require('long'); + +const { + ArrayIndexExpression, + BinaryOpExpression, + ExpressionText, + MemberExpression, + MethodCallExpression, + parse_expression, + ParsedExpression, + QualifierExpression, + RootExpression, + TypeCastExpression, + UnaryOpExpression, +} = require('./parse'); +const { DebuggerValue, JavaTaggedValue, JavaType, LiteralValue } = require('../debugger-types'); +const { Debugger } = require('../debugger'); +const { AndroidThread } = require('../threads'); +const { D } = require('../utils/print'); +const { decodeJavaCharLiteral } = require('../utils/char-decode'); + +/** + * @param {Long.Long} long + */ +function hex_long(long) { + return long.toUnsigned().toString(16).padStart(64/4, '0'); +} + +/** + * Determine what type of primitive a decimal value will require + * @param {string} decimal_value + * @returns {'int'|'long'|'float'|'double'} + */ +function get_decimal_number_type(decimal_value) { + if (/^-?0*\d{0,15}(\.0*)?$/.test(decimal_value)) { + const n = parseInt(decimal_value, 10); + if (n >= -2147483648 && n <= 2147483647) { + return 'int'; + } + return 'long'; + } + // int64: 9223,372036854775807 + let m = decimal_value.match(/^(-?)0*(\d*?)(\d{1,4})(\d{15})(\.0+)?$/); + if (m) { + const sign = m[1]; + if (!m[2]) { + const x = [parseInt(m[3],10), parseInt(m[4],10)]; + if (x[0] < 9223) { + return 'long'; + } + if (x[0] > 9223) { + return 'float'; + } + let limit = 372036854775807 + (sign ? 1 : 0); + if (x[1] <= limit) { + return 'long'; + } + return 'float' + } + // single precision floats allow integers up to +/- 2^127: + // 34028,236692093846346,3374,607431768211455 + // but rounded to a power of 2 (not checked here) + let q = m[2].match(/^(\d*?)(\d{0,5}?)(\d{1,15})$/); + if (q[1]) { + return 'double'; + } + const x = [parseInt(q[2],10), parseInt(q[3],10), parseInt(m[3],10), parseInt(m[4],10)] + if (x[0] > 34028) { + return 'double'; + } + if (x[0] < 34028) { + return 'float'; + } + if (x[1] > 236692093846346) { + return 'double'; + } + if (x[1] < 236692093846346) { + return 'float'; + } + if (x[2] > 3374) { + return 'double'; + } + if (x[2] < 3374) { + return 'float'; + } + let limit = 607431768211455 + (sign ? 1 : 0); + if (x[3] <= limit) { + return 'float'; + } + return 'double'; + } + + if (/^-?\d{0,38}\./.test(decimal_value)) + return 'float'; + return 'double' +} + +/** + * Convert an exponent-formatted number into a normalised decimal equivilent. + * e.g '1.2345e3' -> '1234.5' + * + * If the number does not include an exponent, it is returned unchanged. + * @param {string} n + */ +function decimalise_exponent_number(n) { + const exp = n.match(/^(\D*)0*(\d+)(?:\.(\d+?)0*)?[eE]([+-]?)0*(\d+)(.*)/); + if (!exp) { + return n; + } + let i = exp[2], frac = (exp[3]||''), sign = exp[4]||'+', pow10 = parseInt(exp[5],10); + if (pow10 > 0) { + if (sign === '+') { + let shifted_digits = Math.min(frac.length, pow10); + i += frac.slice(0, shifted_digits); + frac = frac.slice(shifted_digits); + pow10 -= shifted_digits; + i += '0'.repeat(pow10); + } else { + let shifted_digits = Math.min(i.length, pow10); + frac = i.slice(-shifted_digits) + frac; // move up to pow10 digits from i to frac + i = i.slice(0, -shifted_digits); + pow10 -= shifted_digits; + frac = '0'.repeat(pow10) + frac; + } + } + i = (i || '0').match(/^0*(.+)/)[1]; + if (/[1-9]/.test(frac)) i += `.${frac}`; + return `${exp[1]}${i}${exp[6]}` +} + +/** + * @param {number|string} number + */ +function evaluate_number(number) { + let n = number.toString(); + + // normalise exponents into decimal form + n = decimalise_exponent_number(n); + + let number_type, base = 10; + const m = n.match(/^([+-]?)0([bBxX0-7])(.+)/); + if (m) { + switch (m[2]) { + case 'b': base = 2; n = m[1] + m[3]; break; + case 'x': base = 16; n = m[1] + m[3]; break; + default: base = 8; break; + } + } + + if (base !== 16 && /[fFdD]$/.test(n)) { + number_type = /[fF]$/.test(n) ? 'float' : 'double'; + n = n.slice(0, -1); + } else if (/[lL]$/.test(n)) { + number_type = 'long' + n = n.slice(0, -1); + } else { + number_type = get_decimal_number_type(n); + } + + let result; + if (number_type === 'long') { + result = hex_long(Long.fromString(n, false, base)); + } else if (/^[fd]/.test(number_type)) { + result = (base === 10) ? parseFloat(n) : parseInt(n, base); + } else { + result = parseInt(n, base) | 0; + } + + const iszero = /^[+-]?0+(\.0*)?$/.test(result.toString()); + + return new LiteralValue(JavaType[number_type], result, iszero); +} + +/** + * @param {string} char + */ +function evaluate_char(char) { + // JDWP returns char values as uint16's, so we need to set the value as a number + return new LiteralValue(JavaType.char, char.charCodeAt(0)); +} + +/** + * Convert a value to a number + * @param {DebuggerValue} local + */ +function numberify(local) { + if (JavaType.isFloat(local.type)) { + return parseFloat(local.value); + } + const radix = JavaType.isLong(local.type) ? 16 : 10; + return parseInt(local.value, radix); +} + +/** + * Convert a value to a string + * @param {Debugger} dbgr + * @param {DebuggerValue} local + */ +async function stringify(dbgr, local) { + let s = ''; + switch(true) { + case JavaType.isString(local.type): + s = local.string; + break; + case JavaType.isPrimitive(local.type): + s = local.value.toString(); + break; + case local.hasnullvalue: + s = '(null)'; + break; + case JavaType.isReference(local.type): + // call toString() on the object + const str_literal = await dbgr.invokeToString(local.value, local.data.frame.threadid, local.type.signature); + s = str_literal.string; + break; + } + return s; +} + +/** + * @param {string} operator + * @param {boolean} [is_unary] + */ +function invalid_operator(operator, is_unary = false) { + return new Error(`Invalid ${is_unary ? 'type' : 'types'} for operator '${operator}'`); +} + +/** + * + */ +function divide_by_zero() { + return new Error('ArithmeticException: divide by zero'); +} + +/** + * + * @param {*} lhs_local + * @param {*} rhs_local + * @param {string} operator + */ +function evaluate_binary_boolean_expression(lhs_local, rhs_local, operator) { + let a = lhs_local.value, b = rhs_local.value; + switch (operator) { + case '&': case '&&': a = a && b; break; + case '|': case '||': a = a || b; break; + case '^': a = !!(a ^ b); break; + case '==': a = a === b; break; + case '!=': a = a !== b; break; + default: throw invalid_operator(operator); + } + return new LiteralValue(JavaType.boolean, a); +} + +/** + * + * @param {*} lhs_local + * @param {*} rhs_local + * @param {string} operator + */ +function evaluate_binary_float_expression(lhs_local, rhs_local, operator) { + /** @type {number|boolean} */ + let a = numberify(lhs_local), b = numberify(rhs_local); + switch (operator) { + case '+': a += b; break; + case '-': a -= b; break; + case '*': a *= b; break; + case '/': a /= b; break; + case '==': a = a === b; break; + case '!=': a = a !== b; break; + case '<': a = a < b; break; + case '<=': a = a <= b; break; + case '>': a = a > b; break; + case '>=': a = a >= b; break; + default: throw invalid_operator(operator); + } + /** @type {number|boolean|string} */ + let value = a, result_type = 'boolean' + if (typeof a !== 'boolean') { + result_type = (lhs_local.type.signature === 'D' || rhs_local.type.signature === 'D') ? 'double' : 'float'; + } + return new LiteralValue(JavaType[result_type], value); +} + + +/** + * + * @param {DebuggerValue} lhs + * @param {DebuggerValue} rhs + * @param {string} operator + */ +function evaluate_binary_int_expression(lhs, rhs, operator) { + /** @type {number|boolean} */ + let a = numberify(lhs), b = numberify(rhs); + // dividend cannot be zero for / and % + if (/[\/%]/.test(operator) && b === 0) { + throw divide_by_zero(); + } + switch (operator) { + case '+': a += b; break; + case '-': a -= b; break; + case '*': a *= b; break; + case '/': a = Math.trunc(a / b); break; + case '%': a %= b; break; + case '<<': a <<= b; break; + case '>>': a >>= b; break; + case '>>>': a >>>= b; break; + case '&': a &= b; break; + case '|': a |= b; break; + case '^': a ^= b; break; + case '==': a = a === b; break; + case '!=': a = a !== b; break; + case '<': a = a < b; break; + case '<=': a = a <= b; break; + case '>': a = a > b; break; + case '>=': a = a >= b; break; + default: throw invalid_operator(operator); + } + /** @type {number|boolean|string} */ + let value = a, result_type = 'boolean' + if (typeof a !== 'boolean') { + result_type = 'int'; + } + return new LiteralValue(JavaType[result_type], value); +} + +/** + * @param {DebuggerValue} lhs + * @param {DebuggerValue} rhs + * @param {string} operator + */ +function evaluate_binary_long_expression(lhs, rhs, operator) { + function longify(local) { + const radix = JavaType.isLong(local.type) ? 16 : 10; + return Long.fromString(`${local.value}`, false, radix); + } + + /** @type {Long.Long|boolean} */ + let a = longify(lhs), b = longify(rhs); + + // dividend cannot be zero for / and % + if (/[\/%]/.test(operator) && b.isZero()) { + throw divide_by_zero(); + } + + switch (operator) { + case '+': a = a.add(b); break; + case '-': a = a.subtract(b); break; + case '*': a = a.multiply(b); break; + case '/': a = a.divide(b); break; + case '%': a = a.mod(b); break; + case '<<': a = a.shl(b); break; + case '>>': a = a.shr(b); break; + case '>>>': a = a.shru(b); break; + case '&': a = a.and(b); break; + case '|': a = a.or(b); break; + case '^': a = a.xor(b); break; + case '==': a = a.eq(b); break; + case '!=': a = !a.eq(b); break; + case '<': a = a.lt(b); break; + case '<=': a = a.lte(b); break; + case '>': a = a.gt(b); break; + case '>=': a = a.gte(b); break; + default: throw invalid_operator(operator); + } + /** @type {boolean|Long.Long|string} */ + let value = a, result_type = 'boolean'; + if (typeof a !== 'boolean') { + value = hex_long(a); + result_type = 'long'; + } + return new LiteralValue(JavaType[result_type], value); +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {ParsedExpression} lhs + * @param {ParsedExpression} rhs + */ +async function evaluate_assignment_expression(dbgr, locals, thread, lhs, rhs) { + if (!(lhs instanceof RootExpression)) { + throw new Error('Cannot assign value: left-hand-side is not a variable'); + } + // if there are any qualifiers, the last qualifier must not be a method call + const qualified_terms = lhs.qualified_terms.slice(); + const last_qualifier = qualified_terms.pop(); + if ((lhs.root_term_type !== 'ident') || (last_qualifier instanceof MethodCallExpression)) { + throw new Error('Cannot assign value: left-hand-side is not a variable'); + } + + let lhs_value = locals.find(local => local.name === lhs.root_term); + if (!lhs_value) { + throw new Error(`Cannot assign value: variable '${lhs.root_term}' not found`); + } + // evaluate the qualified terms, until the last qualifier + lhs_value = await evaluate_qualifiers(dbgr, locals, thread, lhs_value, qualified_terms); + + // evaluate the rhs + const value = await evaluate_expression(dbgr, locals, thread, rhs); + + // assign the value + if (last_qualifier instanceof ArrayIndexExpression) { + const array_index = await evaluate_expression(dbgr, locals, thread, last_qualifier); + await dbgr.setArrayElements(lhs_value, numberify(array_index), 1, JavaTaggedValue.from(value)); + } + else if (last_qualifier instanceof MemberExpression) { + const field = (await dbgr.findNamedFields(lhs_value.type.signature, last_qualifier.name, true))[0] + await dbgr.setFieldValue(lhs_value, field, JavaTaggedValue.from(value)); + } else { + //await dbgr.setLocalVariableValue(lhs_value, JavaTaggedValue.from(value)); + } + + return value; +} + +/** + * + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {ParsedExpression} lhs + * @param {ParsedExpression} rhs + * @param {string} operator + */ +async function evaluate_binary_expression(dbgr, locals, thread, lhs, rhs, operator) { + + if (operator === '=') { + return evaluate_assignment_expression(dbgr, locals, thread, lhs, rhs); + } + + const [lhs_value, rhs_value] = await Promise.all([ + evaluate_expression(dbgr, locals, thread, lhs), + evaluate_expression(dbgr, locals, thread, rhs) + ]); + + const types_key = `${lhs_value.type.signature}#${rhs_value.type.signature}` + + if (/[BCIJS]#[BCIJS]/.test(types_key) && /J/.test(types_key)) { + // both expressions are integers - one is a long + return evaluate_binary_long_expression(lhs_value, rhs_value, operator); + } + + if (/[BCIS]#[BCIS]/.test(types_key)) { + // both expressions are (non-long) integer types + return evaluate_binary_int_expression(lhs_value, rhs_value, operator); + } + + if (/[BCIJSFD]#[BCIJSFD]/.test(types_key)) { + // both expressions are number types - one is a float or double + return evaluate_binary_float_expression(lhs_value, rhs_value, operator); + } + + if (/Z#Z/.test(types_key)) { + // both expressions are boolean types + return evaluate_binary_boolean_expression(lhs_value, rhs_value, operator); + } + + // any + operator with a lhs of type String is coerced into a string append + if (JavaType.isString(lhs_value.type) && operator === '+') { + const rhs_str = await stringify(dbgr, rhs_value); + return dbgr.createJavaStringLiteral(lhs_value.string + rhs_str, { israw: true }); + } + + // anything else is an invalid combination + throw invalid_operator(operator); +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {string} operator + * @param {*} expr + */ +async function evaluate_unary_expression(dbgr, locals, thread, operator, expr) { + /** @type {DebuggerValue} */ + let local = await evaluate_expression(dbgr, locals, thread, expr); + const key = `${operator}${local.type.signature}`; + switch(true) { + case /!Z/.test(key): + return new LiteralValue(JavaType.boolean, !local.value); + case /~C/.test(key): + return evaluate_number(~local.value.charCodeAt(0)); + case /~[BIS]/.test(key): + return evaluate_number(~local.value); + case /~J/.test(key): + return new LiteralValue(JavaType.long, hex_long(Long.fromString(local.value, false, 16).not())); + case /-C/.test(key): + return evaluate_number(-local.value.charCodeAt(0)); + case /-[BCIS]/.test(key): + return evaluate_number(-local.value); + case /-J/.test(key): + return new LiteralValue(JavaType.long, hex_long(Long.fromString(local.value, false, 16).neg())); + case /\+[BCIJS]/.test(key): + return local; + default: + throw invalid_operator(operator, true); + } +} + +/** + * + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {string} identifier + * @returns {Promise} + */ +async function evaluate_identifier(dbgr, locals, identifier) { + const local = locals.find(l => l.name === identifier); + if (local) { + return local; + } + // if it's not a local, it could be the start of a package name or a type + const classes = await dbgr.getAllClasses(); + return evaluate_qualified_type_name(dbgr, identifier, classes); +} + +/** + * + * @param {Debugger} dbgr + * @param {string} dotted_name + * @param {*[]} classes + */ +async function evaluate_qualified_type_name(dbgr, dotted_name, classes) { + const exact_class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace(/\./g,'[$/]')};$`); + const exact_class = classes.find(c => exact_class_matcher.test(c.type.signature)); + if (exact_class) { + return dbgr.getTypeValue(exact_class.type.signature); + } + + const class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace('.','[$/]')}/`); + const matching_classes = classes.filter(c => class_matcher.test(c.type.signature)); + if (matching_classes.length === 0) { + // the dotted name doesn't match any packages + throw new Error(`'${dotted_name}' is not a package, type or variable name`); + } + return new DebuggerValue('package', null, dotted_name, true, false, 'package', {matching_classes}); +} + +/** + * + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {RootExpression} expr + * @returns {Promise} + */ +async function evaluate_root_term(dbgr, locals, expr) { + switch (expr.root_term_type) { + case 'boolean': + return new LiteralValue(JavaType.boolean, expr.root_term === 'true'); + case 'null': + return LiteralValue.Null; + case 'ident': + return evaluate_identifier(dbgr, locals, expr.root_term); + case 'hexint': + case 'octint': + case 'decint': + case 'decfloat': + return evaluate_number(expr.root_term); + case 'char': + case 'echar': + case 'uchar': + return evaluate_char(decodeJavaCharLiteral(expr.root_term)) + case 'string': + // we must get the runtime to create string instances + return await dbgr.createJavaStringLiteral(expr.root_term); + default: + return null; + } +} + +/** + * + * @param {Debugger} dbgr + * @param {DebuggerValue} value + * @param {QualifierExpression[]} qualified_terms + * @returns {Promise<[number, DebuggerValue]>} + */ +async function evaluate_package_qualifiers(dbgr, value, qualified_terms) { + let i = 0; + for (;;) { + // while the value is a package identifier... + if (value.vtype !== 'package') { + break; + } + // ... and the next term is a member expression... + const term = qualified_terms[i]; + if (term instanceof MemberExpression) { + // search for a valid type + value = await evaluate_qualified_type_name(dbgr, `${value.value}.${term.name}`, value.data.matching_classes); + i++; + continue; + } + break; + } + if (value.vtype === 'package') { + throw new Error('not available'); + } + + // return the number of qualified terms we used and the resulting value + return [i, value]; +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {DebuggerValue} value + * @param {QualifierExpression[]} qualified_terms + */ +async function evaluate_qualifiers(dbgr, locals, thread, value, qualified_terms) { + let pkg_members; + [pkg_members, value] = await evaluate_package_qualifiers(dbgr, value, qualified_terms); + + for (let i = pkg_members; i < qualified_terms.length; i++) { + const term = qualified_terms[i]; + if (term instanceof MemberExpression) { + // if this term is a member name, check if it's really a method call + const next_term = qualified_terms[i + 1]; + if (next_term instanceof MethodCallExpression) { + value = await evaluate_methodcall(dbgr, locals, thread, term.name, next_term, value); + i++; + continue; + } + value = await evaluate_member(dbgr, locals, thread, term, value); + continue; + } + if (term instanceof ArrayIndexExpression) { + value = await evaluate_array_element(dbgr, locals, thread, term.indexExpression, value); + continue; + } + throw new Error('not available'); + } + + return value; +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {RootExpression} expr + */ +async function evaluate_root_expression(dbgr, locals, thread, expr) { + let value = await evaluate_root_term(dbgr, locals, expr); + if (!value || !value.valid) { + throw new Error('not available'); + } + + // we've evaluated the root term variable - work out the rest + value = await evaluate_qualifiers(dbgr, locals, thread, value, expr.qualified_terms); + + return value; +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {ParsedExpression} expr + * @returns {Promise} + */ +function evaluate_expression(dbgr, locals, thread, expr) { + + if (expr instanceof RootExpression) { + return evaluate_root_expression(dbgr, locals, thread, expr); + } + if (expr instanceof BinaryOpExpression) { + return evaluate_binary_expression(dbgr, locals, thread, expr.lhs, expr.rhs, expr.operator); + } + if (expr instanceof UnaryOpExpression) { + return evaluate_unary_expression(dbgr, locals, thread, expr.operator, expr.rhs); + } + if (expr instanceof TypeCastExpression) { + return evaluate_cast(dbgr, locals, thread, expr.cast_type, expr.rhs); + } + throw new Error('not available'); +} + + +/** + * + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {string} index_expr + * @param {DebuggerValue} arr_local + */ +async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_local) { + if (arr_local.type.signature[0] !== '[') { + throw new Error(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`); + } + if (arr_local.hasnullvalue) { + throw new Error('NullPointerException'); + } + + const idx_local = await evaluate_expression(dbgr, locals, thread, index_expr); + if (!JavaType.isArrayIndex(idx_local.type)) { + throw new Error('TypeError: array index is not an integer value'); + } + + const idx = numberify(idx_local); + if (idx < 0 || idx >= arr_local.arraylen) { + throw new Error(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`); + } + + const element_values = await dbgr.getArrayElementValues(arr_local, idx, 1); + return element_values[0]; +} + +/** + * Build a regular expression which matches the possible parameter types for a value + * @param {Debugger} dbgr + * @param {DebuggerValue} v + */ +async function getParameterSignatureRegex(dbgr, v) { + if (v.type.signature == 'Lnull;') { + return /^[LT[]/; // null matches any reference type + } + if (/^L/.test(v.type.signature)) { + // for class reference types, retrieve a list of inherited classes + // since subclass instances can be passed as arguments + const sigs = await dbgr.getClassInheritanceList(v.type.signature); + const re_sigs = sigs.map(signature => signature.replace(/[$]/g, '\\$')); + return new RegExp(`(^${re_sigs.join('$)|(^')}$)`); + } + if (/^\[/.test(v.type.signature)) { + // for array types, only an exact array match or Object is allowed + return new RegExp(`^(${v.type.signature})|(${JavaType.Object.signature})$`); + } + switch(v.type.signature) { + case 'I': + // match bytes/shorts/ints/longs/floats/doubles literals within range + if (v.value >= -128 && v.value <= 127) + return /^[BSIJFD]$/ + if (v.value >= -32768 && v.value <= 32767) + return /^[SIJFD]$/ + return /^[IJFD]$/; + case 'F': + return /^[FD]$/; // floats can be assigned to floats or doubles + default: + // anything else must be an exact match (no implicit cast is valid) + return new RegExp(`^${v.type.signature}$`); + } +} + +/** + * @param {Debugger} dbgr + * @param {*} type + * @param {string} method_name + * @param {DebuggerValue[]} args + */ +async function findCompatibleMethod(dbgr, type, method_name, args) { + // find any methods matching the member name with any parameters in the signature + const methods = await dbgr.findNamedMethods(type.signature, method_name, /^/, false); + if (!methods[0]) { + throw new Error(`Error: method '${type.name}.${method_name}' not found`); + } + + // filter the method based upon the types of parameters + const arg_type_matchers = []; + for (let arg of args) { + arg_type_matchers.push(await getParameterSignatureRegex(dbgr, arg)); + } + + // find the first method where the argument types match the parameter types + const matching_method = methods.find(method => { + // extract a list of parameter types from the method signature + const param_type_re = /\[*([BSIJFDCZ]|([LT][^;]+;))/g; + const parameter_types = []; + for (let x; x = param_type_re.exec(method.sig); ) { + parameter_types.push(x[0]); + } + // the last type is always the return value + parameter_types.pop(); + // check if the arguments and parameters match + if (parameter_types.length !== arg_type_matchers.length) { + return false; + } + // are there any argument types that don't match the corresponding parameter type? + if (arg_type_matchers.find((m, idx) => !m.test(parameter_types[idx]))) { + return false; + } + // we found a match + return true; + }); + + if (!matching_method) { + throw new Error(`Error: incompatible parameters for method '${method_name}'`); + } + + return matching_method; +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {string} method_name + * @param {MethodCallExpression} m + * @param {DebuggerValue} obj_local + */ +async function evaluate_methodcall(dbgr, locals, thread, method_name, m, obj_local) { + if (obj_local.hasnullvalue) { + throw new Error('NullPointerException'); + } + + // evaluate any parameters + const param_values = await Promise.all(m.arguments.map(arg => evaluate_expression(dbgr, locals, thread, arg))); + + // find a method in the object type matching the name and argument types + const method = await findCompatibleMethod(dbgr, obj_local.type, method_name, param_values); + + return dbgr.invokeMethod( + obj_local.value, + thread.threadid, + method, + param_values + ); +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {MemberExpression} member + * @param {DebuggerValue} value + */ +async function evaluate_member(dbgr, locals, thread, member, value) { + if (!JavaType.isReference(value.type)) { + throw new Error('TypeError: value is not a reference type'); + } + if (value.hasnullvalue) { + throw new Error('NullPointerException'); + } + if (JavaType.isArray(value.type)) { + // length is a 'fake' field of arrays, so special-case it + if (member.name === 'length') { + return evaluate_number(value.arraylen); + } + } + // we also special-case :super (for object instances) + if (member.name === ':super' && JavaType.isClass(value.type)) { + return dbgr.getSuperInstance(value); + } + + // check if the value is an enclosed type + const enclosed_type = await dbgr.getTypeValue(`${value.type.signature.replace(/;$/,'')}$${member.name};`); + if (enclosed_type.valid) { + return enclosed_type; + } + + // anything else must be a real field + return dbgr.getFieldValue(value, member.name, true) +} + + +/** + * @param {*} type + * @param {*} local + */ +function incompatible_cast(type, local) { + return new Error(`Incompatible cast from ${local.type.typename} to ${type}`); +} + +/** + * @param {Long.Long} value + * @param {8|16|32} bits + */ +function signed_from_long(value, bits) { + return (parseInt(value.toString(16).slice(-bits >> 3),16) << (32-bits)) >> (32-bits); +} + +/** + * @param {string} type + * @param {DebuggerValue} local + */ +function cast_from_long(type, local) { + const value = Long.fromString(local.value, true, 16); + switch (true) { + case (type === 'byte'): + return evaluate_number(signed_from_long(value, 8)); + case (type === 'short'): + return evaluate_number(signed_from_long(value, 16)); + case (type === 'int'): + return evaluate_number(signed_from_long(value, 32)); + case (type === 'char'): + return evaluate_char(String.fromCharCode(signed_from_long(value, 16) & 0xffff)); + case (type === 'float'): + return evaluate_number(value.toSigned().toNumber() + 'F'); + case (type === 'double'): + return evaluate_number(value.toSigned().toNumber() + 'D'); + default: + throw incompatible_cast(type, local); + } +} + +/** + * @param {Debugger} dbgr + * @param {DebuggerValue[]} locals + * @param {AndroidThread} thread + * @param {string} cast_type + * @param {ParsedExpression} rhs + */ +async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) { + let local = await evaluate_expression(dbgr, locals, thread, rhs); + // check if a conversion is unnecessary + if (cast_type === local.type.typename) { + return local; + } + + // boolean cannot be converted from anything else + if (cast_type === 'boolean' || local.type.typename === 'boolean') { + throw incompatible_cast(cast_type, local); + } + + switch (true) { + case local.type.typename === 'long': + // conversion from long to something else + local = cast_from_long(cast_type, local); + break; + case (cast_type === 'byte'): + local = evaluate_number((local.value << 24) >> 24); + break; + case (cast_type === 'short'): + local = evaluate_number((local.value << 16) >> 16); + break; + case (cast_type === 'int'): + local = evaluate_number((local.value | 0)); + break; + case (cast_type === 'long'): + local = evaluate_number(local.value + 'L'); + break; + case (cast_type === 'char'): + local = evaluate_char(String.fromCharCode(local.value | 0)); + break; + case (cast_type === 'float'): + case (cast_type === 'double'): + break; + default: + throw incompatible_cast(cast_type, local); + } + local.type = JavaType[cast_type]; + return local; +} + +/** + * @param {string} expression + * @param {AndroidThread} thread + * @param {DebuggerValue[]} locals + * @param {Debugger} dbgr + */ +async function evaluate(expression, thread, locals, dbgr) { + D('evaluate: ' + expression); + await dbgr.ensureConnected(); + + // the thread must be in the paused state + if (thread && !thread.paused) { + throw new Error('not available'); + } + + // parse the expression + const e = new ExpressionText(expression.trim()) + if (!e.expr) { + return null; + } + const parsed_expression = parse_expression(e); + + // if there's anything left, it's an error + if (!parsed_expression || e.expr) { + // the expression is not well-formed + throw new Error(`Invalid expression: ${expression.trim()}`); + } + + // the expression is well-formed - start the (asynchronous) evaluation + const value = await evaluate_expression(dbgr, locals, thread, parsed_expression); + return value; +} + +module.exports = { + evaluate, +} diff --git a/src/expression/parse.js b/src/expression/parse.js new file mode 100644 index 0000000..0d3648e --- /dev/null +++ b/src/expression/parse.js @@ -0,0 +1,323 @@ +/** + * Operator precedence levels. + * Lower number = higher precedence. + * Operators with equal precedence are evaluated left-to-right. + */ +const operator_precedences = { + '*': 1, '%': 1, '/': 1, + '+': 2, '-': 2, + '<<': 3, '>>': 3, '>>>': 3, + '<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4, + '==': 5, '!=': 5, + '&': 6, '^': 7, '|': 8, + '&&': 9, '||': 10, + '?': 11, + '=': 12, +} + +const lowest_precedence = 13; + +class ExpressionText { + /** + * @param {string} text + */ + constructor(text) { + this.expr = text; + this.precedence_stack = [lowest_precedence]; + } + + get current_precedence() { + return this.precedence_stack[0]; + } +} + +class ParsedExpression { +} + +class RootExpression extends ParsedExpression { + /** + * @param {string} root_term + * @param {string} root_term_type + * @param {QualifierExpression[]} qualified_terms + */ + constructor(root_term, root_term_type, qualified_terms) { + super(); + this.root_term = root_term; + this.root_term_type = root_term_type; + this.qualified_terms = qualified_terms; + } +} + +class TypeCastExpression extends ParsedExpression { + /** + * + * @param {string} cast_type + * @param {ParsedExpression} rhs + */ + constructor(cast_type, rhs) { + super(); + this.cast_type = cast_type; + this.rhs = rhs; + } +} + +class BinaryOpExpression extends ParsedExpression { + /** + * @param {ParsedExpression} lhs + * @param {string} operator + * @param {ParsedExpression} rhs + */ + constructor(lhs, operator, rhs) { + super(); + this.lhs = lhs; + this.operator = operator; + this.rhs = rhs; + } +} + +class UnaryOpExpression extends ParsedExpression { + /** + * @param {string} operator + * @param {ParsedExpression} rhs + */ + constructor(operator, rhs) { + super(); + this.operator = operator; + this.rhs = rhs; + } +} + +class TernaryExpression extends ParsedExpression { + constructor(condition) { + super(); + this.condition = condition; + this.ternary_true = null; + this.ternary_false = null; + } +} + +class QualifierExpression extends ParsedExpression { + +} + +class ArrayIndexExpression extends QualifierExpression { + constructor(e) { + super(); + this.indexExpression = e; + } +} + +class MethodCallExpression extends QualifierExpression { + arguments = []; +} + +class MemberExpression extends QualifierExpression { + constructor(name) { + super(); + this.name = name; + } +} + +/** + * Remove characters from the expression followed by any leading whitespace/comments + * @param {ExpressionText} e + * @param {number|string} length_or_text + */ +function strip(e, length_or_text) { + if (typeof length_or_text === 'string') { + if (!e.expr.startsWith(length_or_text)) { + return false; + } + length_or_text = length_or_text.length; + } + e.expr = e.expr.slice(length_or_text).trimLeft(); + for (;;) { + const comment = e.expr.match(/(^\/\/.+)|(^\/\*[\d\D]*?\*\/)/); + if (!comment) break; + e.expr = e.expr.slice(comment[0].length).trimLeft(); + } + return true; +} + +/** + * @param {ExpressionText} e + * @returns {(MemberExpression|ArrayIndexExpression|MethodCallExpression)[]} + */ +function parse_qualified_terms(e) { + const res = []; + while (/^[([.]/.test(e.expr)) { + if (strip(e, '.')) { + // member access + const name_match = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg + if (!name_match) { + return null; + } + const member = new MemberExpression(name_match[0]); + strip(e, member.name.length) + res.push(member); + } + else if (strip(e, '(')) { + // method call + const call = new MethodCallExpression(); + if (!strip(e, ')')) { + for (let arg; ;) { + if ((arg = parse_expression(e)) === null) { + return null; + } + call.arguments.push(arg); + if (strip(e, ',')) continue; + if (strip(e, ')')) break; + return null; + } + } + res.push(call); + } + else if (strip(e, '[')) { + // array index + const index_expr = parse_expression(e); + if (index_expr === null) { + return null; + } + if (!strip(e, ']')) { + return null; + } + res.push(new ArrayIndexExpression(index_expr)); + } + } + return res; +} + +/** + * @param {ExpressionText} e + */ +function parseBracketOrCastExpression(e) { + if (!strip(e, '(')) { + return null; + } + let res = parse_expression(e); + if (!res) { + return null; + } + if (!strip(e, ')')) { + return null; + } + if (res instanceof RootExpression) { + if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.qualified_terms.length) { + // primitive typecast + const castexpr = parse_expression_term(e); + if (!castexpr) { + return null; + } + res = new TypeCastExpression(res.root_term, castexpr); + } + } + return res; +} + +/** + * + * @param {ExpressionText} e + * @param {string} unop + */ +function parseUnaryExpression(e, unop) { + strip(e, unop.length); + let res = parse_expression_term(e); + if (!res) { + return null; + } + const op = unop.replace(/\s+/g, ''); + for (let i = op.length - 1; i >= 0; --i) { + res = new UnaryOpExpression(op[i], res); + } + return res; +} + +/** + * @param {ExpressionText} e + */ +function parse_expression_term(e) { + if (e.expr[0] === '(') { + return parseBracketOrCastExpression(new ExpressionText(e.expr)); + } + const unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/); + if (unop) { + return parseUnaryExpression(e, unop[0]); + } + const root_term_types = ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string']; + const root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/); + if (!root_term) { + return null; + } + strip(e, root_term[0].length); + const root_term_type = root_term_types[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1]; + const qualified_terms = parse_qualified_terms(e); + if (qualified_terms === null) { + return null; + } + // the root term is not allowed to be a method call + if (qualified_terms[0] instanceof MethodCallExpression) { + return null; + } + return new RootExpression(root_term[0], root_term_type, qualified_terms); +} + +/** + * @param {string} s + */ +function getBinaryOperator(s) { + const binary_op_match = s.match(/^([/%*&|^+-]=|<<=|>>>?=|[>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/); + return binary_op_match ? binary_op_match[0] : null; +} + +/** + * @param {ExpressionText} e + * @returns {ParsedExpression} + */ +function parse_expression(e) { + let res = parse_expression_term(e); + + for (; ;) { + const binary_operator = getBinaryOperator(e.expr); + if (!binary_operator) { + break; + } + const prec_diff = operator_precedences[binary_operator] - e.current_precedence; + if (prec_diff > 0) { + // bigger number -> lower precendence -> end of (sub)expression + break; + } + if (prec_diff === 0 && binary_operator !== '?') { + // equal precedence, ltr evaluation + break; + } + // higher or equal precendence + e.precedence_stack.unshift(e.current_precedence + prec_diff); + strip(e, binary_operator.length); + if (binary_operator === '?') { + res = new TernaryExpression(res); + res.ternary_true = parse_expression(e); + if (!strip(e, ':')) { + return null; + } + res.ternary_false = parse_expression(e); + } else { + res = new BinaryOpExpression(res, binary_operator, parse_expression(e)); + } + e.precedence_stack.shift(); + } + return res; +} + +module.exports = { + ArrayIndexExpression, + BinaryOpExpression, + ExpressionText, + MemberExpression, + MethodCallExpression, + parse_expression, + ParsedExpression, + QualifierExpression, + RootExpression, + TypeCastExpression, + UnaryOpExpression, +} diff --git a/src/expressions.js b/src/expressions.js deleted file mode 100644 index dc23b48..0000000 --- a/src/expressions.js +++ /dev/null @@ -1,535 +0,0 @@ -'use strict' -const Long = require('long'); -const $ = require('./jq-promise'); -const { D } = require('./util'); -const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./globals'); - -/* - Asynchronously evaluate an expression -*/ -exports.evaluate = function(expression, thread, locals, vars, dbgr) { - D('evaluate: ' + expression); - - const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]); - const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]); - - if (thread && !thread.paused) - return reject_evaluation('not available'); - - // special case for evaluating exception messages - // - this is called if the user tries to evaluate ':msg' from the locals - if (expression === exmsg_var_name) { - if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) { - var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name); - if (msglocal) { - return resolve_evaluation(vars._local_to_variable(msglocal).value); - } - } - return reject_evaluation('not available'); - } - - const parse_array_or_fncall = function (e) { - var arg, res = { arr: [], call: null }; - // pre-call array indexes - while (e.expr[0] === '[') { - e.expr = e.expr.slice(1).trim(); - if ((arg = parse_expression(e)) === null) return null; - res.arr.push(arg); - if (e.expr[0] !== ']') return null; - e.expr = e.expr.slice(1).trim(); - } - if (res.arr.length) return res; - // method call - if (e.expr[0] === '(') { - res.call = []; e.expr = e.expr.slice(1).trim(); - if (e.expr[0] !== ')') { - for (; ;) { - if ((arg = parse_expression(e)) === null) return null; - res.call.push(arg); - if (e.expr[0] === ')') break; - if (e.expr[0] !== ',') return null; - e.expr = e.expr.slice(1).trim(); - } - } - e.expr = e.expr.slice(1).trim(); - // post-call array indexes - while (e.expr[0] === '[') { - e.expr = e.expr.slice(1).trim(); - if ((arg = parse_expression(e)) === null) return null; - res.arr.push(arg); - if (e.expr[0] !== ']') return null; - e.expr = e.expr.slice(1).trim(); - } - } - return res; - } - const parse_expression_term = function (e) { - if (e.expr[0] === '(') { - e.expr = e.expr.slice(1).trim(); - var subexpr = { expr: e.expr }; - var res = parse_expression(subexpr); - if (res) { - if (subexpr.expr[0] !== ')') return null; - e.expr = subexpr.expr.slice(1).trim(); - if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) { - // primitive typecast - var castexpr = parse_expression_term(e); - if (castexpr) castexpr.typecast = res.root_term; - res = castexpr; - } - } - return res; - } - var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/); - if (unop) { - var op = unop[0].replace(/\s/g, ''); - e.expr = e.expr.slice(unop[0].length).trim(); - var res = parse_expression_term(e); - if (res) { - for (var i = op.length - 1; i >= 0; --i) - res = { operator: op[i], rhs: res }; - } - return res; - } - var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/); - if (!root_term) return null; - var res = { - root_term: root_term[0], - root_term_type: ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'][[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1], - array_or_fncall: null, - members: [], - typecast: '' - } - e.expr = e.expr.slice(res.root_term.length).trim(); - if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null; - // the root term is not allowed to be a method call - if (res.array_or_fncall.call) return null; - while (e.expr[0] === '.') { - // member expression - e.expr = e.expr.slice(1).trim(); - var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg - if (!member_name) return null; - res.members.push(m = { member: member_name[0], array_or_fncall: null }) - e.expr = e.expr.slice(m.member.length).trim(); - if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null; - } - return res; - } - const prec = { - '*': 1, '%': 1, '/': 1, - '+': 2, '-': 2, - '<<': 3, '>>': 3, '>>>': 3, - '<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4, - '==': 5, '!=': 5, - '&': 6, '^': 7, '|': 8, '&&': 9, '||': 10, '?': 11, - } - const parse_expression = function (e) { - var res = parse_expression_term(e); - - if (!e.currprec) e.currprec = [12]; - for (; ;) { - var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/); - if (!binary_operator) break; - var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0]; - if (precdiff > 0) { - // bigger number -> lower precendence -> end of (sub)expression - break; - } - if (precdiff === 0 && binary_operator[0] !== '?') { - // equal precedence, ltr evaluation - break; - } - // higher or equal precendence - e.currprec.unshift(e.currprec[0] + precdiff); - e.expr = e.expr.slice(binary_operator[0].length).trim(); - // current or higher precendence - if (binary_operator[0] === '?') { - res = { condition: res, operator: binary_operator[0], ternary_true: null, ternary_false: null }; - res.ternary_true = parse_expression(e); - if (e.expr[0] === ':') { - e.expr = e.expr.slice(1).trim(); - res.ternary_false = parse_expression(e); - } - } else { - res = { lhs: res, operator: binary_operator[0], rhs: parse_expression(e) }; - } - e.currprec.shift(); - } - return res; - } - const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16); - const evaluate_number = (n) => { - n += ''; - var numtype, m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base = 10; - if (m) { - switch (m[2]) { - case 'b': base = 2; n = m[1] + m[3]; break; - case 'x': base = 16; n = m[1] + m[3]; break; - default: base = 8; break; - } - } - if (base !== 16 && /[fFdD]$/.test(n)) { - numtype = /[fF]$/.test(n) ? 'float' : 'double'; - n = n.slice(0, -1); - } else if (/[lL]$/.test(n)) { - numtype = 'long' - n = n.slice(0, -1); - } else { - numtype = /\./.test(n) ? 'double' : 'int'; - } - if (numtype === 'long') n = hex_long(Long.fromString(n, false, base)); - else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base); - else n = parseInt(n, base) | 0; - - const iszero = /^[+-]?0+(\.0*)?$/.test(n); - return { vtype: 'literal', name: '', hasnullvalue: iszero, type: JTYPES[numtype], value: n, valid: true }; - } - const evaluate_char = (char) => { - return { vtype: 'literal', name: '', char: char, hasnullvalue: false, type: JTYPES.char, value: char.charCodeAt(0), valid: true }; - } - const numberify = (local) => { - //if (local.type.signature==='C') return local.char.charCodeAt(0); - if (/^[FD]$/.test(local.type.signature)) - return parseFloat(local.value); - if (local.type.signature === 'J') - return parseInt(local.value, 16); - return parseInt(local.value, 10); - } - const stringify = (local) => { - var s; - if (JTYPES.isString(local.type)) s = local.string; - else if (JTYPES.isChar(local.type)) s = local.char; - else if (JTYPES.isPrimitive(local.type)) s = '' + local.value; - else if (local.hasnullvalue) s = '(null)'; - if (typeof s === 'string') - return $.Deferred().resolveWith(this, [s]); - return dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature) - .then(s => s.string); - } - const evaluate_expression = (expr) => { - var q = $.Deferred(), local; - if (expr.operator) { - const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary ? 'type' : 'types'} for operator '${expr.operator}'`), - divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero'); - var lhs_local; - return !expr.lhs - ? // unary operator - evaluate_expression(expr.rhs) - .then(rhs_local => { - if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) { - rhs_local.value = !rhs_local.value; - return rhs_local; - } - else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) { - switch (rhs_local.type.typename) { - case 'long': rhs_local.value = rhs_local.value.replace(/./g, c => (15 - parseInt(c, 16)).toString(16)); break; - default: rhs_local = evaluate_number('' + ~rhs_local.value); break; - } - return rhs_local; - } - else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) { - if (expr.operator === '+') return rhs_local; - switch (rhs_local.type.typename) { - case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value, false, 16).neg()); break; - default: rhs_local = evaluate_number('' + (-rhs_local.value)); break; - } - return rhs_local; - } - return invalid_operator('unary'); - }) - : // binary operator - evaluate_expression(expr.lhs) - .then(x => (lhs_local = x) && evaluate_expression(expr.rhs)) - .then(rhs_local => { - if ((lhs_local.type.signature === 'J' && JTYPES.isInteger(rhs_local.type)) - || (rhs_local.type.signature === 'J' && JTYPES.isInteger(lhs_local.type))) { - // one operand is a long, the other is an integer -> the result is a long - var a, b, lbase, rbase; - lbase = lhs_local.type.signature === 'J' ? 16 : 10; - rbase = rhs_local.type.signature === 'J' ? 16 : 10; - a = Long.fromString('' + lhs_local.value, false, lbase); - b = Long.fromString('' + rhs_local.value, false, rbase); - switch (expr.operator) { - case '+': a = a.add(b); break; - case '-': a = a.subtract(b); break; - case '*': a = a.multiply(b); break; - case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero(); - case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero(); - case '<<': a = a.shl(b); break; - case '>>': a = a.shr(b); break; - case '>>>': a = a.shru(b); break; - case '&': a = a.and(b); break; - case '|': a = a.or(b); break; - case '^': a = a.xor(b); break; - case '==': a = a.eq(b); break; - case '!=': a = !a.eq(b); break; - case '<': a = a.lt(b); break; - case '<=': a = a.lte(b); break; - case '>': a = a.gt(b); break; - case '>=': a = a.gte(b); break; - default: return invalid_operator(); - } - if (typeof a === 'boolean') - return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true }; - return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.long, value: hex_long(a), valid: true }; - } - else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) { - // both are (non-long) integer types - var a = numberify(lhs_local), b = numberify(rhs_local); - switch (expr.operator) { - case '+': a += b; break; - case '-': a -= b; break; - case '*': a *= b; break; - case '/': if (b) { a = Math.trunc(a / b); break } return divide_by_zero(); - case '%': if (b) { a %= b; break; } return divide_by_zero(); - case '<<': a <<= b; break; - case '>>': a >>= b; break; - case '>>>': a >>>= b; break; - case '&': a &= b; break; - case '|': a |= b; break; - case '^': a ^= b; break; - case '==': a = a === b; break; - case '!=': a = a !== b; break; - case '<': a = a < b; break; - case '<=': a = a <= b; break; - case '>': a = a > b; break; - case '>=': a = a >= b; break; - default: return invalid_operator(); - } - if (typeof a === 'boolean') - return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true }; - return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.int, value: '' + a, valid: true }; - } - else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) { - var a = numberify(lhs_local), b = numberify(rhs_local); - switch (expr.operator) { - case '+': a += b; break; - case '-': a -= b; break; - case '*': a *= b; break; - case '/': a /= b; break; - case '==': a = a === b; break; - case '!=': a = a !== b; break; - case '<': a = a < b; break; - case '<=': a = a <= b; break; - case '>': a = a > b; break; - case '>=': a = a >= b; break; - default: return invalid_operator(); - } - if (typeof a === 'boolean') - return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true }; - // one of them must be a float or double - var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))]; - return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES[result_type], value: '' + a, valid: true }; - } - else if (lhs_local.type.signature === 'Z' && rhs_local.type.signature === 'Z') { - // boolean operands - var a = lhs_local.value, b = rhs_local.value; - switch (expr.operator) { - case '&': case '&&': a = a && b; break; - case '|': case '||': a = a || b; break; - case '^': a = !!(a ^ b); break; - case '==': a = a === b; break; - case '!=': a = a !== b; break; - default: return invalid_operator(); - } - return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true }; - } - else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) { - return stringify(rhs_local).then(rhs_str => createJavaString(dbgr, lhs_local.string + rhs_str, { israw: true })); - } - return invalid_operator(); - }); - } - switch (expr.root_term_type) { - case 'boolean': - local = { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: expr.root_term !== 'false', valid: true }; - break; - case 'null': - const nullvalue = '0000000000000000'; // null reference value - local = { vtype: 'literal', name: '', hasnullvalue: true, type: JTYPES.null, value: nullvalue, valid: true }; - break; - case 'ident': - local = locals && locals.find(l => l.name === expr.root_term); - break; - case 'hexint': - case 'octint': - case 'decint': - case 'decfloat': - local = evaluate_number(expr.root_term); - break; - case 'char': - case 'echar': - case 'uchar': - local = evaluate_char(decode_char(expr.root_term.slice(1, -1))) - break; - case 'string': - // we must get the runtime to create string instances - q = createJavaString(dbgr, expr.root_term); - local = { valid: true }; // make sure we don't fail the evaluation - break; - } - if (!local || !local.valid) return reject_evaluation('not available'); - // we've got the root term variable - work out the rest - q = expr.array_or_fncall.arr.reduce((q, index_expr) => { - return q.then(function (index_expr, local) { return evaluate_array_element.call(this, index_expr, local) }.bind(this, index_expr)); - }, q); - q = expr.members.reduce((q, m) => { - return q.then(function (m, local) { return evaluate_member.call(this, m, local) }.bind(this, m)); - }, q); - if (expr.typecast) { - q = q.then(function (type, local) { return evaluate_cast.call(this, type, local) }.bind(this, expr.typecast)) - } - // if it's a string literal, we are already waiting for the runtime to create the string - // - otherwise, start the evalaution... - if (expr.root_term_type !== 'string') - q.resolveWith(this, [local]); - return q; - } - const evaluate_array_element = (index_expr, arr_local) => { - if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`); - if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException'); - return evaluate_expression(index_expr) - .then(function (arr_local, idx_local) { - if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value'); - var idx = numberify(idx_local); - if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`); - return dbgr.getarrayvalues(arr_local, idx, 1) - }.bind(this, arr_local)) - .then(els => els[0]) - } - const evaluate_methodcall = (m, obj_local) => { - // until we can figure out why method invokes with parameters crash the debugger, disallow parameterised calls - if (m.array_or_fncall.call.length) - return reject_evaluation('Error: method calls with parameter values are not supported'); - - // find any methods matching the member name with any parameters in the signature - return dbgr.findNamedMethods(obj_local.type.signature, m.member, /^/) - .then(methods => { - if (!methods[0]) - return reject_evaluation(`Error: method '${m.member}()' not found`); - // evaluate any parameters (and wait for the results) - return $.when({methods},...m.array_or_fncall.call.map(evaluate_expression)); - }) - .then((x,...paramValues) => { - // filter the method based upon the types of parameters - note that null types and integer literals can match multiple types - paramValues = paramValues = paramValues.map(p => p[0]); - var matchers = paramValues.map(p => { - switch(true) { - case p.type.signature === 'I': - // match bytes/shorts/ints/longs/floats/doubles within range - if (p.value >= -128 && p.value <= 127) return /^[BSIJFD]$/ - if (p.value >= -32768 && p.value <= 32767) return /^[SIJFD]$/ - return /^[IJFD]$/; - case p.type.signature === 'F': - return /^[FD]$/; - case p.type.signature === 'Lnull;': - return /^[LT\[]/; // any reference type - default: - // anything else must be an exact signature match (for now - in reality we should allow subclassed type) - return new RegExp(`^${p.type.signature.replace(/[$]/g,x=>'\\'+x)}$`); - } - }); - var methods = x.methods.filter(m => { - // extract a list of parameter types - var paramtypere = /\[*([BSIJFDCZ]|([LT][^;]+;))/g; - for (var x, ptypes=[]; x = paramtypere.exec(m.sig); ) { - ptypes.push(x[0]); - } - // the last paramter type is the return value - ptypes.pop(); - // check if they match - if (ptypes.length !== paramValues.length) - return; - return matchers.filter(m => { - return !m.test(ptypes.shift()) - }).length === 0; - }); - if (!methods[0]) - return reject_evaluation(`Error: incompatible parameters for method '${m.member}'`); - // convert the parameters to exact debugger-compatible values - paramValues = paramValues.map(p => { - if (p.type.signature.length === 1) - return { type: p.type.typename, value: p.value}; - return { type: 'oref', value: p.value }; - }) - return dbgr.invokeMethod(obj_local.value, thread.threadid, obj_local.type.signature, m.member, methods[0].genericsig || methods[0].sig, paramValues, {}); - }); - } - const evaluate_member = (m, obj_local) => { - if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type'); - if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException'); - var chain; - if (m.array_or_fncall.call) { - chain = evaluate_methodcall(m, obj_local); - } - // length is a 'fake' field of arrays, so special-case it - else if (JTYPES.isArray(obj_local.type) && m.member === 'length') { - chain = $.Deferred().resolve(evaluate_number(obj_local.arraylen)); - } - // we also special-case :super (for object instances) - else if (JTYPES.isObject(obj_local.type) && m.member === ':super') { - chain = dbgr.getsuperinstance(obj_local); - } - // anything else must be a real field - else { - chain = dbgr.getFieldValue(obj_local, m.member, true) - } - - return chain.then(local => { - if (m.array_or_fncall.arr.length) { - var q = $.Deferred(); - m.array_or_fncall.arr.reduce((q, index_expr) => { - return q.then(function (index_expr, local) { return evaluate_array_element(index_expr, local) }.bind(this, index_expr)); - }, q); - return q.resolveWith(this, [local]); - } - }); - } - const evaluate_cast = (type, local) => { - if (type === local.type.typename) return local; - const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`); - // boolean cannot be converted from anything else - if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast(); - if (local.type.typename === 'long') { - // long to something else - var value = Long.fromString(local.value, true, 16); - switch (true) { - case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2), 16) << 24) >> 24); break; - case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4), 16) << 16) >> 16); break; - case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8), 16) | 0)); break; - case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4), 16))); break; - case (type === 'float'): local = evaluate_number(value.toSigned().toNumber() + 'F'); break; - case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break; - default: return incompatible_cast(); - } - } else { - switch (true) { - case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break; - case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break; - case (type === 'int'): local = evaluate_number((local.value | 0)); break; - case (type === 'long'): local = evaluate_number(local.value + 'L'); break; - case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value | 0)); break; - case (type === 'float'): break; - case (type === 'double'): break; - default: return incompatible_cast(); - } - } - local.type = JTYPES[type]; - return local; - } - - var e = { expr: expression.trim() }; - var parsed_expression = parse_expression(e); - // if there's anything left, it's an error - if (parsed_expression && !e.expr) { - // the expression is well-formed - start the (asynchronous) evaluation - return evaluate_expression(parsed_expression) - .then(local => { - var v = vars._local_to_variable(local); - return resolve_evaluation(v.value, v.variablesReference); - }); - } - - // the expression is not well-formed - return reject_evaluation('not available'); -} diff --git a/src/globals.js b/src/globals.js deleted file mode 100644 index 1ff927e..0000000 --- a/src/globals.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict' - -const path = require('path'); - -// some commonly used Java types in debugger-compatible format -const JTYPES = { - byte: {typename:'byte',signature:'B'}, - short: {typename:'short',signature:'S'}, - int: {typename:'int',signature:'I'}, - long: {typename:'long',signature:'J'}, - float: {typename:'float',signature:'F'}, - double: {typename:'double',signature:'D'}, - char: {typename:'char',signature:'C'}, - boolean: {typename:'boolean',signature:'Z'}, - null: {typename:'null',signature:'Lnull;'}, // null has no type really, but we need something for literals - String: {typename:'String',signature:'Ljava/lang/String;'}, - Object: {typename:'Object',signature:'Ljava/lang/Object;'}, - isArray(t) { return t.signature[0]==='[' }, - isObject(t) { return t.signature[0]==='L' }, - isReference(t) { return /^[L[]/.test(t.signature) }, - isPrimitive(t) { return !JTYPES.isReference(t.signature) }, - isInteger(t) { return /^[BCIJS]$/.test(t.signature) }, - isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) }, - isString(t) { return t.signature === this.String.signature }, - isChar(t) { return t.signature === this.char.signature }, - isBoolean(t) { return t.signature === this.boolean.signature }, - fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] }, -} - -function signatureToFullyQualifiedType(sig) { - var arr = sig.match(/^\[+/) || ''; - if (arr) { - arr = '[]'.repeat(arr[0].length); - sig = sig.slice(0, arr.length/2); - } - var m = sig.match(/^((L([^<;]+).)|T([^;]+).|.)/); - if (!m) return ''; - if (m[3]) { - return m[3].replace(/[/$]/g,'.') + arr; - } else if (m[4]) { - return m[4].replace(/[/$]/g, '.') + arr; - } - return JTYPES.fromPrimSig(sig[0]) + arr; -} - -// the special name given to exception message fields -const exmsg_var_name = ':msg'; - -function createJavaString(dbgr, s, opts) { - const raw = (opts && opts.israw) ? s : s.slice(1,-1).replace(/\\u[0-9a-fA-F]{4}|\\./,decode_char); - // return a deferred, which resolves to a local variable named 'literal' - return dbgr.createstring(raw); -} - -function decode_char(c) { - switch(true) { - case /^\\[^u]$/.test(c): - // backslash escape - var x = {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v','0':String.fromCharCode(0)}[c[1]]; - return x || c[1]; - case /^\\u[0-9a-fA-F]{4}$/.test(c): - // unicode escape - return String.fromCharCode(parseInt(c.slice(2),16)); - case c.length===1 : - return c; - } - throw new Error('Invalid character value'); -} - -function ensure_path_end_slash(p) { - return p + (/[\\/]$/.test(p) ? '' : path.sep); -} - -function is_subpath_of(fpn, subpath) { - if (!subpath || !fpn) return false; - subpath = ensure_path_end_slash(''+subpath); - return fpn.slice(0,subpath.length) === subpath; -} - -function variableRefToThreadId(variablesReference) { - return (variablesReference / 1e9)|0; -} - - -Object.assign(exports, { - JTYPES, exmsg_var_name, ensure_path_end_slash, is_subpath_of, decode_char, variableRefToThreadId, createJavaString, signatureToFullyQualifiedType -}); diff --git a/src/index.d.js b/src/index.d.js new file mode 100644 index 0000000..e397929 --- /dev/null +++ b/src/index.d.js @@ -0,0 +1,121 @@ +/** + * @typedef {string} hex64 + * @typedef {hex64} JavaRefID + * @typedef {number} VSCThreadID + * @typedef {number} VSCVariableReference + * A variable reference is a number, encoding the thread, stack level and variable index, using: + * + * variableReference = {threadid * 1e9} + {level * 1e6} + varindex + * + * This allows 1M variables (locals, fields, array elements) per call stack frame + * and 1000 frames per call stack + + * @typedef {number} byte + * + * @typedef {JavaRefID} JavaFrameID + * @typedef {JavaRefID} JavaThreadID + * @typedef {JavaRefID} JavaClassID + * @typedef {JavaRefID} JavaMethodID + * @typedef {JavaRefID} JavaFieldID + * @typedef {JavaRefID} JavaObjectID + * @typedef {JavaRefID} JavaTypeID + * + * @typedef JavaFrame + * @property {JavaFrameID} frameid + * @property {JavaLocation} location + * + * @typedef JavaClassInfo + * @property {*} reftype + * @property {*} status + * @property {JavaType} type + * @property {JavaTypeID} typeid + * + * @typedef JavaMethod + * @property {string} genericsig + * @property {JavaMethodID} methodid + * @property {byte} modbits + * @property {string} name + * @property {string} sig + * + * @typedef JavaSource + * @property {string} sourcefile + * + * @typedef JavaLocation + * @property {JavaClassID} cid + * @property {hex64} idx + * @property {JavaMethodID} mid + * @property {1} type + * + * @typedef JavaLineTable + * @property {hex64} start + * @property {hex64} end + * @property {JavaLineTableEntry[]} lines + * + * @typedef JavaLineTableEntry + * @property {hex64} linecodeidx + * @property {number} linenum + * + * + * @typedef JavaField + * @property {JavaFieldID} fieldid + * @property {string} name + * @property {JavaType} type + * @property {string} genericsig + * @property {number} modbits + * + * @typedef JavaVar + * @property {*} codeidx + * @property {string} name + * @property {JavaType} type + * @property {string} genericsig + * @property {number} length + * @property {number} slot + * + * @typedef JavaVarTable + * @property {number} argCnt + * @property {JavaVar[]} vars + * + * @typedef {'byte'|'short'|'int'|'long'|'boolean'|'char'|'float'|'double'|'void'|'oref'} JavaValueType + * + * @typedef HitMod + * @property {1} modkind + * @property {number} count + * @property {() => void} encode + * + * @typedef ClassMatchMod + * @property {5} modkind + * @property {string} pattern + * + * @typedef LocMod + * @property {7} modkind + * @property {*} loc + * @property {() => void} encode + * + * @typedef ExOnlyMod + * @property {8} modkind + * @property {*} reftypeid + * @property {boolean} caught + * @property {boolean} uncaught + **/ + + +/** + * @typedef {"local" | "literal" | "field" | "exception" | "return" | "arrelem" | "super" | "class" | "package"} DebuggerValueType + * @typedef {'in'|'over'|'out'} DebuggerStepType + * @typedef {'set'|'notloaded'|'enabled'|'removed'} BreakpointState + * @typedef {string} BreakpointID + * @typedef {string} CMLKey + * @typedef {number} JDWPRequestID + * @typedef {JDWPRequestID} StepID + * @typedef {'caught'|'uncaught'|'both'} ExceptionBreakMode + * + */ + +/** + * @typedef ADBFileTransferParams + * @property {string} pathname + * @property {Buffer} data + * @property {number} mtime + * @property {number} perms + * + */ diff --git a/src/jdwp.js b/src/jdwp.js index 87aa4e8..f0b4af5 100644 --- a/src/jdwp.js +++ b/src/jdwp.js @@ -1,577 +1,659 @@ -const $ = require('./jq-promise'); -const { btoa,D,E,getutf8bytes,fromutf8bytes,intToHex } = require('./util'); -/* - JDWP - The Java Debug Wire Protocol -*/ -function _JDWP() { - var gCommandId = 0; - var gCommandList = []; - var gEventCallbacks = {}; +const { D, E } = require('./utils/print'); +const { + DebuggerMethodInfo, + DebuggerTypeInfo, + JavaTaggedValue, +} = require('./debugger-types'); +const { JavaType } = require('./debugger-types'); - function Command(name, cs, cmd, outdatafn, replydecodefn) { +/** the next command ID */ +let gCommandId = 0; + +/** + * The in-progress JDWP commands, mapped by ID + * @type {Map} + */ +const gCommandList = new Map(); + +/** + * The list of registered JDWP event callback objects, mapped by ID. + * These are called when JDWP sends a composite event, triggered by a breakpoint, exception or class-prepare. + * @type {Map} + */ +const gEventCallbacks = new Map(); + +/** + * The singleton instance of the `DataCoderClass`, initialised after the Java Object ID sizes are retrieved. + * @type {DataCoderClass} + **/ +let DataCoder; + +/** + * Class representing a single JDWP command + */ +class Command { + + /** + * @param {string} name + * @param {byte} commandset + * @param {byte} command + * @param {()=>byte[]} outdatafn + * @param {(o)=>*} [replydecodefn] + */ + constructor(name, commandset, command, outdatafn, replydecodefn) { this.length = 11; this.id = ++gCommandId; this.flags = 0; - this.commandset = cs; - this.command = cmd; - this.rawdata = outdatafn?outdatafn():[]; + this.commandset = commandset; + this.command = command; + this.rawdata = outdatafn ? outdatafn() : []; this.length = 11 + this.rawdata.length; - gCommandList[this.id] = this; + gCommandList.set(this.id, this); this.name = name; this.replydecodefn = replydecodefn; - this.deferred = $.Deferred(); } - Command.prototype = { - promise : function() { - return this.deferred.promise(); - }, - toRawString : function() { - var s = ''; - s += String.fromCharCode((this.length >> 24)&255); - s += String.fromCharCode((this.length >> 16)&255); - s += String.fromCharCode((this.length >> 8)&255); - s += String.fromCharCode((this.length)&255); - s += String.fromCharCode((this.id >> 24)&255); - s += String.fromCharCode((this.id >> 16)&255); - s += String.fromCharCode((this.id >> 8)&255); - s += String.fromCharCode((this.id)&255); - s += String.fromCharCode(this.flags); - s += String.fromCharCode(this.commandset); - s += String.fromCharCode(this.command); - var i=this.rawdata.length, j=0; - while (--i>=0) { - s += String.fromCharCode(this.rawdata[j++]); - } - return s; - }, - tob64 : function() { - return btoa(this.toRawString()); + /** + * Return a buffer with the raw JDWP command bytes + */ + toBuffer() { + const buf = Buffer.allocUnsafe(11 + this.rawdata.length); + buf.writeUInt32BE(this.length, 0); + buf.writeUInt32BE(this.id, 4); + buf[8] = this.flags; + buf[9] = this.commandset; + buf[10] = this.command; + if (this.rawdata.length) { + Buffer.from(this.rawdata).copy(buf, 11); } - }; + return buf; + } +} - function Reply(s) { - this.length = s.charCodeAt(0) << 24; - this.length += s.charCodeAt(1) << 16; - this.length += s.charCodeAt(2) << 8; - this.length += s.charCodeAt(3); - this.id = s.charCodeAt(4) << 24; - this.id += s.charCodeAt(5) << 16; - this.id += s.charCodeAt(6) << 8; - this.id += s.charCodeAt(7); - this.flags = s.charCodeAt(8)|0; - this.errorcode = s.charCodeAt(9) << 8; - this.errorcode += s.charCodeAt(10); - this.rawdata = new Array(s.length-11); - var i=0, j=this.rawdata.length; - while (--j>=0) { - this.rawdata[i]=s.charCodeAt(i+11); - i++; - } - this.command = gCommandList[this.id]; +/** + * Class representing a single JDWP reply + */ +class Reply { - if (this.errorcode===16484) { - // errorcode===16484 (0x4064) means a composite event command (set 64,cmd 100) sent from the VM - this.errorcode=0; - this.isevent=!0; - this.decoded=DataCoder.decodeCompositeEvent({ - idx:0, - data:this.rawdata.slice() - }); - // call any registered event callbacks - for (var i in this.decoded.events) { - var event = this.decoded.events[i]; - var cbinfo = event.reqid && gEventCallbacks[event.reqid]; - if (cbinfo) { - var e = { - data:cbinfo.callback.data, - event:event, - reply:this, - }; - cbinfo.callback.fn.call(cbinfo.callback.ths, e); - } - } - return; - } + /** + * @param {Buffer} s + */ + constructor(s) { + this.length = s.readUInt32BE(0); + this.id = s.readUInt32BE(4); + this.flags = s[8]; + this.errorcode = s.readUInt16BE(9); + this.rawdata = s.slice(11); + // look up the matching command by ID + this.command = gCommandList.get(this.id); + gCommandList.delete(this.id); + this.isevent = false; + if (this.errorcode === 16484) { + // errorcode===16484 (0x4064) means a composite event command (set 64,cmd 100) sent from the VM + this.errorcode = 0; + this.isevent = true; + this.handleCompositeEvent(); + return; + } + if (this.errorcode !== 0) { - E(`JDWP command failed '${this.command.name}'. Error ${this.errorcode}`, this); - } + // https://docs.oracle.com/javase/7/docs/platform/jpda/jdwp/jdwp-protocol.html#JDWP_Error + E(`JDWP command failed '${this.command.name}'. Error ${this.errorcode}`, this); + } if (!this.errorcode && this.command && this.command.replydecodefn) { // try and decode the values this.decoded = this.command.replydecodefn({ - idx:0, - data:this.rawdata.slice() + idx: 0, + data: this.rawdata, }); return; } this.decoded = { - empty: true, - errorcode: this.errorcode, - }; + empty: true, + errorcode: this.errorcode, + }; } - this.decodereply = function(ths,s) { - var reply = new Reply(s); - if (reply.command) { - reply.command.deferred.resolveWith(ths, [reply.decoded, reply.command, reply]); + handleCompositeEvent() { + this.decoded = DataCoder.decodeCompositeEvent({ + idx: 0, + data: this.rawdata, + }); + // call any registered event callbacks + this.decoded.events.forEach(event => { + const cbinfo = event.reqid && gEventCallbacks.get(event.reqid); + if (cbinfo) { + const e = { + data: cbinfo.callback.data, + event, + reply: this, + }; + cbinfo.callback.fn.call(cbinfo.callback.ths, e); + } + }); + } +} + +/** + * JDWP data decoder class + */ + +class DataCoderClass { + + constructor(id_sizes) { + this.id_sizes = id_sizes; + this.null_ref_type_id = '00'.repeat(id_sizes.reftypeidsize); + } + + nullRefValue() { + return this.null_ref_type_id; + } + + decodeString(o) { + let utf8len = o.data.readUInt32BE((o.idx += 4) - 4); + if (utf8len > 10000) { + utf8len = 10000; // just to prevent hangs if the decoding is wrong } + return o.data.slice(o.idx, o.idx += utf8len).toString(); + } + + decodeLong(o) { + const res1 = o.data.readUInt32BE((o.idx += 4) - 4); + const res2 = o.data.readUInt32BE((o.idx += 4) - 4); + return `${res1.toString(16).padStart(8,'0')}${res2.toString(16).padStart(8,'0')}`; + } + + decodeInt(o) { + return o.data.readInt32BE((o.idx += 4) - 4); + } + + decodeShort(o) { + return o.data.readInt16BE((o.idx += 2) - 2); + } + + decodeByte(o) { + return o.data.readInt8(o.idx++); + } + + decodeChar(o) { + return o.data.readUInt16BE((o.idx += 2) - 2); + } + + decodeBoolean(o) { + return o.data[o.idx++] !== 0; + } + + decodeDecimal(bytes, signBits, exponentBits, fractionBits, eMin, eMax, littleEndian) { + let byte_bits = bytes.map(byte => `0000000${byte.toString(2)}`.slice(-8)); + if (littleEndian) { + byte_bits = byte_bits.reverse(); + } + const binary = byte_bits.join(''); + + const sign = (binary[0] === '1') ? -1 : 1; + let exponent = parseInt(binary.substr(signBits, exponentBits), 2) - eMax; + const significandBase = binary.substr(signBits + exponentBits, fractionBits); + let significandBin = `1${significandBase}`; + let val = 1; + let significand = 0; + + if (exponent+eMax === ((eMax*2)+1)) { + if (significandBase.indexOf('1') < 0) { + return sign > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; + } + return Number.NaN; + } + if (exponent === -eMax) { + if (significandBase.indexOf('1') === -1) { + return 0; + } + exponent = eMin; + significandBin = `0${significandBase}`; + } + + for (let bit of significandBin) { + significand += val * parseInt(bit,2); + val = val / 2; + } + + return sign * significand * Math.pow(2, exponent); + } + + decodeFloat(o) { + const bytes = o.data.slice(o.idx, o.idx+=4); + return this.decodeDecimal(bytes, 1, 8, 23, -126, 127, false); + } + + decodeDouble(o) { + const bytes = o.data.slice(o.idx, o.idx+=8); + return this.decodeDecimal(bytes, 1, 11, 52, -1022, 1023, false); + } + + decodeRef(o, bytes) { + return o.data.slice(o.idx, o.idx += bytes).toString('hex'); + } + + decodeTRef(o) { + return this.decodeRef(o,this.id_sizes.reftypeidsize); + } + + decodeORef(o) { + return this.decodeRef(o,this.id_sizes.objectidsize); + } + + decodeMRef(o) { + return this.decodeRef(o,this.id_sizes.methodidsize); + } + + decodeRefType (o) { + return DataCoderClass.mapValue(this.decodeByte(o), [null,'class','interface','array']); + } + + decodeStatus (o) { + return DataCoderClass.mapFlags(this.decodeInt(o), ['verified','prepared','initialized','error']); + } + + decodeThreadStatus(o) { + return ['zombie','running','sleeping','monitor','wait'][this.decodeInt(o)] || ''; + } + + decodeSuspendStatus(o) { + return this.decodeInt(o) ? 'suspended': ''; + } + + decodeTaggedObjectID(o) { + return this.decodeValue(o); + } + + decodeValue(o) { + return this.tagtoDecoder(o.data[o.idx++]).call(this, o); + } + + tagtoDecoder(tag) { + switch (tag) { + case 91: + case 76: + case 115: + case 116: + case 103: + case 108: + case 99: + return this.decodeORef; + case 66: + return this.decodeByte; + case 90: + return this.decodeBoolean; + case 67: + return this.decodeChar; + case 83: + return this.decodeShort; + case 70: + return this.decodeFloat; + case 73: + return this.decodeInt; + case 68: + return this.decodeDouble; + case 74: + return this.decodeLong; + case 86: + return function() { return 'void'; }; + } + } + + static mapValue(value,values) { + return {value: value, string:values[value] }; + } + + static mapFlags(value,values) { + const res = { + value, + string:'[]', + }; + const flgs = []; + for (let i = value,j = 0; i; i>>=1) { + if ((i&1)&&(values[j])) + flgs.push(values[j]); + j++; + } + res.string = '['+flgs.join('|')+']'; + return res; + } + + decodeList(o, list) { + const res = {}; + while (list.length) { + const next = list.shift(); + for ( let key in next) { + switch(next[key]) { + case 'string': res[key]=this.decodeString(o); break; + case 'int': res[key]=this.decodeInt(o); break; + case 'long': res[key]=this.decodeLong(o); break; + case 'byte': res[key]=this.decodeByte(o); break; + case 'fref': res[key]=this.decodeRef(o,this.id_sizes.fieldidsize); break; + case 'mref': res[key]=this.decodeRef(o,this.id_sizes.methodidsize); break; + case 'oref': res[key]=this.decodeRef(o,this.id_sizes.objectidsize); break; + case 'tref': res[key]=this.decodeRef(o,this.id_sizes.reftypeidsize); break; + case 'frameid': res[key]=this.decodeRef(o,this.id_sizes.frameidsize); break; + case 'reftype': res[key]=this.decodeRefType(o); break; + case 'status': res[key]=this.decodeStatus(o); break; + case 'location': res[key]=this.decodeLocation(o); break; + case 'signature': res[key]=this.decodeTypeFromSignature(o); break; + case 'codeindex': res[key]=this.decodeLong(o); break; + } + } + } + return res; + } + + decodeLocation(o) { + return { + type: o.data[o.idx++], + cid: this.decodeTRef(o), + mid: this.decodeMRef(o), + idx: this.decodeLong(o), + }; + } + + decodeTypeFromSignature(o) { + const signature = this.decodeString(o); + return JavaType.from(signature); + } + + decodeCompositeEvent(o) { + const rd = o.data; + const res = {}; + res.suspend = rd[o.idx++]; + res.events = []; + let arrlen = this.decodeInt(o); + while (--arrlen >= 0) { + // all event types return kind+requestid as their first entries + const event = { + kind:{ + name:'', + value:rd[o.idx++], + } + + }; + const eventkinds = ['','step','breakpoint','framepop','exception','userdefined','threadstart','threadend','classprepare','classunload','classload']; + event.kind.name = eventkinds[event.kind.value]; + switch(event.kind.value) { + case 1: // step + case 2: // breakpoint + event.reqid = this.decodeInt(o); + event.threadid = this.decodeORef(o); + event.location = this.decodeLocation(o); + break; + case 4: // exception + event.reqid = this.decodeInt(o); + event.threadid = this.decodeORef(o); + event.throwlocation = this.decodeLocation(o); + event.exception = this.decodeTaggedObjectID(o); + event.catchlocation = this.decodeLocation(o); // 0 = uncaught + break; + case 6: // thread start + case 7: // thread end + event.reqid = this.decodeInt(o); + event.threadid = this.decodeORef(o); + event.state = event.kind.value === 6 ? 'start' : 'end'; + break; + case 8: // classprepare + event.reqid = this.decodeInt(o); + event.threadid = this.decodeORef(o); + event.reftype = this.decodeByte(o); + event.typeid = this.decodeTRef(o); + event.type = this.decodeTypeFromSignature(o); + event.status = this.decodeStatus(o); + break; + } + res.events.push(event); + } + return res; + } + + + /** + * @param {byte[]} res + * @param {number} i + */ + encodeByte(res, i) { + res.push(i&255); + } + + + /** + * @param {byte[]} res + * @param {boolean} b + */ + encodeBoolean(res, b) { + res.push(b?1:0); + } + + + /** + * @param {byte[]} res + * @param {number} i + */ + encodeShort(res, i) { + res.push((i>>8)&255); + res.push((i)&255); + } + + + /** + * @param {byte[]} res + * @param {number} i + */ + encodeInt(res, i) { + res.push((i>>24)&255); + res.push((i>>16)&255); + res.push((i>>8)&255); + res.push((i)&255); + } + + + /** + * @param {byte[]} res + * @param {number|string} c + */ + encodeChar(res, c) { + // c can either be a 1 char string or an integer + this.encodeShort(res, typeof c === 'string' ? c.charCodeAt(0) : c); + } + + + /** + * @param {byte[]} res + * @param {string} s + */ + encodeString(res, s) { + const utf8_bytes = Buffer.from(s, 'utf8'); + this.encodeInt(res, utf8_bytes.length); + for (let i = 0; i < utf8_bytes.length; i++) + res.push(utf8_bytes[i]); + } + + + /** + * @param {byte[]} res + * @param {JavaRefID} ref + */ + encodeRef(res, ref) { + if (ref === null) { + ref = this.nullRefValue(); + } + for(let i = 0; i < ref.length; i+=2) { + res.push(parseInt(ref.substring(i,i+2), 16)); + } + } + + + /** + * @param {byte[]} res + * @param {string} l + */ + encodeLong(res, l) { + for(let i = 0; i < l.length; i+=2) { + res.push(parseInt(l.substring(i,i+2), 16)); + } + } + + + /** + * @param {byte[]} res + * @param {number|string} value + */ + encodeDouble(res, value) { + if (typeof value === 'string') { + value = (parseInt(value.slice(0,-12),16) * Math.pow(2,48)) + (parseInt(value.slice(-12),16)); + } + let hiWord = 0, loWord = 0; + switch (value) { + case Number.POSITIVE_INFINITY: hiWord = 0x7FF00000; break; + case Number.NEGATIVE_INFINITY: hiWord = 0xFFF00000; break; + case +0.0: hiWord = 0x00000000; break;//0x40000000; break; + case -0.0: hiWord = 0x80000000; break;//0xC0000000; break; + default: + if (Number.isNaN(value)) { hiWord = 0x7FF80000; break; } + + if (value <= -0.0) { + hiWord = 0x80000000; + value = -value; + } + + let exponent = Math.floor(Math.log(value) / Math.log(2)); + let significand = Math.floor((value / Math.pow(2, exponent)) * Math.pow(2, 52)); + + loWord = significand & 0xFFFFFFFF; + significand /= Math.pow(2, 32); + + exponent += 1023; + if (exponent >= 0x7FF) { + exponent = 0x7FF; + significand = 0; + } else if (exponent < 0) exponent = 0; + + hiWord = hiWord | (exponent << 20); + hiWord = hiWord | (significand & ~(-1 << 20)); + break; + } + this.encodeInt(res, hiWord); + this.encodeInt(res, loWord); + } + + + /** + * @param {byte[]} res + * @param {number|string} value + */ + encodeFloat(res, value) { + if (typeof value === 'string') { + value = (parseInt(value.slice(0,-12),16) * Math.pow(2,48)) + (parseInt(value.slice(-12),16)); + } + let bytes = 0; + switch (value) { + case Number.POSITIVE_INFINITY: bytes = 0x7F800000; break; + case Number.NEGATIVE_INFINITY: bytes = 0xFF800000; break; + case +0.0: bytes = 0x00000000; break;//0x40000000; break; + case -0.0: bytes = 0x80000000; break;//0xC0000000l + default: + if (Number.isNaN(value)) { bytes = 0x7FC00000; break; } + + if (value <= -0.0) { + bytes = 0x80000000; + value = -value; + } + + let exponent = Math.floor(Math.log(value) / Math.log(2)); + let significand = ((value / Math.pow(2, exponent)) * 0x00800000) | 0; + + exponent += 127; + if (exponent >= 0xFF) { + exponent = 0xFF; + significand = 0; + } else if (exponent < 0) exponent = 0; + + bytes = bytes | (exponent << 23); + bytes = bytes | (significand & ~(-1 << 23)); + break; + } + + this.encodeInt(res, bytes); + } + + + /** + * @param {byte[]} res + * @param {JavaValueType} valuetype + * @param {*} data + */ + encodeValue(res, valuetype, data) { + switch(valuetype) { + case 'byte': this.encodeByte(res, data); break; + case 'short': this.encodeShort(res, data); break; + case 'int': this.encodeInt(res, data); break; + case 'long': this.encodeLong(res, data); break; + case 'boolean': this.encodeBoolean(res, data); break; + case 'char': this.encodeChar(res, data); break; + case 'float': this.encodeFloat(res, data); break; + case 'double': this.encodeDouble(res, data); break; + // note that strings are encoded as object references... + case 'oref': this.encodeRef(res,data); break; + default: + D(`invalid value type: ${valuetype} - assuming oref`); + this.encodeRef(res,data); break; + } + } + + + + /** + * @param {byte[]} res + * @param {JavaValueType} valuetype + * @param {*} value + */ + encodeTaggedValue(res, valuetype, value) { + switch(valuetype) { + case 'byte': res.push(66); break; + case 'short': res.push(83); break; + case 'int': res.push(73); break; + case 'long': res.push(74); break; + case 'boolean': res.push(90); break; + case 'char': res.push(67); break; + case 'float': res.push(70); break; + case 'double': res.push(68); break; + case 'void': res.push(86); break; + // note that strings are encoded as object references... + case 'oref': res.push(76); break; + default: + D(`invalid tagged value type: ${valuetype} - assuming oref`); + res.push(76); break; + } + this.encodeValue(res, valuetype, value); + } + +}; + +/** + * JDWP - The Java Debug Wire Protocol + */ +class JDWP { + + static decodeReply(buffer) { + const reply = new Reply(buffer); return reply; }; - this.signaturetotype = function(s) { - return DataCoder.signaturetotype(s); + static initDataCoder(idsizes) { + DataCoder = new DataCoderClass(idsizes); } - this.setIDSizes = function(idsizes) { - DataCoder._idsizes = idsizes; - } - - var DataCoder = { - _idsizes:null, - - nullRefValue: function() { - if (!this._idsizes._nullreftypeid) { - var x = '00', len = this._idsizes.reftypeidsize * 2; // each byte needs 2 chars - while (x.length < len) x += x; - this._idsizes._nullreftypeid = x.slice(0, len); // should be power of 2, but just in case... - } - return this._idsizes._nullreftypeid; - }, - - decodeString: function(o) { - var rd = o.data; - var utf8len=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]); - if (utf8len > 10000) - utf8len = 10000; // just to prevent hangs if the decoding is wrong - var res=fromutf8bytes(o.data.slice(o.idx, o.idx+utf8len)); - o.idx+= utf8len; - return res; - }, - decodeLong: function(o, hexstring) { - var rd = o.data; - var res1=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]); - var res2=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]); - return intToHex(res1>>>0,8)+intToHex(res2>>>0,8); // >>> 0 ensures +ve value - }, - decodeInt: function(o) { - var rd = o.data; - var res=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]); - return res; - }, - decodeByte: function(o) { - var i = o.data[o.idx++]; - return i<128?i:i-256; - }, - decodeShort: function(o) { - var i = (o.data[o.idx++]<<8)+o.data[o.idx++]; - return i<32768?i:i-65536; - }, - decodeChar: function(o) { - return (o.data[o.idx++]<<8)+o.data[o.idx++]; // uint16 - }, - decodeBoolean: function(o) { - return o.data[o.idx++] != 0; - }, - decodeDecimal: function(bytes, signBits, exponentBits, fractionBits, eMin, eMax, littleEndian) { - var totalBits = (signBits + exponentBits + fractionBits); - - var binary = ""; - for (var i = 0, l = bytes.length; i < l; i++) { - var bits = bytes[i].toString(2); - while (bits.length < 8) - bits = "0" + bits; - - if (littleEndian) - binary = bits + binary; - else - binary += bits; - } - - var sign = (binary.charAt(0) == '1')?-1:1; - var exponent = parseInt(binary.substr(signBits, exponentBits), 2) - eMax; - var significandBase = binary.substr(signBits + exponentBits, fractionBits); - var significandBin = '1'+significandBase; - var i = 0; - var val = 1; - var significand = 0; - - if (exponent+eMax===((eMax*2)+1)) { - if (significandBase.indexOf('1')<0) - return sign>0?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY; - return Number.NaN; - } - if (exponent == -eMax) { - if (significandBase.indexOf('1') == -1) - return 0; - else { - exponent = eMin; - significandBin = '0'+significandBase; - } - } - - while (i < significandBin.length) { - significand += val * parseInt(significandBin.charAt(i)); - val = val / 2; - i++; - } - - return sign * significand * Math.pow(2, exponent); - }, - decodeFloat: function(o) { - var bytes = o.data.slice(o.idx, o.idx+=4); - return this.decodeDecimal(bytes, 1, 8, 23, -126, 127, false); - }, - decodeDouble: function(o) { - var bytes = o.data.slice(o.idx, o.idx+=8); - return this.decodeDecimal(bytes, 1, 11, 52, -1022, 1023, false); - }, - decodeRef: function(o, bytes) { - var rd = o.data; - var res = ''; - while (--bytes>=0) { - res += ('0'+rd[o.idx++].toString(16)).slice(-2); - } - return res; - }, - decodeTRef: function(o) { - return this.decodeRef(o,this._idsizes.reftypeidsize); - }, - decodeORef: function(o) { - return this.decodeRef(o,this._idsizes.objectidsize); - }, - decodeMRef: function(o) { - return this.decodeRef(o,this._idsizes.methodidsize); - }, - decodeRefType : function(o) { - return this.mapvalue(this.decodeByte(o), [null,'class','interface','array']); - }, - decodeStatus : function(o) { - return this.mapflags(this.decodeInt(o), ['verified','prepared','initialized','error']); - }, - decodeThreadStatus : function(o) { - return ['zombie','running','sleeping','monitor','wait'][this.decodeInt(o)] || ''; - }, - decodeSuspendStatus : function(o) { - return this.decodeInt(o) ? 'suspended': ''; - }, - decodeTaggedObjectID : function(o) { - return this.decodeValue(o); - }, - decodeValue : function(o) { - var rd = o.data; - return this.tagtodecoder(rd[o.idx++]).call(this, o); - }, - tagtodecoder: function(tag) { - switch (tag) { - case 91: - case 76: - case 115: - case 116: - case 103: - case 108: - case 99: - return this.decodeORef; - case 66: - return this.decodeByte; - case 90: - return this.decodeBoolean; - case 67: - return this.decodeChar; - case 83: - return this.decodeShort; - case 70: - return this.decodeFloat; - case 73: - return this.decodeInt; - case 68: - return this.decodeDouble; - case 74: - return this.decodeLong; - case 86: - return function() { return 'void'; }; - } - }, - mapvalue : function(value,values) { - return {value: value, string:values[value] }; - }, - mapflags : function(value,values) { - var res = {value: value, string:'[]'}; - var flgs=[]; - for (var i=value,j=0;i;i>>=1) { - if ((i&1)&&(values[j])) - flgs.push(values[j]); - j++; - } - res.string = '['+flgs.join('|')+']'; - return res; - }, - decodeList: function(o, list) { - var res = {}; - while (list.length) { - var next = list.shift(); - for ( var key in next) { - switch(next[key]) { - case 'string': res[key]=this.decodeString(o); break; - case 'int': res[key]=this.decodeInt(o); break; - case 'long': res[key]=this.decodeLong(o); break; - case 'byte': res[key]=this.decodeByte(o); break; - case 'fref': res[key]=this.decodeRef(o,this._idsizes.fieldidsize); break; - case 'mref': res[key]=this.decodeRef(o,this._idsizes.methodidsize); break; - case 'oref': res[key]=this.decodeRef(o,this._idsizes.objectidsize); break; - case 'tref': res[key]=this.decodeRef(o,this._idsizes.reftypeidsize); break; - case 'frameid': res[key]=this.decodeRef(o,this._idsizes.frameidsize); break; - case 'reftype': res[key]=this.decodeRefType(o); break; - case 'status': res[key]=this.decodeStatus(o); break; - case 'location': res[key]=this.decodeLocation(o); break; - case 'signature': res[key]=this.decodeTypeFromSignature(o); break; - case 'codeindex': res[key]=this.decodeLong(o, true); break; - } - } - } - return res; - }, - decodeLocation : function(o) { - return { - type: o.data[o.idx++], - cid: this.decodeTRef(o), - mid: this.decodeMRef(o), - idx: this.decodeLong(o, true), - }; - }, - decodeTypeFromSignature : function(o) { - var sig = this.decodeString(o); - return this.signaturetotype(sig); - }, - decodeCompositeEvent: function (o) { - var rd = o.data; - var res = {}; - res.suspend = rd[o.idx++]; - res.events = []; - var arrlen = this.decodeInt(o); - while (--arrlen>=0) { - // all event types return kind+requestid as their first entries - var event = { - kind:{name:'', value:rd[o.idx++]}, - }; - var eventkinds = ['','step','breakpoint','framepop','exception','userdefined','threadstart','threadend','classprepare','classunload','classload']; - event.kind.name = eventkinds[event.kind.value]; - switch(event.kind.value) { - case 1: // step - case 2: // breakpoint - event.reqid = this.decodeInt(o); - event.threadid = this.decodeORef(o); - event.location = this.decodeLocation(o); - break; - case 4: // exception - event.reqid = this.decodeInt(o); - event.threadid = this.decodeORef(o); - event.throwlocation = this.decodeLocation(o); - event.exception = this.decodeTaggedObjectID(o); - event.catchlocation = this.decodeLocation(o); // 0 = uncaught - break; - case 6: // thread start - case 7: // thread end - event.reqid = this.decodeInt(o); - event.threadid = this.decodeORef(o); - event.state = event.kind.value === 6 ? 'start' : 'end'; - break; - case 8: // classprepare - event.reqid = this.decodeInt(o); - event.threadid = this.decodeORef(o); - event.reftype = this.decodeByte(o); - event.typeid = this.decodeTRef(o); - event.type = this.decodeTypeFromSignature(o); - event.status = this.decodeStatus(o); - break; - } - res.events.push(event); - } - return res; - }, - - encodeByte : function(res, i) { - res.push(i&255); - }, - encodeBoolean : function(res, b) { - res.push(b?1:0); - }, - encodeShort : function(res, i) { - res.push((i>>8)&255); - res.push((i)&255); - }, - encodeInt : function(res, i) { - res.push((i>>24)&255); - res.push((i>>16)&255); - res.push((i>>8)&255); - res.push((i)&255); - }, - encodeChar: function(res, c) { - // c can either be a 1 char string or an integer - this.encodeShort(res, typeof c === 'string' ? c.charCodeAt(0) : c); - }, - encodeString : function(res, s) { - var utf8bytes = getutf8bytes(s); - this.encodeInt(res, utf8bytes.length); - for (var i=0; i < utf8bytes.length; i++) - res.push(utf8bytes[i]); - }, - encodeRef: function(res, ref) { - if (ref === null) ref = this.nullRefValue(); - for(var i=0; i < ref.length; i+=2) { - res.push(parseInt(ref.substring(i,i+2), 16)); - } - }, - encodeLong: function(res, l) { - for(var i=0; i < l.length; i+=2) { - res.push(parseInt(l.substring(i,i+2), 16)); - } - }, - encodeDouble: function(res, value) { - var hiWord = 0, loWord = 0; - switch (value) { - case Number.POSITIVE_INFINITY: hiWord = 0x7FF00000; break; - case Number.NEGATIVE_INFINITY: hiWord = 0xFFF00000; break; - case +0.0: hiWord = 0x00000000; break;//0x40000000; break; - case -0.0: hiWord = 0x80000000; break;//0xC0000000; break; - default: - if (Number.isNaN(value)) { hiWord = 0x7FF80000; break; } - - if (value <= -0.0) { - hiWord = 0x80000000; - value = -value; - } - - var exponent = Math.floor(Math.log(value) / Math.log(2)); - var significand = Math.floor((value / Math.pow(2, exponent)) * Math.pow(2, 52)); - - loWord = significand & 0xFFFFFFFF; - significand /= Math.pow(2, 32); - - exponent += 1023; - if (exponent >= 0x7FF) { - exponent = 0x7FF; - significand = 0; - } else if (exponent < 0) exponent = 0; - - hiWord = hiWord | (exponent << 20); - hiWord = hiWord | (significand & ~(-1 << 20)); - break; - } - this.encodeInt(res, hiWord); - this.encodeInt(res, loWord); - }, - encodeFloat: function(res, value) { - var bytes = 0; - switch (value) { - case Number.POSITIVE_INFINITY: bytes = 0x7F800000; break; - case Number.NEGATIVE_INFINITY: bytes = 0xFF800000; break; - case +0.0: bytes = 0x00000000; break;//0x40000000; break; - case -0.0: bytes = 0x80000000; break;//0xC0000000l - default: - if (Number.isNaN(value)) { bytes = 0x7FC00000; break; } - - if (value <= -0.0) { - bytes = 0x80000000; - value = -value; - } - - var exponent = Math.floor(Math.log(value) / Math.log(2)); - var significand = ((value / Math.pow(2, exponent)) * 0x00800000) | 0; - - exponent += 127; - if (exponent >= 0xFF) { - exponent = 0xFF; - significand = 0; - } else if (exponent < 0) exponent = 0; - - bytes = bytes | (exponent << 23); - bytes = bytes | (significand & ~(-1 << 23)); - break; - } - - this.encodeInt(res, bytes); - }, - encodeValue: function(res, key, data) { - switch(key) { - case 'byte': this.encodeByte(res, data); break; - case 'short': this.encodeShort(res, data); break; - case 'int': this.encodeInt(res, data); break; - case 'long': this.encodeLong(res, data); break; - case 'boolean': this.encodeBoolean(res, data); break; - case 'char': this.encodeChar(res, data); break; - case 'float': this.encodeFloat(res, data); break; - case 'double': this.encodeDouble(res, data); break; - // note that strings are encoded as object references... - case 'oref': this.encodeRef(res,data); break; - } - }, - - encodeTaggedValue: function(res, key, data) { - switch(key) { - case 'byte': res.push(66); break; - case 'short': res.push(83); break; - case 'int': res.push(73); break; - case 'long': res.push(74); break; - case 'boolean': res.push(90); break; - case 'char': res.push(67); break; - case 'float': res.push(70); break; - case 'double': res.push(68); break; - case 'void': res.push(86); break; - // note that strings are encoded as object references... - case 'oref': res.push(76); break; - } - this.encodeValue(res, key, data); - }, - - signaturetotype:function(signature) { - var m = signature.match(/^L([^$]+)\/([^$\/]+)(\$.+)?;$/); - if (m) { - return { - signature: signature, - package: m[1].replace(/\//g,'.'), - typename: (m[2]+(m[3]||'')).replace(/\$(?=[^\d])/g,'.'), - anonymous: /\$\d/.test(m[3]), - } - } - m = signature.match(/^(\[+)(.+)$/); - if (m) { - var elementtype = this.signaturetotype(m[1].slice(0,-1) + m[2]); - return { - signature:signature, - arraydims:m[1].length, - elementtype: elementtype, - typename:elementtype.typename+'[]', - } - } - var primitivetypes = { - B: { signature:'B', typename:'byte', primitive:true, }, - C: { signature:'C', typename:'char', primitive:true, }, - F: { signature:'F', typename:'float', primitive:true, }, - D: { signature:'D', typename:'double', primitive:true, }, - I: { signature:'I', typename:'int', primitive:true, }, - J: { signature:'J', typename:'long', primitive:true, }, - S: { signature:'S', typename:'short', primitive:true, }, - V: { signature:'V', typename:'void', primitive:true, }, - Z: { signature:'Z', typename:'boolean', primitive:true, }, - } - var res = (signature.length===1)?primitivetypes[signature[0]]:null; - if (res) return res; - return { - signature:signature, - typename:signature, - invalid:true, - } - }, - }; - - //var Commands = { - this.Commands = { - version:function() { + static Commands = { + version() { return new Command('version',1, 1, null, function (o) { @@ -579,95 +661,131 @@ function _JDWP() { } ); }, - idsizes:function() { + idsizes() { return new Command('IDSizes', 1, 7, function() { return []; }, function(o) { - return DataCoder.decodeList(o, [{fieldidsize:'int'},{methodidsize:'int'},{objectidsize:'int'},{reftypeidsize:'int'},{frameidsize:'int'}]); + const ints = []; + for (let i = 0; i < o.data.length; i += 4) { + ints.push((o.data[i]<<24) + (o.data[i+1]<<16) + (o.data[i+2]<<8) + (o.data[i+3])); + } + const [fieldidsize,methodidsize,objectidsize,reftypeidsize,frameidsize] = ints; + return { + fieldidsize, + methodidsize, + objectidsize, + reftypeidsize, + frameidsize, + } } ); }, - classinfo:function(ci) { - return new Command('ClassesBySignature:'+ci.name, 1, 2, + + /** + * + * @param {string} signature + */ + classinfo(signature) { + return new Command('ClassesBySignature:'+signature, 1, 2, function() { - var res=[]; - DataCoder.encodeString(res, ci.type.signature); + const res = []; + DataCoder.encodeString(res, signature); return res; }, function(o) { - var arrlen = DataCoder.decodeInt(o); - var res = []; - while (--arrlen>=0) { + let arrlen = DataCoder.decodeInt(o); + const res = []; + while (--arrlen >= 0) { res.push(DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'},{status:'status'}])); } return res; } ); }, + + /** + * + * @param {DebuggerTypeInfo} ci + */ fields:function(ci) { // not supported by Dalvik return new Command('Fields:'+ci.name, 2, 4, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); return res; }, function(o) { - var arrlen = DataCoder.decodeInt(o); - var res = []; - while (--arrlen>=0) { + let arrlen = DataCoder.decodeInt(o); + const res = []; + while (--arrlen >= 0) { res.push(DataCoder.decodeList(o, [{fieldid:'fref'},{name:'string'},{sig:'string'},{modbits:'int'}])); } return res; } ); }, + + /** + * + * @param {DebuggerTypeInfo} ci + */ methods:function(ci) { // not supported by Dalvik - use methodsWithGeneric return new Command('Methods:'+ci.name, 2, 5, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); return res; }, function(o) { - var arrlen = DataCoder.decodeInt(o); - var res = []; - while (--arrlen>=0) { + let arrlen = DataCoder.decodeInt(o); + const res = []; + while (--arrlen >= 0) { res.push(DataCoder.decodeList(o, [{methodid:'mref'},{name:'string'},{sig:'string'},{modbits:'int'}])); } return res; } ); }, - GetStaticFieldValues:function(typeid, fields) { + + /** + * + * @param {JavaTypeID} typeid + * @param {*[]} fields + */ + GetStaticFieldValues(typeid, fields) { return new Command('GetStaticFieldValues:'+typeid, 2, 6, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, typeid); DataCoder.encodeInt(res, fields.length); - for (var i in fields) { + for (const i in fields) { DataCoder.encodeRef(res, fields[i].fieldid); } return res; }, function(o) { - var res = []; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { - var v = DataCoder.decodeValue(o); + const res = []; + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { + const v = DataCoder.decodeValue(o); res.push(v); } return res; } ); }, - sourcefile:function(ci) { + + /** + * @param {DebuggerTypeInfo} ci + */ + sourcefile(ci) { return new Command('SourceFile:'+ci.name, 2, 7, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); return res; }, @@ -676,46 +794,60 @@ function _JDWP() { } ); }, - fieldsWithGeneric:function(ci) { + + /** + * @param {DebuggerTypeInfo} ci + */ + fieldsWithGeneric(ci) { return new Command('FieldsWithGeneric:'+ci.name, 2, 14, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); return res; }, function(o) { - var arrlen = DataCoder.decodeInt(o); - var res = []; - while (--arrlen>=0) { - var field = DataCoder.decodeList(o, [{fieldid:'fref'},{name:'string'},{type:'signature'},{genericsig:'string'},{modbits:'int'}]); - field.typeid = ci.info.typeid; + let arrlen = DataCoder.decodeInt(o); + /** @type {JavaField[]} */ + const res = []; + while (--arrlen >= 0) { + /** @type {JavaField} */ + // @ts-ignore + const field = DataCoder.decodeList(o, [{fieldid:'fref'},{name:'string'},{type:'signature'},{genericsig:'string'},{modbits:'int'}]); res.push(field); } return res; } ); }, - methodsWithGeneric:function(ci) { + + /** + * @param {DebuggerTypeInfo} ci + */ + methodsWithGeneric(ci) { return new Command('MethodsWithGeneric:'+ci.name, 2, 15, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); return res; }, function(o) { - var arrlen = DataCoder.decodeInt(o); - var res = []; - while (--arrlen>=0) { + let arrlen = DataCoder.decodeInt(o); + const res = []; + while (--arrlen >= 0) { res.push(DataCoder.decodeList(o, [{methodid:'mref'},{name:'string'},{sig:'string'},{genericsig:'string'},{modbits:'int'}])); } return res; } ); }, - superclass:function(ci) { + + /** + * @param {DebuggerTypeInfo} ci + */ + superclass(ci) { return new Command('Superclass:'+ci.name, 3, 1, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); return res; }, @@ -724,10 +856,14 @@ function _JDWP() { } ); }, - signature:function(typeid) { + + /** + * @param {JavaTypeID} typeid + */ + signature(typeid) { return new Command('Signature:'+typeid, 2, 1, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, typeid); return res; }, @@ -736,41 +872,50 @@ function _JDWP() { } ); }, - // nestedTypes is not implemented on android - nestedTypes:function(ci) { + + /** + * nestedTypes is not implemented on android + * @param {DebuggerTypeInfo} ci + */ + nestedTypes(ci) { return new Command('NestedTypes:'+ci.name, 2, 8, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); return res; }, function(o) { - var res=[]; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { - var v = DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'}]); + const res = []; + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { + const v = DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'}]); res.push(v); } return res; } ); }, - lineTable:function(ci, mi) { + + /** + * @param {DebuggerTypeInfo} ci + * @param {DebuggerMethodInfo} mi + */ + lineTable(ci, mi) { return new Command('Linetable:'+ci.name+","+mi.name, 6, 1, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); DataCoder.encodeRef(res, mi.methodid); return res; }, function(o) { - var res = {}; - res.start = DataCoder.decodeLong(o, true); - res.end = DataCoder.decodeLong(o, true); + const res = {}; + res.start = DataCoder.decodeLong(o); + res.end = DataCoder.decodeLong(o); res.lines = []; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { - var line = DataCoder.decodeList(o, [{linecodeidx:'codeindex'},{linenum:'int'}]); + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { + const line = DataCoder.decodeList(o, [{linecodeidx:'codeindex'},{linenum:'int'}]); res.lines.push(line); } // sort the lines by...um..line number @@ -782,76 +927,108 @@ function _JDWP() { } ); }, - VariableTableWithGeneric:function(ci, mi) { + + /** + * @param {DebuggerTypeInfo} ci + * @param {DebuggerMethodInfo} mi + */ + VariableTableWithGeneric(ci, mi) { // VariableTable is not supported by Dalvik return new Command('VariableTableWithGeneric:'+ci.name+","+mi.name, 6, 5, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, ci.info.typeid); DataCoder.encodeRef(res, mi.methodid); return res; }, function(o) { - var res = {}; + /** @type {JavaVarTable} */ + const res = {}; res.argCnt = DataCoder.decodeInt(o); res.vars = []; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { - var v = DataCoder.decodeList(o, [{codeidx:'codeindex'},{name:'string'},{type:'signature'},{genericsig:'string'},{length:'int'},{slot:'int'}]); + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { + /** @type {JavaVar} */ + // @ts-ignore + const v = DataCoder.decodeList(o, [{codeidx:'codeindex'},{name:'string'},{type:'signature'},{genericsig:'string'},{length:'int'},{slot:'int'}]); res.vars.push(v); } return res; } ); }, - Frames:function(threadid, start, count) { + + /** + * @param {JavaThreadID} threadid + * @param {number} start + * @param {number} count + */ + Frames(threadid, start = 0, count = -1) { return new Command('Frames:'+threadid, 11, 6, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, threadid); - DataCoder.encodeInt(res, start||0); - DataCoder.encodeInt(res, count||-1); + DataCoder.encodeInt(res, start); + DataCoder.encodeInt(res, count); return res; }, function(o) { - var res = []; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { - var v = DataCoder.decodeList(o, [{frameid:'frameid'},{location:'location'}]); + /** @type {JavaFrame[]} */ + const res = []; + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { + /** @type {JavaFrame} */ + // @ts-ignore + const v = DataCoder.decodeList(o, [{frameid:'frameid'},{location:'location'}]); res.push(v); } return res; } ); }, - GetStackValues:function(threadid, frameid, slots) { + + /** + * + * @param {JavaThreadID} threadid + * @param {JavaFrameID} frameid + * @param {*[]} slots + */ + GetStackValues(threadid, frameid, slots) { return new Command('GetStackValues:'+threadid, 16, 1, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, threadid); DataCoder.encodeRef(res, frameid); DataCoder.encodeInt(res, slots.length); - for (var i in slots) { + for (const i in slots) { DataCoder.encodeInt(res, slots[i].slot); DataCoder.encodeByte(res, slots[i].tag); } return res; }, function(o) { - var res = []; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { - var v = DataCoder.decodeValue(o); + const res = []; + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { + const v = DataCoder.decodeValue(o); res.push(v); } return res; } ); }, - SetStackValue:function(threadid, frameid, slot, data) { + + /** + * + * @param {JavaThreadID} threadid + * @param {JavaFrameID} frameid + * @param {number} slot + * @param {JavaTaggedValue} data + */ + SetStackValue(threadid, frameid, slot, data) { return new Command('SetStackValue:'+threadid, 16, 2, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, threadid); DataCoder.encodeRef(res, frameid); DataCoder.encodeInt(res, 1); @@ -859,16 +1036,20 @@ function _JDWP() { DataCoder.encodeTaggedValue(res, data.valuetype, data.value); return res; }, - function(o) { + function() { // there's no return data - if we reach here, the update was successfull return true; } ); }, - GetObjectType:function(objectid) { + + /** + * @param {JavaObjectID} objectid + */ + GetObjectType(objectid) { return new Command('GetObjectType:'+objectid, 9, 1, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, objectid); return res; }, @@ -878,54 +1059,76 @@ function _JDWP() { } ); }, - GetFieldValues:function(objectid, fields) { + + /** + * + * @param {JavaObjectID} objectid + * @param {JavaField[]} fields + */ + GetFieldValues(objectid, fields) { return new Command('GetFieldValues:'+objectid, 9, 2, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, objectid); DataCoder.encodeInt(res, fields.length); - for (var i in fields) { + for (const i in fields) { DataCoder.encodeRef(res, fields[i].fieldid); } return res; }, function(o) { - var res = []; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { - var v = DataCoder.decodeValue(o); + const res = []; + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { + const v = DataCoder.decodeValue(o); res.push(v); } return res; } ); }, - SetFieldValue:function(objectid, field, data) { + + /** + * + * @param {JavaObjectID} objectid + * @param {*} field + * @param {*} data + */ + SetFieldValue(objectid, field, data) { return new Command('SetFieldValue:'+objectid, 9, 3, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, objectid); DataCoder.encodeInt(res, 1); DataCoder.encodeRef(res, field.fieldid); DataCoder.encodeValue(res, data.valuetype, data.value); return res; }, - function(o) { - // there's no return data - if we reach here, the update was successfull + function() { + // there's no return data - if we reach here, the update was successful return true; } ); }, - InvokeMethod:function(objectid, threadid, classid, methodid, args) { + + /** + * + * @param {JavaObjectID} objectid + * @param {JavaThreadID} threadid + * @param {JavaClassID} classid + * @param {JavaMethodID} methodid + * @param {JavaTaggedValue[]} args + */ + InvokeMethod(objectid, threadid, classid, methodid, args) { return new Command('InvokeMethod:'+[objectid, threadid, classid, methodid, args].join(','), 9, 6, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, objectid); DataCoder.encodeRef(res, threadid); DataCoder.encodeRef(res, classid); DataCoder.encodeRef(res, methodid); DataCoder.encodeInt(res, args.length); - args.forEach(arg => DataCoder.encodeValue(res, arg.type, arg.value)); + args.forEach(arg => DataCoder.encodeTaggedValue(res, arg.valuetype, arg.value)); DataCoder.encodeInt(res, 1); // INVOKE_SINGLE_THREADED return res; }, @@ -937,10 +1140,41 @@ function _JDWP() { } ); }, - GetArrayLength:function(arrobjid) { + + /** + * @param {JavaThreadID} threadid + * @param {JavaClassID} classid + * @param {JavaMethodID} methodid + * @param {JavaTaggedValue[]} args + */ + InvokeStaticMethod(threadid, classid, methodid, args) { + return new Command('InvokeStaticMethod:'+[threadid, classid, methodid, args].join(','), 3, 3, + function() { + const res = []; + DataCoder.encodeRef(res, classid); + DataCoder.encodeRef(res, threadid); + DataCoder.encodeRef(res, methodid); + DataCoder.encodeInt(res, args.length); + args.forEach(arg => DataCoder.encodeTaggedValue(res, arg.valuetype, arg.value)); + DataCoder.encodeInt(res, 1); // INVOKE_SINGLE_THREADED + return res; + }, + function(o) { + return { + return_value: DataCoder.decodeValue(o), + exception: DataCoder.decodeTaggedObjectID(o), + } + } + ); + }, + + /** + * @param {JavaObjectID} arrobjid + */ + GetArrayLength(arrobjid) { return new Command('GetArrayLength:'+arrobjid, 13, 1, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, arrobjid); return res; }, @@ -949,52 +1183,73 @@ function _JDWP() { } ); }, - GetArrayValues:function(arrobjid, idx, count) { + + /** + * + * @param {JavaObjectID} arrobjid + * @param {number} idx + * @param {number} count + */ + GetArrayValues(arrobjid, idx, count) { return new Command('GetArrayValues:'+arrobjid, 13, 2, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, arrobjid); DataCoder.encodeInt(res, idx); DataCoder.encodeInt(res, count); return res; }, function(o) { - var res = []; - var tag = DataCoder.decodeByte(o); - var decodefn = DataCoder.tagtodecoder(tag); + const res = []; + const tag = DataCoder.decodeByte(o); + let decodefn = DataCoder.tagtoDecoder(tag); // objects are decoded as values - if (decodefn===DataCoder.decodeORef) + if (decodefn === DataCoder.decodeORef) { decodefn = DataCoder.decodeValue; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { - var v = decodefn.call(DataCoder, o); + } + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { + const v = decodefn.call(DataCoder, o); res.push(v); } return res; } ); }, - SetArrayElements:function(arrobjid, idx, count, data) { + + /** + * + * @param {JavaObjectID} arrobjid + * @param {number} idx + * @param {number} count + * @param {JavaTaggedValue} data + */ + SetArrayElements(arrobjid, idx, count, data) { return new Command('SetArrayElements:'+arrobjid, 13, 3, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, arrobjid); DataCoder.encodeInt(res, idx); DataCoder.encodeInt(res, count); - for (var i=0; i < count; i++) + for (let i = 0; i < count; i++) DataCoder.encodeValue(res, data.valuetype, data.value); return res; }, - function(o) { + function() { // there's no return data - if we reach here, the update was successfull return true; } ); }, - GetStringValue:function(strobjid) { + + /** + * + * @param {JavaObjectID} strobjid + */ + GetStringValue(strobjid) { return new Command('GetStringValue:'+strobjid, 10, 1, function() { - var res=[]; + const res = []; DataCoder.encodeRef(res, strobjid); return res; }, @@ -1003,10 +1258,15 @@ function _JDWP() { } ); }, - CreateStringObject:function(text) { + + /** + * + * @param {string} text + */ + CreateStringObject(text) { return new Command('CreateStringObject:'+text.substring(0,20), 1, 11, function() { - var res=[]; + const res = []; DataCoder.encodeString(res, text); return res; }, @@ -1015,45 +1275,73 @@ function _JDWP() { } ); }, - SetEventRequest:function(kindname, kind, suspend, modifiers, modifiercb, onevent) { + + /** + * + * @param {string} kindname + * @param {byte} kind + * @param {byte} suspend + * @param {*[]} modifiers + * @param {(a,b,c) => void} modifiercb + * @param {(o) => void} onevent + */ + SetEventRequest(kindname, kind, suspend, modifiers, modifiercb, onevent) { return new Command('SetEventRequest:'+kindname, 15, 1, function() { - var res=[kind,suspend]; + const res = [kind, suspend]; DataCoder.encodeInt(res, modifiers.length); - for (var i=0;i 0) { // remember when setting a hitcount, the event is automatically cancelled after being fired mods.unshift({ - modkind:1, + modkind: 1, count: hitcount, encode(res) { res.push(this.modkind); @@ -1100,17 +1405,32 @@ function _JDWP() { onevent ); }, - ClearStep:function(requestid) { + + /** + * + * @param {StepID} requestid + */ + ClearStep(requestid) { // kind(1=step) - return this.ClearEvent("step",1,requestid); + return this.ClearEvent("step", 1, requestid); }, - ClearBreakpoint:function(requestid) { + + /** + * + * @param {number} requestid + */ + ClearBreakpoint(requestid) { // kind(2=breakpoint) return this.ClearEvent("breakpoint",2,requestid); }, - ThreadStartNotify:function(onevent) { + + /** + * + * @param {*} onevent + */ + ThreadStartNotify(onevent) { // a wrapper around SetEventRequest - var mods = []; + const mods = []; // kind(6=threadstart) // suspendpolicy(0=none,1=event-thread,2=all) return this.SetEventRequest("threadstart",6,1,mods, @@ -1118,9 +1438,14 @@ function _JDWP() { onevent ); }, - ThreadEndNotify:function(onevent) { + + /** + * + * @param {*} onevent + */ + ThreadEndNotify(onevent) { // a wrapper around SetEventRequest - var mods = []; + const mods = []; // kind(7=threadend) // suspendpolicy(0=none,1=event-thread,2=all) return this.SetEventRequest("threadend",7,1,mods, @@ -1128,11 +1453,16 @@ function _JDWP() { onevent ); }, - OnClassPrepare:function(pattern, onevent) { + + /** + * @param {string} pattern + * @param {*} onevent + */ + OnClassPrepare(pattern, onevent) { // a wrapper around SetEventRequest - var mods = [{ - modkind:5, // classmatch - pattern: pattern, + const mods = [{ + modkind: 5, // classmatch + pattern, }]; // kind(8=classprepare) // suspendpolicy(0=none,1=event-thread,2=all) @@ -1144,21 +1474,35 @@ function _JDWP() { onevent ); }, - ClearExceptionBreak:function(requestid) { + + /** + * + * @param {number} requestid + */ + ClearExceptionBreak(requestid) { // kind(4=exception) return this.ClearEvent("exception",4,requestid); }, - SetExceptionBreak:function(pattern, caught, uncaught, onevent) { + + /** + * + * @param {string} pattern + * @param {boolean} caught + * @param {boolean} uncaught + * @param {*} onevent + */ + SetExceptionBreak(pattern, caught, uncaught, onevent) { // a wrapper around SetEventRequest - var mods = [{ - modkind:8, // exceptiononly + /** @type {(ExOnlyMod|ClassMatchMod)[]} */ + const mods = [{ + modkind: 8, // exceptiononly reftypeid: DataCoder.nullRefValue(), // exception class - caught: caught, - uncaught: uncaught, + caught, + uncaught, }]; pattern && mods.unshift({ - modkind:5, // classmatch - pattern: pattern, + modkind: 5, // classmatch + pattern, }); // kind(4=exception) // suspendpolicy(0=none,1=event-thread,2=all) @@ -1177,80 +1521,104 @@ function _JDWP() { onevent ); }, - allclasses:function() { + + allclasses() { // not supported by android + throw new Error(`allclasses not supported`); }, - AllClassesWithGeneric:function() { + + AllClassesWithGeneric() { return new Command('allclasses',1,20, null, function(o) { - var res = []; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { + const res = []; + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { res.push(DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'},{type:'signature'},{genericSignature:'string'},{status:'status'}])); } return res; } ); }, - suspend:function() { + + suspend() { return new Command('suspend',1, 8, null, null); }, - resume:function() { + + resume() { return new Command('resume',1, 9, null, null); }, - suspendthread:function(threadid) { + + /** + * + * @param {JavaThreadID} threadid + */ + suspendthread(threadid) { return new Command('suspendthread:'+threadid,11, 2, function() { - var res = []; - DataCoder.encodeRef(res, this); + const res = []; + DataCoder.encodeRef(res, threadid); return res; - }.bind(threadid), + }, null ); }, - resumethread:function(threadid) { + + /** + * + * @param {JavaThreadID} threadid + */ + resumethread(threadid) { return new Command('resumethread:'+threadid,11, 3, function() { - var res = []; - DataCoder.encodeRef(res, this); + const res = []; + DataCoder.encodeRef(res, threadid); return res; - }.bind(threadid), + }, null ); }, - allthreads:function() { + + allthreads() { return new Command('allthreads',1, 4, null, function(o) { - var res = []; - var arrlen = DataCoder.decodeInt(o); - while (--arrlen>=0) { + const res = []; + let arrlen = DataCoder.decodeInt(o); + while (--arrlen >= 0) { res.push(DataCoder.decodeTRef(o)); } return res; } ); }, - threadname:function(threadid) { + + /** + * @param {JavaThreadID} threadid + */ + threadname(threadid) { return new Command('threadname',11,1, function() { - var res=[]; - DataCoder.encodeRef(res, this); + const res = []; + DataCoder.encodeRef(res, threadid); return res; - }.bind(threadid), + }, function(o) { return DataCoder.decodeString(o); } ); }, - threadstatus:function(threadid) { + + /** + * @param {JavaThreadID} threadid + */ + threadstatus(threadid) { return new Command('threadstatus',11,4, function() { - var res=[]; - DataCoder.encodeRef(res, this); + const res = []; + DataCoder.encodeRef(res, threadid); return res; - }.bind(threadid), + }, function(o) { return { thread: DataCoder.decodeThreadStatus(o), @@ -1262,4 +1630,6 @@ function _JDWP() { }; } -exports._JDWP = _JDWP; +module.exports = { + JDWP, +} diff --git a/src/jq-promise.js b/src/jq-promise.js deleted file mode 100644 index ce40665..0000000 --- a/src/jq-promise.js +++ /dev/null @@ -1,137 +0,0 @@ -// a very stripped down polyfill implementation of jQuery's promise methods -const util = require('util'); // for util.inspect -var $ = this; - -// Deferred wraps a Promise into a jQuery-like object -var Deferred = exports.Deferred = function(p, parent) { - var o = { - _isdeferred:true, - _original:null, - _promise:null, - _fns:null, - _context:null, - _parent:null, - _root:null, - promise() { - return this; - }, - then(fn) { - var thendef = $.Deferred(null, this); - var p = this._promise.then(function(a) { - var res = this.fn.apply(a._ctx, a._args); - if (res === undefined) - return a; - if (res && res._isdeferred) - return res._promise; - return {_ctx:a._ctx, _args:[res]} - }.bind({def:thendef,fn:fn})); - thendef._promise = thendef._original = p; - return thendef; - }, - always(fn) { - var thendef = this.then(fn); - this.fail(function() { - // we cannot bind thendef to the function because we need the caller's this to resolve the thendef - return thendef.resolveWith(this, Array.prototype.map.call(arguments,x=>x))._promise; - }); - return thendef; - }, - fail(fn) { - var faildef = $.Deferred(null, this); - var p = this._promise.catch(function(a) { - if (a.stack) { - util.E(a.stack); - a = [a]; - } - if (this.def._context === null && this.def._parent) - this.def._context = this.def._parent._context; - if (this.def._context === null && this.def._root) - this.def._context = this.def._root._context; - var res = this.fn.apply(this.def._context,a); - if (res === undefined) - return a; - if (res && res._isdeferred) - return res._promise; - return res; - }.bind({def:faildef,fn:fn})); - faildef._promise = faildef._original = p; - return faildef; - }, - state() { - var m = util.inspect(this._original).match(/^Promise\s*\{\s*<(\w+)>/); // urgh! - // anything that's not pending or rejected is resolved - return m ? m[1] : 'resolved'; - }, - resolve:function() { - return this.resolveWith(null, Array.prototype.map.call(arguments,x=>x)); - }, - resolveWith:function(ths, args) { - if (typeof(args) === 'undefined') args = []; - if (!Array.isArray(args)) - throw new Error('resolveWith must be passed an array of arguments'); - if (this._root) { - this._root.resolveWith(ths, args); - return this; - } - if (ths === null || ths === undefined) ths = this; - this._fns[0]({_ctx:ths,_args:args}); - return this; - }, - reject:function() { - return this.rejectWith(null, Array.prototype.map.call(arguments,x=>x)); - }, - rejectWith:function(ths,args) { - if (typeof(args) === 'undefined') args = []; - if (!Array.isArray(args)) - throw new Error('rejectWith must be passed an array of arguments'); - if (this._root) { - this._root.rejectWith(ths, args); - return this; - } - this._context = ths; - this._fns[1](args); - return this; - }, - } - if (parent) { - o._original = o._promise = p; - o._parent = parent; - o._root = parent._root || parent; - } else { - o._original = o._promise = new Promise((res,rej) => { - o._fns = [res,rej]; - }); - } - return o; -} - -// $.when() is jQuery's version of Promise.all() -// - this version just scans the array of arguments waiting on any Deferreds in turn before finally resolving the return Deferred -var when = exports.when = function() { - if (arguments.length === 1 && Array.isArray(arguments[0])) { - return when.apply(this,...arguments).then(() => [...arguments]); - } - var x = { - def: $.Deferred(), - args: Array.prototype.map.call(arguments,x=>x), - idx:0, - next(x) { - if (x.idx >= x.args.length) { - return process.nextTick(x => { - x.def.resolveWith(null, x.args); - }, x); - } - if ((x.args[x.idx]||{})._isdeferred) { - x.args[x.idx].then(function() { - var x = this, result = Array.prototype.map.call(arguments,x=>x); - x.args[x.idx] = result; - x.idx++; x.next(x); - }.bind(x)); - return; - } - x.idx++; x.next(x); - }, - }; - x.next(x); - return x.def; -} diff --git a/src/jsconfig.json b/src/jsconfig.json index b7caa7d..4788fcd 100644 --- a/src/jsconfig.json +++ b/src/jsconfig.json @@ -1,9 +1,10 @@ { "compilerOptions": { "module": "commonjs", - "target": "es6", + "target": "es2018", + "checkJs": true, "lib": [ - "es6" + "es2018" ] }, "exclude": [ diff --git a/src/logcat.js b/src/logcat.js index e248786..bed8b9f 100644 --- a/src/logcat.js +++ b/src/logcat.js @@ -7,88 +7,134 @@ const WebSocketServer = require('ws').Server; // our stuff const { ADBClient } = require('./adbclient'); const { AndroidContentProvider } = require('./contentprovider'); -const $ = require('./jq-promise'); -const { D } = require('./util'); +const { D } = require('./utils/print'); -/* - Class to setup and store logcat data +/** + * WebSocketServer instance + * @type {WebSocketServer} + */ +let Server = null; + +/** + * Promise resolved once the WebSocketServer is listening + * @type {Promise} + */ +let wss_inited; + +/** + * hashmap of all LogcatContent instances, keyed on device id + * @type {Map} + */ +const LogcatInstances = new Map(); + +/** + * Class to manage logcat data transferred between device and a WebView. + * + * Each LogcatContent instance receives logcat lines via ADB, formats them into + * HTML and sends them to a WebSocketClient running within a WebView page. + * + * The order goes: + * - a new LogcatContent instance is created + * - if this is the first instance, create the WebSocketServer + * - set up handlers to receive logcat messages from ADB + * - upon the first get content(), return the templated HTML page - this is designed to bootstrap the view and create a WebSocket client. + * - when the client connects, start sending logcat messages over the websocket */ class LogcatContent { + /** + * @param {string} deviceid + */ constructor(deviceid) { this._logcatid = deviceid; this._logs = []; this._htmllogs = []; this._oldhtmllogs = []; - this._prevlogs = null; this._notifying = 0; this._refreshRate = 200; // ms - this._state = ''; + this._state = 'connecting'; this._htmltemplate = ''; this._adbclient = new ADBClient(deviceid); - this._initwait = new Promise((resolve, reject) => { - this._state = 'connecting'; - LogcatContent.initWebSocketServer() - .then(() => { - return this._adbclient.logcat({ - onlog: this.onLogcatContent.bind(this), - onclose: this.onLogcatDisconnect.bind(this), - }); - }).then(() => { - this._state = 'connected'; - this._initwait = null; - resolve(this.content); - }).fail(e => { - this._state = 'connect_failed'; - reject(e); - }) - }); - LogcatContent.byLogcatID[this._logcatid] = this; + this._initwait = this.initialise(); + LogcatInstances.set(this._logcatid, this); } - get content() { + + /** + * Ensures the websocket server is initialised and sets up + * logcat handlers for ADB. + * Once everything is ready, returns the initial HTML bootstrap content + * @returns {Promise} + */ + async initialise() { + try { + // create the WebSocket server instance + await initWebSocketServer(); + // register handlers for logcat + await this._adbclient.startLogcatMonitor({ + onlog: this.onLogcatContent.bind(this), + onclose: this.onLogcatDisconnect.bind(this), + }); + this._state = 'connected'; + this._initwait = null; + } catch (err) { + return `Logcat initialisation failed. ${err.message}`; + } + // retrieve the initial content + return this.content(); + } + + /** + * @returns {Promise} + */ + async content() { if (this._initwait) return this._initwait; if (this._state !== 'disconnected') return this.htmlBootstrap({connected:true, status:'',oldlogs:''}); // if we're in the disconnected state, and this.content is called, it means the user has requested // this logcat again - check if the device has reconnected - return this._initwait = new Promise((resolve/*, reject*/) => { - // clear the logs first - if we successfully reconnect, we will be retrieving the entire logcat again - this._prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs }; - this._logs = []; this._htmllogs = []; this._oldhtmllogs = []; - this._adbclient.logcat({ + return this._initwait = this.tryReconnect(); + } + + async tryReconnect() { + // clear the logs first - if we successfully reconnect, we will be retrieving the entire logcat again + const prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs }; + this._logs = []; this._htmllogs = []; this._oldhtmllogs = []; + try { + await this._adbclient.startLogcatMonitor({ onlog: this.onLogcatContent.bind(this), onclose: this.onLogcatDisconnect.bind(this), - }).then(() => { - // we successfully reconnected - this._state = 'connected'; - this._prevlogs = null; - this._initwait = null; - resolve(this.content); - }).fail((/*e*/) => { - // reconnection failed - put the logs back and return the cached info - this._logs = this._prevlogs._logs; - this._htmllogs = this._prevlogs._htmllogs; - this._oldhtmllogs = this._prevlogs._oldhtmllogs; - this._prevlogs = null; - this._initwait = null; - var cached_content = this.htmlBootstrap({connected:false, status:'Device disconnected',oldlogs: this._oldhtmllogs.join(os.EOL)}); - resolve(cached_content); }) - }); + // we successfully reconnected + this._state = 'connected'; + this._initwait = null; + return this.content(); + } catch(err) { + // reconnection failed - put the logs back and return the cached info + this._logs = prevlogs._logs; + this._htmllogs = prevlogs._htmllogs; + this._oldhtmllogs = prevlogs._oldhtmllogs; + this._initwait = null; + const cached_content = this.htmlBootstrap({ + connected: false, + status: 'Device disconnected', + oldlogs: this._oldhtmllogs.join(os.EOL), + }); + return cached_content; + } } + sendClientMessage(msg) { - LogcatContent._wss.clients.forEach(client => { - if (client._logcatid === this._logcatid) { - client.send(msg + '\n'); // include a newline to try and persuade a buffer write - } - }) + const clients = [...Server.clients].filter(client => client['_logcatid'] === this._logcatid); + clients.forEach(client => client.send(msg+'\n')); // include a newline to try and persuade a buffer write } + sendDisconnectMsg() { this.sendClientMessage(':disconnect'); } + onClientConnect(client) { if (this._oldhtmllogs.length) { - var lines = '
' + this._oldhtmllogs.join(os.EOL) + '
'; + const lines = '
' + this._oldhtmllogs.join(os.EOL) + '
'; client.send(lines); } // if the window is tabbed away and then returned to, vscode assumes the content @@ -98,6 +144,7 @@ class LogcatContent { if (this._state === 'disconnected') this.sendDisconnectMsg(); } + onClientMessage(client, message) { if (message === 'cmd:clear_logcat') { if (this._state !== 'connected') return; @@ -107,31 +154,33 @@ class LogcatContent { this._logs = []; this._htmllogs = []; this._oldhtmllogs = []; this.sendClientMessage(':logcat_cleared'); }) - .fail(e => { + .catch(e => { D('Clear logcat command failed: ' + e.message); }) } } + updateLogs() { // no point in formatting the data if there are no connected clients - var clients = [...LogcatContent._wss.clients].filter(client => client._logcatid === this._logcatid); + const clients = [...Server.clients].filter(client => client['_logcatid'] === this._logcatid); if (clients.length) { - var lines = '
' + this._htmllogs.join('') + '
'; + const lines = '
' + this._htmllogs.join('') + '
'; clients.forEach(client => client.send(lines)); } // once we've updated all the clients, discard the info this._oldhtmllogs = this._htmllogs.concat(this._oldhtmllogs).slice(0, 10000); this._htmllogs = [], this._logs = []; } + htmlBootstrap(vars) { if (!this._htmltemplate) this._htmltemplate = fs.readFileSync(path.join(__dirname,'res/logcat.html'), 'utf8'); vars = Object.assign({ logcatid: this._logcatid, - wssport: LogcatContent._wssport, + wssport: Server.options.port, }, vars); // simple value replacement using !{name} as the placeholder - var html = this._htmltemplate.replace(/!\{(.*?)\}/g, (match,expr) => ''+(vars[expr.trim()]||'')); + const html = this._htmltemplate.replace(/!\{(.*?)\}/g, (match,expr) => ''+(vars[expr.trim()]||'')); return html; } renotify() { @@ -146,13 +195,13 @@ class LogcatContent { } onLogcatContent(e) { if (e.logs.length) { - var mrlast = e.logs.slice(); + const mrlast = e.logs.slice(); this._logs = this._logs.concat(mrlast); mrlast.forEach(log => { if (!(log = log.trim())) return; // replace html-interpreted chars - var m = log.match(/^\d\d-\d\d\s+?\d\d:\d\d:\d\d\.\d+?\s+?(.)/); - var style = (m && m[1]) || ''; + const m = log.match(/^\d\d-\d\d\s+?\d\d:\d\d:\d\d\.\d+?\s+?(.)/); + const style = (m && m[1]) || ''; log = log.replace(/[&"'<>]/g, c => ({ '&': '&', '"': '"', "'": ''', '<': '<', '>': '>' }[c])); this._htmllogs.unshift(`
${log}
`); @@ -167,73 +216,85 @@ class LogcatContent { } } -// hashmap of all LogcatContent instances, keyed on device id -LogcatContent.byLogcatID = {}; - -LogcatContent.initWebSocketServer = function () { - - if (LogcatContent._wssdone) { +function initWebSocketServer() { + if (wss_inited) { // already inited - return LogcatContent._wssdone; + return wss_inited; } // retrieve the logcat websocket port - var default_wssport = 7038; - var wssport = AndroidContentProvider.getLaunchConfigSetting('logcatPort', default_wssport); - if (typeof wssport !== 'number' || wssport <= 0 || wssport >= 65536 || wssport !== (wssport|0)) - wssport = default_wssport; + const default_wssport = 7038; + let start_port = AndroidContentProvider.getLaunchConfigSetting('logcatPort', default_wssport); + if (typeof start_port !== 'number' || start_port <= 0 || start_port >= 65536 || start_port !== (start_port|0)) { + start_port = default_wssport; + } - LogcatContent._wssdone = $.Deferred(); - ({ - wss: null, - startport: wssport, - port: wssport, - retries: 0, - tryCreateWSS() { - const wsopts = { - host: '127.0.0.1', - port: this.port, - clientTracking: true, - }; - this.wss = new WebSocketServer(wsopts, () => { - // success - save the info and resolve the deferred - LogcatContent._wssport = this.port; - LogcatContent._wssstartport = this.startport; - LogcatContent._wss = this.wss; - this.wss.on('connection', (client, req) => { - // the client uses the url path to signify which logcat data it wants - client._logcatid = req.url.match(/^\/?(.*)$/)[1]; - var lc = LogcatContent.byLogcatID[client._logcatid]; - if (lc) lc.onClientConnect(client); - else client.close(); - client.on('message', function(message) { - var lc = LogcatContent.byLogcatID[this._logcatid]; - if (lc) lc.onClientMessage(this, message); - }.bind(client)); - /*client.on('close', e => { - console.log('client close'); - });*/ - // try and make sure we don't delay writes - client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true); - }); - this.wss = null; - LogcatContent._wssdone.resolveWith(LogcatContent, []); - }); - this.wss.on('error', (/*err*/) => { - if (!LogcatContent._wss) { - // listen failed -try the next port - this.retries++ , this.port++; - this.tryCreateWSS(); - } - }) + wss_inited = new Promise((resolve, reject) => { + let retries = 100; + tryCreateWebSocketServer(start_port, retries, (err, server) => { + if (err) { + wss_inited = null; + reject(err); + } else { + Server = server; + resolve(); + } + }); + }); + return wss_inited; +} + +/** + * + * @param {number} port + * @param {number} retries + * @param {(err,server?) => void} cb + */ +function tryCreateWebSocketServer(port, retries, cb) { + const wsopts = { + host: '127.0.0.1', + port, + clientTracking: true, + }; + new WebSocketServer(wsopts) + .on('listening', function() { + cb(null, this); + }) + .on('connection', (client, req) => { + onWebSocketClientConnection(client, req); + }) + .on('error', err => { + if (retries <= 0) { + cb(err); + } else { + tryCreateWebSocketServer(port + 1, retries - 1, cb); + } + }) +} + +function onWebSocketClientConnection(client, req) { + // the client uses the url path to signify which logcat data it wants + client._logcatid = req.url.match(/^\/?(.*)$/)[1]; + const lc = LogcatInstances.get(client._logcatid); + if (!lc) { + client.close(); + return; + } + lc.onClientConnect(client); + client.on('message', function(message) { + const lc = LogcatInstances.get(this._logcatid); + if (lc) { + lc.onClientMessage(this, message); } - }).tryCreateWSS(); - return LogcatContent._wssdone; + }.bind(client)); + + // try and make sure we don't delay writes + client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true); } function getADBPort() { - var defaultPort = 5037; - var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort); + const defaultPort = 5037; + const adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort); if (typeof adbPort === 'number' && adbPort === (adbPort|0)) return adbPort; return defaultPort; @@ -243,13 +304,13 @@ function openLogcatWindow(vscode) { new ADBClient().test_adb_connection() .then(err => { // if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number) - var adbport = getADBPort(); - var autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true); + const adbport = getADBPort(); + const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true); if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) { - var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb'); - var adbargs = ['-P',''+adbport,'start-server']; + const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb'); + const adbargs = ['-P',''+adbport,'start-server']; try { - /*var stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'}); + /*const stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'}); } catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves } }) @@ -262,22 +323,26 @@ function openLogcatWindow(vscode) { case 1: return devices; // only one device - just show it } - var multidevicewait = $.Deferred(), prefix = 'Android: View Logcat - ', all = '[ Display All ]'; - var devicelist = devices.map(d => prefix + d.serial); + const prefix = 'Android: View Logcat - ', all = '[ Display All ]'; + const devicelist = devices.map(d => prefix + d.serial); //devicelist.push(prefix + all); - vscode.window.showQuickPick(devicelist) + return vscode.window.showQuickPick(devicelist) .then(which => { if (!which) return; // user cancelled which = which.slice(prefix.length); - new ADBClient().list_devices() + return new ADBClient().list_devices() .then(devices => { - if (which === all) return multidevicewait.resolveWith(this,[devices]); - var found = devices.find(d => d.serial===which); - if (found) return multidevicewait.resolveWith(this,[[found]]); + if (which === all) { + return devices + } + const found = devices.find(d => d.serial === which); + if (found) { + return [found]; + } vscode.window.showInformationMessage('Logcat cannot be displayed. The device is disconnected'); + return null; }); - }); - return multidevicewait; + }, () => null); }) .then(devices => { if (!Array.isArray(devices)) return; // user cancelled (or no devices connected) @@ -292,19 +357,21 @@ function openLogcatWindow(vscode) { } ); const logcat = new LogcatContent(device.serial); - logcat.content.then(html => { + logcat.content().then(html => { panel.webview.html = html; }); return; } - var uri = AndroidContentProvider.getReadLogcatUri(device.serial); - return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two); + const uri = AndroidContentProvider.getReadLogcatUri(device.serial); + vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two); }); }) - .fail((/*e*/) => { + .catch((/*e*/) => { vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?'); }); } -exports.LogcatContent = LogcatContent; -exports.openLogcatWindow = openLogcatWindow; +module.exports = { + LogcatContent, + openLogcatWindow, +} diff --git a/src/manifest.js b/src/manifest.js new file mode 100644 index 0000000..ff624a8 --- /dev/null +++ b/src/manifest.js @@ -0,0 +1,95 @@ +const fs = require('fs'); +const dom = require('xmldom').DOMParser; +const unzipper = require('unzipper'); +const xpath = require('xpath'); + +const { decode_binary_xml } = require('./apk-decoder'); + +/** + * Extracts and decodes the compiled AndroidManifest.xml from an APK + * @param {string} apk_fpn file path to APK + * @returns {Promise} + */ +async function extractManifestFromAPK(apk_fpn) { + const data = await extractFileFromAPK(apk_fpn, /^AndroidManifest\.xml$/); + return decode_binary_xml(data); +} + + +/** + * Extracts a single file from an APK + * @param {string} apk_fpn + * @param {RegExp} file_match + */ +function extractFileFromAPK(apk_fpn, file_match) { + return new Promise((resolve, reject) => { + const file_chunks = []; + let cb_once = (err, data) => { + cb_once = () => {}; + err ? reject(err) : resolve(data); + } + fs.createReadStream(apk_fpn) + .pipe(unzipper.ParseOne(file_match)) + .on('data', chunk => { + file_chunks.push(chunk); + }) + .once('error', err => { + cb_once(err); + }) + .once('end', () => { + cb_once(null, Buffer.concat(file_chunks)); + }); + }) +} + + +/** + * Parses a manifest file to extract package, activities and launch activity + * @param {string} xml AndroidManifest XML text + */ +function parseManifest(xml) { + const result = { + /** + * The package name + */ + package: '', + /** + * the list of Activities stored in the manifest + * @type {string[]} + */ + activities: [], + /** + * the name of the Activity with: + * - intent-filter action = android.intent.action.MAIN and + * - intent-filter category = android.intent.category.LAUNCHER + */ + launcher: '', + } + const doc = new dom().parseFromString(xml); + // extract the package name from the manifest + const pkg_xpath = '/manifest/@package'; + result.package = xpath.select1(pkg_xpath, doc).value; + const android_select = xpath.useNamespaces({"android": "http://schemas.android.com/apk/res/android"}); + + // extract a list of all the (named) activities declared in the manifest + const activity_xpath = '/manifest/application/activity/@android:name'; + const activity_nodes = android_select(activity_xpath, doc); + if (activity_nodes) { + result.activities = activity_nodes.map(n => n.value); + } + + // extract the default launcher activity + const launcher_xpath = '/manifest/application/activity[intent-filter/action[@android:name="android.intent.action.MAIN"] and intent-filter/category[@android:name="android.intent.category.LAUNCHER"]]/@android:name'; + const launcher_nodes = android_select(launcher_xpath, doc); + // should we warn if there's more than one? + if (launcher_nodes && launcher_nodes.length >= 1) { + result.launcher = launcher_nodes[0].value + } + + return result; +} + +module.exports = { + extractManifestFromAPK, + parseManifest, +} diff --git a/src/minwebsocket.js b/src/minwebsocket.js deleted file mode 100644 index da0c0e7..0000000 --- a/src/minwebsocket.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - A dummy websocket implementation for passing messages internally using a WS-like protocol -*/ -var Servers = {}; - -function isfn(x) { return typeof(x) === 'function' } - -function WebSocketClient(url) { - // we only support localhost addresses in this implementation - var match = url.match(/^ws:\/\/127\.0\.0\.1:(\d+)$/); - var port = match && parseInt(match[1],10); - if (!port || port <= 0 || port >= 65536) - throw new Error('Invalid websocket url'); - var server = Servers[port]; - if (!server) throw new Error('Connection refused'); // 'port' already in use :) - server.addClient(this); - this._ws = { - port: port, - server: server, - outgoing:[], - }; -} - -WebSocketClient.prototype.send = function(message) { - this._ws.outgoing.push(message); - if (this._ws.outgoing.length > 1) return; - process.nextTick(function(client) { - if (!client || !client._ws || !client._ws.server) - return; - client._ws.server.receive(client, client._ws.outgoing); - client._ws.outgoing = []; - }, this); -} - -WebSocketClient.prototype.receive = function(messages) { - if (isfn(this.onmessage)) - messages.forEach(m => { - this.onmessage({ - data:m - }); - }); -} - -WebSocketClient.prototype.close = function() { - process.nextTick(() => { - this._ws.server.rmClient(this); - this._ws.server = null; - if (isfn(this.onclose)) - this.onclose(this); - this._ws = null; - }); -} - - - -function WebSocketServer(port) { - if (typeof(port) !== 'number' || port <= 0 || port >= 65536) - throw new Error('Invalid websocket server port'); - if (Servers[''+port]) - throw new Error('Address in use'); - this.port = port; - this.clients = []; - Servers[''+port] = this; -} - -WebSocketServer.prototype.addClient = function(client) { - var status; - this.clients.push(status = { - server:this, - client: client, - onmessage:null, - onclose:null, - outgoing:[], - send: function(message) { - this.outgoing.push(message); - if (this.outgoing.length > 1) return; - process.nextTick(function(status) { - if (!status || !status.client) - return; - status.client.receive(status.outgoing); - status.outgoing = []; - }, this); - } - }); - process.nextTick((status) => { - if (isfn(this.onconnection)) - this.onconnection({ - status: status, - accept:function() { - process.nextTick((status) => { - if (isfn(status.client.onopen)) - status.client.onopen(status.client); - }, this.status); - return this.status; - } - }); - }, status); -} - -WebSocketServer.prototype.rmClient = function(client) { - for (var i = this.clients.length-1; i >= 0; --i) { - if (this.clients[i].client === client) { - if (isfn(this.clients[i].onclose)) - this.clients[i].onclose(); - this.clients.splice(i, 1); - } - } -} - -WebSocketServer.prototype.receive = function(client, messages) { - var status = this.clients.filter(c => c.client === client)[0]; - if (!status) return; - if (!isfn(status.onmessage)) return; - messages.forEach(m => { - status.onmessage({ - data: m, - }); - }); -} - -exports.WebSocketClient = WebSocketClient; -exports.WebSocketServer = WebSocketServer; diff --git a/src/package-searcher.js b/src/package-searcher.js new file mode 100644 index 0000000..409ecf3 --- /dev/null +++ b/src/package-searcher.js @@ -0,0 +1,92 @@ +const fs = require('fs'); +const path = require('path'); +const { hasValidSourceFileExtension } = require('./utils/source-file'); + +class PackageInfo { + /** + * + * @param {string} app_root + * @param {string} src_folder + * @param {string[]} files + * @param {string} pkg_name + * @param {string} package_path + */ + constructor(app_root, src_folder, files, pkg_name, package_path) { + this.package = pkg_name; + this.package_path = package_path; + this.srcroot = path.join(app_root, src_folder), + this.public_classes = files.reduce( + (classes, f) => { + // any file with a Java-identifier-compatible name and a valid extension + const m = f.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\.\w+$/); + if (m && hasValidSourceFileExtension(f)) { + classes.push(m[1]); + } + return classes; + }, []); + } + + /** + * Scan known app folders looking for file changes and package folders + * @param {string} app_root app root directory path + */ + static scanSourceSync(app_root) { + try { + let subpaths = fs.readdirSync(app_root,'utf8'); + const done_subpaths = new Set(); + const src_packages = { + /** + * most recent modification time of a source file + */ + last_src_modified: 0, + /** + * Map of packages detected + * @type {Map} + */ + packages: new Map(), + }; + while (subpaths.length) { + const subpath = subpaths.shift(); + // just in case someone has some crazy circular links going on + if (done_subpaths.has(subpath)) { + continue; + } + done_subpaths.add(subpath); + let subfiles = []; + const package_path = path.join(app_root, subpath); + try { + const stat = fs.statSync(package_path); + src_packages.last_src_modified = Math.max(src_packages.last_src_modified, stat.mtime.getTime()); + if (!stat.isDirectory()) { + continue; + } + subfiles = fs.readdirSync(package_path, 'utf8'); + } + catch (err) { + continue; + } + // ignore folders not starting with a known top-level Android folder + if (!(/^(assets|res|src|main|java|kotlin)([\\/]|$)/.test(subpath))) { + continue; + } + // is this a package folder + const pkgmatch = subpath.match(/^(src|main|java|kotlin)[\\/](.+)/); + if (pkgmatch && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(pkgmatch[2].split(/[\\/]/).pop())) { + // looks good - add it to the list + const src_folder = pkgmatch[1]; // src, main, java or kotlin + const package_name = pkgmatch[2].replace(/[\\/]/g,'.'); + src_packages.packages.set(package_name, new PackageInfo(app_root, src_folder, subfiles, package_name, package_path)); + } + // add the subfiles to the list to process + subpaths = subfiles.map(sf => path.join(subpath,sf)).concat(subpaths); + } + return src_packages; + } catch(err) { + throw new Error('Source path error: ' + err.message); + } + } + +} +module.exports = { + PackageInfo +} diff --git a/src/services.js b/src/services.js deleted file mode 100644 index 4f1356f..0000000 --- a/src/services.js +++ /dev/null @@ -1,322 +0,0 @@ -const chrome = require('./chrome-polyfill').chrome; -const { new_socketfd } = require('./sockets'); -const { create_chrome_socket, accept_chrome_socket, destroy_chrome_socket } = chrome; - -var start_request = function(fd) { - - if (fd.closeState) return; - - // read service passed from client - D('waiting for adb request...'); - readx_with_data(fd, function(err, data) { - if (err) { - D('SS: error %o', err); - return; - } - handle_request(fd, data.asString()); - start_request(fd); - }); -} - -var handle_request = exports.handle_request = function(fd, service) { - if (!service){ - D('SS: no service'); - sendfailmsg(fd, 'No service received'); - return false; - } - D('adb request: %s', service); - - if (service.slice(0,4) === 'host') { - // trim 'host:' - return handle_host_request(service.slice(5), 'kTransportAny', null, fd); - } - - if (!fd.transport) { - D('No transport configured - using any found'); - var t = acquire_one_transport('CS_DEVICE', 'kTransportAny', null); - t = check_one_transport(t, '', fd); - if (!t) return false; - fd.transport = t; - } - - // once we call open_device_service, the fd belongs to the transport - open_device_service(fd.transport, fd, service, function(err, serviceinfo) { - if (err) { - sendfailmsg(fd, 'Device connection failed'); - return; - } - D('device service opened: %o', serviceinfo); - send_okay(fd); - }); - return true; -} - -var sendfailmsg = function(fd, reason) { - reason = reason.slice(0, 0xffff); - var msg = 'FAIL' + intToHex(reason.length,4) + reason; - writex(fd, msg); -} - -var handle_host_request = function(service, ttype, serial, replyfd) { - var transport; - - if (service === 'kill') { - cl('service kill request'); - send_okay(replyfd); - killall_devices(); - //window.close(); - return false; - } - - if (service.slice(0,9) === 'transport') { - var t,serialmatch; - switch(service.slice(9)) { - case '-any': - t = acquire_one_transport('CS_ANY','kTransportAny',null); - break; - case '-local': - t = acquire_one_transport('CS_ANY','kTransportLocal',null); - break; - case '-usb': - t = acquire_one_transport('CS_ANY','kTransportUsb',null); - break; - default: - if (serialmatch = service.slice(9).match(/^:(.+)/)) - t = acquire_one_transport('CS_ANY','kTransportAny',serialmatch[1]); - break; - } - t = check_one_transport(t, serialmatch&&serialmatch[1], replyfd); - if (!t) return false; - - // set the transport in the fd - the client can use it - // to send raw data directly to the device - D('transport configured: %o', t); - replyfd.transport = t; - adb_writebytes(replyfd, "OKAY"); - return false; - } - - if (service.slice(0,7) === 'devices') { - var use_long = service.slice(7)==='-l'; - D('Getting device list'); - var transports = list_transports(use_long); - D('Wrote device list'); - send_msg_with_okay(replyfd, transports); - return false; - } - - if (service === 'version') { - var version = intToHex(ADB_SERVER_VERSION, 4); - send_msg_with_okay(replyfd, version); - return false; - } - - if (service.slice(0,9) === 'emulator:') { - var port = service.slice(9); - port = port&&parseInt(port, 10)||0; - if (!port || port <= 0 || port >= 65536) { - D('Invalid emulator port: %s', service); - return false; - } - local_connect(port, function(err) { - - }); - // no reply needed - return false; - } - - if (service.slice(0,9) === 'get-state') { - transport = acquire_one_transport('CS_ANY', ttype, serial, null); - transport = check_one_transport(transport, serial, replyfd); - if (!transport) return false; - var state = connection_state_name(transport); - send_msg_with_okay(replyfd, state); - return false; - } - - if (service === 'killforward-all') { - remove_all_forward_listeners(); - writex(replyfd, 'OKAY'); - return false; - } - - var fwdmatch = service.match(/^forward:(tcp:\d+);(jdwp:\d+)/); - if (fwdmatch) { - transport = acquire_one_transport('CS_ANY', ttype, serial, null); - transport = check_one_transport(transport, serial, replyfd); - if (!transport) return false; - - install_forward_listener(fwdmatch[1], fwdmatch[2], transport, function(err) { - if (err) return sendfailmsg(replyfd, err.msg); - // on the host, 1st OKAY is connect, 2nd OKAY is status - writex(replyfd, 'OKAY'); - writex(replyfd, 'OKAY'); - }); - return false; - } - - if (service === 'track-devices') { - writex(replyfd, 'OKAY'); - add_device_tracker(replyfd); - // fd now belongs to the tracker - return true; - } - - if (service === 'track-devices-extended') { - writex(replyfd, 'OKAY'); - add_device_tracker(replyfd, true); - // fd now belongs to the tracker - return true; - } - - cl('Ignoring host service request: %s', service); - return false; -} - -var check_one_transport = function(t, serial, replyfd) { - var which = serial||'(null)'; - switch((t||[]).length) { - case 0: - sendfailmsg(replyfd, "device '"+which+"' not found"); - return null; - case 1: t = t[0]; - break; - default: - sendfailmsg(replyfd, 'more than one device/emulator'); - return null; - } - switch(t.connection_state) { - case 'CS_DEVICE': break; - case 'CS_UNAUTHORIZED': - sendfailmsg(replyfd, 'device unauthorized.\r\nCheck for a confirmation dialog on your device or reconnect the device.'); - return null; - default: - sendfailmsg(replyfd, 'Device not ready'); - return null; - } - return t; -} - -var forward_listeners = {}; - -var install_forward_listener = function(local, remote, t, cb) { - var localport = parseInt(local.split(':').pop(), 10); - - var socket = chrome.socket; - - create_chrome_socket('forward listener:'+localport, function(socketInfo) { - if (chrome.runtime.lastError) { - return cb({msg:chrome.runtime.lastError.message||'socket creation failed'}); - } - socket.listen(socketInfo.socketId, '127.0.0.1', localport, 5, - function(result) { - if (chrome.runtime.lastError) { - var err = {msg:chrome.runtime.lastError.message||'socket listen failed'}; - destroy_setup(socketInfo); - return cb(err); - } - if (result < 0) { - destroy_setup(socketInfo); - return cb({msg:'Cannot bind to socket'}); - } - - forward_listeners[localport] = { - port:localport, - socketId: socketInfo.socketId, - connectors_fd: null, - connect_cb:function(){}, - }; - - accept_chrome_socket('forward server:'+localport, socketInfo.socketId, function(acceptInfo) { - accept_forward_connection(socketInfo.socketId, acceptInfo, localport, local, remote, t); - }); - - // listener is ready - D('started forward listener on port %d: %d', localport, socketInfo.socketId); - cb(); - } - ); - }); - - function destroy_setup(socketInfo) { - destroy_chrome_socket(socketInfo.socketId); - } -} - -var connect_forward_listener = exports.connect_forward_listener = function(port, opts, cb) { - - // if we're implementing the adb service, this will already be created - // if we're connecting via the adb executable, we need to create a dummy entry - if (!forward_listeners[port]) { - if (opts && opts.create) { - forward_listeners[port] = { - is_external_adb: true, - port:port, - socketId: null, - connectors_fd: null, - connect_cb:function(){}, - } - } else { - D('Refusing forward connection request - forwarder for port %d does not exist', port); - return cb(); - } - } - - create_chrome_socket('forward client:'+port, function(createInfo) { - // save the receiver info - forward_listeners[port].connectors_fd = new_socketfd(createInfo.socketId); - forward_listeners[port].connect_cb = cb; - - // do the connect - everything from here on is handled in the accept routine - chrome.socket.connect(createInfo.socketId, '127.0.0.1', port, function(result) { - chrome.socket.setNoDelay(createInfo.socketId, true, function(result) { - var x = forward_listeners[port]; - if (x.is_external_adb) { - delete forward_listeners[port]; - x.connect_cb(x.connectors_fd); - } - }); - }); - }); -} - -var accept_forward_connection = exports.accept_forward_connection = function(listenerSocketId, acceptInfo, port, local, remote, t) { - if (chrome.runtime.lastError) { - D('Forward port socket accept failed: '+port); - var listener = remove_forward_listener(listenerSocketId); - return listener.connect_cb(); - } - - // on accept - create the remote connection to the device - D('Binding forward port connection to remote port %s', remote); - var sfd = new_socketfd(acceptInfo.socketId); - - // remove the listener - var listener = remove_forward_listener(listenerSocketId); - - chrome.socket.setNoDelay(acceptInfo.socketId, true, function(result) { - // start the connection as a service - open_device_service(t, sfd, remote, function(err) { - listener.connect_cb(listener.connectors_fd); - }); - }); -} - -var remove_forward_listener = exports.remove_forward_listener = function(socketId) { - for (var port in forward_listeners) { - if (forward_listeners[port].socketId === socketId) { - var x = forward_listeners[port]; - delete forward_listeners[port]; - destroy_chrome_socket(x.socketId); - D('removed forward listener: %d', x.socketId); - return x; - } - } -} - -var remove_all_forward_listeners = exports.remove_all_forward_listeners = function() { - var ports = Object.keys(forward_listeners); - while (ports.length) { - remove_forward_listener(forward_listeners[ports.pop()].socketId); - } -} \ No newline at end of file diff --git a/src/sockets.js b/src/sockets.js deleted file mode 100644 index 13c3bc6..0000000 --- a/src/sockets.js +++ /dev/null @@ -1,290 +0,0 @@ -const chrome = require('./chrome-polyfill').chrome; -const { create_chrome_socket, destroy_chrome_socket } = chrome; -const { D, remove_from_list } = require('./util'); - -// array of local_sockets -var _local_sockets = []; - -var _new_local_socket_id = 1000; -var new_local_socket = function(t, fd, close_fd_on_local_socket_close) { - var x = { - id:++_new_local_socket_id, - fd:fd, - close_fd_on_local_socket_close: !!close_fd_on_local_socket_close, - transport:t, - enqueue: local_socket_enqueue, - ready: local_socket_ready_notify, - close: local_socket_close, - peer:null, - //socketbuffer: [], - } - _local_sockets.push(x); - return x; -} - -var find_local_socket = function(local_socket_id, peer_socket_id) { - for (var i=0; i < _local_sockets.length; i++) { - var ls = _local_sockets[i]; - if (ls.id === local_socket_id) { - if (!peer_socket_id) return ls; - if (!ls.peer) continue; - if (ls.peer.id === peer_socket_id) return ls; - } - } - return null; -} - -var local_socket_ready = function(s) { - D("LS(%d): ready()\n", s.id); -} - -var local_socket_ready_notify = function(s) { - s.ready = local_socket_ready; - send_okay(s.fd); - s.ready(s); -} - -var local_socket_enqueue = function(s, p) { - D("LS(%d): enqueue()\n", s.id, p.len); - - if (s.fd.closed) return false; - - D("LS: enqueue() - writing %d bytes to fd:%d %o\n", p.len, s.fd.n, s.fd); - adb_writebytes(s.fd, p.data, p.len); - //s.socketbuffer.push({data:p.data, len:p.len}); - return true; -} - -var local_socket_close = function(s) { - // flush the data to the output socket - /*var totallen = s.socketbuffer.reduce(function(n, x) { return n+x.len },0); - adb_writebytes(s.fd, intToHex(totallen,4)); - s.socketbuffer.forEach(function(x) { - adb_writebytes(s.fd, x.data, x.len); - });*/ - - if (s.peer) { - s.peer.peer = null; - s.peer.close(s.peer); - s.peer = null; - } - - if (s.fd && s.close_fd_on_local_socket_close) { - s.fd.close(); - } - - var id = s.id; - var idx = _local_sockets.indexOf(s); - if (idx >= 0) _local_sockets.splice(idx, 1); - D("LS(%d): closed()\n", id); -} - -var local_socket_force_close_all = function(t) { - // called when a transport disconnects without a clean finish - var lsarr = _local_sockets.reduce(function(res, ls) { - if (ls && ls.transport === t) res.push(ls); - return res; - }, []); - lsarr.forEach(function(ls) { - D('force closing socket: %o', ls); - local_socket_close(ls); - }); -} - -var remote_socket_ready = function(s, cb) { - D("entered remote_socket_ready RS(%d) OKAY fd=%d peer.fd=%d\n", - s.id, s.fd, s.peer.fd); - p = get_apacket(); - p.msg.command = A_OKAY; - p.msg.arg0 = s.peer.id; - p.msg.arg1 = s.id; - send_packet(p, s.transport, cb); -} - -var remote_socket_close = function(s) { - if (s.peer) { - s.peer.peer = null; - s.peer.close(s.peer); - } - D("RS(%d): closed\n", s.id); -} - -var create_remote_socket = function(id, t) { - var s = { - id: id, - transport: t, - peer:null, - ready: remote_socket_ready, - close: remote_socket_close, - - // a remote socket is a normal socket with an extra disconnect function - disconnect:null, - } - D("RS(%d): created\n", s.id); - - // when a - return s; -} - - -var loopback_clients = []; - -var get_socket_fd_from_fdn = exports.get_socket_fd_from_fdn = function(n) { - for (var i=0; i < loopback_clients.length; i++) { - if (loopback_clients[i].n === n) - return loopback_clients[i]; - } - return null; -} - -var socket_loopback_client = exports.socket_loopback_client = function(port, cb) { - create_chrome_socket('socket_loopback_client', function(createInfo) { - chrome.socket.connect(createInfo.socketId, '127.0.0.1', port, function(result) { - if (result < 0) { - destroy_chrome_socket(createInfo.socketId); - return cb(); - } - chrome.socket.setNoDelay(createInfo.socketId, true, function(result) { - var x = new_socketfd(createInfo.socketId); - return cb(x); - }); - }); - }); -} - -var new_socketfd = exports.new_socketfd = function(socketId) { - var x = { - n: socketId, - isSocket:true, - connected:true, - closed:false, - // readbytes and writebytes are used by readx and writex - readbytes:function(len, cb) { - slc_read(this, len, function(err, data){ - cb(err, data); - }); - }, - writebytes:function(data, cb) { - slc_write(this, data, cb||function(){}); - }, - close:function() { - slc_close(this, function(){}); - } - }; - loopback_clients.push(x); - return x; -} - -var slc_readwithkick = function(sfd, cb) { - - /*if (sfd.reader_cb_stack.length) { - return cb(null, new Uint8Array(0)); - }*/ - - //var readinfo = {cb:cb, expired:false}; - //sfd.reader_cb_stack.push(readinfo); - - var kicker = setTimeout(function() { - if (!kicker) return; - kicker = null; - D('reader kick expired - retuning nothing'); - //readinfo.expired = true; - cb(null, new Uint8Array(0)); - }, 100); - - slc_read_stacked_(sfd, function(err, data) { - if (!kicker) { - D('Discarding data recevied after kick expired'); - return; - } - clearTimeout(kicker); - kicker = null; - cb(err, data); - }); -}; - -var slc_read = function(sfd, minlen, cb) { - //sfd.reader_cb_stack.push({cb:cb, expired:false}); - slc_read_stacked_(sfd, minlen, cb); -} - -var slc_read_stacked_ = function(sfd, minlen, cb) { - var params = [sfd.n]; - switch(typeof(minlen)) { - case 'number': params.push(minlen); break; - case 'function': cb = minlen; // fall through - default: minlen = 'any'; - }; - var buffer = new Uint8Array(minlen==='any'?65536:minlen); - var buffer_offset = 0; - var onread = function(readInfo) { - if (chrome.runtime.lastError) { - slc_close(sfd, function() { - cb({msg: 'socket read error. Terminating socket'}); - }); - return; - } - if (readInfo.resultCode < 0) return cb(readInfo); - - buffer.set(new Uint8Array(readInfo.data), buffer_offset); - buffer_offset += readInfo.data.byteLength; - if (typeof(minlen)==='number' &&buffer_offset < minlen) { - // read more - params[1] = minlen - buffer_offset; - chrome.socket.read.apply(chrome.socket, params); - return; - } - buffer = buffer.subarray(0, buffer_offset); - buffer.asString = function() { return arrayBufferToString(this); } - return cb(null, buffer); - }; - params.push(onread); - chrome.socket.read.apply(chrome.socket, params); -} - -var slc_write = function(sfd, data, cb) { - var buf = data.buffer; - if (buf.byteLength !== data.byteLength) { - buf = buf.slice(0, data.byteLength); - } - chrome.socket.write(sfd.n, buf, function(writeInfo) { - if (chrome.runtime.lastError) { - slc_close(sfd, function() { - cb({msg: 'socket write error. Terminating socket'}); - }); - return; - } - if (writeInfo.bytesWritten !== data.byteLength) - return cb({msg: 'socket write mismatch. wanted:'+data.byteLength+', sent:'+writeInfo.bytesWritten}); - cb(); - }); -} - -var slc_shutdown = function(sfd, cb) { - if (sfd.connected) { - sfd.connected = false; - chrome.socket.disconnect(sfd.n); - } - cb(); -} - -var slc_close = function(sfd, cb) { - if (sfd.connected) { - sfd.connected = false; - chrome.socket.disconnect(sfd.n); - } - sfd.closed = true; - destroy_chrome_socket(sfd.n); - remove_from_list(loopback_clients, sfd); - cb(); -} - - -var fd_loopback_client = function() { - var s = []; - adb_socketpair(s, 'fd_loopback_client', true); - D('fd_loopback_client created. server fd:%d, client fd:%d', s[1].n, s[0].n); - // return one side and pass the other side to the request handler - start_request(s[1]); - return s[0]; -} diff --git a/src/sockets/adbsocket.js b/src/sockets/adbsocket.js new file mode 100644 index 0000000..7bb0150 --- /dev/null +++ b/src/sockets/adbsocket.js @@ -0,0 +1,143 @@ +const AndroidSocket = require('./androidsocket'); + + +/** + * Manages a socket connection to Android Debug Bridge + */ +class ADBSocket extends AndroidSocket { + + /** + * The port number to run ADB on. + * The value can be overriden by the adbPort value in each configuration. + */ + static ADBPort = 5037; + + constructor() { + super('ADBSocket'); + } + + /** + * Reads and checks the reply from an ADB command + * @param {boolean} [throw_on_fail] true if the function should throw on non-OKAY status + */ + async read_adb_status(throw_on_fail = true) { + // read back the status + const status = await this.read_bytes(4, 'latin1') + if (status !== 'OKAY' && throw_on_fail) { + throw new Error(`ADB command failed. Status: '${status}'`); + } + return status; + } + + /** + * Reads and decodes an ADB reply. The reply is always in the form XXXXnnnn where XXXX is a 4 digit ascii hex length + */ + async read_adb_reply() { + const hexlen = await this.read_bytes(4, 'latin1'); + if (/[^\da-fA-F]/.test(hexlen)) { + throw new Error('Bad ADB reply - invalid length data'); + } + return this.read_bytes(parseInt(hexlen, 16), 'latin1'); + } + + /** + * Writes a command to the ADB socket + * @param {string} command + */ + write_adb_command(command) { + const command_bytes = Buffer.from(command); + const command_length = Buffer.from(('000' + command_bytes.byteLength.toString(16)).slice(-4)); + return this.write_bytes(Buffer.concat([command_length, command_bytes])); + } + + /** + * Sends an ADB command and checks the returned status + * @param {String} command ADB command to send + * @returns {Promise} OKAY status or rejected + */ + async cmd_and_status(command) { + await this.write_adb_command(command); + return this.read_adb_status(); + } + + /** + * Sends an ADB command, checks the returned status and then reads the return reply + * @param {String} command ADB command to send + * @returns {Promise} reply string or rejected if the status is not OKAY + */ + async cmd_and_reply(command) { + await this.cmd_and_status(command); + return this.read_adb_reply(); + } + + /** + * Sends an ADB command, checks the returned status and then reads raw data from the socket + * @param {string} command + */ + async cmd_and_read_stdout(command) { + await this.cmd_and_status(command); + return this.read_stdout(); + } + + /** + * Copies a file to the device, setting the file time and permissions + * @param {ADBFileTransferParams} file file parameters + */ + async transfer_file(file) { + await this.cmd_and_status('sync:'); + + // initiate the file send + const filename_and_perms = `${file.pathname},${file.perms}`; + const send_and_fileinfo = Buffer.from(`SEND\0\0\0\0${filename_and_perms}`); + send_and_fileinfo.writeUInt32LE(filename_and_perms.length, 4); + await this.write_bytes(send_and_fileinfo); + + // send the file data + await this.write_file_data(file.data); + + // send the DONE message with the new filetime + const done_and_mtime = Buffer.from('DONE\0\0\0\0'); + done_and_mtime.writeUInt32LE(file.mtime, 4); + await this.write_bytes(done_and_mtime); + + // read the final status and any error message + const result = await this.read_adb_status(false); + const failmsg = await this.read_le_length_data('latin1'); + + // finish the transfer mode + await this.write_bytes('QUIT\0\0\0\0'); + + if (result !== 'OKAY') { + throw new Error(`File transfer failed. ${failmsg}`); + } + return true; + } + + /** + * @param {Buffer} data + */ + async write_file_data(data) { + const dtinfo = { + transferred: 0, + transferring: 0, + chunk_size: 10240, + }; + + for (;;) { + dtinfo.transferred += dtinfo.transferring; + const remaining = data.byteLength - dtinfo.transferred; + if (remaining <= 0 || isNaN(remaining)) { + return dtinfo.transferred; + } + const datalen = Math.min(remaining, dtinfo.chunk_size); + + const cmd = Buffer.concat([Buffer.from(`DATA\0\0\0\0`), data.slice(dtinfo.transferred, dtinfo.transferred + datalen)]); + cmd.writeUInt32LE(datalen, 4); + + dtinfo.transferring = datalen; + await this.write_bytes(cmd); + } + } +} + +module.exports = ADBSocket; diff --git a/src/sockets/androidsocket.js b/src/sockets/androidsocket.js new file mode 100644 index 0000000..384d861 --- /dev/null +++ b/src/sockets/androidsocket.js @@ -0,0 +1,159 @@ +const net = require('net'); +const EventEmitter = require('events'); + +/** + * Common socket class for ADBSocket and JDWPSocket + */ +class AndroidSocket extends EventEmitter { + constructor(which) { + super() + this.which = which; + this.socket = null; + this.socket_error = null; + this.socket_ended = false; + this.readbuffer = Buffer.alloc(0); + } + + connect(port, hostname) { + return new Promise((resolve, reject) => { + if (this.socket) { + return reject(new Error(`${this.which} Socket connect failed. Socket already connected.`)); + } + const connection_error = err => { + return reject(new Error(`${this.which} Socket connect failed. ${err.message}.`)); + } + const post_connection_error = err => { + this.socket_error = err; + this.socket.end(); + } + let error_handler = connection_error; + this.socket = new net.Socket() + .once('connect', () => { + error_handler = post_connection_error; + this.socket + .on('data', buffer => { + this.readbuffer = Buffer.concat([this.readbuffer, buffer]); + this.emit('data-changed'); + }) + .once('end', () => { + this.socket_ended = true; + this.emit('socket-ended'); + if (!this.socket_disconnecting) { + this.socket_disconnecting = this.socket_error ? Promise.reject(this.socket_error) : Promise.resolve(); + } + }); + resolve(); + }) + .on('error', err => error_handler(err)); + this.socket.connect(port, hostname); + }); + } + + disconnect() { + if (!this.socket_disconnecting) { + this.socket_disconnecting = new Promise(resolve => { + this.socket.end(); + this.socket = null; + this.once('socket-ended', resolve); + }); + } + return this.socket_disconnecting; + } + + /** + * + * @param {number|'length+data'|undefined} length + * @param {string} [format] + */ + async read_bytes(length, format) { + //D(`reading ${length} bytes`); + let actual_length = length; + if (typeof actual_length === 'undefined') { + if (this.readbuffer.byteLength > 0 || this.socket_ended) { + actual_length = this.readbuffer.byteLength; + } + } + if (actual_length < 0) { + throw new Error(`${this.which} socket read failed. Attempt to read ${actual_length} bytes.`); + } + if (length === 'length+data' && this.readbuffer.byteLength >= 4) { + length = actual_length = this.readbuffer.readUInt32BE(0); + } + if (this.socket_ended) { + if (actual_length <= 0 || (this.readbuffer.byteLength < actual_length)) { + this.check_socket_active('read'); + } + } + // do we have enough data in the buffer? + if (this.readbuffer.byteLength >= actual_length) { + //D(`got ${actual_length} bytes`); + let data = this.readbuffer.slice(0, actual_length); + this.readbuffer = this.readbuffer.slice(actual_length); + if (format) { + data = data.toString(format); + } + return Promise.resolve(data); + } + // wait for the socket to update and then retry the read + await this.wait_for_socket_data(); + return this.read_bytes(length, format); + } + + wait_for_socket_data() { + return new Promise((resolve, reject) => { + let done = 0; + let onDataChanged = () => { + if ((done += 1) !== 1) return; + this.off('socket-ended', onSocketEnded); + resolve(); + } + let onSocketEnded = () => { + if ((done += 1) !== 1) return; + this.off('data-changed', onDataChanged); + reject(new Error(`${this.which} socket read failed. Socket closed.`)); + } + this.once('data-changed', onDataChanged); + this.once('socket-ended', onSocketEnded); + }); + } + + async read_le_length_data(format) { + const len = await this.read_bytes(4); + return this.read_bytes(len.readUInt32LE(0), format); + } + + read_stdout(format = 'latin1') { + return this.read_bytes(undefined, format); + } + + /** + * Writes a raw command to the socket + * @param {string|Buffer} bytes + */ + write_bytes(bytes) { + return new Promise((resolve, reject) => { + this.check_socket_active('write'); + try { + const flushed = this.socket.write(bytes, () => { + flushed ? resolve() : this.socket.once('drain', resolve); + }); + } catch (e) { + this.socket_error = e; + reject(new Error(`${this.which} socket write failed. ${e.message}`)); + } + }); + } + + /** + * + * @param {'read'|'write'} action + */ + check_socket_active(action) { + if (this.socket_ended) { + throw new Error(`${this.which} socket ${action} failed. Socket closed.`); + } + + } +} + +module.exports = AndroidSocket; diff --git a/src/sockets/jdwpsocket.js b/src/sockets/jdwpsocket.js new file mode 100644 index 0000000..26ea9eb --- /dev/null +++ b/src/sockets/jdwpsocket.js @@ -0,0 +1,122 @@ +const AndroidSocket = require('./androidsocket'); + +/** + * Manages a JDWP connection to the device + * The debugger uses ADB to setup JDWP port forwarding to the device - this class + * connects to the local forwarding port + */ +class JDWPSocket extends AndroidSocket { + /** + * @param {(data)=>*} decode_reply function used for decoding raw JDWP data + * @param {()=>void} on_disconnect function called when the socket disconnects + */ + constructor(decode_reply, on_disconnect) { + super('JDWP') + this.decode_reply = decode_reply; + this.on_disconnect = on_disconnect; + /** @type {Map<*,function>} */ + this.cmds_in_progress = new Map(); + this.cmd_queue = []; + } + + /** + * Performs the JDWP handshake and begins reading the socket for JDWP events/replies + */ + async start() { + const handshake = 'JDWP-Handshake'; + await this.write_bytes(handshake); + const handshake_reply = await this.read_bytes(handshake.length, 'latin1'); + if (handshake_reply !== handshake) { + throw new Error('JDWP handshake failed'); + } + this.start_jdwp_reply_reader(); + return true; + } + + /** + * Continuously reads replies from the JDWP socket. After each reply is read, + * it's matched up with its corresponding command using the request ID. + */ + async start_jdwp_reply_reader() { + for (;;) { + let data; + try { + data = await this.read_bytes('length+data'/* , 'latin1' */) + } catch (e) { + // ignore socket closed errors (sent when the debugger disconnects) + if (!/socket closed/i.test(e.message)) + throw e; + if (typeof this.on_disconnect === 'function') { + this.on_disconnect(); + } + return; + } + const reply = this.decode_reply(data); + const on_reply = this.cmds_in_progress.get(reply.command); + if (on_reply) { + on_reply(reply); + } + } + } + + /** + * Send a single command to the device and wait for the reply + * @param {*} command + */ + process_cmd(command) { + return new Promise(resolve => { + // add the command to the in-progress set + this.cmds_in_progress.set(command, reply => { + // once the command has completed, delete it from in-progress and resolve the promise + this.cmds_in_progress.delete(command); + resolve(reply); + }); + // send the raw command bytes to the device + this.write_bytes(command.toBuffer()); + }); + } + + /** + * Drain the queue of JDWP commands waiting to be sent to the device + */ + async run_cmd_queue() { + for (;;) { + if (this.cmd_queue.length === 0) { + return; + } + const { command, resolve, reject } = this.cmd_queue[0]; + const reply = await this.process_cmd(command); + if (reply.errorcode) { + class JDWPCommandError extends Error { + constructor(reply) { + super(`JDWP command failed '${reply.command.name}'. Error ${reply.errorcode}`); + this.command = reply.command; + this.errorcode = reply.errorcode; + } + } + reject(new JDWPCommandError(reply)); + } else { + resolve(reply); + } + this.cmd_queue.shift(); + } + } + + /** + * Queue a command to be sent to the device and wait for the reply + * @param {*} command + */ + async cmd_and_reply(command) { + return new Promise((resolve, reject) => { + const queuelen = this.cmd_queue.push({ + command, + resolve, reject + }) + if (queuelen === 1) { + this.run_cmd_queue(); + } + }) + } +} + +module.exports = JDWPSocket; diff --git a/src/stack-frame.js b/src/stack-frame.js new file mode 100644 index 0000000..f5294e3 --- /dev/null +++ b/src/stack-frame.js @@ -0,0 +1,276 @@ +const { Debugger } = require('./debugger'); +const { DebuggerFrameInfo, DebuggerValue, JavaType, LiteralValue, VariableValue } = require('./debugger-types'); +const { assignVariable } = require('./expression/assign'); +const { NumberBaseConverter } = require('./utils/nbc'); +const { VariableManager } = require('./variable-manager'); + +/** + * @param {DebuggerValue[]} variables + * @param {boolean} thisFirst + * @param {boolean} allCapsLast + */ +function sortVariables(variables, thisFirst, allCapsLast) { + return variables.sort((a,b) => { + if (a.name === b.name) return 0; + if (thisFirst) { + if (a.name === 'this') return -1; + if (b.name === 'this') return +1; + } + if (allCapsLast) { + const acaps = !/[a-z]/.test(a.name); + const bcaps = !/[a-z]/.test(b.name); + if (acaps !== bcaps) { + return acaps ? +1 : -1; + } + } + return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); + }); +} + +class DebuggerStackFrame extends VariableManager { + + /** + * @param {Debugger} dbgr + * @param {DebuggerFrameInfo} frame + * @param {VSCVariableReference} frame_variable_reference + */ + constructor(dbgr, frame, frame_variable_reference) { + super(frame_variable_reference ); + this.variableReference = frame_variable_reference; + this.dbgr = dbgr; + this.frame = frame; + /** @type {DebuggerValue[]} */ + this.locals = null; + } + + /** + * Return the list of local values for this stack frame + * @returns {Promise} + */ + async getLocals() { + if (this.locals) { + return this.locals; + } + const fetch_locals = async () => { + const values = await this.dbgr.getLocals(this.frame); + // display the variables in (case-insensitive) alphabetical order, with 'this' first and all-caps last + return this.locals = sortVariables(values, true, false); + } + // @ts-ignore + return this.locals = fetch_locals(); + } + + async getLocalVariables() { + const values = await this.getLocals(); + return values.map(value => this.makeVariableValue(value)); + } + + /** + * @param {VSCVariableReference} variablesReference + * @param {string} name + * @param {DebuggerValue} value + */ + async setVariableValue(variablesReference, name, value) { + + /** @type {DebuggerValue[]} */ + let variables; + if (variablesReference === this.variableReference) { + variables = this.locals; + } else { + const varinfo = this.variableValues.get(variablesReference); + if (!varinfo || !varinfo.cached) { + throw new Error(`Variable '${name}' not found`); + } + variables = varinfo.cached; + } + + const var_idx = variables.findIndex(v => v.name === name); + + try { + const updated_value = await assignVariable(this.dbgr, variables[var_idx], name, value); + variables[var_idx] = updated_value; + return this.makeVariableValue(updated_value); + } catch(e) { + throw new Error(`Variable update failed. ${e.message}`); + } + } + + /** + * @param {VSCVariableReference} variablesReference + * @returns {Promise} + */ + async getExpandableValues(variablesReference) { + const varinfo = this.variableValues.get(variablesReference); + if (!varinfo) { + return []; + } + if (varinfo.cached) { + // return the cached version + return varinfo.cached.map(v => this.makeVariableValue(v)); + } + if (varinfo.primitive) { + // convert the primitive value into alternate formats + return this.getPrimitive(varinfo); + } + + /** @type {DebuggerValue[]} */ + let values = []; + if (varinfo.objvar) { + // object fields request + values = sortVariables(await this.getObjectFields(varinfo), false, true); + } + else if (varinfo.arrvar) { + // array elements request + const arr = await this.getArrayElements(varinfo); + if (arr.isSubrange) { + // @ts-ignore + return arr.values; + } + // @ts-ignore + values = arr.values; + } + else if (varinfo.bigstring) { + values = [await this.getBigString(varinfo)]; + } + + return (varinfo.cached = values).map(v => this.makeVariableValue(v)); + } + + async getObjectFields(varinfo) { + const supertype = await this.dbgr.getSuperType(varinfo.objvar); + const fields = await this.dbgr.getFieldValues(varinfo.objvar); + // add an extra msg field for exceptions + if (varinfo.exception) { + const call = await this.dbgr.invokeToString(varinfo.objvar.value, varinfo.threadid, varinfo.objvar.type.signature); + call.name = ":message"; + fields.unshift(call); + } + // add a ":super" member, unless the super is Object + if (supertype && supertype.signature !== JavaType.Object.signature) { + fields.unshift(new DebuggerValue('super', supertype, varinfo.objvar.value, true, false, ':super', null)); + } + return fields; + } + + async getArrayElements(varinfo) { + const range = varinfo.range, + count = range[1] - range[0]; + // should always have a +ve count, but just in case... + if (count <= 0) { + return null; + } + // counts over 110 are shown as subranges + if (count > 110) { + return { + isSubrange: true, + values: this.getArraySubrange(varinfo.arrvar, count, range), + }; + } + // get the elements for the specified range + const elements = await this.dbgr.getArrayElementValues(varinfo.arrvar, range[0], count); + return { + isSubrange: false, + values: elements, + } + } + + /** + * + * @param {*} arrvar + * @param {number} count + * @param {[number,number]} range + */ + getArraySubrange(arrvar, count, range) { + // create subranges in the sub-power of 10 + const subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100); + /** @type {VariableValue[]} */ + const variables = []; + + for (let i = range[0]; i < range[1]; i+= subrangelen) { + const varinfo = { + varref: 0, + arrvar, + range: [i, Math.min(i+subrangelen, range[1])], + }; + const varref = this._addVariable(varinfo); + const variable = new VariableValue(`[${varinfo.range[0]}..${varinfo.range[1]-1}]`, '', null, varref, ''); + variables.push(variable); + } + + return variables; + } + + async getBigString(varinfo) { + const string = await this.dbgr.getStringText(varinfo.bigstring.value); + const res = new LiteralValue(JavaType.String, string); + res.name = ''; + res.string = string; + return res; + } + + getPrimitive(varinfo) { + /** @type {VariableValue[]} */ + const variables = []; + const bits = { + J:64, + I:32, + S:16, + B:8, + }[varinfo.signature]; + + /** + * + * @param {number|hex64} n + * @param {number} base + * @param {number} len + */ + function convert(n, base, len) { + let converted; + if (typeof n === 'string') { + converted = { + 2: () => n.replace(/./g, c => parseInt(c,16).toString(2)), + 10: () => NumberBaseConverter.hexToDec(n, false), + 16: () => n, + }[base](); + } else { + converted = n.toString(base); + } + return converted.padStart(len, '0'); + } + + /** + * @param {number|hex64} u + * @param {8|16|32|64} bits + */ + function getIntFormats(u, bits) { + const bases = [2, 10, 16]; + const min_lengths = [bits, 1, bits/4]; + const base_names = ['', '', '']; + return base_names.map((name, i) => new VariableValue(name, convert(u, bases[i], min_lengths[i]))); + } + + switch(varinfo.signature) { + case 'Ljava/lang/String;': + variables.push(new VariableValue('', varinfo.value.toString())); + break; + case 'C': + variables.push(new VariableValue('', varinfo.value.charCodeAt(0).toString())); + break; + case 'J': + // because JS cannot handle 64bit ints, we need a bit of extra work + const v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,''); + variables.push(...getIntFormats(v64hex, 64)); + break; + default:// integer/short/byte value + const u = varinfo.value >>> 0; + variables.push(...getIntFormats(u, bits)); + break; + } + return variables; + } + +} + +module.exports = { + DebuggerStackFrame, +} diff --git a/src/threads.js b/src/threads.js index a4306a6..5f10f86 100644 --- a/src/threads.js +++ b/src/threads.js @@ -1,23 +1,78 @@ -'use strict' +const { Debugger } = require('./debugger'); +const { DebuggerException, DebuggerFrameInfo, SourceLocation } = require('./debugger-types'); +const { DebuggerStackFrame } = require('./stack-frame'); +const { VariableManager } = require('./variable-manager'); -const { AndroidVariables } = require('./variables'); -const $ = require('./jq-promise'); +// vscode doesn't like thread id reuse (the Android runtime is OK with it) +let nextVSCodeThreadId = 0; + +/** + * Scales used to build VSCVariableReferences. + * Each reference contains a thread id, frame id and variable index. + * eg. VariableReference 1005000000 has thread:1 and frame:5 + * + * The variable index is the bottom 1M values. + * - A 0 value is used for locals scope + * - A 1 value is used for exception scope + * - Values above 10 are used for variables + */ +const var_ref_thread_scale = 1e9; +const var_ref_frame_scale = 1e6; +const var_ref_global_frame = 999e6; + +class ThreadPauseInfo { + + /** + * @param {string} reason + * @param {SourceLocation} location + * @param {DebuggerException} last_exception + */ + constructor(reason, location, last_exception) { + this.when = Date.now(); // when + this.reasons = [reason]; // why + this.location = location; // where + this.last_exception = last_exception; + /** + * @type {Map} + */ + this.stack_frames = new Map(); + + /** + * instance used to manage variables created for expressions evaluated in the global context + * @type {VariableManager} + */ + this.global_vars = null; + + this.stoppedEvent = null; // event we (eventually) send to vscode + } + + /** + * @param {number} frameId + */ + getLocals(frameId) { + return this.stack_frames.get(frameId).locals; + } +} /* Class used to manage a single thread reported by JDWP */ class AndroidThread { - constructor(session, threadid, vscode_threadid) { - // the AndroidDebugSession instance - this.session = session; + /** + * + * @param {Debugger} dbgr + * @param {string} name + * @param {JavaThreadID} threadid + */ + constructor(dbgr, name, threadid) { // the Android debugger instance - this.dbgr = session.dbgr; + this.dbgr = dbgr; // the java thread id (hex string) this.threadid = threadid; // the vscode thread id (number) - this.vscode_threadid = vscode_threadid; + this.vscode_threadid = (nextVSCodeThreadId += 1); // the (Java) name of the thread - this.name = null; + this.name = name; // the thread break info this.paused = null; // the timeout during a step which, if it expires, we allow other threads to break @@ -28,102 +83,101 @@ class AndroidThread { return new Error(`Thread ${this.vscode_threadid} not suspended`); } - addStackFrameVariable(frame, level) { - if (!this.paused) throw this.threadNotSuspendedError(); - var frameId = (this.vscode_threadid * 1e9) + (level * 1e6); - var stack_frame_var = { - frame, frameId, - locals: null, + /** + * @param {DebuggerFrameInfo} frame + * @param {number} call_stack_level + */ + createStackFrameVariable(frame, call_stack_level) { + if (!this.paused) { + throw this.threadNotSuspendedError(); } - return this.paused.stack_frame_vars[frameId] = stack_frame_var; + const frameId = AndroidThread.makeFrameVariableReference(this.vscode_threadid, call_stack_level) ; + const stack_frame = new DebuggerStackFrame(this.dbgr, frame, frameId); + this.paused.stack_frames.set(frameId, stack_frame); + return stack_frame; } - allocateExceptionScopeReference(frameId) { - if (!this.paused) return; - if (!this.paused.last_exception) return; - this.paused.last_exception.frameId = frameId; - this.paused.last_exception.scopeRef = frameId + 1; - } - - getVariables(variablesReference) { - if (!this.paused) - return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]); - - // is this reference a stack frame - var stack_frame_var = this.paused.stack_frame_vars[variablesReference]; - if (stack_frame_var) { - // frame locals request - return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(varref)); + /** + * Retrieve the variable manager used to maintain variableReferences for + * expressions evaluated in the global context for this thread. + */ + getGlobalVariableManager() { + if (!this.paused) { + throw this.threadNotSuspendedError(); } - - // is this refrence an exception scope - if (this.paused.last_exception && variablesReference === this.paused.last_exception.scopeRef) { - var stack_frame_var = this.paused.stack_frame_vars[this.paused.last_exception.frameId]; - return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(this.paused.last_exception.scopeRef)); + if (!this.paused.global_vars) { + const globalFrameId = AndroidThread.makeGlobalVariableReference(this.vscode_threadid) ; + this.paused.global_vars = new VariableManager(globalFrameId); } - - // work out which stack frame this reference is for - var frameId = Math.trunc(variablesReference/1e6) * 1e6; - var stack_frame_var = this.paused.stack_frame_vars[frameId]; - - return stack_frame_var.locals.getVariables(variablesReference); + return this.paused.global_vars; } - _ensureLocals(varinfo) { - if (!this.paused) - return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]); - - // evaluate can call this using frameId as the argument - if (typeof varinfo === 'number') - return this._ensureLocals(this.paused.stack_frame_vars[varinfo]); - - // if we're currently processing it (or we've finished), just return the promise - if (this.paused.locals_done[varinfo.frameId]) - return this.paused.locals_done[varinfo.frameId]; - - // create a new promise - var def = this.paused.locals_done[varinfo.frameId] = $.Deferred(); - - this.dbgr.getlocals(this.threadid, varinfo.frame, {def:def,varinfo:varinfo}) - .then((locals,x) => { - // make sure we are still paused... - if (!this.paused) - throw this.threadNotSuspendedError(); - - // sort the locals by name, except for 'this' which always goes first - locals.sort((a,b) => { - if (a.name === b.name) return 0; - if (a.name === 'this') return -1; - if (b.name === 'this') return +1; - return a.name.localeCompare(b.name); - }) - - // create a new local variable with the results and resolve the promise - var varinfo = x.varinfo; - varinfo.cached = locals; - x.varinfo.locals = new AndroidVariables(this.session, x.varinfo.frameId + 2); // 0 = stack frame, 1 = exception, 2... others - x.varinfo.locals.setVariable(varinfo.frameId, varinfo); - - var last_exception = this.paused.last_exception; - if (last_exception) { - x.varinfo.locals.setVariable(last_exception.scopeRef, last_exception); - } - - x.def.resolveWith(this, [varinfo.frameId]); - }) - .fail(e => { - x.def.rejectWith(this, [e]); - }) - return def; + /** + * set a new VSCode thread ID for this thread + */ + allocateNewThreadID() { + this.vscode_threadid = (nextVSCodeThreadId += 1); } - setVariableValue(args) { - var frameId = Math.trunc(args.variablesReference/1e6) * 1e6; - var stack_frame_var = this.paused.stack_frame_vars[frameId]; - return this._ensureLocals(stack_frame_var).then(varref => { - return this.paused.stack_frame_vars[varref].locals.setVariableValue(args); - }); + clearStepTimeout() { + if (this.stepTimeout) { + clearTimeout(this.stepTimeout); + this.stepTimeout = null; + } + } + + /** + * @param {VSCVariableReference} variablesReference + */ + findStackFrame(variablesReference) { + if (!this.paused) { + return null; + } + const stack_frame_ref = AndroidThread.variableRefToFrameId(variablesReference); + return this.paused.stack_frames.get(stack_frame_ref); + } + + /** + * @param {string} reason + * @param {SourceLocation} location + * @param {DebuggerException} last_exception + */ + setPaused(reason, location, last_exception) { + this.paused = new ThreadPauseInfo(reason, location, last_exception); + this.clearStepTimeout(); + } + + /** + * @param {VSCThreadID} vscode_threadid + * @param {number} call_stack_level + * @returns {VSCVariableReference} + */ + static makeFrameVariableReference(vscode_threadid, call_stack_level) { + return (vscode_threadid * var_ref_thread_scale) + (call_stack_level * var_ref_frame_scale) + } + + static makeGlobalVariableReference(vscode_threadid) { + return (vscode_threadid * var_ref_thread_scale) + var_ref_global_frame; + } + + /** + * Convert a variable reference ID to a VSCode thread ID + * @param {VSCVariableReference} variablesReference + */ + static variableRefToThreadId(variablesReference) { + return Math.trunc(variablesReference / var_ref_thread_scale); + } + + /** + * Convert a variable reference ID to a frame ID + * @param {VSCVariableReference} variablesReference + */ + static variableRefToFrameId(variablesReference) { + return Math.trunc(variablesReference / var_ref_frame_scale) * var_ref_frame_scale; } } -exports.AndroidThread = AndroidThread; + +module.exports = { + AndroidThread, +} diff --git a/src/transport.js b/src/transport.js deleted file mode 100644 index 2541df7..0000000 --- a/src/transport.js +++ /dev/null @@ -1,424 +0,0 @@ -const D = function(){};// require('./util').D; - -var transport_list = []; -var next_connect_device_service_id = 1; - -var open_device_service = exports.open_device_service = function(t, fd, service, cb) { - D('open_device_service %s on device %s', service, t.serial); - - var p = get_apacket(); - p.msg.command = A_OPEN; - p.msg.arg0 = ++next_connect_device_service_id; - p.msg.data_length = service.length+1; - p.data.set(str2u8arr(service)); - - var serviceinfo = { - service: service, - transport: t, - localid: p.msg.arg0, - remoteid: 0, - state: 'init', - nextokay:null, - nextwrte:null, - nextclse:on_device_close_reply, - clientfd: fd, - isjdwp: /^jdwp\:\d+/.test(service), - islogcat: /^(shell:)?logcat/.test(service), - }; - t.open_services.push(serviceinfo); - - serviceinfo.nextokay = on_device_open_okay; - serviceinfo.state = 'talking'; - send_packet(p, t, function(err) { - if (err) { - serviceinfo.state = 'init-error'; - remove_device_service(serviceinfo); - return cb(err); - } - }); - - function ignore_response(err, p, serviceinfo, receivecb) { - D('ignore_response, p=%o', p); - receivecb(); - } - - function on_device_open_okay(err, p, serviceinfo, receivecb) { - D('on_device_open_okay: %s, err:%o', serviceinfo.service, err); - if (err) { - receivecb(); - cb(err); - return; - } - serviceinfo.state = 'ready'; - serviceinfo.nextokay = ignore_response; - serviceinfo.nextwrte = on_device_write_reply; - - // ack the packet receive callback - receivecb(); - // ack the open_device_service callback - cb(null, serviceinfo); - - // start reading from the client - read_from_client(serviceinfo); - } - - function read_from_client(serviceinfo) { - D('Waiting for client data'); - serviceinfo.clientfd.readbytes(function(err, data) { - if (err) { - // read error - the client probably closed the connection - send_close_device_service(serviceinfo, function(err) { - remove_device_service(serviceinfo); - }); - return; - } - D('client WRTE %d bytes to device', data.byteLength); - // send the data to the device - var p = get_apacket(); - p.msg.command = A_WRTE; - p.msg.arg0 = serviceinfo.localid; - p.msg.arg1 = serviceinfo.remoteid; - p.msg.data_length = data.byteLength; - p.data.set(data); - if (serviceinfo.isjdwp) - print_jdwp_data('out',data); - - serviceinfo.nextokay = function(err, p, serviceinfo, receivecb) { - if (err) { - // if we fail to write, just abort - remove_device_service(serviceinfo); - receivecb(); - return; - } - D('client WRTE - got OKAY'); - serviceinfo.nextokay = ignore_response; - receivecb(); - // read and send more - read_from_client(serviceinfo); - } - - send_packet(p, t, function(err) { - if (err) { - // if we fail to write, just abort - remove_device_service(serviceinfo); - return; - } - // we must wait until the next OKAY until we can write more - D('client WRTE - waiting for OKAY'); - }); - }); - } - - function on_device_write_reply(err, p, serviceinfo, receivecb) { - D('device WRTE received'); - if (err) { - serviceinfo.state = 'write reply error'; - remove_device_service(serviceinfo); - receivecb(); - return; - }; - - // when we receive a WRTE, we must reply with an OKAY as the very next packet. - // - we can't wait for the data to be forwarded because the reader might post - // something in between - D('sending OKAY'); - send_ready(serviceinfo.localid, serviceinfo.remoteid, serviceinfo.transport, function(err){ - if (err) { - serviceinfo.state = 'write okay error'; - remove_device_service(serviceinfo); - return; - } - D('sent OKAY'); - }); - - if (serviceinfo.isjdwp) - print_jdwp_data('dev', p.data); - - // write the data to the client - serviceinfo.clientfd.writebytes(new Uint8Array(p.data.buffer.slice(0, p.msg.data_length)), function(err) { - // ack the packet receive callback - receivecb(); - }); - } - - function on_device_close_reply(err, p, serviceinfo, receivecb) { - var t = serviceinfo.transport; - D('on_device_close_reply %s (by device) on device %s', serviceinfo.service, t.serial); - serviceinfo.state = 'closed (by device)'; - remove_device_service(serviceinfo); - // ack the packet receive callback - receivecb(); - } - -} - -var find_open_device_service = exports.find_open_device_service = function(t, localid, remoteid) { - for (var i=0; i < t.open_services.length; i++) { - var s = t.open_services[i]; - if (s.localid === localid && (!remoteid ||(s.remoteid === remoteid))) { - return s; - } - } - return null; -} - -var send_close_device_service = exports.send_close_device_service = function(serviceinfo, cb) { - D('send_close_device_service: %s, device:%s', serviceinfo.service, serviceinfo.transport.serial); - var p = get_apacket(); - - p.msg.command = A_CLSE; - p.msg.arg0 = serviceinfo.localid; - p.msg.arg1 = serviceinfo.remoteid; - - serviceinfo.nextreply = on_close_request_reply; - serviceinfo.state = 'talking'; - send_packet(p, serviceinfo.transport, function(err) { - if (err) { - serviceinfo.state = 'error'; - } else { - serviceinfo.state = 'closed'; - } - // ack the close_device_service request as soon as we - // send the packet - don't wait for the reply - return cb(err); - }); - - function on_close_request_reply(which, serviceinfo, receivecb) { - // ack the packet receive callback - receivecb(); - } -} - -var remove_device_service = exports.remove_device_service = function(serviceinfo) { - var fd; - if (fd=serviceinfo.clientfd) { - serviceinfo.clientfd=null; - fd.close(); - } - remove_from_list(serviceinfo.transport.open_services, serviceinfo); -} - -var register_transport = exports.register_transport = function(t, cb) { - t.terminated = false; - t.open_services = []; - transport_list.push(t); - - // start the reader - function read_next_packet_from_transport(t, packetcount) { - var p = new_apacket(); - t.read_from_remote(p, t, function(err, p) { - if (t.terminated) { - return; - } - if (err) { - D('Error reading next packet from transport:%s - terminating.', t.serial); - kick_transport(t); - unregister_transport(t); - return; - } - p.which = intToCharString(p.msg.command); - D('Read packet:%d (%s) from transport:%s', packetcount, p.which, t.serial); - var pc = packetcount++; - handle_packet(p, t, function(err) { - D('packet:%d handled, err:%o', pc, err); - read_next_packet_from_transport(t, packetcount); - }); - }); - } - read_next_packet_from_transport(t, 0); - - D("transport: %s registered\n", t.serial); - D('new transport list: %o', transport_list.slice()); - update_transports(); - - ui.update_device_property(t.deviceinfo, 'status', 'Connecting...'); - send_connect(t, cb); -} - -var unregister_transport = exports.unregister_transport = function(t) { - if (t.fd) - t.fd.close(); - // kill any connected services - while (t.open_services.length) { - remove_device_service(t.open_services.pop()); - } - - remove_from_list(transport_list, t); - D("transport: %s unregistered\n", t.serial); - D('remaining transports: %o', transport_list.slice()); - t.serial = 'REMOVED:' + t.serial; - t.terminated = true; - update_transports(); - ui.update_device_property(t.deviceinfo, 'status', 'Disconnected', '#8B0E0E'); - ui.remove_disconnected_device(t.deviceinfo); -} - -var kick_transport = exports.kick_transport = function(t) { - if (t && !t.kicked) { - t.kicked = true; - t.kick(t); - } -} - -var write_packet_to_transport = exports.write_packet_to_transport = function(t, p, cb) { - if (t.terminated) { - D('Refusing to write packet to terminated transport: %s', t.serial); - return cb({msg:'device not found'}); - } - t.write_to_remote(p, t, function(err) { - cb(err); - }); -} - -var send_packet = exports.send_packet = function(p, t, cb) { - p.msg.magic = p.msg.command ^ 0xffffffff; - - var count = p.msg.data_length; - var x = new Uint8Array(p.data); - var sum = 0, i=0; - while(count-- > 0){ - sum += x[i++]; - } - p.msg.data_check = sum; - - write_packet_to_transport(t, p, cb); -} - -var acquire_one_transport = exports.acquire_one_transport = function(connection_state, transport_type, serial) { - var candidates = []; - for (var i=0, tl=transport_list; i < tl.length; i++) { - if (connection_state !== 'CS_ANY' && tl[i].connection_state !== connection_state) - continue; - if (transport_type !== 'kTransportAny' && tl[i].transport_type !== transport_type) - continue; - if (serial && tl[i].serial !== serial) - continue; - candidates.push(tl[i]); - } - return candidates; -} - -var statename = exports.statename = function(t) { - if (/^CS_.+/.test(t.connection_state)) - return t.connection_state.slice(3).toLowerCase(); - return 'unknown state: ' + t.connection_state; -} - -var typename = exports.typename = function(t) { - if (/^kTransport.+/.test(t.type)) - return t.type.slice(10).toLowerCase(); - return 'unknown type: ' + t.type; -} - -var format_transport = exports.format_transport = function(t, format) { - var serial = t.serial || '???????????'; - - if (!format) { - return serial+'\t'+statename(t); - } else if (format === 'extended') { - return '{'+[ - '"device":'+JSON.stringify(t.device), - '"model":'+JSON.stringify(t.model||t.deviceinfo.productName), - '"product":'+JSON.stringify(t.product), - '"serial":'+JSON.stringify(serial), - '"status":'+JSON.stringify(statename(t)), - '"type":'+JSON.stringify(typename(t)), - ].join(',') + '}'; - } else { - return [ - serial+'\t'+statename(t), - t.devpath||'', - t.product?'product:'+t.product.replace(/\s+/,'_'):'', - t.model?'model:'+t.model.replace(/\s+/,'_'):'', - t.device?'device:'+t.device.replace(/\s+/,'_'):'' - ].join(' '); - } -} - -var list_transports = exports.list_transports = function(format) { - return transport_list.map(function(t) { - return format_transport(t, format); - }).join('\n')+'\n'; -} - -var update_transports = exports.update_transports = function() { - write_transports_to_trackers(_device_trackers.normal); - write_transports_to_trackers(_device_trackers.extended, null, true); -} - -var readx_with_data = exports.readx_with_data = function(fd, cb) { - readx(fd, 4, function(err, buf) { - if (err) return cb(err); - var dlen = buf.intFromHex(); - if (dlen < 0 || dlen > 0xffff) - return cb({msg:'Invalid data len: ' + dlen}); - readx(fd, dlen, function(err, buf) { - if (err) return cb(err); - return cb(null, buf); - }); - }); -} - -var readx = exports.readx = function(fd, len, cb) { - D('readx: fd:%o wanted=%d', fd, len); - fd.readbytes(len, function(err, buf) { - if (err) return cb(err); - cb(err, buf); - }); -} - -var writex = exports.writex = function(fd, bytes, len) { - if (typeof(bytes) === 'string') { - var buf = new Uint8Array(bytes.length); - for (var i=0; i < bytes.length; i++) - buf[i] = bytes.charCodeAt(i); - bytes = buf; - } - if (typeof(len) !== 'number') - len = bytes.byteLength; - D('writex: fd:%o writing=%d', fd, len); - fd.writebytes(bytes.subarray(0,len)); -} - -var writex_with_data = exports.writex_with_data = function(fd, data, len) { - if (typeof(len) === 'undefined'); - len = data.byteLength||data.length||0; - writex(fd, intToHex(len, 4)); - writex(fd, data, len); -} - -var _device_trackers = { - normal:[], - extended:[], -} -var add_device_tracker = exports.add_device_tracker = function(fd, extended) { - _device_trackers[extended?'extended':'normal'].push(fd); - write_transports_to_trackers([fd], null, extended); - readtracker(fd, extended); - D('Device tracker added. Trackers: %o', _device_trackers); - - function readtracker(fd, extended) { - chrome.socket.read(fd.n, function(readInfo) { - if (chrome.runtime.lastError || readInfo.resultCode < 0) { - remove_from_list(_device_trackers[extended?'extended':'normal'], fd); - D('Device tracker socket read failed - closing. Trackers: %o', _device_trackers); - fd.close(); - return; - } - D('Ignoring data read from device tracker socket'); - readtracker(fd, extended); - }); - } -} - -var write_transports_to_trackers = exports.write_transports_to_trackers = function(fds, transports, extended) { - if (!fds || !fds.length) - return; - if (!transports) { - return write_transports_to_trackers(fds, list_transports(extended?'extended':''), extended); - } - D('Writing transports: %s', transports); - fds.slice().forEach(function(fd) { - writex_with_data(fd, str2u8arr(transports)); - }); -} diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 1e2bbdb..0000000 --- a/src/util.js +++ /dev/null @@ -1,635 +0,0 @@ -const crypto = require('crypto'); - -var nofn = function () { }; -const messagePrintCallbacks = new Set(); -var D = exports.D = (...args) => (console.log(...args), messagePrintCallbacks.forEach(cb => cb(...args))) -var E = exports.E = (...args) => (console.error(...args), messagePrintCallbacks.forEach(cb => cb(...args))) -var W = exports.W = (...args) => (console.warn(...args), messagePrintCallbacks.forEach(cb => cb(...args))) -var DD = nofn, cl = D, printf = D; -var print_jdwp_data = nofn;// _print_jdwp_data; -var print_packet = nofn;//_print_packet; - -exports.onMessagePrint = function(cb) { - messagePrintCallbacks.add(cb); -} - -Array.first = function (arr, fn, defaultvalue) { - var idx = Array.indexOfFirst(arr, fn); - return idx < 0 ? defaultvalue : arr[idx]; -} - -Array.indexOfFirst = function (arr, fn) { - if (!Array.isArray(arr)) return -1; - for (var i = 0; i < arr.length; i++) - if (fn(arr[i], i, arr)) - return i; - return -1; -} - -var isEmptyObject = exports.isEmptyObject = function (o) { - return typeof (o) === 'object' && !Object.keys(o).length; -} - -var leftpad = exports.leftpad = function (char, len, s) { - while (s.length < len) - s = char + s; - return s; -} - -var intToHex = exports.intToHex = function (i, minlen) { - var s = i.toString(16); - if (minlen) s = leftpad('0', minlen, s); - return s; -} - -var intFromHex = exports.intFromHex = function (s, maxlen, defaultvalue) { - s = s.slice(0, maxlen); - if (!/^[0-9a-fA-F]+$/.test(s)) return defaultvalue; - return parseInt(s, 16); -} - -var fdcache = []; - -var index_of_file_fdn = function (n) { - if (n <= 0) return -1; - for (var i = 0; i < fdcache.length; i++) { - if (fdcache[i] && fdcache[i].n === n) - return i; - } - return -1; -} - -var get_file_fd_from_fdn = function (n) { - var idx = index_of_file_fdn(n); - if (idx < 0) return null; - return fdcache[idx]; -} - -var remove_fd_from_cache = function (fd) { - if (!fd) return; - var idx = index_of_file_fdn(fd.n); - if (idx >= 0) fdcache.splice(idx, 1); -} - -// add an offset so we don't conflict with tcp socketIds -var min_fd_num = 100000; -var _new_fd_count = 0; -var new_fd = this.new_fd = function (name, raw) { - var rwpipe = raw ? new Uint8Array(0) : []; - var fd = { - name: name, - n: min_fd_num + (++_new_fd_count), - raw: !!raw, - readpipe: rwpipe, - writepipe: rwpipe, - reader: null, - readerlen: 0, - kickingreader: false, - total: { read: 0, written: 0 }, - duplex: null, - closed: '', - read: function (cb) { - if (this.raw) - throw 'Cannot read from raw fd'; - if (this.reader && this.reader !== cb) - throw 'multiple readers?'; - this.reader = cb; - this._kickreader(); - }, - write: function (data) { - if (this.closed) { - D('Ignoring attempt to write to closed file: %o', this); - return; - } - if (this.raw) { - D('Ignoring attempt to write object to raw file: %o', this); - return; - } - this.writepipe.push(data); - if (this.duplex) { - this.duplex._kickreader(); - } - }, - - readbytes: function (len, cb) { - if (!this.raw) - throw 'Cannot readbytes from non-raw fd'; - if (this.reader) - throw 'multiple readers?'; - this.reader = cb; - this.readerlen = len; - this._kickreader(); - }, - - writebytes: function (buffer) { - if (this.closed) { - D('Ignoring attempt to write to closed file: %o', this); - return; - } - if (!this.raw) { - D('Ignoring attempt to write bytes to non-raw file: %o', this); - return; - } - if (!buffer || !buffer.byteLength) { - // kick the reader when writing 0 bytes - this._kickreaders(); - return; - } - this.total.written += buffer.byteLength; - var newbuf = new Uint8Array(this.writepipe.byteLength + buffer.byteLength); - newbuf.set(this.writepipe); - newbuf.set(buffer, this.writepipe.byteLength); - this.writepipe = newbuf; - if (this.duplex) - this.duplex.readpipe = newbuf; - else - this.readpipe = newbuf; - D('new buffer size: %d (fd:%d)', this.writepipe.byteLength, this.n); - this._kickreaders(); - }, - - cancelread: function (flushfirst) { - if (flushfirst) - this.flush(); - this.reader = null; - this.readerlen = 0; - }, - - write_eof: function () { - this.flush(); - // eof is only relevant for read-until-close readers - if (this.raw && this.reader && this.readerlen === -1) { - this.reader({ err: 'eof' }); - } - }, - - flush: function () { - this._doread(); - }, - - close: function () { - if (this.closed) - return; - console.trace('Closing file %d: %o', this.n, this); - this.closed = 'closed'; - if (this.duplex) - this.duplex.close(); - // last kick to finish off any read-until-close readers - this._kickreaders(); - // remove this entry from the cache - remove_fd_from_cache(this); - }, - - _kickreaders: function () { - if (this.duplex) - this.duplex._kickreader(); - else - this._kickreader(); - }, - - _kickreader: function () { - if (!this.reader) return; - if (this.kickingreader) return; - var t = this; - t.kickingreader = setTimeout(function () { - t.kickingreader = false; - t._doreadcheckclose(); - }, 0); - }, - - _doreadcheckclose: function () { - var cs = this.closed; - this._doread(); - if (cs) { - // they've had one last read - no more - var rucreader = this.readerlen === -1; - var rucreadercb = this.reader; - this.reader = null; - this.readerlen = 0; - if (rucreader && rucreadercb) { - // terminate the read-until-close reader - D('terminating ruc reader. fd: %o', this); - rucreadercb({ err: 'File closed' }); - } - } - }, - - _doread: function () { - if (this.raw) { - if (!this.reader) return; - if (this.readerlen > this.readpipe.byteLength) return; - if (this.readerlen && !this.readpipe.byteLength) return; - var cb = this.reader, len = this.readerlen; - this.reader = null, this.readerlen = 0; - var data; - if (len) { - var readlen = len > 0 ? len : this.readpipe.byteLength; - data = this.readpipe.subarray(0, readlen); - this.readpipe = this.readpipe.subarray(readlen); - if (this.duplex) - this.duplex.writepipe = this.readpipe; - else - this.writepipe = this.readpipe; - this.total.read += readlen; - } else { - data = new Uint8Array(0); - } - - data.asString = function () { - return uint8ArrayToString(this); - }; - data.intFromHex = function (len) { - len = len || this.byteLength; - var x = this.asString().slice(0, len); - if (!/^[0-9a-fA-F]+/.test(x)) return -1; - return parseInt(x, 16); - } - cb(null, data); - - if (len < 0) { - // reset the reader - this.readbytes(len, cb); - } - return; - } - if (this.reader && this.readpipe.length) { - var cb = this.reader; - this.reader = null; - cb(this.readpipe.shift()); - } - } - } - - fdcache.push(fd); - return fd; -} - -var intToCharString = function (n) { - return String.fromCharCode( - (n >> 0) & 255, - (n >> 8) & 255, - (n >> 16) & 255, - (n >> 24) & 255 - ); -} - -var stringToUint8Array = function (s) { - var x = new Uint8Array(s.length); - for (var i = 0; i < s.length; i++) - x[i] = s.charCodeAt(i); - return x; -} - -var uint8ArrayToString = function (a) { - var s = new Array(a.byteLength); - for (var i = 0; i < a.byteLength; i++) - s[i] = a[i]; - return String.fromCharCode.apply(String, s); -} - -// asynchronous array iterater -var iterate = function (arr, o) { - var isrange = typeof (arr) === 'number'; - if (isrange) - arr = { length: arr < 0 ? 0 : arr }; - var x = { - value: arr, - isrange: isrange, - first: o.first || nofn, - each: o.each || (function () { this.next(); }), - last: o.last || nofn, - success: o.success || nofn, - error: o.error || nofn, - complete: o.complete || nofn, - _idx: 0, - _donefirst: false, - _donelast: false, - abort: function (err) { - this.error(err); - this.complete(); - return; - }, - finish: function (res) { - // finish early - if (typeof (res) !== 'undefined') this.result = res; - this.success(res || this.result); - this.complete(); - return; - }, - iteratefirst: function () { - if (!this.value.length) { - this.finish(); - return; - } - this.first(this.value[this._idx], this._idx, this); - this.each(this.value[this._idx], this._idx, this); - }, - iteratenext: function () { - if (++this._idx >= this.value.length) { - this.last(this.value[this._idx], this._idx, this); - this.finish(); - return; - } - this.each(this.value[this._idx], this._idx, this); - }, - next: function () { - var t = this; - setTimeout(function () { - t.iteratenext(); - }, 0); - }, - nextorabort: function (err) { - if (err) this.abort(err); - else this.next(); - }, - }; - setTimeout(function () { x.iteratefirst(); }, 0); - return x; -}; - -var iterate_repeat = function (arr, count, o, j) { - iterate(arr, { - each: function (value, i, it) { - o.each(value, i, j || 0, it); - }, - success: function () { - if (!--count) { - o.success && o.success(); - o.complete && o.complete(); - return; - } - iterate_repeat(arr, count, o, (j || 0) + 1); - }, - error: function (err) { - o.error && o.error(); - o.complete && o.complete(); - } - }); -} - -/** - * Convert from an ArrayBuffer to a string. - * @param {ArrayBuffer} buffer The array buffer to convert. - * @return {string} The textual representation of the array. - */ -var arrayBufferToString = exports.arrayBufferToString = function (buffer) { - var array = new Uint8Array(buffer); - var str = ''; - for (var i = 0; i < array.length; ++i) { - str += String.fromCharCode(array[i]); - } - return str; -}; - -/** - * Convert from an UTF-8 array to UTF-8 string. - * @param {array} UTF-8 array - * @return {string} UTF-8 string - */ -var ary2utf8 = (function () { - - var patterns = [ - { pattern: '0xxxxxxx', bytes: 1 }, - { pattern: '110xxxxx', bytes: 2 }, - { pattern: '1110xxxx', bytes: 3 }, - { pattern: '11110xxx', bytes: 4 }, - { pattern: '111110xx', bytes: 5 }, - { pattern: '1111110x', bytes: 6 } - ]; - patterns.forEach(function (item) { - item.header = item.pattern.replace(/[^10]/g, ''); - item.pattern01 = item.pattern.replace(/[^10]/g, '0'); - item.pattern01 = parseInt(item.pattern01, 2); - item.mask_length = item.header.length; - item.data_length = 8 - item.header.length; - var mask = ''; - for (var i = 0, len = item.mask_length; i < len; i++) { - mask += '1'; - } - for (var i = 0, len = item.data_length; i < len; i++) { - mask += '0'; - } - item.mask = mask; - item.mask = parseInt(item.mask, 2); - }); - - return function (ary) { - var codes = []; - var cur = 0; - while (cur < ary.length) { - var first = ary[cur]; - var pattern = null; - for (var i = 0, len = patterns.length; i < len; i++) { - if ((first & patterns[i].mask) == patterns[i].pattern01) { - pattern = patterns[i]; - break; - } - } - if (pattern == null) { - throw 'utf-8 decode error'; - } - var rest = ary.slice(cur + 1, cur + pattern.bytes); - cur += pattern.bytes; - var code = ''; - code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length); - for (var i = 0, len = rest.length; i < len; i++) { - code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6); - } - codes.push(parseInt(code, 2)); - } - return String.fromCharCode.apply(null, codes); - }; - -})(); - -/** - * Convert from an UTF-8 string to UTF-8 array. - * @param {string} UTF-8 string - * @return {array} UTF-8 array - */ -var utf82ary = (function () { - - var patterns = [ - { pattern: '0xxxxxxx', bytes: 1 }, - { pattern: '110xxxxx', bytes: 2 }, - { pattern: '1110xxxx', bytes: 3 }, - { pattern: '11110xxx', bytes: 4 }, - { pattern: '111110xx', bytes: 5 }, - { pattern: '1111110x', bytes: 6 } - ]; - patterns.forEach(function (item) { - item.header = item.pattern.replace(/[^10]/g, ''); - item.mask_length = item.header.length; - item.data_length = 8 - item.header.length; - item.max_bit_length = (item.bytes - 1) * 6 + item.data_length; - }); - - var code2utf8array = function (code) { - var pattern = null; - var code01 = code.toString(2); - for (var i = 0, len = patterns.length; i < len; i++) { - if (code01.length <= patterns[i].max_bit_length) { - pattern = patterns[i]; - break; - } - } - if (pattern == null) { - throw 'utf-8 encode error'; - } - var ary = []; - for (var i = 0, len = pattern.bytes - 1; i < len; i++) { - ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2)); - code01 = code01.slice(0, -6); - } - ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2)); - return ary; - }; - - return function (str) { - var codes = []; - for (var i = 0, len = str.length; i < len; i++) { - var code = str.charCodeAt(i); - Array.prototype.push.apply(codes, code2utf8array(code)); - } - return codes; - }; - -})(); - -/** - * Convert a string to an ArrayBuffer. - * @param {string} string The string to convert. - * @return {ArrayBuffer} An array buffer whose bytes correspond to the string. - */ -var stringToArrayBuffer = exports.stringToArrayBuffer = function (string) { - var buffer = new ArrayBuffer(string.length); - var bufferView = new Uint8Array(buffer); - for (var i = 0; i < string.length; i++) { - bufferView[i] = string.charCodeAt(i); - } - return buffer; -}; - -var str2ab = exports.str2ab = stringToArrayBuffer; -var ab2str = exports.ab2str = arrayBufferToString; -var str2u8arr = exports.str2u8arr = function (s) { - return new Uint8Array(str2ab(s)); -} - -exports.getutf8bytes = function (str) { - var utf8 = []; - for (var i = 0; i < str.length; i++) { - var charcode = str.charCodeAt(i); - if (charcode < 0x80) utf8.push(charcode); - else if (charcode < 0x800) { - utf8.push(0xc0 | (charcode >> 6), - 0x80 | (charcode & 0x3f)); - } - else if (charcode < 0xd800 || charcode >= 0xe000) { - utf8.push(0xe0 | (charcode >> 12), - 0x80 | ((charcode >> 6) & 0x3f), - 0x80 | (charcode & 0x3f)); - } - // surrogate pair - else { - i++; - // UTF-16 encodes 0x10000-0x10FFFF by - // subtracting 0x10000 and splitting the - // 20 bits of 0x0-0xFFFFF into two halves - charcode = 0x10000 + (((charcode & 0x3ff) << 10) - | (str.charCodeAt(i) & 0x3ff)); - utf8.push(0xf0 | (charcode >> 18), - 0x80 | ((charcode >> 12) & 0x3f), - 0x80 | ((charcode >> 6) & 0x3f), - 0x80 | (charcode & 0x3f)); - } - } - return utf8; -} - -exports.fromutf8bytes = function (array) { - var out, i, len, c; - var char2, char3; - - out = ""; - len = array.length; - i = 0; - while (i < len) { - c = array[i++]; - switch (c >> 4) { - case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: - // 0xxxxxxx - out += String.fromCharCode(c); - break; - case 12: case 13: - // 110x xxxx 10xx xxxx - char2 = array[i++]; - out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); - break; - case 14: - // 1110 xxxx 10xx xxxx 10xx xxxx - char2 = array[i++]; - char3 = array[i++]; - out += String.fromCharCode(((c & 0x0F) << 12) | - ((char2 & 0x3F) << 6) | - ((char3 & 0x3F) << 0)); - break; - } - } - - return out; -} - -exports.arraybuffer_concat = function () { - var bufs = [], total = 0; - for (var i = 0; i < arguments.length; i++) { - var a = arguments[i]; - if (!a || !a.byteLength) continue; - bufs.push(a); - total += a.byteLength; - } - switch (bufs.length) { - case 0: return new Uint8Array(0); - case 1: return new Uint8Array(bufs[0]); - } - var res = new Uint8Array(total); - for (var i = 0, j = 0; i < bufs.length; i++) { - res.set(bufs[i], j); - j += bufs[i].byteLength; - } - return res; -} - -exports.remove_from_list = function (arr, item, searchfn) { - if (!searchfn) searchfn = function (a, b) { return a === b; }; - for (var i = 0; i < arr.length; i++) { - var found = searchfn(arr[i], item); - if (found) { - return { - item: arr.splice(i, 1)[0], - index: i, - } - } - } - D('Object %o not removed from list %o', item, arr); -} - -exports.dumparr = function (arr, offset, count) { - offset = offset || 0; - count = count || (count === 0 ? 0 : arr.length); - if (count > arr.length - offset) - count = arr.length - offset; - var s = ''; - while (count--) { - s += ' ' + ('00' + arr[offset++].toString(16)).slice(-2); - } - return s.slice(1); -} - -exports.btoa = function (arr) { - return Buffer.from(arr, 'binary').toString('base64'); -} - -exports.atob = function (base64) { - return Buffer.from(base64, 'base64').toString('binary'); -} diff --git a/src/utils/char-decode.js b/src/utils/char-decode.js new file mode 100644 index 0000000..ca1f4dc --- /dev/null +++ b/src/utils/char-decode.js @@ -0,0 +1,53 @@ +const BACKSLASH_ESCAPE_MAP = { + b: '\b', + f: '\f', + r: '\r', + n: '\n', + t: '\t', + v: '\v', + '0': '\0', + '\\': '\\', +}; + +/** + * De-escape backslash escaped characters + * @param {string} c + */ +function decode_char(c) { + switch(true) { + case /^\\u[0-9a-fA-F]{4}$/.test(c): + // unicode escape + return String.fromCharCode(parseInt(c.slice(2),16)); + + case /^\\.$/.test(c): + // backslash escape + const char = BACKSLASH_ESCAPE_MAP[c[1]]; + return char || c[1]; + + case c.length === 1: + return c; + } + throw new Error('Invalid character value'); +} + +/** + * Convert a Java string literal to a raw string + * @param {string} s + */ +function decodeJavaStringLiteral(s) { + return s.slice(1, -1).replace(/\\u[0-9a-fA-F]{4}|\\./g, decode_char); +} + +/** + * Convert a Java char literal to a raw character + * @param {string} s + */ +function decodeJavaCharLiteral(s) { + return decode_char(s.slice(1, -1)); +} + +module.exports = { + decode_char, + decodeJavaCharLiteral, + decodeJavaStringLiteral, +} diff --git a/src/nbc.js b/src/utils/nbc.js similarity index 69% rename from src/nbc.js rename to src/utils/nbc.js index 7bbade0..ab92e69 100644 --- a/src/nbc.js +++ b/src/utils/nbc.js @@ -3,29 +3,35 @@ const NumberBaseConverter = { // Adds two arrays for the given base (10 or 16), returning the result. add(x, y, base) { - var z = [], n = Math.max(x.length, y.length), carry = 0, i = 0; - while (i < n || carry) { - var xi = i < x.length ? x[i] : 0; - var yi = i < y.length ? y[i] : 0; - var zi = carry + xi + yi; + const z = [], n = Math.max(x.length, y.length); + let carry = 0; + for (let i = 0; i < n || carry; i++) { + const xi = i < x.length ? x[i] : 0; + const yi = i < y.length ? y[i] : 0; + const zi = carry + xi + yi; z.push(zi % base); carry = Math.floor(zi / base); - i++; } return z; }, // Returns a*x, where x is an array of decimal digits and a is an ordinary // JavaScript number. base is the number base of the array x. multiplyByNumber(num, x, base) { - if (num < 0) return null; - if (num == 0) return []; - var result = [], power = x; + if (num < 0) { + return null; + } + if (num == 0) { + return []; + } + let result = [], power = x; for(;;) { if (num & 1) { result = this.add(result, power, base); } num = num >> 1; - if (num === 0) return result; + if (num === 0) { + return result; + } power = this.add(power, power, base); } }, @@ -37,12 +43,12 @@ const NumberBaseConverter = { convertBase(str, fromBase, toBase) { if (fromBase === 10 && /[eE]/.test(str)) { // convert exponents to a string of zeros - var s = str.split(/[eE]/); + const s = str.split(/[eE]/); str = s[0] + '0'.repeat(parseInt(s[1],10)); // works for 0/+ve exponent,-ve throws } - var digits = str.split('').map(d => parseInt(d,fromBase)).reverse(); - var outArray = [], power = [1]; - for (var i = 0; i < digits.length; i++) { + const digits = str.split('').map(d => parseInt(d,fromBase)).reverse(); + let outArray = [], power = [1]; + for (let i = 0; i < digits.length; i++) { if (digits[i]) { outArray = this.add(outArray, this.multiplyByNumber(digits[i], power, toBase), toBase); } @@ -51,8 +57,11 @@ const NumberBaseConverter = { return outArray.reverse().map(d => d.toString(toBase)).join(''); }, decToHex(decstr, minlen) { - var res, isneg = decstr[0] === '-'; - if (isneg) decstr = decstr.slice(1) + let res; + const isneg = decstr[0] === '-'; + if (isneg) { + decstr = decstr.slice(1); + } decstr = decstr.match(/^0*(.+)$/)[1]; // strip leading zeros if (decstr.length < 16 && !/[eE]/.test(decstr)) { // 16 = Math.pow(2,52).toString(10).length // less than 52 bits - just use parseInt @@ -63,27 +72,32 @@ const NumberBaseConverter = { if (isneg) { res = NumberBaseConverter.twosComplement(res, 16); if (/^[0-7]/.test(res)) res = 'f'+res; //msb must be set for -ve numbers - } else if (/^[^0-7]/.test(res)) + } else if (/^[^0-7]/.test(res)) { res = '0' + res; // msb must not be set for +ve numbers + } if (minlen && res.length < minlen) { res = (isneg?'f':'0').repeat(minlen - res.length) + res; } return res; }, hexToDec(hexstr, signed) { - var res, isneg = /^[^0-7]/.test(hexstr); + const isneg = /^[^0-7]/.test(hexstr); if (hexstr.match(/^0*(.+)$/)[1].length*4 < 52) { // less than 52 bits - just use parseInt - res = parseInt(hexstr, 16); - if (signed && isneg) res = -res; + let res = parseInt(hexstr, 16); + if (signed && isneg) { + res = -res; + } return res.toString(10); } if (isneg) { hexstr = NumberBaseConverter.twosComplement(hexstr, 16); } - res = (isneg ? '-' : '') + NumberBaseConverter.convertBase(hexstr, 16, 10); + const res = (isneg ? '-' : '') + NumberBaseConverter.convertBase(hexstr, 16, 10); return res; }, }; -Object.assign(exports, NumberBaseConverter); +module.exports = { + NumberBaseConverter, +} diff --git a/src/utils/print.js b/src/utils/print.js new file mode 100644 index 0000000..668b46d --- /dev/null +++ b/src/utils/print.js @@ -0,0 +1,51 @@ +/** + * Set of callbacks to be called when any message is output to the console + * @type {Set} + * */ +const messagePrintCallbacks = new Set(); + +function callMessagePrintCallbacks(args) { + messagePrintCallbacks.forEach(cb => cb(...args)); +} + +/** + * print a debug message to the console + * @param {...any} args + */ +function D(...args) { + console.log(...args); + callMessagePrintCallbacks(args); +} + +/** + * print an error message to the console + * @param {...any} args + */ +function E(...args) { + console.error(...args); + callMessagePrintCallbacks(args); +} + +/** + * print a warning message to the console + * @param {...any} args + */ +function W(...args) { + console.warn(...args); + callMessagePrintCallbacks(args); +} + +/** + * Adds a callback to be called when any message is output + * @param {Function} cb + */ +function onMessagePrint(cb) { + messagePrintCallbacks.add(cb); +} + +module.exports = { + D, + E, + W, + onMessagePrint, +} diff --git a/src/utils/source-file.js b/src/utils/source-file.js new file mode 100644 index 0000000..8b5103e --- /dev/null +++ b/src/utils/source-file.js @@ -0,0 +1,21 @@ +/** + * Returns true if the string has an extension we recognise as a source file + * @param {string} s + */ +function hasValidSourceFileExtension(s) { + return /\.(java|kt)$/i.test(s); +} + +function splitSourcePath(filepath) { + const m = filepath.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.(java|kt)$/); + return { + pkg: m[1].replace(/\/+/g, '.'), + type: m[2], + qtype: `${m[1]}/${m[2]}`, + } +} + +module.exports = { + hasValidSourceFileExtension, + splitSourcePath, +} diff --git a/src/utils/thread.js b/src/utils/thread.js new file mode 100644 index 0000000..b6b7a61 --- /dev/null +++ b/src/utils/thread.js @@ -0,0 +1,11 @@ +/** + * Returns a Promise which resolves after the specified period. + * @param {number} ms wait time in milliseconds + */ +function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +module.exports = { + sleep, +} diff --git a/src/variable-manager.js b/src/variable-manager.js new file mode 100644 index 0000000..eef2386 --- /dev/null +++ b/src/variable-manager.js @@ -0,0 +1,166 @@ +const { DebuggerValue, JavaType, VariableValue } = require('./debugger-types'); +const { NumberBaseConverter } = require('./utils/nbc'); + +/** + * Class to manage variable references used by VS code. + * + * This class is primarily used to manage references to variables created in stack frames, but is + * also used in 'standalone' mode for repl expressions evaluated in the global context. + */ +class VariableManager { + /** + * @param {VSCVariableReference} base_variable_reference The reference value for values stored by this manager + */ + constructor(base_variable_reference) { + // expandable variables get allocated new variable references. + this._expandable_prims = false; + + /** @type {VSCVariableReference} */ + this.nextVariableRef = base_variable_reference + 10; + + /** @type {Map} */ + this.variableValues = new Map(); + + /** @type {Map} */ + this.objIdCache = new Map(); + } + + _addVariable(varinfo) { + varinfo.varref = this.nextVariableRef += 1; + this._setVariable(varinfo.varref, varinfo) + return varinfo.varref; + } + + /** + * + * @param {VSCVariableReference} variablesReference + * @param {*} value + */ + _setVariable(variablesReference, value) { + this.variableValues.set(variablesReference, value); + } + + _getObjectIdReference(type, objvalue) { + // we need the type signature because we must have different id's for + // an instance and it's supertype instance (which obviously have the same objvalue) + const key = type.signature + objvalue; + let value = this.objIdCache.get(key); + if (!value) { + this.objIdCache.set(key, value = this.nextVariableRef += 1); + } + return value; + } + + /** + * Convert to a VariableValue object used by VSCode + * @param {DebuggerValue} v + */ + makeVariableValue(v) { + let varref = 0; + let value = ''; + const evaluateName = v.fqname || v.name; + const formats = {}; + const full_typename = v.type.fullyQualifiedName(); + switch(true) { + case v.hasnullvalue && JavaType.isReference(v.type): + // null object or array type + value = 'null'; + break; + case v.vtype === 'class': + value = full_typename; + break; + case v.type.signature === JavaType.Object.signature: + // Object doesn't really have anything worth seeing, so just treat it as unexpandable + value = v.type.typename; + break; + case v.type.signature === JavaType.String.signature: + value = JSON.stringify(v.string); + if (v.biglen) { + // since this is a big string - make it viewable on expand + varref = this._addVariable({ + bigstring: v, + }); + value = `String (length:${v.biglen})`; + } + else if (this._expandable_prims) { + // as a courtesy, allow strings to be expanded to see their length + varref = this._addVariable({ + signature: v.type.signature, + primitive: true, + value: v.string.length + }); + } + break; + case JavaType.isArray(v.type): + // non-null array type - if it's not zero-length add another variable reference so the user can expand + if (v.arraylen) { + varref = this._getObjectIdReference(v.type, v.value); + this._setVariable(varref, { + varref, + arrvar: v, + range:[0, v.arraylen], + }); + } + value = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound + break; + case JavaType.isClass(v.type): + // non-null object instance - add another variable reference so the user can expand + varref = this._getObjectIdReference(v.type, v.value); + this._setVariable(varref, { + varref, + objvar: v, + }); + value = v.type.typename; + break; + case v.type.signature === JavaType.char.signature: + // character types have a integer value + const char = String.fromCodePoint(v.value); + const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\','\0':'0'}; + if (cmap[char]) { + value = `'\\${cmap[char]}'`; + } else if (v.value < 32) { + value = `'\\u${v.value.toString(16).padStart(4,'0')}'`; + } else value = `'${char}'`; + break; + case v.type.signature === JavaType.long.signature: + // because JS cannot handle 64bit ints, we need a bit of extra work + const v64hex = v.value.replace(/[^0-9a-fA-F]/g,''); + value = formats.dec = NumberBaseConverter.hexToDec(v64hex, true); + formats.hex = '0x' + v64hex.replace(/^0+/, '0'); + formats.oct = formats.bin = ''; + // 24 bit chunks... + for (let s = v64hex; s; s = s.slice(0,-6)) { + const uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits + formats.oct = uint.toString(8) + formats.oct; + formats.bin = uint.toString(2) + formats.bin; + } + formats.oct = '0c' + formats.oct.replace(/^0+/, '0'); + formats.bin = '0b' + formats.bin.replace(/^0+/, '0'); + break; + case JavaType.isInteger(v.type): + value = formats.dec = v.value.toString(); + const uint = (v.value >>> 0); + formats.hex = '0x' + uint.toString(16); + formats.oct = '0c' + uint.toString(8); + formats.bin = '0b' + uint.toString(2); + break; + default: + // other primitives: boolean, etc + value = v.value.toString(); + break; + } + // as a courtesy, allow integer and character values to be expanded to show the value in alternate bases + if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) { + varref = this._addVariable({ + signature: v.type.signature, + primitive: true, + value: v.value, + }); + } + return new VariableValue(v.name, value, full_typename, varref, evaluateName); + } +} + +module.exports = { + VariableManager, +} diff --git a/src/variables.js b/src/variables.js deleted file mode 100644 index d142263..0000000 --- a/src/variables.js +++ /dev/null @@ -1,410 +0,0 @@ -'use strict' - -const { JTYPES, exmsg_var_name, createJavaString } = require('./globals'); -const NumberBaseConverter = require('./nbc'); -const $ = require('./jq-promise'); - -/* - Class used to manage stack frame locals and other evaluated expressions -*/ -class AndroidVariables { - - constructor(session, baseId) { - this.session = session; - this.dbgr = session.dbgr; - // the incremental reference id generator for stack frames, locals, etc - this.nextId = baseId; - // hashmap of variables and frames - this.variableHandles = {}; - // hashmap - this.objIdCache = {}; - // allow primitives to be expanded to show more info - this._expandable_prims = false; - } - - addVariable(varinfo) { - var variablesReference = ++this.nextId; - this.variableHandles[variablesReference] = varinfo; - return variablesReference; - } - - clear() { - this.variableHandles = {}; - } - - setVariable(variablesReference, varinfo) { - this.variableHandles[variablesReference] = varinfo; - } - - _getObjectIdReference(type, objvalue) { - // we need the type signature because we must have different id's for - // an instance and it's supertype instance (which obviously have the same objvalue) - var key = type.signature + objvalue; - return this.objIdCache[key] || (this.objIdCache[key] = ++this.nextId); - } - - getVariables(variablesReference) { - var varinfo = this.variableHandles[variablesReference]; - if (!varinfo) { - return $.Deferred().resolve([]); - } - else if (varinfo.cached) { - return $.Deferred().resolve(this._local_to_variable(varinfo.cached)); - } - else if (varinfo.objvar) { - // object fields request - return this.dbgr.getsupertype(varinfo.objvar, {varinfo}) - .then((supertype, x) => { - x.supertype = supertype; - return this.dbgr.getfieldvalues(x.varinfo.objvar, x); - }) - .then((fields, x) => { - // add an extra msg field for exceptions - if (!x.varinfo.exception) return; - x.fields = fields; - return this.dbgr.invokeToString(x.varinfo.objvar.value, x.varinfo.threadid, varinfo.objvar.type.signature, x) - .then((call,x) => { - call.name = exmsg_var_name; - x.fields.unshift(call); - return $.Deferred().resolveWith(this, [x.fields, x]); - }); - }) - .then((fields, x) => { - // ignore supertypes of Object - x.supertype && x.supertype.signature!=='Ljava/lang/Object;' && fields.unshift({ - vtype:'super', - name:':super', - hasnullvalue:false, - type: x.supertype, - value: x.varinfo.objvar.value, - valid:true, - }); - // create the fully qualified names to use for evaluation - fields.forEach(f => f.fqname = `${x.varinfo.objvar.fqname || x.varinfo.objvar.name}.${f.name}`); - x.varinfo.cached = fields; - return this._local_to_variable(fields); - }); - } - else if (varinfo.arrvar) { - // array elements request - var range = varinfo.range, count = range[1] - range[0]; - // should always have a +ve count, but just in case... - if (count <= 0) return $.Deferred().resolve([]); - // add some hysteresis - if (count > 110) { - // create subranges in the sub-power of 10 - var subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100), variables = []; - for (var i=range[0],varref,v; i < range[1]; i+= subrangelen) { - varref = ++this.nextId; - v = this.variableHandles[varref] = { varref:varref, arrvar:varinfo.arrvar, range:[i, Math.min(i+subrangelen, range[1])] }; - variables.push({name:`[${v.range[0]}..${v.range[1]-1}]`,type:'',value:'',variablesReference:varref}); - } - return $.Deferred().resolve(variables); - } - // get the elements for the specified range - return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count, {varinfo}) - .then((elements, x) => { - elements.forEach(el => el.fqname = `${x.varinfo.arrvar.fqname || x.varinfo.arrvar.name}[${el.name}]`); - x.varinfo.cached = elements; - return this._local_to_variable(elements); - }); - } - else if (varinfo.bigstring) { - return this.dbgr.getstringchars(varinfo.bigstring.value) - .then((s) => { - return this._local_to_variable([{name:'',hasnullvalue:false,string:s,type:JTYPES.String,valid:true}]); - }); - } - else if (varinfo.primitive) { - // convert the primitive value into alternate formats - var variables = [], bits = {J:64,I:32,S:16,B:8}[varinfo.signature]; - const pad = (u,base,len) => ('0000000000000000000000000000000'+u.toString(base)).slice(-len); - switch(varinfo.signature) { - case 'Ljava/lang/String;': - variables.push({name:'',type:'',value:varinfo.value.toString(),variablesReference:0}); - break; - case 'C': - variables.push({name:'',type:'',value:varinfo.value.charCodeAt(0).toString(),variablesReference:0}); - break; - case 'J': - // because JS cannot handle 64bit ints, we need a bit of extra work - var v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,''); - const s4 = { hi:parseInt(v64hex.slice(0,8),16), lo:parseInt(v64hex.slice(-8),16) }; - variables.push( - {name:'',type:'',value:pad(s4.hi,2,32)+pad(s4.lo,2,32),variablesReference:0} - ,{name:'',type:'',value:NumberBaseConverter.hexToDec(v64hex,false),variablesReference:0} - ,{name:'',type:'',value:pad(s4.hi,16,8)+pad(s4.lo,16,8),variablesReference:0} - ); - break; - default:// integer/short/byte value - const u = varinfo.value >>> 0; - variables.push( - {name:'',type:'',value:pad(u,2,bits),variablesReference:0} - ,{name:'',type:'',value:u.toString(10),variablesReference:0} - ,{name:'',type:'',value:pad(u,16,bits/4),variablesReference:0} - ); - break; - } - return $.Deferred().resolve(variables); - } - else if (varinfo.frame) { - // frame locals request - this should be handled by AndroidDebugThread instance - return $.Deferred().resolve([]); - } else { - // something else? - return $.Deferred().resolve([]); - } - } - - /** - * Converts locals (or other vars) in debugger format into Variable objects used by VSCode - */ - _local_to_variable(v) { - if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._local_to_variable(v)); - var varref = 0, objvalue, evaluateName = v.fqname || v.name, formats = {}, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename; - switch(true) { - case v.hasnullvalue && JTYPES.isReference(v.type): - // null object or array type - objvalue = 'null'; - break; - case v.type.signature === JTYPES.Object.signature: - // Object doesn't really have anything worth seeing, so just treat it as unexpandable - objvalue = v.type.typename; - break; - case v.type.signature === JTYPES.String.signature: - objvalue = JSON.stringify(v.string); - if (v.biglen) { - // since this is a big string - make it viewable on expand - varref = ++this.nextId; - this.variableHandles[varref] = {varref:varref, bigstring:v}; - objvalue = `String (length:${v.biglen})`; - } - else if (this._expandable_prims) { - // as a courtesy, allow strings to be expanded to see their length - varref = ++this.nextId; - this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.string.length}; - } - break; - case JTYPES.isArray(v.type): - // non-null array type - if it's not zero-length add another variable reference so the user can expand - if (v.arraylen) { - varref = this._getObjectIdReference(v.type, v.value); - this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] }; - } - objvalue = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound - break; - case JTYPES.isObject(v.type): - // non-null object instance - add another variable reference so the user can expand - varref = this._getObjectIdReference(v.type, v.value); - this.variableHandles[varref] = {varref:varref, objvar:v}; - objvalue = v.type.typename; - break; - case v.type.signature === 'C': - const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'}; - if (cmap[v.char]) { - objvalue = `'\\${cmap[v.char]}'`; - } else if (v.value < 32) { - objvalue = v.value ? `'\\u${('000'+v.value.toString(16)).slice(-4)}'` : "'\\0'"; - } else objvalue = `'${v.char}'`; - break; - case v.type.signature === 'J': - // because JS cannot handle 64bit ints, we need a bit of extra work - var v64hex = v.value.replace(/[^0-9a-fA-F]/g,''); - objvalue = formats.dec = NumberBaseConverter.hexToDec(v64hex, true); - formats.hex = '0x' + v64hex.replace(/^0+/, '0'); - formats.oct = formats.bin = ''; - // 24 bit chunks... - for (var s=v64hex,uint; s; s = s.slice(0,-6)) { - uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits - formats.oct = uint.toString(8) + formats.oct; - formats.bin = uint.toString(2) + formats.bin; - } - formats.oct = '0c' + formats.oct.replace(/^0+/, '0'); - formats.bin = '0b' + formats.bin.replace(/^0+/, '0'); - break; - case /^[BIS]$/.test(v.type.signature): - objvalue = formats.dec = v.value.toString(); - var uint = (v.value >>> 0); - formats.hex = '0x' + uint.toString(16); - formats.oct = '0c' + uint.toString(8); - formats.bin = '0b' + uint.toString(2); - break; - default: - // other primitives: boolean, etc - objvalue = v.value.toString(); - break; - } - // as a courtesy, allow integer and character values to be expanded to show the value in alternate bases - if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) { - varref = ++this.nextId; - this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.value}; - } - return { - name: v.name, - type: typename, - value: objvalue, - evaluateName, - variablesReference: varref, - } - } - - setVariableValue(args) { - const failSetVariableRequest = reason => $.Deferred().reject(new Error(reason)); - - var v = this.variableHandles[args.variablesReference]; - if (!v || !v.cached) { - return failSetVariableRequest(`Variable '${args.name}' not found`); - } - - var destvar = v.cached.find(v => v.name===args.name); - if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) { - return failSetVariableRequest(`The value is read-only and cannot be updated.`); - } - - // be nice and remove any superfluous whitespace - var value = args.value.trim(); - - if (!value) { - // just ignore blank requests - var vsvar = this._local_to_variable(destvar); - return $.Deferred().resolve(vsvar); - } - - // non-string reference types can only set to null - if (/^L/.test(destvar.type.signature) && destvar.type.signature !== JTYPES.String.signature) { - if (value !== 'null') { - return failSetVariableRequest('Object references can only be set to null'); - } - } - - // convert the new value into a debugger-compatible object - var m, num, data, datadef; - switch(true) { - case value === 'null': - data = {valuetype:'oref',value:null}; // null object reference - break; - case /^(true|false)$/.test(value): - data = {valuetype:'boolean',value:value!=='false'}; // boolean literal - break; - case !!(m=value.match(/^[+-]?0x([0-9a-f]+)$/i)): - // hex integer- convert to decimal and fall through - if (m[1].length < 52/4) - value = parseInt(value, 16).toString(10); - else - value = NumberBaseConverter.hexToDec(value); - m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/); - // fall-through - case !!(m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/)): - // decimal integer - num = parseFloat(value, 10); // parseInt() can't handle exponents - switch(true) { - case (num >= -128 && num <= 127): data = {valuetype:'byte',value:num}; break; - case (num >= -32768 && num <= 32767): data = {valuetype:'short',value:num}; break; - case (num >= -2147483648 && num <= 2147483647): data = {valuetype:'int',value:num}; break; - case /inf/i.test(num): return failSetVariableRequest(`Value '${value}' exceeds the maximum number range.`); - case /^[FD]$/.test(destvar.type.signature): data = {valuetype:'float',value:num}; break; - default: - // long (or larger) - need to use the arbitrary precision class - data = {valuetype:'long',value:NumberBaseConverter.decToHex(value, 16)}; - switch(true){ - case data.value.length > 16: - case num > 0 && data.value.length===16 && /[^0-7]/.test(data.value[0]): - // number exceeds signed 63 bit - make it a float - data = {valuetype:'float',value:num}; - break; - } - } - break; - case !!(m=value.match(/^(Float|Double)\s*\.\s*(POSITIVE_INFINITY|NEGATIVE_INFINITY|NaN)$/)): - // Java special float constants - data = {valuetype:m[1].toLowerCase(),value:{POSITIVE_INFINITY:Infinity,NEGATIVE_INFINITY:-Infinity,NaN:NaN}[m[2]]}; - break; - case !!(m=value.match(/^([+-])?infinity$/i)):// allow js infinity - data = {valuetype:'float',value:m[1]!=='-'?Infinity:-Infinity}; - break; - case !!(m=value.match(/^nan$/i)): // allow js nan - data = {valuetype:'float',value:NaN}; - break; - case !!(m=value.match(/^[+-]?[0-9]+[eE][-][0-9]+([dDfF])?$/)): - case !!(m=value.match(/^[+-]?[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?([dDfF])?$/)): - // decimal float - num = parseFloat(value); - data = {valuetype:/^[dD]$/.test(m[1]) ? 'double': 'float',value:num}; - break; - case !!(m=value.match(/^'(?:\\u([0-9a-fA-F]{4})|\\([bfrntv0'])|(.))'$/)): - // character literal - var cvalue = m[1] ? String.fromCharCode(parseInt(m[1],16)) : - m[2] ? {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v',0:'\0',"'":"'"}[m[2]] - : m[3] - data = {valuetype:'char',value:cvalue}; - break; - case !!(m=value.match(/^"[^"\\\n]*(\\.[^"\\\n]*)*"$/)): - // string literal - we need to get the runtime to create a new string first - datadef = createJavaString(this.dbgr, value).then(stringlit => ({valuetype:'oref', value:stringlit.value})); - break; - default: - // invalid literal - return failSetVariableRequest(`'${value}' is not a valid Java literal.`); - } - - if (!datadef) { - // as a nicety, if the destination is a string, stringify any primitive value - if (data.valuetype !== 'oref' && destvar.type.signature === JTYPES.String.signature) { - datadef = createJavaString(this.dbgr, data.value.toString(), {israw:true}) - .then(stringlit => ({valuetype:'oref', value:stringlit.value})); - } else if (destvar.type.signature.length===1) { - // if the destination is a primitive, we need to range-check it here - // Neither our debugger nor the JDWP endpoint validates primitives, so we end up with - // weirdness if we allow primitives to be set with out-of-range values - var validmap = { - B:'byte,char', // char may not fit - we special-case this later - S:'byte,short,char', - I:'byte,short,int,char', - J:'byte,short,int,long,char', - F:'byte,short,int,long,char,float', - D:'byte,short,int,long,char,double,float', - C:'byte,short,char',Z:'boolean', - isCharInRangeForByte: c => c.charCodeAt(0) < 256, - }; - var is_in_range = (validmap[destvar.type.signature]||'').indexOf(data.valuetype) >= 0; - if (destvar.type.signature === 'B' && data.valuetype === 'char') - is_in_range = validmap.isCharInRangeForByte(data.value); - if (!is_in_range) { - return failSetVariableRequest(`'${value}' is not compatible with variable type: ${destvar.type.typename}`); - } - // check complete - make sure the type matches the destination and use a resolved deferred with the value - if (destvar.type.signature!=='C' && data.valuetype === 'char') - data.value = data.value.charCodeAt(0); // convert char to it's int value - if (destvar.type.signature==='J' && typeof data.value === 'number') - data.value = NumberBaseConverter.decToHex(''+data.value,16); // convert ints to hex-string longs - data.valuetype = destvar.type.typename; - - datadef = $.Deferred().resolveWith(this,[data]); - } - } - - return datadef.then(data => { - // setxxxvalue sets the new value and then returns a new local for the variable - switch(destvar.vtype) { - case 'field': return this.dbgr.setfieldvalue(destvar, data); - case 'local': return this.dbgr.setlocalvalue(destvar, data); - case 'arrelem': - var idx = parseInt(args.name, 10), count=1; - if (idx < 0 || idx >= destvar.data.arrobj.arraylen) throw new Error('Array index out of bounds'); - return this.dbgr.setarrayvalues(destvar.data.arrobj, idx, count, data); - default: throw new Error('Unsupported variable type'); - } - }) - .then(newlocalvar => { - if (destvar.vtype === 'arrelem') newlocalvar = newlocalvar[0]; - Object.assign(destvar, newlocalvar); - var vsvar = this._local_to_variable(destvar); - return vsvar; - }) - .fail(e => { - return failSetVariableRequest(`Variable update failed. ${e.message||''}`); - }); - } -} - -exports.AndroidVariables = AndroidVariables; diff --git a/src/wsproxy.js b/src/wsproxy.js deleted file mode 100644 index 56e4997..0000000 --- a/src/wsproxy.js +++ /dev/null @@ -1,440 +0,0 @@ -const WebSocketServer = require('./minwebsocket').WebSocketServer; -const { atob, btoa, ab2str, str2u8arr, arrayBufferToString, intFromHex, intToHex, D,E,W, get_file_fd_from_fdn } = require('./util'); -const { connect_forward_listener } = require('./services'); -const { get_socket_fd_from_fdn, socket_loopback_client } = require('./sockets'); -const { readx, writex } = require('./transport'); - -var dprintfln = ()=>{};//D; -WebSocketServer.DEFAULT_ADB_PORT = 5037; - -var proxy = { - - Server: function(port, adbport) { - // Listen for websocket connections. - var wsServer = new WebSocketServer(port); - wsServer.adbport = adbport; - wsServer.setADBPort = function(port) { - if (typeof(port) === 'undefined') - return this.adbport = WebSocketServer.DEFAULT_ADB_PORT; - return this.adbport = port; - } - - // A list of connected websockets. - var connectedSockets = []; - - function indexof_connected_socket(socketinfo) { - if (!socketinfo) return -1; - for (var i=0; i < connectedSockets.length; i++) - if (connectedSockets[i] === socketinfo) - return i; - return -1; - } - - wsServer.onconnection = function(req) { - - var ws = req.accept(); - var si = { - wsServer: wsServer, - ws: ws, - fn: check_client_version, - fdarr: [], - }; - connectedSockets.push(si); - - ws.onmessage = function(e) { - si.fn(si, e); - }; - - // When a socket is closed, remove it from the list of connected sockets. - ws.onclose = function() { - while (si.fdarr.length) { - si.fdarr.pop().close(); - } - var idx = indexof_connected_socket(si); - if (idx>=0) connectedSockets.splice(idx, 1); - else D('Cannot find disconnected socket in connectedSockets'); - }; - - return true; - }; - - D('WebSocketServer started. Listening on port: %d', port); - - return wsServer; - } -} - -var check_client_version = function(si, e) { - if (e.data !== 'vscadb client version 1') { - D('Wrong client version: ', e.data); - return end_of_connection(si); - } - si.fn = handle_proxy_command; - si.ws.send('vscadb proxy version 1'); -} - -var end_of_connection = function(si) { - if (!si || !si.ws) return; - si.ws.close(); -} - -var handle_proxy_command = function(si, e) { - if (!e || !e.data || e.data.length<2) return end_of_connection(si); - var cmd = e.data.slice(0,2); - var fn = proxy_command_fns[cmd]; - if (!fn) { - E('Unknown command: %s', e.data); - return end_of_connection(si); - } - fn(si, e); -} - -function end_of_command(si, respfmt) { - if (!si || !si.ws || !respfmt) return; - // format the response - we allow %s, %d and %xX - var response = respfmt; - var fmtidx = 0; - for (var i=2; i < arguments.length; i++) { - var fmt = response.slice(fmtidx).match(/%([sdxX])/); - if (!fmt) break; - response = [response.slice(0,fmt.index),arguments[i],response.slice(fmt.index+2)]; - switch(fmt[1]) { - case 'x': response[1] = response[1].toString(16).toLowerCase(); break; - case 'X': response[1] = response[1].toString(16).toUpperCase(); break; - } - response = response.join(''); - fmtidx = fmt.index + arguments[i].length; - } - si.ws.send(response); -} - -function readsckt(fd, n, cb) { - readx(fd, n, cb); -} - -function write_adb_command(fd, cmd) { - dprintfln('write_adb_command: %s',cmd); - // write length in hex first - writex(fd, intToHex(cmd.length, 4)); - // then the command - writex(fd, cmd); -} - -function read_adb_status(adbfd, extra, cb) { - - // read back the status - readsckt(adbfd, 4+extra, function(err, data) { - if (err) return cb(); - var status = ab2str(data); - dprintfln("adb status: %s", status); - cb(status); - }); -} - -function read_adb_reply(adbfd, b64encode, cb) { - - // read reply length - readsckt(adbfd, 4, function(err, data) { - if (err) return cb(); - var n = intFromHex(ab2str(data)); - dprintfln("adb expected reply: %d bytes", n); - // read reply - readsckt(adbfd, n, function(err, data) { - if (err) return cb(); - var n = data.byteLength; - dprintfln("adb reply: %d bytes", n); - var response = ab2str(data); - if (n === 0) response = '\n'; // always send something - dprintfln("%s",response); - if (b64encode) response = btoa(response); - return cb(response); - }); - }); -} - -const min_fd_num = 1000; -var fdn_to_fd = function(n) { - var fd; - if (n >= min_fd_num) fd = get_file_fd_from_fdn(n); - else fd = get_socket_fd_from_fdn(n); - if (!fd) throw new Error('Invalid file descriptor number: '+n); - return fd; -} - -var retryread = function(fd, len, cb) { - fd.readbytes(len, cb); -} - -var retryreadfill = function(fd, len, cb) { - var buf = new Uint8Array(len); - var totalread = 0; - var readmore = function(amount) { - fd.readbytes(amount, function(err, data) { - if (err) return cb(err); - buf.set(data, totalread); - totalread += data.byteLength; - var diff = len - totalread; - if (diff > 0) return readmore(diff); - cb(err, buf); - }); - }; - readmore(len); -} - -var be2le = function(buf) { - var x = new Uint8Array(buf); - var a = x[0]; - a = (a<<8)+x[1]; - a = (a<<8)+x[2]; - a = (a<<8)+x[3]; - return a; -} - -var jdwpReplyMonitor = function(fd, si, packets) { - if (!packets) { - packets = 0; - dprintfln("jdwpReplyMonitor thread started. jdwpfd:%d.", fd.n); - } - - //dprintfln("WAITING FOR JDWP DATA...."); - //int* pjdwpdatalen = (int*)&buffer[0]; - //*pjdwpdatalen=0; - retryread(fd, 4, function(err, data) { - if (err) return terminate(); - - var m = data.byteLength; - if (m != 4) { - dprintfln("rj %d len read", m); - return terminate(); - } - m = be2le(data.buffer.slice(0,4)); - //dprintfln("STARTING JDWP DATA: %.8x....", m); - - var lenstr = arrayBufferToString(data.buffer); - - retryreadfill(fd, m-4, function(err, data) { - if (err) return terminate(); - - var n = data.byteLength + 4; - if (n != m) { - dprintfln("rj read incomplete %d/%d", (n+4),m); - return terminate(); - } - //dprintfln("GOT JDWP DATA...."); - dprintfln("rj encoding %d bytes", n); - var response = "rj ok "; - response += btoa(lenstr + arrayBufferToString(data.buffer)); - - si.ws.send(response); - //dprintfln("SENT JDWP REPLY...."); - packets++; - - jdwpReplyMonitor(fd, si, packets); - }); - }); - - function terminate() { - // try and send a final event reply indicating the VM has disconnected - var vmdisconnect = [ - 0,0,0,17, // len - 100,100,100,100, // id - 0, //flags - 0x40,0x64, // errcode = composite event - 0, //suspend - 0,0,0,1, // eventcount - 100, // eventkind=VM_DISCONNECTED - ]; - var response = "rj ok "; - response += btoa(ab2str(new Uint8Array(vmdisconnect))); - si.ws.send(response); - dprintfln("jdwpReplyMonitor thread finished. Sent:%d packets.", packets); - } -} - - -var stdoutMonitor = function(fd, si, packets) { - if (!packets) { - packets = 0; - dprintfln("stdoutMonitor thread started. jdwpfd:%d, wsfd:%o.", fd.n, si); - } - - retryread(fd, function(err, data) { - if (err) return terminate(); - var response = 'so ok '+btoa(ab2str(new Uint8Array(data))); - si.ws.send(response); - packets++; - - stdoutMonitor(fd, si, packets); - }); - - function terminate() { - // send a unique terminating string to indicate the stdout monitor has finished - var eoso = "eoso:d10d9798-1351-11e5-bdd9-5b316631f026"; - var response = "so ok " + btoa(eoso); - si.ws.send(response); - dprintfln("stdoutMonitor thread finished. Sent:%d packets.", packets); - } -} - -// commands are: -// cn - create adb socket -// cp - create custom-port socket -// wa - write_adb_command -// rs [extra] - read_adb_status -// ra - read_adb_reply -// rj - read jdwp-formatted reply -// rx - read raw data from adb socket -// wx - write raw data to adb socket -// dc - disconnect adb sockets - -var proxy_command_fns = { - cn:function(si, e) { - // create adb socket - socket_loopback_client(si.wsServer.adbport, function(fd) { - if (!fd) { - return end_of_command(si, 'cn error connection failed'); - } - si.fdarr.push(fd); - return end_of_command(si, 'cn ok %d', fd.n); - }); - }, - - cp:function(si, e) { - var x = e.data.split(' '), port; - port = parseInt(x[1], 10); - connect_forward_listener(port, {create:true}, function(sfd) { - return end_of_command(si, 'cp ok %d', sfd.n); - }); - }, - - wa:function(si, e) { - var x = e.data.split(' '), fd, buffer; - try { - var fdn = parseInt(x[1], 10); - fd = fdn_to_fd(fdn); - buffer = atob(x[2]); - } catch(err) { - return end_of_command(si, 'wa error wrong parameters'); - } - write_adb_command(fd, buffer); - return end_of_command(si, 'wa ok'); - }, - - // rs fd [extra] - rs:function(si, e) { - var x = e.data.split(' '), fd, extra; - try { - var fdn = parseInt(x[1], 10); - fd = fdn_to_fd(fdn); - // optional additional bytes - used for sync-responses which - // send status+length as 8 bytes - extra = parseInt(atob(x[2]||'MA==')); - } catch(err) { - return end_of_command(si, 'rs error wrong parameters'); - } - read_adb_status(fd, extra, function(status) { - return end_of_command(si, 'rs ok %s', status||''); - }) - }, - - ra:function(si, e) { - var x = e.data.split(' '), fd; - try { - var fdn = parseInt(x[1], 10); - fd = fdn_to_fd(fdn); - } catch(err) { - return end_of_command(si, 'ra error wrong parameters'); - } - read_adb_reply(fd, true, function(b64adbreply) { - if (!b64adbreply) { - return end_of_command('ra error read failed'); - } - return end_of_command(si, 'ra ok %s', b64adbreply); - }); - }, - - rj:function(si, e) { - var x = e.data.split(' '), fd; - try { - var fdn = parseInt(x[1], 10); - fd = fdn_to_fd(fdn); - } catch(err) { - return end_of_command(si, 'rj error wrong parameters'); - } - jdwpReplyMonitor(fd, si); - return end_of_command(si, 'rj ok'); - }, - - rx:function(si, e) { - var x = e.data.split(' '), fd; - try { - var fdn = parseInt(x[1], 10); - fd = fdn_to_fd(fdn); - } catch(err) { - return end_of_command(si, 'rx error wrong parameters'); - } - if (fd.isSocket) { - fd.readbytes(doneread); - } else { - fd.readbytes(fd.readpipe.byteLength, doneread); - } - function doneread(err, data) { - if (err) { - return end_of_command(si, 'rx ok nomore'); - } - end_of_command(si, 'rx ok ' + btoa(ab2str(data))); - } - }, - - so:function(si, e) { - var x = e.data.split(' '), fd; - try { - var fdn = parseInt(x[1], 10); - fd = fdn_to_fd(fdn); - } catch(err) { - return end_of_command(si, 'so error wrong parameters'); - } - stdoutMonitor(fd, si); - return end_of_command(si, 'so ok'); - }, - - wx:function(si, e) { - var x = e.data.split(' '), fd, buffer; - try { - var fdn = parseInt(x[1], 10); - fd = fdn_to_fd(fdn); - buffer = atob(x[2]); - } catch(err) { - return end_of_command(si, 'wx error wrong parameters'); - } - - fd.writebytes(str2u8arr(buffer), function(err) { - if (err) - return end_of_command(si, 'wx error device write failed'); - end_of_command(si, 'wx ok'); - }); - }, - - dc:function(si, e) { - var x = e.data.split(' '); - if (x[1] === 'all') { - while (si.fdarr.length) { - si.fdarr.pop().close(); - } - return end_of_command(si, 'dc ok'); - } - - var n = parseInt(x[1],10); - for (var i=0; i < si.fdarr.length; i++) { - if (si.fdarr[i].n === n) { - var fd = si.fdarr.splice(i,1)[0]; - fd.close(); - break; - } - } - return end_of_command(si, 'dc ok'); - } - -} - -exports.proxy = proxy; diff --git a/test/extension.test.js b/test/extension.test.js index c3c1517..f13caed 100644 --- a/test/extension.test.js +++ b/test/extension.test.js @@ -6,12 +6,12 @@ // // The module 'assert' provides assertion methods from node -var assert = require('assert'); +const assert = require('assert'); // You can import and use all API from the 'vscode' module // as well as import your extension to test it -var vscode = require('vscode'); -var myExtension = require('../extension'); +//const vscode = require('vscode'); +//const myExtension = require('../extension'); // Defines a Mocha test suite to group tests of similar kind together suite("Extension Tests", function() { diff --git a/test/index.js b/test/index.js index 5604517..a0c8b63 100644 --- a/test/index.js +++ b/test/index.js @@ -10,7 +10,7 @@ // to report the results back to the caller. When the tests are finished, return // a possible error to the callback or null if none. -var testRunner = require('vscode/lib/testrunner'); +const testRunner = require('vscode/lib/testrunner'); // You can directly control Mocha options by uncommenting the following lines // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info