mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 09:59:25 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0672e54401 | ||
|
|
f92f247ef6 | ||
|
|
133b7061b2 | ||
|
|
7e958620a8 | ||
|
|
989de8254a | ||
|
|
8f2f7d2fd4 | ||
|
|
99544b527d | ||
|
|
614fcbd2ba | ||
|
|
b04e4328a6 | ||
|
|
1a00cdb291 | ||
|
|
d1fd889433 | ||
|
|
a7e4cac6df | ||
|
|
c6df24ab95 | ||
|
|
23184ea4c2 | ||
|
|
b7ba47b811 | ||
|
|
033f5c80ab | ||
|
|
0cbb56ca9b | ||
|
|
684dd39181 | ||
|
|
52ab704acd | ||
|
|
45e2dc2fe1 | ||
|
|
30ed5dea3b | ||
|
|
0eb44130a6 | ||
|
|
d1e7c86092 | ||
|
|
690f9dc23a | ||
|
|
27ecd41b68 | ||
|
|
756a1cea29 | ||
|
|
fc2ce97a23 | ||
|
|
de8abc62bc | ||
|
|
8cc31476b3 |
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@@ -8,7 +8,10 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${execPath}",
|
"runtimeExecutable": "${execPath}",
|
||||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
||||||
"stopOnEntry": false
|
"stopOnEntry": false,
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Server",
|
"name": "Server",
|
||||||
@@ -16,7 +19,10 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"cwd": "${workspaceRoot}",
|
"cwd": "${workspaceRoot}",
|
||||||
"program": "${workspaceRoot}/src/debugMain.js",
|
"program": "${workspaceRoot}/src/debugMain.js",
|
||||||
"args": [ "--server=4711" ]
|
"args": [ "--server=4711" ],
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Launch Tests",
|
"name": "Launch Tests",
|
||||||
|
|||||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,5 +1,40 @@
|
|||||||
# Change Log
|
# 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
|
### version 0.5.0
|
||||||
* Debugger support for Kotlin source files
|
* Debugger support for Kotlin source files
|
||||||
* Exception UI
|
* Exception UI
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||||
60
README.md
60
README.md
@@ -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 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**.
|
* 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`.
|
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.
|
* 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).
|
* 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.
|
* 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`.
|
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:
|
The following settings are used to configure the debugger:
|
||||||
|
```jsonc
|
||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"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
|
// Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn
|
||||||
"staleBuild": "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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Every coffee makes a difference, so thanks for adding your support.
|
||||||
|
|
||||||
## Questions / Problems
|
## Questions / Problems
|
||||||
|
|
||||||
|
|||||||
26
extension.js
26
extension.js
@@ -3,15 +3,6 @@
|
|||||||
const vscode = require('vscode');
|
const vscode = require('vscode');
|
||||||
const { AndroidContentProvider } = require('./src/contentprovider');
|
const { AndroidContentProvider } = require('./src/contentprovider');
|
||||||
const { openLogcatWindow } = require('./src/logcat');
|
const { openLogcatWindow } = require('./src/logcat');
|
||||||
const state = require('./src/state');
|
|
||||||
|
|
||||||
function getADBPort() {
|
|
||||||
var defaultPort = 5037;
|
|
||||||
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
|
||||||
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
|
||||||
return adbPort;
|
|
||||||
return defaultPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this method is called when your extension is activated
|
// this method is called when your extension is activated
|
||||||
// your extension is activated the very first time the command is executed
|
// your extension is activated the very first time the command is executed
|
||||||
@@ -20,29 +11,20 @@ function activate(context) {
|
|||||||
/* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */
|
/* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */
|
||||||
AndroidContentProvider.register(context, vscode.workspace);
|
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
|
// The commandId parameter must match the command field in package.json
|
||||||
var disposables = [
|
const disposables = [
|
||||||
// add the view logcat handler
|
// add the view logcat handler
|
||||||
vscode.commands.registerCommand('android-dev-ext.view_logcat', () => {
|
vscode.commands.registerCommand('android-dev-ext.view_logcat', () => {
|
||||||
openLogcatWindow(vscode);
|
openLogcatWindow(vscode);
|
||||||
}),
|
}),
|
||||||
// watch for changes in the launch config
|
|
||||||
vscode.workspace.onDidChangeConfiguration(e => {
|
|
||||||
wsproxyserver.setADBPort(getADBPort());
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
|
|
||||||
var spliceparams = [context.subscriptions.length,0].concat(disposables);
|
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);
|
||||||
Array.prototype.splice.apply(context.subscriptions,spliceparams);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.activate = activate;
|
|
||||||
|
|
||||||
// this method is called when your extension is deactivated
|
// this method is called when your extension is deactivated
|
||||||
function deactivate() {
|
function deactivate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.activate = activate;
|
||||||
exports.deactivate = deactivate;
|
exports.deactivate = deactivate;
|
||||||
BIN
images/bmac-code.png
Normal file
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
1280
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
61
package.json
61
package.json
@@ -2,12 +2,12 @@
|
|||||||
"name": "android-dev-ext",
|
"name": "android-dev-ext",
|
||||||
"displayName": "Android",
|
"displayName": "Android",
|
||||||
"description": "Android debugging support for VS Code",
|
"description": "Android debugging support for VS Code",
|
||||||
"version": "0.5.0",
|
"version": "1.0.0",
|
||||||
"publisher": "adelphes",
|
"publisher": "adelphes",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.8.0"
|
"vscode": "^1.24.0"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Debuggers"
|
"Debuggers"
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"debuggers": [
|
"debuggers": [
|
||||||
{
|
{
|
||||||
"type": "android",
|
"type": "android",
|
||||||
"label": "Android Debug",
|
"label": "Android",
|
||||||
"program": "./src/debugMain.js",
|
"program": "./src/debugMain.js",
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"configurationAttributes": {
|
"configurationAttributes": {
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
"apkFile": {
|
"apkFile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Fully qualified path to the built APK (Android Application Package)",
|
"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": {
|
"adbPort": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -79,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",
|
"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
|
"default": 1
|
||||||
},
|
},
|
||||||
|
"launchActivity": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Manually specify the activity to run when the app is started.",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
"logcatPort": {
|
"logcatPort": {
|
||||||
"type": "integer",
|
"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",
|
"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
|
"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": {
|
"staleBuild": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
|
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
|
||||||
@@ -93,6 +110,11 @@
|
|||||||
"type": "string",
|
"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.",
|
"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": ""
|
"default": ""
|
||||||
|
},
|
||||||
|
"trace": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Set to true to output debugging logs for diagnostics",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,10 +122,10 @@
|
|||||||
"initialConfigurations": [
|
"initialConfigurations": [
|
||||||
{
|
{
|
||||||
"type": "android",
|
"type": "android",
|
||||||
"name": "Android Debug",
|
"name": "Android",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
"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
|
"adbPort": 5037
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -116,7 +138,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "${2:Launch App}",
|
"name": "${2:Launch App}",
|
||||||
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
"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
|
"adbPort": 5037
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,23 +148,24 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
|
||||||
"test": "node ./node_modules/vscode/bin/test"
|
"test": "node ./node_modules/vscode/bin/test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vscode-debugprotocol": "^1.20.0",
|
"long": "^4.0.0",
|
||||||
"vscode-debugadapter": "^1.20.0",
|
"unzipper": "^0.10.10",
|
||||||
"long": "^3.2.0",
|
"uuid": "^3.3.2",
|
||||||
"ws": "^1.1.1",
|
"vscode-debugadapter": "^1.40.0",
|
||||||
|
"vscode-debugprotocol": "^1.40.0",
|
||||||
|
"ws": "^7.1.2",
|
||||||
"xmldom": "^0.1.27",
|
"xmldom": "^0.1.27",
|
||||||
"xpath": "^0.0.23"
|
"xpath": "^0.0.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^2.0.3",
|
"@types/mocha": "^5.2.5",
|
||||||
"vscode": "^1.0.0",
|
"@types/node": "^10.12.5",
|
||||||
"mocha": "^2.3.3",
|
"@types/vscode": "1.24.0",
|
||||||
"eslint": "^3.6.0",
|
"eslint": "^5.9.0",
|
||||||
"@types/node": "^6.0.40",
|
"mocha": "^5.2.0",
|
||||||
"@types/mocha": "^2.2.32"
|
"typescript": "^3.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
983
src/adbclient.js
983
src/adbclient.js
File diff suppressed because it is too large
Load Diff
282
src/apk-decoder.js
Normal file
282
src/apk-decoder.js
Normal 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
145
src/apk-file-info.js
Normal 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,
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
'use strict'
|
const vscode = require('vscode');
|
||||||
// vscode stuff
|
const { workspace, EventEmitter, Uri } = vscode;
|
||||||
const { workspace, EventEmitter, Uri } = require('vscode');
|
|
||||||
|
|
||||||
class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
class AndroidContentProvider {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._docs = {}; // hashmap<url, LogcatContent>
|
/** @type {Map<Uri,*>} */
|
||||||
|
this._docs = new Map(); // Map<uri, LogcatContent>
|
||||||
this._onDidChange = new EventEmitter();
|
this._onDidChange = new EventEmitter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,13 +27,15 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
|||||||
* [document](TextDocument). Resources allocated should be released when
|
* [document](TextDocument). Resources allocated should be released when
|
||||||
* the corresponding document has been [closed](#workspace.onDidCloseTextDocument).
|
* 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 {Uri} uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
|
||||||
* @param token A cancellation token.
|
* @param {vscode.CancellationToken} token A cancellation token.
|
||||||
* @return A string or a thenable that resolves to such.
|
* @return {string|Thenable<string>} A string or a thenable that resolves to such.
|
||||||
*/
|
*/
|
||||||
provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable<string>;*/ {
|
provideTextDocumentContent(uri, token) {
|
||||||
var doc = this._docs[uri];
|
const doc = this._docs.get(uri);
|
||||||
if (doc) return this._docs[uri].content;
|
if (doc) {
|
||||||
|
return doc.content();
|
||||||
|
}
|
||||||
switch (uri.authority) {
|
switch (uri.authority) {
|
||||||
// android-dev-ext://logcat/read?<deviceid>
|
// android-dev-ext://logcat/read?<deviceid>
|
||||||
case 'logcat': return this.provideLogcatDocumentContent(uri);
|
case 'logcat': return this.provideLogcatDocumentContent(uri);
|
||||||
@@ -41,38 +43,51 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
|||||||
throw new Error('Document Uri not recognised');
|
throw new Error('Document Uri not recognised');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uri} uri
|
||||||
|
*/
|
||||||
provideLogcatDocumentContent(uri) {
|
provideLogcatDocumentContent(uri) {
|
||||||
// LogcatContent depends upon AndroidContentProvider, so we must delay-load this
|
// LogcatContent depends upon AndroidContentProvider, so we must delay-load this
|
||||||
const { LogcatContent } = require('./logcat');
|
const { LogcatContent } = require('./logcat');
|
||||||
var doc = this._docs[uri] = new LogcatContent(this, uri);
|
const doc = new LogcatContent(uri.query);
|
||||||
return doc.content;
|
this._docs.set(uri, doc);
|
||||||
|
return doc.content();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// the statics
|
|
||||||
AndroidContentProvider.SCHEME = 'android-dev-ext';
|
AndroidContentProvider.SCHEME = 'android-dev-ext';
|
||||||
|
|
||||||
AndroidContentProvider.register = (ctx, workspace) => {
|
AndroidContentProvider.register = (ctx, workspace) => {
|
||||||
var provider = new AndroidContentProvider();
|
const provider = new AndroidContentProvider();
|
||||||
var registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
|
const registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
|
||||||
ctx.subscriptions.push(registration);
|
ctx.subscriptions.push(registration, provider);
|
||||||
ctx.subscriptions.push(provider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidContentProvider.getReadLogcatUri = (deviceId) => {
|
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({
|
return uri.with({
|
||||||
query: deviceId
|
query: deviceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidContentProvider.getLaunchConfigSetting = (name, defvalue) => {
|
AndroidContentProvider.getLaunchConfigSetting = (name, defvalue) => {
|
||||||
// there's surely got to be a better way than this...
|
// there's surely got to be a better way than this...
|
||||||
var configs = workspace.getConfiguration('launch.configurations');
|
const configs = workspace.getConfiguration('launch.configurations');
|
||||||
for (var i=0,config; config=configs.get(''+i); i++) {
|
for (let i = 0, config; config = configs.get(`${i}`); i++) {
|
||||||
if (config.type!=='android') continue;
|
if (config.type!=='android') {
|
||||||
if (config.request!=='launch') continue;
|
continue;
|
||||||
if (config[name]) return config[name];
|
}
|
||||||
|
if (config.request!=='launch') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(config, name)) {
|
||||||
|
return config[name];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return defvalue;
|
return defvalue;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.AndroidContentProvider = AndroidContentProvider;
|
module.exports = {
|
||||||
|
AndroidContentProvider,
|
||||||
|
}
|
||||||
|
|||||||
1680
src/debugMain.js
1680
src/debugMain.js
File diff suppressed because it is too large
Load Diff
772
src/debugger-types.js
Normal file
772
src/debugger-types.js
Normal 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,
|
||||||
|
}
|
||||||
3229
src/debugger.js
3229
src/debugger.js
File diff suppressed because it is too large
Load Diff
109
src/expression/assign.js
Normal file
109
src/expression/assign.js
Normal 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
983
src/expression/evaluate.js
Normal 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
323
src/expression/parse.js
Normal 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,
|
||||||
|
}
|
||||||
@@ -1,535 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
const Long = require('long');
|
|
||||||
const $ = require('./jq-promise');
|
|
||||||
const { D } = require('./util');
|
|
||||||
const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./globals');
|
|
||||||
|
|
||||||
/*
|
|
||||||
Asynchronously evaluate an expression
|
|
||||||
*/
|
|
||||||
exports.evaluate = function(expression, thread, locals, vars, dbgr) {
|
|
||||||
D('evaluate: ' + expression);
|
|
||||||
|
|
||||||
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
|
|
||||||
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]);
|
|
||||||
|
|
||||||
if (thread && !thread.paused)
|
|
||||||
return reject_evaluation('not available');
|
|
||||||
|
|
||||||
// special case for evaluating exception messages
|
|
||||||
// - this is called if the user tries to evaluate ':msg' from the locals
|
|
||||||
if (expression === exmsg_var_name) {
|
|
||||||
if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) {
|
|
||||||
var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
|
|
||||||
if (msglocal) {
|
|
||||||
return resolve_evaluation(vars._local_to_variable(msglocal).value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reject_evaluation('not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parse_array_or_fncall = function (e) {
|
|
||||||
var arg, res = { arr: [], call: null };
|
|
||||||
// pre-call array indexes
|
|
||||||
while (e.expr[0] === '[') {
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
if ((arg = parse_expression(e)) === null) return null;
|
|
||||||
res.arr.push(arg);
|
|
||||||
if (e.expr[0] !== ']') return null;
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
}
|
|
||||||
if (res.arr.length) return res;
|
|
||||||
// method call
|
|
||||||
if (e.expr[0] === '(') {
|
|
||||||
res.call = []; e.expr = e.expr.slice(1).trim();
|
|
||||||
if (e.expr[0] !== ')') {
|
|
||||||
for (; ;) {
|
|
||||||
if ((arg = parse_expression(e)) === null) return null;
|
|
||||||
res.call.push(arg);
|
|
||||||
if (e.expr[0] === ')') break;
|
|
||||||
if (e.expr[0] !== ',') return null;
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
// post-call array indexes
|
|
||||||
while (e.expr[0] === '[') {
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
if ((arg = parse_expression(e)) === null) return null;
|
|
||||||
res.arr.push(arg);
|
|
||||||
if (e.expr[0] !== ']') return null;
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
const parse_expression_term = function (e) {
|
|
||||||
if (e.expr[0] === '(') {
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
var subexpr = { expr: e.expr };
|
|
||||||
var res = parse_expression(subexpr);
|
|
||||||
if (res) {
|
|
||||||
if (subexpr.expr[0] !== ')') return null;
|
|
||||||
e.expr = subexpr.expr.slice(1).trim();
|
|
||||||
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) {
|
|
||||||
// primitive typecast
|
|
||||||
var castexpr = parse_expression_term(e);
|
|
||||||
if (castexpr) castexpr.typecast = res.root_term;
|
|
||||||
res = castexpr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
|
|
||||||
if (unop) {
|
|
||||||
var op = unop[0].replace(/\s/g, '');
|
|
||||||
e.expr = e.expr.slice(unop[0].length).trim();
|
|
||||||
var res = parse_expression_term(e);
|
|
||||||
if (res) {
|
|
||||||
for (var i = op.length - 1; i >= 0; --i)
|
|
||||||
res = { operator: op[i], rhs: res };
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
|
||||||
if (!root_term) return null;
|
|
||||||
var res = {
|
|
||||||
root_term: root_term[0],
|
|
||||||
root_term_type: ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'][[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1],
|
|
||||||
array_or_fncall: null,
|
|
||||||
members: [],
|
|
||||||
typecast: ''
|
|
||||||
}
|
|
||||||
e.expr = e.expr.slice(res.root_term.length).trim();
|
|
||||||
if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
|
||||||
// the root term is not allowed to be a method call
|
|
||||||
if (res.array_or_fncall.call) return null;
|
|
||||||
while (e.expr[0] === '.') {
|
|
||||||
// member expression
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
|
|
||||||
if (!member_name) return null;
|
|
||||||
res.members.push(m = { member: member_name[0], array_or_fncall: null })
|
|
||||||
e.expr = e.expr.slice(m.member.length).trim();
|
|
||||||
if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
const prec = {
|
|
||||||
'*': 1, '%': 1, '/': 1,
|
|
||||||
'+': 2, '-': 2,
|
|
||||||
'<<': 3, '>>': 3, '>>>': 3,
|
|
||||||
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
|
|
||||||
'==': 5, '!=': 5,
|
|
||||||
'&': 6, '^': 7, '|': 8, '&&': 9, '||': 10, '?': 11,
|
|
||||||
}
|
|
||||||
const parse_expression = function (e) {
|
|
||||||
var res = parse_expression_term(e);
|
|
||||||
|
|
||||||
if (!e.currprec) e.currprec = [12];
|
|
||||||
for (; ;) {
|
|
||||||
var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
|
|
||||||
if (!binary_operator) break;
|
|
||||||
var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0];
|
|
||||||
if (precdiff > 0) {
|
|
||||||
// bigger number -> lower precendence -> end of (sub)expression
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (precdiff === 0 && binary_operator[0] !== '?') {
|
|
||||||
// equal precedence, ltr evaluation
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// higher or equal precendence
|
|
||||||
e.currprec.unshift(e.currprec[0] + precdiff);
|
|
||||||
e.expr = e.expr.slice(binary_operator[0].length).trim();
|
|
||||||
// current or higher precendence
|
|
||||||
if (binary_operator[0] === '?') {
|
|
||||||
res = { condition: res, operator: binary_operator[0], ternary_true: null, ternary_false: null };
|
|
||||||
res.ternary_true = parse_expression(e);
|
|
||||||
if (e.expr[0] === ':') {
|
|
||||||
e.expr = e.expr.slice(1).trim();
|
|
||||||
res.ternary_false = parse_expression(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res = { lhs: res, operator: binary_operator[0], rhs: parse_expression(e) };
|
|
||||||
}
|
|
||||||
e.currprec.shift();
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16);
|
|
||||||
const evaluate_number = (n) => {
|
|
||||||
n += '';
|
|
||||||
var numtype, m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base = 10;
|
|
||||||
if (m) {
|
|
||||||
switch (m[2]) {
|
|
||||||
case 'b': base = 2; n = m[1] + m[3]; break;
|
|
||||||
case 'x': base = 16; n = m[1] + m[3]; break;
|
|
||||||
default: base = 8; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (base !== 16 && /[fFdD]$/.test(n)) {
|
|
||||||
numtype = /[fF]$/.test(n) ? 'float' : 'double';
|
|
||||||
n = n.slice(0, -1);
|
|
||||||
} else if (/[lL]$/.test(n)) {
|
|
||||||
numtype = 'long'
|
|
||||||
n = n.slice(0, -1);
|
|
||||||
} else {
|
|
||||||
numtype = /\./.test(n) ? 'double' : 'int';
|
|
||||||
}
|
|
||||||
if (numtype === 'long') n = hex_long(Long.fromString(n, false, base));
|
|
||||||
else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base);
|
|
||||||
else n = parseInt(n, base) | 0;
|
|
||||||
|
|
||||||
const iszero = /^[+-]?0+(\.0*)?$/.test(n);
|
|
||||||
return { vtype: 'literal', name: '', hasnullvalue: iszero, type: JTYPES[numtype], value: n, valid: true };
|
|
||||||
}
|
|
||||||
const evaluate_char = (char) => {
|
|
||||||
return { vtype: 'literal', name: '', char: char, hasnullvalue: false, type: JTYPES.char, value: char.charCodeAt(0), valid: true };
|
|
||||||
}
|
|
||||||
const numberify = (local) => {
|
|
||||||
//if (local.type.signature==='C') return local.char.charCodeAt(0);
|
|
||||||
if (/^[FD]$/.test(local.type.signature))
|
|
||||||
return parseFloat(local.value);
|
|
||||||
if (local.type.signature === 'J')
|
|
||||||
return parseInt(local.value, 16);
|
|
||||||
return parseInt(local.value, 10);
|
|
||||||
}
|
|
||||||
const stringify = (local) => {
|
|
||||||
var s;
|
|
||||||
if (JTYPES.isString(local.type)) s = local.string;
|
|
||||||
else if (JTYPES.isChar(local.type)) s = local.char;
|
|
||||||
else if (JTYPES.isPrimitive(local.type)) s = '' + local.value;
|
|
||||||
else if (local.hasnullvalue) s = '(null)';
|
|
||||||
if (typeof s === 'string')
|
|
||||||
return $.Deferred().resolveWith(this, [s]);
|
|
||||||
return dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature)
|
|
||||||
.then(s => s.string);
|
|
||||||
}
|
|
||||||
const evaluate_expression = (expr) => {
|
|
||||||
var q = $.Deferred(), local;
|
|
||||||
if (expr.operator) {
|
|
||||||
const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary ? 'type' : 'types'} for operator '${expr.operator}'`),
|
|
||||||
divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero');
|
|
||||||
var lhs_local;
|
|
||||||
return !expr.lhs
|
|
||||||
? // unary operator
|
|
||||||
evaluate_expression(expr.rhs)
|
|
||||||
.then(rhs_local => {
|
|
||||||
if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) {
|
|
||||||
rhs_local.value = !rhs_local.value;
|
|
||||||
return rhs_local;
|
|
||||||
}
|
|
||||||
else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) {
|
|
||||||
switch (rhs_local.type.typename) {
|
|
||||||
case 'long': rhs_local.value = rhs_local.value.replace(/./g, c => (15 - parseInt(c, 16)).toString(16)); break;
|
|
||||||
default: rhs_local = evaluate_number('' + ~rhs_local.value); break;
|
|
||||||
}
|
|
||||||
return rhs_local;
|
|
||||||
}
|
|
||||||
else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) {
|
|
||||||
if (expr.operator === '+') return rhs_local;
|
|
||||||
switch (rhs_local.type.typename) {
|
|
||||||
case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value, false, 16).neg()); break;
|
|
||||||
default: rhs_local = evaluate_number('' + (-rhs_local.value)); break;
|
|
||||||
}
|
|
||||||
return rhs_local;
|
|
||||||
}
|
|
||||||
return invalid_operator('unary');
|
|
||||||
})
|
|
||||||
: // binary operator
|
|
||||||
evaluate_expression(expr.lhs)
|
|
||||||
.then(x => (lhs_local = x) && evaluate_expression(expr.rhs))
|
|
||||||
.then(rhs_local => {
|
|
||||||
if ((lhs_local.type.signature === 'J' && JTYPES.isInteger(rhs_local.type))
|
|
||||||
|| (rhs_local.type.signature === 'J' && JTYPES.isInteger(lhs_local.type))) {
|
|
||||||
// one operand is a long, the other is an integer -> the result is a long
|
|
||||||
var a, b, lbase, rbase;
|
|
||||||
lbase = lhs_local.type.signature === 'J' ? 16 : 10;
|
|
||||||
rbase = rhs_local.type.signature === 'J' ? 16 : 10;
|
|
||||||
a = Long.fromString('' + lhs_local.value, false, lbase);
|
|
||||||
b = Long.fromString('' + rhs_local.value, false, rbase);
|
|
||||||
switch (expr.operator) {
|
|
||||||
case '+': a = a.add(b); break;
|
|
||||||
case '-': a = a.subtract(b); break;
|
|
||||||
case '*': a = a.multiply(b); break;
|
|
||||||
case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero();
|
|
||||||
case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero();
|
|
||||||
case '<<': a = a.shl(b); break;
|
|
||||||
case '>>': a = a.shr(b); break;
|
|
||||||
case '>>>': a = a.shru(b); break;
|
|
||||||
case '&': a = a.and(b); break;
|
|
||||||
case '|': a = a.or(b); break;
|
|
||||||
case '^': a = a.xor(b); break;
|
|
||||||
case '==': a = a.eq(b); break;
|
|
||||||
case '!=': a = !a.eq(b); break;
|
|
||||||
case '<': a = a.lt(b); break;
|
|
||||||
case '<=': a = a.lte(b); break;
|
|
||||||
case '>': a = a.gt(b); break;
|
|
||||||
case '>=': a = a.gte(b); break;
|
|
||||||
default: return invalid_operator();
|
|
||||||
}
|
|
||||||
if (typeof a === 'boolean')
|
|
||||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
|
||||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.long, value: hex_long(a), valid: true };
|
|
||||||
}
|
|
||||||
else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) {
|
|
||||||
// both are (non-long) integer types
|
|
||||||
var a = numberify(lhs_local), b = numberify(rhs_local);
|
|
||||||
switch (expr.operator) {
|
|
||||||
case '+': a += b; break;
|
|
||||||
case '-': a -= b; break;
|
|
||||||
case '*': a *= b; break;
|
|
||||||
case '/': if (b) { a = Math.trunc(a / b); break } return divide_by_zero();
|
|
||||||
case '%': if (b) { a %= b; break; } return divide_by_zero();
|
|
||||||
case '<<': a <<= b; break;
|
|
||||||
case '>>': a >>= b; break;
|
|
||||||
case '>>>': a >>>= b; break;
|
|
||||||
case '&': a &= b; break;
|
|
||||||
case '|': a |= b; break;
|
|
||||||
case '^': a ^= b; break;
|
|
||||||
case '==': a = a === b; break;
|
|
||||||
case '!=': a = a !== b; break;
|
|
||||||
case '<': a = a < b; break;
|
|
||||||
case '<=': a = a <= b; break;
|
|
||||||
case '>': a = a > b; break;
|
|
||||||
case '>=': a = a >= b; break;
|
|
||||||
default: return invalid_operator();
|
|
||||||
}
|
|
||||||
if (typeof a === 'boolean')
|
|
||||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
|
||||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.int, value: '' + a, valid: true };
|
|
||||||
}
|
|
||||||
else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) {
|
|
||||||
var a = numberify(lhs_local), b = numberify(rhs_local);
|
|
||||||
switch (expr.operator) {
|
|
||||||
case '+': a += b; break;
|
|
||||||
case '-': a -= b; break;
|
|
||||||
case '*': a *= b; break;
|
|
||||||
case '/': a /= b; break;
|
|
||||||
case '==': a = a === b; break;
|
|
||||||
case '!=': a = a !== b; break;
|
|
||||||
case '<': a = a < b; break;
|
|
||||||
case '<=': a = a <= b; break;
|
|
||||||
case '>': a = a > b; break;
|
|
||||||
case '>=': a = a >= b; break;
|
|
||||||
default: return invalid_operator();
|
|
||||||
}
|
|
||||||
if (typeof a === 'boolean')
|
|
||||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
|
||||||
// one of them must be a float or double
|
|
||||||
var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))];
|
|
||||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES[result_type], value: '' + a, valid: true };
|
|
||||||
}
|
|
||||||
else if (lhs_local.type.signature === 'Z' && rhs_local.type.signature === 'Z') {
|
|
||||||
// boolean operands
|
|
||||||
var a = lhs_local.value, b = rhs_local.value;
|
|
||||||
switch (expr.operator) {
|
|
||||||
case '&': case '&&': a = a && b; break;
|
|
||||||
case '|': case '||': a = a || b; break;
|
|
||||||
case '^': a = !!(a ^ b); break;
|
|
||||||
case '==': a = a === b; break;
|
|
||||||
case '!=': a = a !== b; break;
|
|
||||||
default: return invalid_operator();
|
|
||||||
}
|
|
||||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
|
||||||
}
|
|
||||||
else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) {
|
|
||||||
return stringify(rhs_local).then(rhs_str => createJavaString(dbgr, lhs_local.string + rhs_str, { israw: true }));
|
|
||||||
}
|
|
||||||
return invalid_operator();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
switch (expr.root_term_type) {
|
|
||||||
case 'boolean':
|
|
||||||
local = { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: expr.root_term !== 'false', valid: true };
|
|
||||||
break;
|
|
||||||
case 'null':
|
|
||||||
const nullvalue = '0000000000000000'; // null reference value
|
|
||||||
local = { vtype: 'literal', name: '', hasnullvalue: true, type: JTYPES.null, value: nullvalue, valid: true };
|
|
||||||
break;
|
|
||||||
case 'ident':
|
|
||||||
local = locals && locals.find(l => l.name === expr.root_term);
|
|
||||||
break;
|
|
||||||
case 'hexint':
|
|
||||||
case 'octint':
|
|
||||||
case 'decint':
|
|
||||||
case 'decfloat':
|
|
||||||
local = evaluate_number(expr.root_term);
|
|
||||||
break;
|
|
||||||
case 'char':
|
|
||||||
case 'echar':
|
|
||||||
case 'uchar':
|
|
||||||
local = evaluate_char(decode_char(expr.root_term.slice(1, -1)))
|
|
||||||
break;
|
|
||||||
case 'string':
|
|
||||||
// we must get the runtime to create string instances
|
|
||||||
q = createJavaString(dbgr, expr.root_term);
|
|
||||||
local = { valid: true }; // make sure we don't fail the evaluation
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!local || !local.valid) return reject_evaluation('not available');
|
|
||||||
// we've got the root term variable - work out the rest
|
|
||||||
q = expr.array_or_fncall.arr.reduce((q, index_expr) => {
|
|
||||||
return q.then(function (index_expr, local) { return evaluate_array_element.call(this, index_expr, local) }.bind(this, index_expr));
|
|
||||||
}, q);
|
|
||||||
q = expr.members.reduce((q, m) => {
|
|
||||||
return q.then(function (m, local) { return evaluate_member.call(this, m, local) }.bind(this, m));
|
|
||||||
}, q);
|
|
||||||
if (expr.typecast) {
|
|
||||||
q = q.then(function (type, local) { return evaluate_cast.call(this, type, local) }.bind(this, expr.typecast))
|
|
||||||
}
|
|
||||||
// if it's a string literal, we are already waiting for the runtime to create the string
|
|
||||||
// - otherwise, start the evalaution...
|
|
||||||
if (expr.root_term_type !== 'string')
|
|
||||||
q.resolveWith(this, [local]);
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
const evaluate_array_element = (index_expr, arr_local) => {
|
|
||||||
if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
|
|
||||||
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
|
||||||
return evaluate_expression(index_expr)
|
|
||||||
.then(function (arr_local, idx_local) {
|
|
||||||
if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value');
|
|
||||||
var idx = numberify(idx_local);
|
|
||||||
if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`);
|
|
||||||
return dbgr.getarrayvalues(arr_local, idx, 1)
|
|
||||||
}.bind(this, arr_local))
|
|
||||||
.then(els => els[0])
|
|
||||||
}
|
|
||||||
const evaluate_methodcall = (m, obj_local) => {
|
|
||||||
// until we can figure out why method invokes with parameters crash the debugger, disallow parameterised calls
|
|
||||||
if (m.array_or_fncall.call.length)
|
|
||||||
return reject_evaluation('Error: method calls with parameter values are not supported');
|
|
||||||
|
|
||||||
// find any methods matching the member name with any parameters in the signature
|
|
||||||
return dbgr.findNamedMethods(obj_local.type.signature, m.member, /^/)
|
|
||||||
.then(methods => {
|
|
||||||
if (!methods[0])
|
|
||||||
return reject_evaluation(`Error: method '${m.member}()' not found`);
|
|
||||||
// evaluate any parameters (and wait for the results)
|
|
||||||
return $.when({methods},...m.array_or_fncall.call.map(evaluate_expression));
|
|
||||||
})
|
|
||||||
.then((x,...paramValues) => {
|
|
||||||
// filter the method based upon the types of parameters - note that null types and integer literals can match multiple types
|
|
||||||
paramValues = paramValues = paramValues.map(p => p[0]);
|
|
||||||
var matchers = paramValues.map(p => {
|
|
||||||
switch(true) {
|
|
||||||
case p.type.signature === 'I':
|
|
||||||
// match bytes/shorts/ints/longs/floats/doubles within range
|
|
||||||
if (p.value >= -128 && p.value <= 127) return /^[BSIJFD]$/
|
|
||||||
if (p.value >= -32768 && p.value <= 32767) return /^[SIJFD]$/
|
|
||||||
return /^[IJFD]$/;
|
|
||||||
case p.type.signature === 'F':
|
|
||||||
return /^[FD]$/;
|
|
||||||
case p.type.signature === 'Lnull;':
|
|
||||||
return /^[LT\[]/; // any reference type
|
|
||||||
default:
|
|
||||||
// anything else must be an exact signature match (for now - in reality we should allow subclassed type)
|
|
||||||
return new RegExp(`^${p.type.signature.replace(/[$]/g,x=>'\\'+x)}$`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var methods = x.methods.filter(m => {
|
|
||||||
// extract a list of parameter types
|
|
||||||
var paramtypere = /\[*([BSIJFDCZ]|([LT][^;]+;))/g;
|
|
||||||
for (var x, ptypes=[]; x = paramtypere.exec(m.sig); ) {
|
|
||||||
ptypes.push(x[0]);
|
|
||||||
}
|
|
||||||
// the last paramter type is the return value
|
|
||||||
ptypes.pop();
|
|
||||||
// check if they match
|
|
||||||
if (ptypes.length !== paramValues.length)
|
|
||||||
return;
|
|
||||||
return matchers.filter(m => {
|
|
||||||
return !m.test(ptypes.shift())
|
|
||||||
}).length === 0;
|
|
||||||
});
|
|
||||||
if (!methods[0])
|
|
||||||
return reject_evaluation(`Error: incompatible parameters for method '${m.member}'`);
|
|
||||||
// convert the parameters to exact debugger-compatible values
|
|
||||||
paramValues = paramValues.map(p => {
|
|
||||||
if (p.type.signature.length === 1)
|
|
||||||
return { type: p.type.typename, value: p.value};
|
|
||||||
return { type: 'oref', value: p.value };
|
|
||||||
})
|
|
||||||
return dbgr.invokeMethod(obj_local.value, thread.threadid, obj_local.type.signature, m.member, methods[0].genericsig || methods[0].sig, paramValues, {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const evaluate_member = (m, obj_local) => {
|
|
||||||
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
|
|
||||||
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
|
||||||
var chain;
|
|
||||||
if (m.array_or_fncall.call) {
|
|
||||||
chain = evaluate_methodcall(m, obj_local);
|
|
||||||
}
|
|
||||||
// length is a 'fake' field of arrays, so special-case it
|
|
||||||
else if (JTYPES.isArray(obj_local.type) && m.member === 'length') {
|
|
||||||
chain = $.Deferred().resolve(evaluate_number(obj_local.arraylen));
|
|
||||||
}
|
|
||||||
// we also special-case :super (for object instances)
|
|
||||||
else if (JTYPES.isObject(obj_local.type) && m.member === ':super') {
|
|
||||||
chain = dbgr.getsuperinstance(obj_local);
|
|
||||||
}
|
|
||||||
// anything else must be a real field
|
|
||||||
else {
|
|
||||||
chain = dbgr.getFieldValue(obj_local, m.member, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain.then(local => {
|
|
||||||
if (m.array_or_fncall.arr.length) {
|
|
||||||
var q = $.Deferred();
|
|
||||||
m.array_or_fncall.arr.reduce((q, index_expr) => {
|
|
||||||
return q.then(function (index_expr, local) { return evaluate_array_element(index_expr, local) }.bind(this, index_expr));
|
|
||||||
}, q);
|
|
||||||
return q.resolveWith(this, [local]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const evaluate_cast = (type, local) => {
|
|
||||||
if (type === local.type.typename) return local;
|
|
||||||
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`);
|
|
||||||
// boolean cannot be converted from anything else
|
|
||||||
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast();
|
|
||||||
if (local.type.typename === 'long') {
|
|
||||||
// long to something else
|
|
||||||
var value = Long.fromString(local.value, true, 16);
|
|
||||||
switch (true) {
|
|
||||||
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2), 16) << 24) >> 24); break;
|
|
||||||
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4), 16) << 16) >> 16); break;
|
|
||||||
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8), 16) | 0)); break;
|
|
||||||
case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4), 16))); break;
|
|
||||||
case (type === 'float'): local = evaluate_number(value.toSigned().toNumber() + 'F'); break;
|
|
||||||
case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break;
|
|
||||||
default: return incompatible_cast();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (true) {
|
|
||||||
case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break;
|
|
||||||
case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break;
|
|
||||||
case (type === 'int'): local = evaluate_number((local.value | 0)); break;
|
|
||||||
case (type === 'long'): local = evaluate_number(local.value + 'L'); break;
|
|
||||||
case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value | 0)); break;
|
|
||||||
case (type === 'float'): break;
|
|
||||||
case (type === 'double'): break;
|
|
||||||
default: return incompatible_cast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
local.type = JTYPES[type];
|
|
||||||
return local;
|
|
||||||
}
|
|
||||||
|
|
||||||
var e = { expr: expression.trim() };
|
|
||||||
var parsed_expression = parse_expression(e);
|
|
||||||
// if there's anything left, it's an error
|
|
||||||
if (parsed_expression && !e.expr) {
|
|
||||||
// the expression is well-formed - start the (asynchronous) evaluation
|
|
||||||
return evaluate_expression(parsed_expression)
|
|
||||||
.then(local => {
|
|
||||||
var v = vars._local_to_variable(local);
|
|
||||||
return resolve_evaluation(v.value, v.variablesReference);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// the expression is not well-formed
|
|
||||||
return reject_evaluation('not available');
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// some commonly used Java types in debugger-compatible format
|
|
||||||
const JTYPES = {
|
|
||||||
byte: {typename:'byte',signature:'B'},
|
|
||||||
short: {typename:'short',signature:'S'},
|
|
||||||
int: {typename:'int',signature:'I'},
|
|
||||||
long: {typename:'long',signature:'J'},
|
|
||||||
float: {typename:'float',signature:'F'},
|
|
||||||
double: {typename:'double',signature:'D'},
|
|
||||||
char: {typename:'char',signature:'C'},
|
|
||||||
boolean: {typename:'boolean',signature:'Z'},
|
|
||||||
null: {typename:'null',signature:'Lnull;'}, // null has no type really, but we need something for literals
|
|
||||||
String: {typename:'String',signature:'Ljava/lang/String;'},
|
|
||||||
Object: {typename:'Object',signature:'Ljava/lang/Object;'},
|
|
||||||
isArray(t) { return t.signature[0]==='[' },
|
|
||||||
isObject(t) { return t.signature[0]==='L' },
|
|
||||||
isReference(t) { return /^[L[]/.test(t.signature) },
|
|
||||||
isPrimitive(t) { return !JTYPES.isReference(t.signature) },
|
|
||||||
isInteger(t) { return /^[BCIJS]$/.test(t.signature) },
|
|
||||||
isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) },
|
|
||||||
isString(t) { return t.signature === this.String.signature },
|
|
||||||
isChar(t) { return t.signature === this.char.signature },
|
|
||||||
isBoolean(t) { return t.signature === this.boolean.signature },
|
|
||||||
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
|
|
||||||
}
|
|
||||||
|
|
||||||
function signatureToFullyQualifiedType(sig) {
|
|
||||||
var arr = sig.match(/^\[+/) || '';
|
|
||||||
if (arr) {
|
|
||||||
arr = '[]'.repeat(arr[0].length);
|
|
||||||
sig = sig.slice(0, arr.length/2);
|
|
||||||
}
|
|
||||||
var m = sig.match(/^((L([^<;]+).)|T([^;]+).|.)/);
|
|
||||||
if (!m) return '';
|
|
||||||
if (m[3]) {
|
|
||||||
return m[3].replace(/[/$]/g,'.') + arr;
|
|
||||||
} else if (m[4]) {
|
|
||||||
return m[4].replace(/[/$]/g, '.') + arr;
|
|
||||||
}
|
|
||||||
return JTYPES.fromPrimSig(sig[0]) + arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the special name given to exception message fields
|
|
||||||
const exmsg_var_name = ':msg';
|
|
||||||
|
|
||||||
function createJavaString(dbgr, s, opts) {
|
|
||||||
const raw = (opts && opts.israw) ? s : s.slice(1,-1).replace(/\\u[0-9a-fA-F]{4}|\\./,decode_char);
|
|
||||||
// return a deferred, which resolves to a local variable named 'literal'
|
|
||||||
return dbgr.createstring(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decode_char(c) {
|
|
||||||
switch(true) {
|
|
||||||
case /^\\[^u]$/.test(c):
|
|
||||||
// backslash escape
|
|
||||||
var x = {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v','0':String.fromCharCode(0)}[c[1]];
|
|
||||||
return x || c[1];
|
|
||||||
case /^\\u[0-9a-fA-F]{4}$/.test(c):
|
|
||||||
// unicode escape
|
|
||||||
return String.fromCharCode(parseInt(c.slice(2),16));
|
|
||||||
case c.length===1 :
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
throw new Error('Invalid character value');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensure_path_end_slash(p) {
|
|
||||||
return p + (/[\\/]$/.test(p) ? '' : path.sep);
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_subpath_of(fpn, subpath) {
|
|
||||||
if (!subpath || !fpn) return false;
|
|
||||||
subpath = ensure_path_end_slash(''+subpath);
|
|
||||||
return fpn.slice(0,subpath.length) === subpath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function variableRefToThreadId(variablesReference) {
|
|
||||||
return (variablesReference / 1e9)|0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Object.assign(exports, {
|
|
||||||
JTYPES, exmsg_var_name, ensure_path_end_slash, is_subpath_of, decode_char, variableRefToThreadId, createJavaString, signatureToFullyQualifiedType
|
|
||||||
});
|
|
||||||
121
src/index.d.js
Normal file
121
src/index.d.js
Normal 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
|
||||||
|
*
|
||||||
|
*/
|
||||||
1815
src/jdwp.js
1815
src/jdwp.js
File diff suppressed because it is too large
Load Diff
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es6",
|
"target": "es2018",
|
||||||
|
"checkJs": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"es6"
|
"es2018"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|||||||
354
src/logcat.js
354
src/logcat.js
@@ -7,87 +7,134 @@ const WebSocketServer = require('ws').Server;
|
|||||||
// our stuff
|
// our stuff
|
||||||
const { ADBClient } = require('./adbclient');
|
const { ADBClient } = require('./adbclient');
|
||||||
const { AndroidContentProvider } = require('./contentprovider');
|
const { AndroidContentProvider } = require('./contentprovider');
|
||||||
const $ = require('./jq-promise');
|
const { D } = require('./utils/print');
|
||||||
const { D } = require('./util');
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
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 {
|
class LogcatContent {
|
||||||
|
|
||||||
constructor(provider/*: AndroidContentProvider*/, uri/*: Uri*/) {
|
/**
|
||||||
this._provider = provider;
|
* @param {string} deviceid
|
||||||
this._uri = uri;
|
*/
|
||||||
this._logcatid = uri.query;
|
constructor(deviceid) {
|
||||||
|
this._logcatid = deviceid;
|
||||||
this._logs = [];
|
this._logs = [];
|
||||||
this._htmllogs = [];
|
this._htmllogs = [];
|
||||||
this._oldhtmllogs = [];
|
this._oldhtmllogs = [];
|
||||||
this._prevlogs = null;
|
|
||||||
this._notifying = 0;
|
this._notifying = 0;
|
||||||
this._refreshRate = 200; // ms
|
this._refreshRate = 200; // ms
|
||||||
this._state = '';
|
this._state = 'connecting';
|
||||||
this._htmltemplate = '';
|
this._htmltemplate = '';
|
||||||
this._adbclient = new ADBClient(uri.query);
|
this._adbclient = new ADBClient(deviceid);
|
||||||
this._initwait = new Promise((resolve, reject) => {
|
this._initwait = this.initialise();
|
||||||
this._state = 'connecting';
|
LogcatInstances.set(this._logcatid, this);
|
||||||
LogcatContent.initWebSocketServer()
|
|
||||||
.then(() => {
|
|
||||||
return this._adbclient.logcat({
|
|
||||||
onlog: this.onLogcatContent.bind(this),
|
|
||||||
onclose: this.onLogcatDisconnect.bind(this),
|
|
||||||
});
|
|
||||||
}).then(() => {
|
|
||||||
this._state = 'connected';
|
|
||||||
this._initwait = null;
|
|
||||||
resolve(this.content);
|
|
||||||
}).fail(e => {
|
|
||||||
this._state = 'connect_failed';
|
|
||||||
reject(e);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
LogcatContent.byLogcatID[this._logcatid] = this;
|
|
||||||
}
|
}
|
||||||
get content() {
|
|
||||||
|
/**
|
||||||
|
* Ensures the websocket server is initialised and sets up
|
||||||
|
* logcat handlers for ADB.
|
||||||
|
* Once everything is ready, returns the initial HTML bootstrap content
|
||||||
|
* @returns {Promise<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),
|
||||||
|
});
|
||||||
|
this._state = 'connected';
|
||||||
|
this._initwait = null;
|
||||||
|
} catch (err) {
|
||||||
|
return `Logcat initialisation failed. ${err.message}`;
|
||||||
|
}
|
||||||
|
// retrieve the initial content
|
||||||
|
return this.content();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async content() {
|
||||||
if (this._initwait) return this._initwait;
|
if (this._initwait) return this._initwait;
|
||||||
if (this._state !== 'disconnected')
|
if (this._state !== 'disconnected')
|
||||||
return this.htmlBootstrap({connected:true, status:'',oldlogs:''});
|
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
|
// 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
|
// this logcat again - check if the device has reconnected
|
||||||
return this._initwait = new Promise((resolve/*, reject*/) => {
|
return this._initwait = this.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 };
|
|
||||||
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
async tryReconnect() {
|
||||||
this._adbclient.logcat({
|
// clear the logs first - if we successfully reconnect, we will be retrieving the entire logcat again
|
||||||
|
const prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs };
|
||||||
|
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
||||||
|
try {
|
||||||
|
await this._adbclient.startLogcatMonitor({
|
||||||
onlog: this.onLogcatContent.bind(this),
|
onlog: this.onLogcatContent.bind(this),
|
||||||
onclose: this.onLogcatDisconnect.bind(this),
|
onclose: this.onLogcatDisconnect.bind(this),
|
||||||
}).then(() => {
|
|
||||||
// we successfully reconnected
|
|
||||||
this._state = 'connected';
|
|
||||||
this._prevlogs = null;
|
|
||||||
this._initwait = null;
|
|
||||||
resolve(this.content);
|
|
||||||
}).fail((/*e*/) => {
|
|
||||||
// reconnection failed - put the logs back and return the cached info
|
|
||||||
this._logs = this._prevlogs._logs;
|
|
||||||
this._htmllogs = this._prevlogs._htmllogs;
|
|
||||||
this._oldhtmllogs = this._prevlogs._oldhtmllogs;
|
|
||||||
this._prevlogs = null;
|
|
||||||
this._initwait = null;
|
|
||||||
var cached_content = this.htmlBootstrap({connected:false, status:'Device disconnected',oldlogs: this._oldhtmllogs.join(os.EOL)});
|
|
||||||
resolve(cached_content);
|
|
||||||
})
|
})
|
||||||
});
|
// we successfully reconnected
|
||||||
|
this._state = 'connected';
|
||||||
|
this._initwait = null;
|
||||||
|
return this.content();
|
||||||
|
} catch(err) {
|
||||||
|
// reconnection failed - put the logs back and return the cached info
|
||||||
|
this._logs = prevlogs._logs;
|
||||||
|
this._htmllogs = prevlogs._htmllogs;
|
||||||
|
this._oldhtmllogs = prevlogs._oldhtmllogs;
|
||||||
|
this._initwait = null;
|
||||||
|
const cached_content = this.htmlBootstrap({
|
||||||
|
connected: false,
|
||||||
|
status: 'Device disconnected',
|
||||||
|
oldlogs: this._oldhtmllogs.join(os.EOL),
|
||||||
|
});
|
||||||
|
return cached_content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendClientMessage(msg) {
|
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
|
clients.forEach(client => client.send(msg+'\n')); // include a newline to try and persuade a buffer write
|
||||||
}
|
}
|
||||||
|
|
||||||
sendDisconnectMsg() {
|
sendDisconnectMsg() {
|
||||||
this.sendClientMessage(':disconnect');
|
this.sendClientMessage(':disconnect');
|
||||||
}
|
}
|
||||||
|
|
||||||
onClientConnect(client) {
|
onClientConnect(client) {
|
||||||
if (this._oldhtmllogs.length) {
|
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);
|
client.send(lines);
|
||||||
}
|
}
|
||||||
// if the window is tabbed away and then returned to, vscode assumes the content
|
// if the window is tabbed away and then returned to, vscode assumes the content
|
||||||
@@ -97,6 +144,7 @@ class LogcatContent {
|
|||||||
if (this._state === 'disconnected')
|
if (this._state === 'disconnected')
|
||||||
this.sendDisconnectMsg();
|
this.sendDisconnectMsg();
|
||||||
}
|
}
|
||||||
|
|
||||||
onClientMessage(client, message) {
|
onClientMessage(client, message) {
|
||||||
if (message === 'cmd:clear_logcat') {
|
if (message === 'cmd:clear_logcat') {
|
||||||
if (this._state !== 'connected') return;
|
if (this._state !== 'connected') return;
|
||||||
@@ -106,31 +154,33 @@ class LogcatContent {
|
|||||||
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
||||||
this.sendClientMessage(':logcat_cleared');
|
this.sendClientMessage(':logcat_cleared');
|
||||||
})
|
})
|
||||||
.fail(e => {
|
.catch(e => {
|
||||||
D('Clear logcat command failed: ' + e.message);
|
D('Clear logcat command failed: ' + e.message);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLogs() {
|
updateLogs() {
|
||||||
// no point in formatting the data if there are no connected clients
|
// 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) {
|
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));
|
clients.forEach(client => client.send(lines));
|
||||||
}
|
}
|
||||||
// once we've updated all the clients, discard the info
|
// once we've updated all the clients, discard the info
|
||||||
this._oldhtmllogs = this._htmllogs.concat(this._oldhtmllogs).slice(0, 10000);
|
this._oldhtmllogs = this._htmllogs.concat(this._oldhtmllogs).slice(0, 10000);
|
||||||
this._htmllogs = [], this._logs = [];
|
this._htmllogs = [], this._logs = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlBootstrap(vars) {
|
htmlBootstrap(vars) {
|
||||||
if (!this._htmltemplate)
|
if (!this._htmltemplate)
|
||||||
this._htmltemplate = fs.readFileSync(path.join(__dirname,'res/logcat.html'), 'utf8');
|
this._htmltemplate = fs.readFileSync(path.join(__dirname,'res/logcat.html'), 'utf8');
|
||||||
vars = Object.assign({
|
vars = Object.assign({
|
||||||
logcatid: this._logcatid,
|
logcatid: this._logcatid,
|
||||||
wssport: LogcatContent._wssport,
|
wssport: Server.options.port,
|
||||||
}, vars);
|
}, vars);
|
||||||
// simple value replacement using !{name} as the placeholder
|
// 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;
|
return html;
|
||||||
}
|
}
|
||||||
renotify() {
|
renotify() {
|
||||||
@@ -145,13 +195,13 @@ class LogcatContent {
|
|||||||
}
|
}
|
||||||
onLogcatContent(e) {
|
onLogcatContent(e) {
|
||||||
if (e.logs.length) {
|
if (e.logs.length) {
|
||||||
var mrlast = e.logs.slice();
|
const mrlast = e.logs.slice();
|
||||||
this._logs = this._logs.concat(mrlast);
|
this._logs = this._logs.concat(mrlast);
|
||||||
mrlast.forEach(log => {
|
mrlast.forEach(log => {
|
||||||
if (!(log = log.trim())) return;
|
if (!(log = log.trim())) return;
|
||||||
// replace html-interpreted chars
|
// replace html-interpreted chars
|
||||||
var m = log.match(/^\d\d-\d\d\s+?\d\d:\d\d:\d\d\.\d+?\s+?(.)/);
|
const m = log.match(/^\d\d-\d\d\s+?\d\d:\d\d:\d\d\.\d+?\s+?(.)/);
|
||||||
var style = (m && m[1]) || '';
|
const style = (m && m[1]) || '';
|
||||||
log = log.replace(/[&"'<>]/g, c => ({ '&': '&', '"': '"', "'": ''', '<': '<', '>': '>' }[c]));
|
log = log.replace(/[&"'<>]/g, c => ({ '&': '&', '"': '"', "'": ''', '<': '<', '>': '>' }[c]));
|
||||||
this._htmllogs.unshift(`<div class="log ${style}">${log}</div>`);
|
this._htmllogs.unshift(`<div class="log ${style}">${log}</div>`);
|
||||||
|
|
||||||
@@ -166,68 +216,85 @@ class LogcatContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hashmap of all LogcatContent instances, keyed on device id
|
function initWebSocketServer() {
|
||||||
LogcatContent.byLogcatID = {};
|
if (wss_inited) {
|
||||||
|
|
||||||
LogcatContent.initWebSocketServer = function () {
|
|
||||||
|
|
||||||
if (LogcatContent._wssdone) {
|
|
||||||
// already inited
|
// already inited
|
||||||
return LogcatContent._wssdone;
|
return wss_inited;
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the logcat websocket port
|
// retrieve the logcat websocket port
|
||||||
var default_wssport = 7038;
|
const default_wssport = 7038;
|
||||||
var wssport = AndroidContentProvider.getLaunchConfigSetting('logcatPort', default_wssport);
|
let start_port = AndroidContentProvider.getLaunchConfigSetting('logcatPort', default_wssport);
|
||||||
if (typeof wssport !== 'number' || wssport <= 0 || wssport >= 65536 || wssport !== (wssport|0))
|
if (typeof start_port !== 'number' || start_port <= 0 || start_port >= 65536 || start_port !== (start_port|0)) {
|
||||||
wssport = default_wssport;
|
start_port = default_wssport;
|
||||||
|
}
|
||||||
|
|
||||||
LogcatContent._wssdone = $.Deferred();
|
wss_inited = new Promise((resolve, reject) => {
|
||||||
({
|
let retries = 100;
|
||||||
wss: null,
|
tryCreateWebSocketServer(start_port, retries, (err, server) => {
|
||||||
startport: wssport,
|
if (err) {
|
||||||
port: wssport,
|
wss_inited = null;
|
||||||
retries: 0,
|
reject(err);
|
||||||
tryCreateWSS() {
|
} else {
|
||||||
this.wss = new WebSocketServer({ host: '127.0.0.1', port: this.port }, () => {
|
Server = server;
|
||||||
// success - save the info and resolve the deferred
|
resolve();
|
||||||
LogcatContent._wssport = this.port;
|
}
|
||||||
LogcatContent._wssstartport = this.startport;
|
});
|
||||||
LogcatContent._wss = this.wss;
|
});
|
||||||
this.wss.on('connection', client => {
|
return wss_inited;
|
||||||
// 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();
|
* @param {number} port
|
||||||
client.on('message', function(message) {
|
* @param {number} retries
|
||||||
var lc = LogcatContent.byLogcatID[this._logcatid];
|
* @param {(err,server?) => void} cb
|
||||||
if (lc) lc.onClientMessage(this, message);
|
*/
|
||||||
}.bind(client));
|
function tryCreateWebSocketServer(port, retries, cb) {
|
||||||
/*client.on('close', e => {
|
const wsopts = {
|
||||||
console.log('client close');
|
host: '127.0.0.1',
|
||||||
});*/
|
port,
|
||||||
// try and make sure we don't delay writes
|
clientTracking: true,
|
||||||
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
|
};
|
||||||
});
|
new WebSocketServer(wsopts)
|
||||||
this.wss = null;
|
.on('listening', function() {
|
||||||
LogcatContent._wssdone.resolveWith(LogcatContent, []);
|
cb(null, this);
|
||||||
});
|
})
|
||||||
this.wss.on('error', (/*err*/) => {
|
.on('connection', (client, req) => {
|
||||||
if (!LogcatContent._wss) {
|
onWebSocketClientConnection(client, req);
|
||||||
// listen failed -try the next port
|
})
|
||||||
this.retries++ , this.port++;
|
.on('error', err => {
|
||||||
this.tryCreateWSS();
|
if (retries <= 0) {
|
||||||
}
|
cb(err);
|
||||||
})
|
} else {
|
||||||
|
tryCreateWebSocketServer(port + 1, retries - 1, cb);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWebSocketClientConnection(client, req) {
|
||||||
|
// the client uses the url path to signify which logcat data it wants
|
||||||
|
client._logcatid = req.url.match(/^\/?(.*)$/)[1];
|
||||||
|
const lc = LogcatInstances.get(client._logcatid);
|
||||||
|
if (!lc) {
|
||||||
|
client.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lc.onClientConnect(client);
|
||||||
|
client.on('message', function(message) {
|
||||||
|
const lc = LogcatInstances.get(this._logcatid);
|
||||||
|
if (lc) {
|
||||||
|
lc.onClientMessage(this, message);
|
||||||
}
|
}
|
||||||
}).tryCreateWSS();
|
}.bind(client));
|
||||||
return LogcatContent._wssdone;
|
|
||||||
|
// try and make sure we don't delay writes
|
||||||
|
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getADBPort() {
|
function getADBPort() {
|
||||||
var defaultPort = 5037;
|
const defaultPort = 5037;
|
||||||
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
const adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
||||||
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
||||||
return adbPort;
|
return adbPort;
|
||||||
return defaultPort;
|
return defaultPort;
|
||||||
@@ -237,13 +304,13 @@ function openLogcatWindow(vscode) {
|
|||||||
new ADBClient().test_adb_connection()
|
new ADBClient().test_adb_connection()
|
||||||
.then(err => {
|
.then(err => {
|
||||||
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
||||||
var adbport = getADBPort();
|
const adbport = getADBPort();
|
||||||
var autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
||||||
if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
|
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');
|
const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||||
var adbargs = ['-P',''+adbport,'start-server'];
|
const adbargs = ['-P',''+adbport,'start-server'];
|
||||||
try {
|
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
|
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -256,34 +323,55 @@ function openLogcatWindow(vscode) {
|
|||||||
case 1:
|
case 1:
|
||||||
return devices; // only one device - just show it
|
return devices; // only one device - just show it
|
||||||
}
|
}
|
||||||
var multidevicewait = $.Deferred(), prefix = 'Android: View Logcat - ', all = '[ Display All ]';
|
const prefix = 'Android: View Logcat - ', all = '[ Display All ]';
|
||||||
var devicelist = devices.map(d => prefix + d.serial);
|
const devicelist = devices.map(d => prefix + d.serial);
|
||||||
//devicelist.push(prefix + all);
|
//devicelist.push(prefix + all);
|
||||||
vscode.window.showQuickPick(devicelist)
|
return vscode.window.showQuickPick(devicelist)
|
||||||
.then(which => {
|
.then(which => {
|
||||||
if (!which) return; // user cancelled
|
if (!which) return; // user cancelled
|
||||||
which = which.slice(prefix.length);
|
which = which.slice(prefix.length);
|
||||||
new ADBClient().list_devices()
|
return new ADBClient().list_devices()
|
||||||
.then(devices => {
|
.then(devices => {
|
||||||
if (which === all) return multidevicewait.resolveWith(this,[devices]);
|
if (which === all) {
|
||||||
var found = devices.find(d => d.serial===which);
|
return devices
|
||||||
if (found) return multidevicewait.resolveWith(this,[[found]]);
|
}
|
||||||
|
const found = devices.find(d => d.serial === which);
|
||||||
|
if (found) {
|
||||||
|
return [found];
|
||||||
|
}
|
||||||
vscode.window.showInformationMessage('Logcat cannot be displayed. The device is disconnected');
|
vscode.window.showInformationMessage('Logcat cannot be displayed. The device is disconnected');
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
});
|
}, () => null);
|
||||||
return multidevicewait;
|
|
||||||
})
|
})
|
||||||
.then(devices => {
|
.then(devices => {
|
||||||
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
|
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
|
||||||
devices.forEach(device => {
|
devices.forEach(device => {
|
||||||
var uri = AndroidContentProvider.getReadLogcatUri(device.serial);
|
if (vscode.window.createWebviewPanel) {
|
||||||
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
|
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?');
|
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.LogcatContent = LogcatContent;
|
module.exports = {
|
||||||
exports.openLogcatWindow = openLogcatWindow;
|
LogcatContent,
|
||||||
|
openLogcatWindow,
|
||||||
|
}
|
||||||
|
|||||||
95
src/manifest.js
Normal file
95
src/manifest.js
Normal 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,
|
||||||
|
}
|
||||||
@@ -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
92
src/package-searcher.js
Normal 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
|
||||||
|
}
|
||||||
322
src/services.js
322
src/services.js
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
290
src/sockets.js
290
src/sockets.js
@@ -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
143
src/sockets/adbsocket.js
Normal 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;
|
||||||
159
src/sockets/androidsocket.js
Normal file
159
src/sockets/androidsocket.js
Normal 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
122
src/sockets/jdwpsocket.js
Normal 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
276
src/stack-frame.js
Normal 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,
|
||||||
|
}
|
||||||
246
src/threads.js
246
src/threads.js
@@ -1,23 +1,78 @@
|
|||||||
'use strict'
|
const { Debugger } = require('./debugger');
|
||||||
|
const { DebuggerException, DebuggerFrameInfo, SourceLocation } = require('./debugger-types');
|
||||||
|
const { DebuggerStackFrame } = require('./stack-frame');
|
||||||
|
const { VariableManager } = require('./variable-manager');
|
||||||
|
|
||||||
const { AndroidVariables } = require('./variables');
|
// vscode doesn't like thread id reuse (the Android runtime is OK with it)
|
||||||
const $ = require('./jq-promise');
|
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 used to manage a single thread reported by JDWP
|
||||||
*/
|
*/
|
||||||
class AndroidThread {
|
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
|
// the Android debugger instance
|
||||||
this.dbgr = session.dbgr;
|
this.dbgr = dbgr;
|
||||||
// the java thread id (hex string)
|
// the java thread id (hex string)
|
||||||
this.threadid = threadid;
|
this.threadid = threadid;
|
||||||
// the vscode thread id (number)
|
// the vscode thread id (number)
|
||||||
this.vscode_threadid = vscode_threadid;
|
this.vscode_threadid = (nextVSCodeThreadId += 1);
|
||||||
// the (Java) name of the thread
|
// the (Java) name of the thread
|
||||||
this.name = null;
|
this.name = name;
|
||||||
// the thread break info
|
// the thread break info
|
||||||
this.paused = null;
|
this.paused = null;
|
||||||
// the timeout during a step which, if it expires, we allow other threads to break
|
// 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`);
|
return new Error(`Thread ${this.vscode_threadid} not suspended`);
|
||||||
}
|
}
|
||||||
|
|
||||||
addStackFrameVariable(frame, level) {
|
/**
|
||||||
if (!this.paused) throw this.threadNotSuspendedError();
|
* @param {DebuggerFrameInfo} frame
|
||||||
var frameId = (this.vscode_threadid * 1e9) + (level * 1e6);
|
* @param {number} call_stack_level
|
||||||
var stack_frame_var = {
|
*/
|
||||||
frame, frameId,
|
createStackFrameVariable(frame, call_stack_level) {
|
||||||
locals: null,
|
if (!this.paused) {
|
||||||
|
throw this.threadNotSuspendedError();
|
||||||
}
|
}
|
||||||
return this.paused.stack_frame_vars[frameId] = stack_frame_var;
|
const frameId = AndroidThread.makeFrameVariableReference(this.vscode_threadid, call_stack_level) ;
|
||||||
|
const stack_frame = new DebuggerStackFrame(this.dbgr, frame, frameId);
|
||||||
|
this.paused.stack_frames.set(frameId, stack_frame);
|
||||||
|
return stack_frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
allocateExceptionScopeReference(frameId) {
|
/**
|
||||||
if (!this.paused) return;
|
* Retrieve the variable manager used to maintain variableReferences for
|
||||||
if (!this.paused.last_exception) return;
|
* expressions evaluated in the global context for this thread.
|
||||||
this.paused.last_exception.frameId = frameId;
|
*/
|
||||||
this.paused.last_exception.scopeRef = frameId + 1;
|
getGlobalVariableManager() {
|
||||||
}
|
if (!this.paused) {
|
||||||
|
throw this.threadNotSuspendedError();
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
if (!this.paused.global_vars) {
|
||||||
// is this refrence an exception scope
|
const globalFrameId = AndroidThread.makeGlobalVariableReference(this.vscode_threadid) ;
|
||||||
if (this.paused.last_exception && variablesReference === this.paused.last_exception.scopeRef) {
|
this.paused.global_vars = new VariableManager(globalFrameId);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
return this.paused.global_vars;
|
||||||
// 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)
|
* set a new VSCode thread ID for this thread
|
||||||
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
*/
|
||||||
|
allocateNewThreadID() {
|
||||||
// evaluate can call this using frameId as the argument
|
this.vscode_threadid = (nextVSCodeThreadId += 1);
|
||||||
if (typeof varinfo === 'number')
|
|
||||||
return this._ensureLocals(this.paused.stack_frame_vars[varinfo]);
|
|
||||||
|
|
||||||
// if we're currently processing it (or we've finished), just return the promise
|
|
||||||
if (this.paused.locals_done[varinfo.frameId])
|
|
||||||
return this.paused.locals_done[varinfo.frameId];
|
|
||||||
|
|
||||||
// create a new promise
|
|
||||||
var def = this.paused.locals_done[varinfo.frameId] = $.Deferred();
|
|
||||||
|
|
||||||
this.dbgr.getlocals(this.threadid, varinfo.frame, {def:def,varinfo:varinfo})
|
|
||||||
.then((locals,x) => {
|
|
||||||
// make sure we are still paused...
|
|
||||||
if (!this.paused)
|
|
||||||
throw this.threadNotSuspendedError();
|
|
||||||
|
|
||||||
// sort the locals by name, except for 'this' which always goes first
|
|
||||||
locals.sort((a,b) => {
|
|
||||||
if (a.name === b.name) return 0;
|
|
||||||
if (a.name === 'this') return -1;
|
|
||||||
if (b.name === 'this') return +1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
})
|
|
||||||
|
|
||||||
// create a new local variable with the results and resolve the promise
|
|
||||||
var varinfo = x.varinfo;
|
|
||||||
varinfo.cached = locals;
|
|
||||||
x.varinfo.locals = new AndroidVariables(this.session, x.varinfo.frameId + 2); // 0 = stack frame, 1 = exception, 2... others
|
|
||||||
x.varinfo.locals.setVariable(varinfo.frameId, varinfo);
|
|
||||||
|
|
||||||
var last_exception = this.paused.last_exception;
|
|
||||||
if (last_exception) {
|
|
||||||
x.varinfo.locals.setVariable(last_exception.scopeRef, last_exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
x.def.resolveWith(this, [varinfo.frameId]);
|
|
||||||
})
|
|
||||||
.fail(e => {
|
|
||||||
x.def.rejectWith(this, [e]);
|
|
||||||
})
|
|
||||||
return def;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setVariableValue(args) {
|
clearStepTimeout() {
|
||||||
var frameId = Math.trunc(args.variablesReference/1e6) * 1e6;
|
if (this.stepTimeout) {
|
||||||
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
clearTimeout(this.stepTimeout);
|
||||||
return this._ensureLocals(stack_frame_var).then(varref => {
|
this.stepTimeout = null;
|
||||||
return this.paused.stack_frame_vars[varref].locals.setVariableValue(args);
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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,
|
||||||
|
}
|
||||||
|
|||||||
424
src/transport.js
424
src/transport.js
@@ -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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
630
src/util.js
630
src/util.js
@@ -1,630 +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
53
src/utils/char-decode.js
Normal 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,
|
||||||
|
}
|
||||||
@@ -3,29 +3,35 @@
|
|||||||
const NumberBaseConverter = {
|
const NumberBaseConverter = {
|
||||||
// Adds two arrays for the given base (10 or 16), returning the result.
|
// Adds two arrays for the given base (10 or 16), returning the result.
|
||||||
add(x, y, base) {
|
add(x, y, base) {
|
||||||
var z = [], n = Math.max(x.length, y.length), carry = 0, i = 0;
|
const z = [], n = Math.max(x.length, y.length);
|
||||||
while (i < n || carry) {
|
let carry = 0;
|
||||||
var xi = i < x.length ? x[i] : 0;
|
for (let i = 0; i < n || carry; i++) {
|
||||||
var yi = i < y.length ? y[i] : 0;
|
const xi = i < x.length ? x[i] : 0;
|
||||||
var zi = carry + xi + yi;
|
const yi = i < y.length ? y[i] : 0;
|
||||||
|
const zi = carry + xi + yi;
|
||||||
z.push(zi % base);
|
z.push(zi % base);
|
||||||
carry = Math.floor(zi / base);
|
carry = Math.floor(zi / base);
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
return z;
|
return z;
|
||||||
},
|
},
|
||||||
// Returns a*x, where x is an array of decimal digits and a is an ordinary
|
// 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.
|
// JavaScript number. base is the number base of the array x.
|
||||||
multiplyByNumber(num, x, base) {
|
multiplyByNumber(num, x, base) {
|
||||||
if (num < 0) return null;
|
if (num < 0) {
|
||||||
if (num == 0) return [];
|
return null;
|
||||||
var result = [], power = x;
|
}
|
||||||
|
if (num == 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let result = [], power = x;
|
||||||
for(;;) {
|
for(;;) {
|
||||||
if (num & 1) {
|
if (num & 1) {
|
||||||
result = this.add(result, power, base);
|
result = this.add(result, power, base);
|
||||||
}
|
}
|
||||||
num = num >> 1;
|
num = num >> 1;
|
||||||
if (num === 0) return result;
|
if (num === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
power = this.add(power, power, base);
|
power = this.add(power, power, base);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -37,12 +43,12 @@ const NumberBaseConverter = {
|
|||||||
convertBase(str, fromBase, toBase) {
|
convertBase(str, fromBase, toBase) {
|
||||||
if (fromBase === 10 && /[eE]/.test(str)) {
|
if (fromBase === 10 && /[eE]/.test(str)) {
|
||||||
// convert exponents to a string of zeros
|
// 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
|
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();
|
const digits = str.split('').map(d => parseInt(d,fromBase)).reverse();
|
||||||
var outArray = [], power = [1];
|
let outArray = [], power = [1];
|
||||||
for (var i = 0; i < digits.length; i++) {
|
for (let i = 0; i < digits.length; i++) {
|
||||||
if (digits[i]) {
|
if (digits[i]) {
|
||||||
outArray = this.add(outArray, this.multiplyByNumber(digits[i], power, toBase), toBase);
|
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('');
|
return outArray.reverse().map(d => d.toString(toBase)).join('');
|
||||||
},
|
},
|
||||||
decToHex(decstr, minlen) {
|
decToHex(decstr, minlen) {
|
||||||
var res, isneg = decstr[0] === '-';
|
let res;
|
||||||
if (isneg) decstr = decstr.slice(1)
|
const isneg = decstr[0] === '-';
|
||||||
|
if (isneg) {
|
||||||
|
decstr = decstr.slice(1);
|
||||||
|
}
|
||||||
decstr = decstr.match(/^0*(.+)$/)[1]; // strip leading zeros
|
decstr = decstr.match(/^0*(.+)$/)[1]; // strip leading zeros
|
||||||
if (decstr.length < 16 && !/[eE]/.test(decstr)) { // 16 = Math.pow(2,52).toString(10).length
|
if (decstr.length < 16 && !/[eE]/.test(decstr)) { // 16 = Math.pow(2,52).toString(10).length
|
||||||
// less than 52 bits - just use parseInt
|
// less than 52 bits - just use parseInt
|
||||||
@@ -63,27 +72,32 @@ const NumberBaseConverter = {
|
|||||||
if (isneg) {
|
if (isneg) {
|
||||||
res = NumberBaseConverter.twosComplement(res, 16);
|
res = NumberBaseConverter.twosComplement(res, 16);
|
||||||
if (/^[0-7]/.test(res)) res = 'f'+res; //msb must be set for -ve numbers
|
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
|
res = '0' + res; // msb must not be set for +ve numbers
|
||||||
|
}
|
||||||
if (minlen && res.length < minlen) {
|
if (minlen && res.length < minlen) {
|
||||||
res = (isneg?'f':'0').repeat(minlen - res.length) + res;
|
res = (isneg?'f':'0').repeat(minlen - res.length) + res;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
hexToDec(hexstr, signed) {
|
hexToDec(hexstr, signed) {
|
||||||
var res, isneg = /^[^0-7]/.test(hexstr);
|
const isneg = /^[^0-7]/.test(hexstr);
|
||||||
if (hexstr.match(/^0*(.+)$/)[1].length*4 < 52) {
|
if (hexstr.match(/^0*(.+)$/)[1].length*4 < 52) {
|
||||||
// less than 52 bits - just use parseInt
|
// less than 52 bits - just use parseInt
|
||||||
res = parseInt(hexstr, 16);
|
let res = parseInt(hexstr, 16);
|
||||||
if (signed && isneg) res = -res;
|
if (signed && isneg) {
|
||||||
|
res = -res;
|
||||||
|
}
|
||||||
return res.toString(10);
|
return res.toString(10);
|
||||||
}
|
}
|
||||||
if (isneg) {
|
if (isneg) {
|
||||||
hexstr = NumberBaseConverter.twosComplement(hexstr, 16);
|
hexstr = NumberBaseConverter.twosComplement(hexstr, 16);
|
||||||
}
|
}
|
||||||
res = (isneg ? '-' : '') + NumberBaseConverter.convertBase(hexstr, 16, 10);
|
const res = (isneg ? '-' : '') + NumberBaseConverter.convertBase(hexstr, 16, 10);
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(exports, NumberBaseConverter);
|
module.exports = {
|
||||||
|
NumberBaseConverter,
|
||||||
|
}
|
||||||
51
src/utils/print.js
Normal file
51
src/utils/print.js
Normal 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
21
src/utils/source-file.js
Normal 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
11
src/utils/thread.js
Normal 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
166
src/variable-manager.js
Normal 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,
|
||||||
|
}
|
||||||
410
src/variables.js
410
src/variables.js
@@ -1,410 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const { JTYPES, exmsg_var_name, createJavaString } = require('./globals');
|
|
||||||
const NumberBaseConverter = require('./nbc');
|
|
||||||
const $ = require('./jq-promise');
|
|
||||||
|
|
||||||
/*
|
|
||||||
Class used to manage stack frame locals and other evaluated expressions
|
|
||||||
*/
|
|
||||||
class AndroidVariables {
|
|
||||||
|
|
||||||
constructor(session, baseId) {
|
|
||||||
this.session = session;
|
|
||||||
this.dbgr = session.dbgr;
|
|
||||||
// the incremental reference id generator for stack frames, locals, etc
|
|
||||||
this.nextId = baseId;
|
|
||||||
// hashmap of variables and frames
|
|
||||||
this.variableHandles = {};
|
|
||||||
// hashmap<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,
|
|
||||||
});
|
|
||||||
// create the fully qualified names to use for evaluation
|
|
||||||
fields.forEach(f => f.fqname = `${x.varinfo.objvar.fqname || x.varinfo.objvar.name}.${f.name}`);
|
|
||||||
x.varinfo.cached = fields;
|
|
||||||
return this._local_to_variable(fields);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (varinfo.arrvar) {
|
|
||||||
// array elements request
|
|
||||||
var range = varinfo.range, count = range[1] - range[0];
|
|
||||||
// should always have a +ve count, but just in case...
|
|
||||||
if (count <= 0) return $.Deferred().resolve([]);
|
|
||||||
// add some hysteresis
|
|
||||||
if (count > 110) {
|
|
||||||
// create subranges in the sub-power of 10
|
|
||||||
var subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100), variables = [];
|
|
||||||
for (var i=range[0],varref,v; i < range[1]; i+= subrangelen) {
|
|
||||||
varref = ++this.nextId;
|
|
||||||
v = this.variableHandles[varref] = { varref:varref, arrvar:varinfo.arrvar, range:[i, Math.min(i+subrangelen, range[1])] };
|
|
||||||
variables.push({name:`[${v.range[0]}..${v.range[1]-1}]`,type:'',value:'',variablesReference:varref});
|
|
||||||
}
|
|
||||||
return $.Deferred().resolve(variables);
|
|
||||||
}
|
|
||||||
// get the elements for the specified range
|
|
||||||
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count, {varinfo})
|
|
||||||
.then((elements, x) => {
|
|
||||||
elements.forEach(el => el.fqname = `${x.varinfo.arrvar.fqname || x.varinfo.arrvar.name}[${el.name}]`);
|
|
||||||
x.varinfo.cached = elements;
|
|
||||||
return this._local_to_variable(elements);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (varinfo.bigstring) {
|
|
||||||
return this.dbgr.getstringchars(varinfo.bigstring.value)
|
|
||||||
.then((s) => {
|
|
||||||
return this._local_to_variable([{name:'<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, evaluateName = v.fqname || v.name, formats = {}, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
|
|
||||||
switch(true) {
|
|
||||||
case v.hasnullvalue && JTYPES.isReference(v.type):
|
|
||||||
// null object or array type
|
|
||||||
objvalue = 'null';
|
|
||||||
break;
|
|
||||||
case v.type.signature === JTYPES.Object.signature:
|
|
||||||
// Object doesn't really have anything worth seeing, so just treat it as unexpandable
|
|
||||||
objvalue = v.type.typename;
|
|
||||||
break;
|
|
||||||
case v.type.signature === JTYPES.String.signature:
|
|
||||||
objvalue = JSON.stringify(v.string);
|
|
||||||
if (v.biglen) {
|
|
||||||
// since this is a big string - make it viewable on expand
|
|
||||||
varref = ++this.nextId;
|
|
||||||
this.variableHandles[varref] = {varref:varref, bigstring:v};
|
|
||||||
objvalue = `String (length:${v.biglen})`;
|
|
||||||
}
|
|
||||||
else if (this._expandable_prims) {
|
|
||||||
// as a courtesy, allow strings to be expanded to see their length
|
|
||||||
varref = ++this.nextId;
|
|
||||||
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.string.length};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case JTYPES.isArray(v.type):
|
|
||||||
// non-null array type - if it's not zero-length add another variable reference so the user can expand
|
|
||||||
if (v.arraylen) {
|
|
||||||
varref = this._getObjectIdReference(v.type, v.value);
|
|
||||||
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
|
|
||||||
}
|
|
||||||
objvalue = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
|
|
||||||
break;
|
|
||||||
case JTYPES.isObject(v.type):
|
|
||||||
// non-null object instance - add another variable reference so the user can expand
|
|
||||||
varref = this._getObjectIdReference(v.type, v.value);
|
|
||||||
this.variableHandles[varref] = {varref:varref, objvar:v};
|
|
||||||
objvalue = v.type.typename;
|
|
||||||
break;
|
|
||||||
case v.type.signature === 'C':
|
|
||||||
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'};
|
|
||||||
if (cmap[v.char]) {
|
|
||||||
objvalue = `'\\${cmap[v.char]}'`;
|
|
||||||
} else if (v.value < 32) {
|
|
||||||
objvalue = v.value ? `'\\u${('000'+v.value.toString(16)).slice(-4)}'` : "'\\0'";
|
|
||||||
} else objvalue = `'${v.char}'`;
|
|
||||||
break;
|
|
||||||
case v.type.signature === 'J':
|
|
||||||
// because JS cannot handle 64bit ints, we need a bit of extra work
|
|
||||||
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
|
|
||||||
objvalue = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
|
|
||||||
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
|
|
||||||
formats.oct = formats.bin = '';
|
|
||||||
// 24 bit chunks...
|
|
||||||
for (var s=v64hex,uint; s; s = s.slice(0,-6)) {
|
|
||||||
uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
|
|
||||||
formats.oct = uint.toString(8) + formats.oct;
|
|
||||||
formats.bin = uint.toString(2) + formats.bin;
|
|
||||||
}
|
|
||||||
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
|
|
||||||
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
|
|
||||||
break;
|
|
||||||
case /^[BIS]$/.test(v.type.signature):
|
|
||||||
objvalue = formats.dec = v.value.toString();
|
|
||||||
var uint = (v.value >>> 0);
|
|
||||||
formats.hex = '0x' + uint.toString(16);
|
|
||||||
formats.oct = '0c' + uint.toString(8);
|
|
||||||
formats.bin = '0b' + uint.toString(2);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// other primitives: boolean, etc
|
|
||||||
objvalue = v.value.toString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
|
|
||||||
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
|
|
||||||
varref = ++this.nextId;
|
|
||||||
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.value};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: v.name,
|
|
||||||
type: typename,
|
|
||||||
value: objvalue,
|
|
||||||
evaluateName,
|
|
||||||
variablesReference: varref,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setVariableValue(args) {
|
|
||||||
const failSetVariableRequest = reason => $.Deferred().reject(new Error(reason));
|
|
||||||
|
|
||||||
var v = this.variableHandles[args.variablesReference];
|
|
||||||
if (!v || !v.cached) {
|
|
||||||
return failSetVariableRequest(`Variable '${args.name}' not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
var destvar = v.cached.find(v => v.name===args.name);
|
|
||||||
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
|
|
||||||
return failSetVariableRequest(`The value is read-only and cannot be updated.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// be nice and remove any superfluous whitespace
|
|
||||||
var value = args.value.trim();
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
// just ignore blank requests
|
|
||||||
var vsvar = this._local_to_variable(destvar);
|
|
||||||
return $.Deferred().resolve(vsvar);
|
|
||||||
}
|
|
||||||
|
|
||||||
// non-string reference types can only set to null
|
|
||||||
if (/^L/.test(destvar.type.signature) && destvar.type.signature !== JTYPES.String.signature) {
|
|
||||||
if (value !== 'null') {
|
|
||||||
return failSetVariableRequest('Object references can only be set to null');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert the new value into a debugger-compatible object
|
|
||||||
var m, num, data, datadef;
|
|
||||||
switch(true) {
|
|
||||||
case value === 'null':
|
|
||||||
data = {valuetype:'oref',value:null}; // null object reference
|
|
||||||
break;
|
|
||||||
case /^(true|false)$/.test(value):
|
|
||||||
data = {valuetype:'boolean',value:value!=='false'}; // boolean literal
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^[+-]?0x([0-9a-f]+)$/i)):
|
|
||||||
// hex integer- convert to decimal and fall through
|
|
||||||
if (m[1].length < 52/4)
|
|
||||||
value = parseInt(value, 16).toString(10);
|
|
||||||
else
|
|
||||||
value = NumberBaseConverter.hexToDec(value);
|
|
||||||
m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/);
|
|
||||||
// fall-through
|
|
||||||
case !!(m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/)):
|
|
||||||
// decimal integer
|
|
||||||
num = parseFloat(value, 10); // parseInt() can't handle exponents
|
|
||||||
switch(true) {
|
|
||||||
case (num >= -128 && num <= 127): data = {valuetype:'byte',value:num}; break;
|
|
||||||
case (num >= -32768 && num <= 32767): data = {valuetype:'short',value:num}; break;
|
|
||||||
case (num >= -2147483648 && num <= 2147483647): data = {valuetype:'int',value:num}; break;
|
|
||||||
case /inf/i.test(num): return failSetVariableRequest(`Value '${value}' exceeds the maximum number range.`);
|
|
||||||
case /^[FD]$/.test(destvar.type.signature): data = {valuetype:'float',value:num}; break;
|
|
||||||
default:
|
|
||||||
// long (or larger) - need to use the arbitrary precision class
|
|
||||||
data = {valuetype:'long',value:NumberBaseConverter.decToHex(value, 16)};
|
|
||||||
switch(true){
|
|
||||||
case data.value.length > 16:
|
|
||||||
case num > 0 && data.value.length===16 && /[^0-7]/.test(data.value[0]):
|
|
||||||
// number exceeds signed 63 bit - make it a float
|
|
||||||
data = {valuetype:'float',value:num};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^(Float|Double)\s*\.\s*(POSITIVE_INFINITY|NEGATIVE_INFINITY|NaN)$/)):
|
|
||||||
// Java special float constants
|
|
||||||
data = {valuetype:m[1].toLowerCase(),value:{POSITIVE_INFINITY:Infinity,NEGATIVE_INFINITY:-Infinity,NaN:NaN}[m[2]]};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^([+-])?infinity$/i)):// allow js infinity
|
|
||||||
data = {valuetype:'float',value:m[1]!=='-'?Infinity:-Infinity};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^nan$/i)): // allow js nan
|
|
||||||
data = {valuetype:'float',value:NaN};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^[+-]?[0-9]+[eE][-][0-9]+([dDfF])?$/)):
|
|
||||||
case !!(m=value.match(/^[+-]?[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?([dDfF])?$/)):
|
|
||||||
// decimal float
|
|
||||||
num = parseFloat(value);
|
|
||||||
data = {valuetype:/^[dD]$/.test(m[1]) ? 'double': 'float',value:num};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^'(?:\\u([0-9a-fA-F]{4})|\\([bfrntv0'])|(.))'$/)):
|
|
||||||
// character literal
|
|
||||||
var cvalue = m[1] ? String.fromCharCode(parseInt(m[1],16)) :
|
|
||||||
m[2] ? {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v',0:'\0',"'":"'"}[m[2]]
|
|
||||||
: m[3]
|
|
||||||
data = {valuetype:'char',value:cvalue};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^"[^"\\\n]*(\\.[^"\\\n]*)*"$/)):
|
|
||||||
// string literal - we need to get the runtime to create a new string first
|
|
||||||
datadef = createJavaString(this.dbgr, value).then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// invalid literal
|
|
||||||
return failSetVariableRequest(`'${value}' is not a valid Java literal.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!datadef) {
|
|
||||||
// as a nicety, if the destination is a string, stringify any primitive value
|
|
||||||
if (data.valuetype !== 'oref' && destvar.type.signature === JTYPES.String.signature) {
|
|
||||||
datadef = createJavaString(this.dbgr, data.value.toString(), {israw:true})
|
|
||||||
.then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
|
||||||
} else if (destvar.type.signature.length===1) {
|
|
||||||
// if the destination is a primitive, we need to range-check it here
|
|
||||||
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
|
|
||||||
// weirdness if we allow primitives to be set with out-of-range values
|
|
||||||
var validmap = {
|
|
||||||
B:'byte,char', // char may not fit - we special-case this later
|
|
||||||
S:'byte,short,char',
|
|
||||||
I:'byte,short,int,char',
|
|
||||||
J:'byte,short,int,long,char',
|
|
||||||
F:'byte,short,int,long,char,float',
|
|
||||||
D:'byte,short,int,long,char,double,float',
|
|
||||||
C:'byte,short,char',Z:'boolean',
|
|
||||||
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
|
|
||||||
};
|
|
||||||
var is_in_range = (validmap[destvar.type.signature]||'').indexOf(data.valuetype) >= 0;
|
|
||||||
if (destvar.type.signature === 'B' && data.valuetype === 'char')
|
|
||||||
is_in_range = validmap.isCharInRangeForByte(data.value);
|
|
||||||
if (!is_in_range) {
|
|
||||||
return failSetVariableRequest(`'${value}' is not compatible with variable type: ${destvar.type.typename}`);
|
|
||||||
}
|
|
||||||
// check complete - make sure the type matches the destination and use a resolved deferred with the value
|
|
||||||
if (destvar.type.signature!=='C' && data.valuetype === 'char')
|
|
||||||
data.value = data.value.charCodeAt(0); // convert char to it's int value
|
|
||||||
if (destvar.type.signature==='J' && typeof data.value === 'number')
|
|
||||||
data.value = NumberBaseConverter.decToHex(''+data.value,16); // convert ints to hex-string longs
|
|
||||||
data.valuetype = destvar.type.typename;
|
|
||||||
|
|
||||||
datadef = $.Deferred().resolveWith(this,[data]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return datadef.then(data => {
|
|
||||||
// setxxxvalue sets the new value and then returns a new local for the variable
|
|
||||||
switch(destvar.vtype) {
|
|
||||||
case 'field': return this.dbgr.setfieldvalue(destvar, data);
|
|
||||||
case 'local': return this.dbgr.setlocalvalue(destvar, data);
|
|
||||||
case 'arrelem':
|
|
||||||
var idx = parseInt(args.name, 10), count=1;
|
|
||||||
if (idx < 0 || idx >= destvar.data.arrobj.arraylen) throw new Error('Array index out of bounds');
|
|
||||||
return this.dbgr.setarrayvalues(destvar.data.arrobj, idx, count, data);
|
|
||||||
default: throw new Error('Unsupported variable type');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(newlocalvar => {
|
|
||||||
if (destvar.vtype === 'arrelem') newlocalvar = newlocalvar[0];
|
|
||||||
Object.assign(destvar, newlocalvar);
|
|
||||||
var vsvar = this._local_to_variable(destvar);
|
|
||||||
return vsvar;
|
|
||||||
})
|
|
||||||
.fail(e => {
|
|
||||||
return failSetVariableRequest(`Variable update failed. ${e.message||''}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.AndroidVariables = AndroidVariables;
|
|
||||||
440
src/wsproxy.js
440
src/wsproxy.js
@@ -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;
|
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
// The module 'assert' provides assertion methods from node
|
// 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
|
// You can import and use all API from the 'vscode' module
|
||||||
// as well as import your extension to test it
|
// as well as import your extension to test it
|
||||||
var vscode = require('vscode');
|
//const vscode = require('vscode');
|
||||||
var myExtension = require('../extension');
|
//const myExtension = require('../extension');
|
||||||
|
|
||||||
// Defines a Mocha test suite to group tests of similar kind together
|
// Defines a Mocha test suite to group tests of similar kind together
|
||||||
suite("Extension Tests", function() {
|
suite("Extension Tests", function() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
// to report the results back to the caller. When the tests are finished, return
|
// to report the results back to the caller. When the tests are finished, return
|
||||||
// a possible error to the callback or null if none.
|
// 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
|
// 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
|
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
|
||||||
|
|||||||
Reference in New Issue
Block a user