44 Commits

Author SHA1 Message Date
Dave Holoway
0672e54401 Version 1 (#83)
* replace jq-promises with native Promises

* updates to use native promises and async await

* Fix variable errors, remove extra parameters and correct export declaratons

* refactor launch request to use async/await

* fix running debugger on custom ADB port

* remove unused files

* move socket_ended check to ensure we don't loop reading 0 bytes

* refactor logcat code and ensure disconnect status is passed on to webview

* Fix warnings

* Clean up util and remove unused functions

* convert Debugger into a class

* update jsconfig target to es2018 and enable checkJS

* more updates to use async/await and more readable refactoring.

- added type definitions and debugger classes
- improved expression evaluation
- refactored expressions into parsing, evaluation and variable assignment
- fixed invoking methods with parameters
- added support for static method invokes
- improved exception display reliability
- refactored launch into smaller functions
- refactored utils into smaller modules
- removed redundant code
- converted JDWP functions to classes

* set version 1.0.0 and update dependencies

* add changelog notes
2020-04-20 12:53:08 +01:00
Dave Holoway
f92f247ef6 version 0.8.0 2019-08-26 11:13:36 +01:00
Dave Holoway
133b7061b2 Fix critical security advisory https://www.npmjs.com/advisories/1118 2019-08-26 10:58:07 +01:00
Dave Holoway
7e958620a8 Allow customised install arguments (#75)
* add pmInstallArgs launch config property

* add missing launchActivity description
correct default trace value

* add launchActivity to the README

* add comment describing why we look for IllegalArgumentException
2019-08-26 10:48:29 +01:00
Dave Holoway
989de8254a Extract manifest directly from APK (#74)
* initial support for decoding manifest from the APK

* add support for overriding AndroidManifest.xml file location in launch config

* correct property name in comment
2019-08-25 19:37:21 +01:00
Dave Holoway
8f2f7d2fd4 version 0.7.1 2019-08-18 19:27:49 +01:00
Dave Holoway
99544b527d Updated README with bmac link (#71) 2019-08-18 19:25:13 +01:00
Dave Holoway
614fcbd2ba version 0.7.0 2019-08-18 16:08:32 +01:00
Dave Holoway
b04e4328a6 upgrade ws to 7.1.2 (#70) 2019-08-18 16:01:06 +01:00
Dave Holoway
1a00cdb291 Replace new Buffer() constructor calls (#69) 2019-08-18 14:53:54 +01:00
dependabot[bot]
d1fd889433 Bump js-yaml from 3.12.0 to 3.13.1 (#59)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-18 14:36:52 +01:00
dependabot[bot]
a7e4cac6df Bump lodash from 4.17.11 to 4.17.14 (#61)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-18 14:35:30 +01:00
dependabot[bot]
c6df24ab95 Bump tar from 2.2.1 to 2.2.2 (#65)
Bumps [tar](https://github.com/npm/node-tar) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Commits](https://github.com/npm/node-tar/compare/v2.2.1...v2.2.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-18 14:33:43 +01:00
dependabot[bot]
23184ea4c2 Bump fstream from 1.0.11 to 1.0.12 (#66)
Bumps [fstream](https://github.com/npm/fstream) from 1.0.11 to 1.0.12.
- [Release notes](https://github.com/npm/fstream/releases)
- [Commits](https://github.com/npm/fstream/compare/v1.0.11...v1.0.12)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-18 14:31:57 +01:00
NathanMcBride666
b7ba47b811 Added support for kotlin src folder (#62)
Signed-off-by: Nathan McBride <noman@hidden.com>
2019-08-18 14:27:43 +01:00
Dave Holoway
033f5c80ab Update README with info about prelaunch builds (#68) 2019-08-18 14:22:07 +01:00
Dave Holoway
0cbb56ca9b Create LICENSE (#67) 2019-08-18 14:03:12 +01:00
Dave Holoway
684dd39181 Fix logcat not displaying (#64)
* update LogcatContent constructor to use single deviceID argument

* add support for WebviewPanel for displaying logcat data

Fixes #63
2019-08-18 13:22:12 +01:00
lbhnrg2021
52ab704acd Fix breakpoints not triggering on Windows 10 (#55)
* fix-windows-breakpoint

* perform a case-insensitve path search for packages

* revert unnecessary changes to packages
2019-04-24 00:50:23 +01:00
Dave Holoway
45e2dc2fe1 version 0.6.2 2018-12-16 21:10:16 +00:00
Dave Holoway
30ed5dea3b Fix logcat not launching (#50)
* use prepare instead of postinstall as vscode is a dev dependency  only

* add missing uuid dependency
update devDependencies
2018-12-16 21:08:01 +00:00
Dave Holoway
0eb44130a6 Downgrade vulnerable event-stream package (#48)
* regenerate package-lock

* update changelog
2018-12-03 14:05:10 +00:00
Dave Holoway
d1e7c86092 version 0.6 (#46) 2018-11-11 20:42:02 +00:00
Dave Holoway
690f9dc23a update debugger label (#45) 2018-11-11 20:34:43 +00:00
Dave Holoway
27ecd41b68 update default apkFile path (#43) 2018-11-11 20:22:40 +00:00
Dave Holoway
756a1cea29 update package dependencies (#41) 2018-11-11 20:11:21 +00:00
Dave Holoway
fc2ce97a23 breakpoints do not get enabled on startup (#40)
* add more trace around breakpoint config during startup

* use 'changed' instead of 'updated' when sending BreakpointEvents
2018-11-11 19:31:30 +00:00
Dave Holoway
de8abc62bc add trace support (#38)
* add basic support for sending console logs to OutputWindow

* add trace config setting

* standardise logs
2018-11-11 17:57:32 +00:00
Dave Holoway
8cc31476b3 fix breakpoints don't trigger when hit (#37)
* add errorcode to empty jdwp results

* use an empty line table if the command request fails
2018-11-11 15:20:28 +00:00
adelphes
494bb83cbf version 0.5.0 2018-05-06 20:32:57 +01:00
Dave Holoway
9fca5cbe8c Merge pull request #28 from adelphes/kotlin-support
add basic support for kotlin source files
2018-05-06 20:13:24 +01:00
adelphes
5f0a02b17f add basic support for kotlin source files 2018-05-06 20:02:31 +01:00
adelphes
da36e8e457 added extension package state info 2017-06-24 19:53:22 +01:00
adelphes
3dbfd8ef2a Improved array handling. Better multidimensional array support. 2017-06-24 16:18:22 +01:00
adelphes
4a31b83eb9 add support for evaluateName so Add To Watch works again 2017-06-24 13:16:36 +01:00
adelphes
261c06f1d6 Ensure the cached fields are populated before showing the Exception UI 2017-06-24 12:20:21 +01:00
adelphes
130d79f6c2 add initial support for method call expressions 2017-06-24 12:02:36 +01:00
adelphes
8baf894fc9 add basic support for Exception UI 2017-06-20 13:40:57 +01:00
adelphes
92bd003122 Refactor expression evaluation into its own file 2017-06-18 13:56:04 +01:00
adelphes
13f116b3b3 format integers into other bases 2017-06-18 13:40:13 +01:00
adelphes
140e48cbd1 Add newlines to output
Cancel certain requests if the thread has been resumed
2017-06-18 12:57:38 +01:00
adelphes
7e8f471df4 revert util cleanup until dependencies are sorted 2017-06-14 16:10:58 +01:00
adelphes
09905eb85a Fix output newlines 2017-06-14 15:57:17 +01:00
adelphes
e76773e8e4 code tidy - fix lint warnings for unused fns, params and locals 2017-05-12 14:45:59 +01:00
48 changed files with 9517 additions and 7755 deletions

10
.vscode/launch.json vendored
View File

@@ -8,7 +8,10 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
"stopOnEntry": false
"stopOnEntry": false,
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Server",
@@ -16,7 +19,10 @@
"request": "launch",
"cwd": "${workspaceRoot}",
"program": "${workspaceRoot}/src/debugMain.js",
"args": [ "--server=4711" ]
"args": [ "--server=4711" ],
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Launch Tests",

View File

@@ -1,5 +1,45 @@
# 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
* Allow `pm install` arguments to be customised as a launch configuration property
* Document `launchActivity` launch configuration property
* Fix critical security advisory https://www.npmjs.com/advisories/1118
### version 0.7.1
* Added the [Buy Me A Coffee](https://www.buymeacoffee.com/adelphes) link to the README
### version 0.7.0
* Fix logcat not displaying
* Fix breakpoints not triggering on Windows
* Added kotlin folder to list of known source locations
* Upgraded dependencies to resolve a number of security vulnerabilites
* Updated README with info about prelaunch build task
* Added MIT license file
### version 0.6.2
* Fix broken logcat command due to missing dependency
### version 0.6.1
* Regenerate package-lock.json to remove event-stream vulnerability - https://github.com/dominictarr/event-stream/issues/116
### version 0.6.0
* Fix issue with breakpoints not enabling correctly
* Fix issue with JDWP failure on breakpoint hit
* Added support for diagnostic logs using trace configuration option
* Updated default apkFile path to match current releases of Android Studio
* Updated package dependencies
### version 0.5.0
* Debugger support for Kotlin source files
* Exception UI
* Fixed some console display issues
### version 0.4.1
* One day I will learn to update the changelog **before** I hit publish
* Updated changelog

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Dave Holoway
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -20,7 +20,7 @@ You must have [Android SDK Platform Tools](https://developer.android.com/studio/
* This is a preview version so expect the unexpected. Please log any issues you find on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
* This extension **will not build your app**.
If you use gradle (or Android Studio), you can build your app from the command-line using `./gradlew assembleDebug`.
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way.
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way. See the section below on how to configure a VSCode task to automatically build your app before launching a debug session.
* Some debugger options are yet to be implemented. You cannot set conditional breakpoints and watch expressions must be simple variables.
* If you require a must-have feature that isn't there yet, let us know on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
* This extension does not provide any additional code completion or other editing enhancements.
@@ -29,7 +29,7 @@ If you use gradle (or Android Studio), you can build your app from the command-l
This extension allows you to debug your App by creating a new Android configuration in `launch.json`.
The following settings are used to configure the debugger:
```jsonc
{
"version": "0.2.0",
"configurations": [
@@ -50,9 +50,65 @@ The following settings are used to configure the debugger:
// Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn
"staleBuild": "warn",
// Fully qualified path to the AndroidManifest.xml file compiled in the APK. Default: appSrcRoot/AndroidManifest.xml
"manifestFile": "${workspaceRoot}/app/src/main/AndroidManifest.xml",
// APK install arguments passed to the Android package manager. Run 'adb shell pm' to show valid arguments. Default: ["-r"]
"pmInstallArgs": ["-r"],
// Manually specify the activity to run when the app is started.
"launchActivity": ".MainActivity"
}
]
}
```
## Building your app automatically
This extension will not build your App. If you would like to run a build each time a debug session is started, you can add a `preLaunchTask` option to your `launch.json` configuration which invokes a build task.
#### .vscode/launch.json
Add a `preLaunchTask` item to the launch configuration:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "android",
"request": "launch",
"name": "App Build & Launch",
"preLaunchTask": "run gradle",
}
]
}
```
Add a new task to run the build command:
#### .vscode/tasks.json
```json
{
"version": "2.0.0",
"tasks": [
{
"label": "run gradle",
"type": "shell",
"command": "${workspaceFolder}/gradlew",
"args": ["assembleDebug"]
}
]
}
```
## Powered by coffee
The Android Developer Extension is a completely free, fully open-source project. If you've found the extension useful, you
can support it by [buying me a coffee](https://www.buymeacoffee.com/adelphes).
If you use ApplePay or Google Pay, you can scan the code with your phone camera:
![BuyMeACoffee Code](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/bmac-code.png)
Every coffee makes a difference, so thanks for adding your support.
## Questions / Problems

View File

@@ -4,14 +4,6 @@ const vscode = require('vscode');
const { AndroidContentProvider } = require('./src/contentprovider');
const { openLogcatWindow } = require('./src/logcat');
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
function activate(context) {
@@ -19,28 +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.activate = activate;
exports.deactivate = deactivate;

BIN
images/bmac-code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

1280
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "0.4.1",
"version": "1.0.0",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
"engines": {
"vscode": "^1.8.0"
"vscode": "^1.24.0"
},
"categories": [
"Debuggers"
@@ -35,12 +35,15 @@
"breakpoints": [
{
"language": "java"
},
{
"language": "kotlin"
}
],
"debuggers": [
{
"type": "android",
"label": "Android Debug",
"label": "Android",
"program": "./src/debugMain.js",
"runtime": "node",
"configurationAttributes": {
@@ -59,7 +62,7 @@
"apkFile": {
"type": "string",
"description": "Fully qualified path to the built APK (Android Application Package)",
"default": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk"
"default": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk"
},
"adbPort": {
"type": "integer",
@@ -76,11 +79,28 @@
"description": "Number of entries to display in call stack views (for locations outside of the project source). 0 shows the entire call stack. Default: 1",
"default": 1
},
"launchActivity": {
"type": "string",
"description": "Manually specify the activity to run when the app is started.",
"default": ""
},
"logcatPort": {
"type": "integer",
"description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038",
"default": 7038
},
"manifestFile": {
"type": "string",
"description": "Overrides the default location of AndroidManifest.xml",
"default": "${workspaceRoot}/app/src/main/AndroidManifest.xml"
},
"pmInstallArgs": {
"type": "array",
"description": "APK install arguments passed to the Android package manager. Run 'adb shell pm' to show valid arguments. Default: [\"-r\"]",
"default": [
"-r"
]
},
"staleBuild": {
"type": "string",
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
@@ -90,6 +110,11 @@
"type": "string",
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
"default": ""
},
"trace": {
"type": "boolean",
"description": "Set to true to output debugging logs for diagnostics",
"default": false
}
}
}
@@ -97,10 +122,10 @@
"initialConfigurations": [
{
"type": "android",
"name": "Android Debug",
"name": "Android",
"request": "launch",
"appSrcRoot": "${workspaceRoot}/app/src/main",
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
"apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk",
"adbPort": 5037
}
],
@@ -113,7 +138,7 @@
"request": "launch",
"name": "${2:Launch App}",
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/app-debug.apk\"",
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk\"",
"adbPort": 5037
}
}
@@ -123,23 +148,24 @@
]
},
"scripts": {
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
},
"dependencies": {
"vscode-debugprotocol": "^1.15.0",
"vscode-debugadapter": "^1.15.0",
"long": "^3.2.0",
"ws": "^1.1.1",
"long": "^4.0.0",
"unzipper": "^0.10.10",
"uuid": "^3.3.2",
"vscode-debugadapter": "^1.40.0",
"vscode-debugprotocol": "^1.40.0",
"ws": "^7.1.2",
"xmldom": "^0.1.27",
"xpath": "^0.0.23"
"xpath": "^0.0.27"
},
"devDependencies": {
"typescript": "^2.0.3",
"vscode": "^1.0.0",
"mocha": "^2.3.3",
"eslint": "^3.6.0",
"@types/node": "^6.0.40",
"@types/mocha": "^2.2.32"
"@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.8.3"
}
}

File diff suppressed because it is too large Load Diff

282
src/apk-decoder.js Normal file
View File

@@ -0,0 +1,282 @@
const START_NAMESPACE_SPEC = {
hdr: '0x0100',
hdrsz: '0x0010',
sz: '0x00000018',
startline: 4,
commentId: 4,
nameId: 4,
valueId: 4,
}
const END_NAMESPACE_SPEC = {
hdr: '0x0101',
hdrsz: '0x0010',
sz: '0x00000018',
startline: 4,
commentId: 4,
nameId: 4,
valueId: 4,
}
const BEGIN_NODE_SPEC = {
hdr: '0x0102',
hdrsz: '0x0010',
sz: 4,
startline: 4,
commentId: 4,
namespaceId: 4,
nameId: 4,
attr: {
offset: 2,
size: 2,
count: 2,
},
id_attr_offset: 2,
cls_attr_offset: 2,
style_attr_offset: 2,
attributes: [{
length: main => main.attr.count,
element_spec: {
ns: 4,
nameId: 4,
commentId: 4,
sz: '0x0008',
zero: '0x00',
type: 1,
value: 4,
}
}]
}
const END_NODE_SPEC = {
hdr: '0x0103',
hdrsz: '0x0010',
sz: 4,
startline: 4,
commentId: 4,
namespaceId: 4,
nameId: 4,
}
function decode_spec_value(o, key, value, buf, idx, main) {
let byteLength;
switch (true) {
case typeof value === 'number': {
// raw integer value
byteLength = value;
o[key] = buf.readIntLE(idx, byteLength);
break;
}
case Array.isArray(value): {
// known-length array of values
const length = value[0].length(main);
byteLength = 0;
o[key] = new Array(length);
for (let i = 0; i < length; i++) {
const bytes = decode_spec_value(o[key], i, value[0].element_spec, buf, idx, main);
idx += bytes;
byteLength += bytes;
}
break;
}
case typeof value === 'object': {
// named sub-spec
o[key] = {};
byteLength = decode_spec(buf, value, o[key], o, idx);
break
}
case /^0x[\da-fA-F]/.test(value): {
// exact integer value
byteLength = (value.length - 2) / 2;
o[key] = buf.readUIntLE(idx, byteLength);
if (parseInt(value) !== o[key]) {
throw new Error(`Bad value. Expected ${value}, got 0x${o[key].toString(16)}`);
}
break;
}
case value === 'length-utf16-null': {
// 2-byte length, utf16 chars, null char
const string_byte_length = buf.readUInt16LE(idx) * 2; // 1 char = 2 bytes
idx += 2;
o[key] = buf.slice(idx, idx + string_byte_length).toString('ucs2');
idx += string_byte_length;
if (buf.readUInt16LE(idx) !== 0) {
throw new Error(`Bad value. Nul char expected but not found.`);
}
byteLength = 2 + string_byte_length + 2;
break;
}
case /^align:\d+$/.test(value): {
// used for arbitrary padding to a specified alignment
const align = parseInt(value.split(':')[1], 10);
byteLength = align - (idx % align);
o[key] = buf.slice(idx, idx + byteLength);
break;
}
default: throw new Error(`Unknown spec value definition: ${value}`);
}
return byteLength;
}
function decode_spec(buf, spec, o = {}, main = o, idx = 0) {
let byteLength = 0;
for (let key of Object.keys(spec)) {
const value = spec[key];
const bytes = decode_spec_value(o, key, value, buf, idx, main);
idx += bytes;
byteLength += bytes;
}
return byteLength;
}
/**
* Converts a binary XML file back into a readable XML document
* @param {Buffer} buf binary XMl content
*/
function decode_binary_xml(buf) {
const xml_spec = {
header: '0x00080003',
headerSize: 4,
stringPool: {
header: '0x0001',
hdrsize: '0x001c',
sz: 4,
stringCount: 4,
styleCount: 4,
flags: 4,
stringStart: 4,
styleStart: 4,
stringOffsets: [{
length: main => main.stringPool.stringCount,
element_spec: 4,
}],
strings: [{
length: main => main.stringPool.stringCount,
element_spec: 'length-utf16-null',
}],
padding: 'align:4',
},
resourceIDPool: {
hdr: '0x0180',
hdrsize: '0x0008',
sz: 4,
resIDs: [{
length: main => (main.resourceIDPool.sz - main.resourceIDPool.hdrsize) / 4,
element_spec: 4,
}]
}
}
const decoded = {};
let idx = decode_spec(buf, xml_spec, decoded);
// after we've extracted the string and id's, it should be time to parse the xml
const node_stack = [{ nodes: [] }];
const namespaces = [];
while (idx < buf.byteLength) {
const id = buf.readUInt16LE(idx);
switch (id) {
case 0x0100: {
// start namespace
const node = {};
idx += decode_spec(buf, START_NAMESPACE_SPEC, node, node, idx);
namespaces.push(node);
break;
}
case 0x0101: {
// end namespace
const node = {};
idx += decode_spec(buf, END_NAMESPACE_SPEC, node, node, idx);
const i = namespaces.findIndex(ns => ns.nameId === node.nameId);
namespaces.splice(i, 1);
break;
}
case 0x0102: {
// begin 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_stack[0].nodes.push(node);
node_stack.unshift(node);
break;
}
case 0x0103: {
// end node
const spec = END_NODE_SPEC;
const node = {};
idx += decode_spec(buf, spec, node, node, idx);
node_stack.shift();
break;
}
default: throw new Error(`Unknown XML element ${id.toString(16)}`);
}
}
decoded.nodes = node_stack[0].nodes;
const xml = toXMLDocument(decoded);
return xml;
}
/**
* Convert the decoded binary XML to a readable XML document
* @param {*} decoded
*/
function toXMLDocument(decoded) {
const strings = decoded.stringPool.strings;
const format = {
nodes: (nodes, indent) => {
return nodes.map(node => format.node(node, indent)).join('\n');
},
node: (node, indent) => {
const parts = [indent, '<', strings[node.nameId]];
for (let ns of node.namespaces.filter(ns => ns.node === node)) {
parts.push(' ', `xmlns:${strings[ns.nameId]}="${strings[ns.valueId]}"`);
}
const attr_indent = node.attributes.length > 1 ? `\n${indent} ` : ' ';
for (let attr of node.attributes) {
parts.push(attr_indent, format.attribute(attr, node.namespaces));
}
if (node.nodes.length) {
parts.push('>\n', format.nodes(node.nodes, indent + ' '), '\n', indent, '</', strings[node.nameId], '>');
} else {
parts.push(' />');
}
return parts.join('');
},
attribute: (attr, namespaces) => {
let value = attr.value;
switch (attr.type) {
case 3:
value = strings[value];
break;
case 16:
value |= 0; // convert to signed integer
break;
case 18:
value = value ? true : false;
break;
case 1: // resource id
case 17: // flags
default:
value = '0x' + value.toString(`16`);
break;
}
let ns = '';
if (attr.ns >= 0) {
ns = `${strings[namespaces.find(ns => ns.valueId === attr.ns).nameId]}:`;
}
return `${ns}${strings[attr.nameId]}="${value}"`;
}
}
return '<?xml version="1.0" encoding="utf-8"?>\n' + format.nodes(decoded.nodes, '');
}
module.exports = {
decode_binary_xml,
}

145
src/apk-file-info.js Normal file
View File

@@ -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,
}

View File

@@ -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;

View File

@@ -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<url, LogcatContent>
/** @type {Map<Uri,*>} */
this._docs = new Map(); // Map<uri, LogcatContent>
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<string>} A string or a thenable that resolves to such.
*/
provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable<string>;*/ {
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?<deviceid>
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(this, uri);
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,
}

File diff suppressed because it is too large Load Diff

772
src/debugger-types.js Normal file
View File

@@ -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<string,PackageInfo>} 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<string,DebuggerTypeInfo | Promise<DebuggerTypeInfo>>}
*/
this.classCache = new Map();
/**
* The class-prepare filters set up on the device
* @type {Set<string>}
*/
this.classPrepareFilters = new Set();
/**
* The set of class signatures already prepared
* @type {Set<string>}
*/
this.preparedClasses = new Set();
/**
* Enabled step JDWP IDs for each thread
* @type {Map<JavaThreadID, StepID>}
*/
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<JavaThreadID, number>}
*/
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<JavaThreadID, *[]>}
*/
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<string, JavaType>} */
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,
}

File diff suppressed because it is too large Load Diff

109
src/expression/assign.js Normal file
View File

@@ -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,
}

983
src/expression/evaluate.js Normal file
View File

@@ -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<DebuggerValue>}
*/
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<DebuggerValue>}
*/
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<DebuggerValue>}
*/
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,
}

323
src/expression/parse.js Normal file
View File

@@ -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,
}

View File

@@ -1,71 +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)]] },
}
// 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
});

121
src/index.d.js Normal file
View File

@@ -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
*
*/

File diff suppressed because it is too large Load Diff

View File

@@ -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) {
console.error(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;
}

View File

@@ -1,9 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"target": "es2018",
"checkJs": true,
"lib": [
"es6"
"es2018"
]
},
"exclude": [

View File

@@ -1,6 +1,4 @@
'use strict'
// vscode stuff
const { EventEmitter, Uri } = require('vscode');
// node and external modules
const fs = require('fs');
const os = require('os');
@@ -9,87 +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<string, LogcatContent>}
*/
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 {
constructor(provider/*: AndroidContentProvider*/, uri/*: Uri*/) {
this._provider = provider;
this._uri = uri;
this._logcatid = uri.query;
/**
* @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._htmltemplate = '';
this._adbclient = new ADBClient(uri.query);
this._initwait = new Promise((resolve, reject) => {
this._state = 'connecting';
LogcatContent.initWebSocketServer()
.then(() => {
return this._adbclient.logcat({
this._htmltemplate = '';
this._adbclient = new ADBClient(deviceid);
this._initwait = this.initialise();
LogcatInstances.set(this._logcatid, this);
}
/**
* 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<string>}
*/
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),
});
}).then(x => {
this._state = 'connected';
this._initwait = null;
resolve(this.content);
}).fail(e => {
this._state = 'connect_failed';
reject(e);
})
});
LogcatContent.byLogcatID[this._logcatid] = this;
} catch (err) {
return `Logcat initialisation failed. ${err.message}`;
}
get content() {
// retrieve the initial content
return this.content();
}
/**
* @returns {Promise<string>}
*/
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) => {
return this._initwait = this.tryReconnect();
}
async tryReconnect() {
// 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 };
const prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs };
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
this._adbclient.logcat({
try {
await this._adbclient.startLogcatMonitor({
onlog: this.onLogcatContent.bind(this),
onclose: this.onLogcatDisconnect.bind(this),
}).then(x => {
})
// we successfully reconnected
this._state = 'connected';
this._prevlogs = null;
this._initwait = null;
resolve(this.content);
}).fail(e => {
return this.content();
} catch(err) {
// 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._logs = prevlogs._logs;
this._htmllogs = prevlogs._htmllogs;
this._oldhtmllogs = prevlogs._oldhtmllogs;
this._initwait = null;
var cached_content = this.htmlBootstrap({connected:false, status:'Device disconnected',oldlogs: this._oldhtmllogs.join(os.EOL)});
resolve(cached_content);
})
const cached_content = this.htmlBootstrap({
connected: false,
status: 'Device disconnected',
oldlogs: this._oldhtmllogs.join(os.EOL),
});
return cached_content;
}
}
sendClientMessage(msg) {
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
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 = '<div class="logblock">' + this._oldhtmllogs.join(os.EOL) + '</div>';
const lines = '<div class="logblock">' + this._oldhtmllogs.join(os.EOL) + '</div>';
client.send(lines);
}
// if the window is tabbed away and then returned to, vscode assumes the content
@@ -99,6 +144,7 @@ class LogcatContent {
if (this._state === 'disconnected')
this.sendDisconnectMsg();
}
onClientMessage(client, message) {
if (message === 'cmd:clear_logcat') {
if (this._state !== 'connected') return;
@@ -108,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 = '<div class="logblock">' + this._htmllogs.join('') + '</div>';
const lines = '<div class="logblock">' + this._htmllogs.join('') + '</div>';
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() {
@@ -147,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 => ({ '&': '&amp;', '"': '&quot;', "'": '&#39;', '<': '&lt;', '>': '&gt;' }[c]));
this._htmllogs.unshift(`<div class="log ${style}">${log}</div>`);
@@ -161,75 +209,92 @@ class LogcatContent {
this.renotify();
}
}
onLogcatDisconnect(e) {
onLogcatDisconnect(/*e*/) {
if (this._state === 'disconnected') return;
this._state = 'disconnected';
this.sendDisconnectMsg();
}
}
// 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() {
this.wss = new WebSocketServer({ host: '127.0.0.1', port: this.port }, () => {
// 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 => {
// the client uses the url path to signify which logcat data it wants
client._logcatid = client.upgradeReq.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);
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();
}
});
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();
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;
}
}).tryCreateWSS();
return LogcatContent._wssdone;
lc.onClientConnect(client);
client.on('message', function(message) {
const lc = LogcatInstances.get(this._logcatid);
if (lc) {
lc.onClientMessage(this, message);
}
}.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;
@@ -239,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
}
})
@@ -258,34 +323,55 @@ 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)
devices.forEach(device => {
var uri = AndroidContentProvider.getReadLogcatUri(device.serial);
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
if (vscode.window.createWebviewPanel) {
const panel = vscode.window.createWebviewPanel(
'androidlogcat', // Identifies the type of the webview. Used internally
`logcat-${device.serial}`, // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
{
enableScripts: true,
}
);
const logcat = new LogcatContent(device.serial);
logcat.content().then(html => {
panel.webview.html = html;
});
return;
}
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,
}

95
src/manifest.js Normal file
View File

@@ -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<string>}
*/
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,
}

View File

@@ -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;

92
src/package-searcher.js Normal file
View File

@@ -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<string,PackageInfo>}
*/
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
}

View File

@@ -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);
}
}

View File

@@ -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];
}

143
src/sockets/adbsocket.js Normal file
View File

@@ -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<string>} 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<string>} 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;

View File

@@ -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;

122
src/sockets/jdwpsocket.js Normal file
View File

@@ -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;

276
src/stack-frame.js Normal file
View File

@@ -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<DebuggerValue[]>}
*/
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<VariableValue[]>}
*/
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 = '<value>';
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 = ['<binary>', '<decimal>', '<hex>'];
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('<length>', varinfo.value.toString()));
break;
case 'C':
variables.push(new VariableValue('<charCode>', 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,
}

11
src/state.js Normal file
View File

@@ -0,0 +1,11 @@
const vscode = require('vscode');
const fs = require('fs');
const path = require('path');
const os = require('os');
var adext = {};
try {
Object.assign(adext, JSON.parse(fs.readFileSync(path.join(path.dirname(__dirname),'package.json'),'utf8')));
} catch (ex) { }
exports.adext = adext;

View File

@@ -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<VSCVariableReference,DebuggerStackFrame>}
*/
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,
}
return this.paused.stack_frame_vars[frameId] = stack_frame_var;
}
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));
}
// 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));
}
// 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);
}
_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)
/**
* @param {DebuggerFrameInfo} frame
* @param {number} call_stack_level
*/
createStackFrameVariable(frame, call_stack_level) {
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);
}
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;
}
x.def.resolveWith(this, [varinfo.frameId]);
})
.fail(e => {
x.def.rejectWith(this, [e]);
})
return def;
/**
* 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();
}
if (!this.paused.global_vars) {
const globalFrameId = AndroidThread.makeGlobalVariableReference(this.vscode_threadid) ;
this.paused.global_vars = new VariableManager(globalFrameId);
}
return this.paused.global_vars;
}
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);
});
/**
* set a new VSCode thread ID for this thread
*/
allocateNewThreadID() {
this.vscode_threadid = (nextVSCodeThreadId += 1);
}
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,
}

View File

@@ -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));
});
}

View File

@@ -1,631 +0,0 @@
const crypto = require('crypto');
var nofn=function(){};
var D=exports.D=console.log.bind(console);
var E=exports.E=console.error.bind(console);
var W=exports.W=console.warn.bind(console);
var DD=nofn,cl=D,printf=D;
var print_jdwp_data = nofn;// _print_jdwp_data;
var print_packet = nofn;//_print_packet;
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 new Buffer(arr,'binary').toString('base64');
}
exports.atob = function(base64) {
return new Buffer(base64, 'base64').toString('binary');
}

53
src/utils/char-decode.js Normal file
View File

@@ -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,
}

View File

@@ -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,
}

51
src/utils/print.js Normal file
View File

@@ -0,0 +1,51 @@
/**
* Set of callbacks to be called when any message is output to the console
* @type {Set<Function>}
* */
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,
}

21
src/utils/source-file.js Normal file
View File

@@ -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,
}

11
src/utils/thread.js Normal file
View File

@@ -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,
}

166
src/variable-manager.js Normal file
View File

@@ -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<VSCVariableReference,*>} */
this.variableValues = new Map();
/** @type {Map<JavaObjectID,VSCVariableReference>} */
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,
}

View File

@@ -1,389 +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<objectid, variablesReference>
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,
});
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)
.then((elements) => {
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:'<value>',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:'<length>',type:'',value:varinfo.value.toString(),variablesReference:0});
break;
case 'C':
variables.push({name:'<charCode>',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:'<binary>',type:'',value:pad(s4.hi,2,32)+pad(s4.lo,2,32),variablesReference:0}
,{name:'<decimal>',type:'',value:NumberBaseConverter.hexToDec(v64hex,false),variablesReference:0}
,{name:'<hex>',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:'<binary>',type:'',value:pad(u,2,bits),variablesReference:0}
,{name:'<decimal>',type:'',value:u.toString(10),variablesReference:0}
,{name:'<hex>',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, 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 final 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 = NumberBaseConverter.hexToDec(v64hex, true);
break;
default:
// other primitives: int, 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,
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;

View File

@@ -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 <port> - create custom-port socket
// wa <fd> <base64cmd> - write_adb_command
// rs <fd> [extra] - read_adb_status
// ra <fd> - read_adb_reply
// rj <fd> - read jdwp-formatted reply
// rx <fd> <len> - read raw data from adb socket
// wx <fd> <base64data> - write raw data to adb socket
// dc <fd|all> - 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;

View File

@@ -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() {

View File

@@ -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