mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 18:08:29 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d064b9a3f4 | ||
|
|
6439e1b8b7 | ||
|
|
a4ce09d309 | ||
|
|
1535f133d9 | ||
|
|
44d887dd6c | ||
|
|
9aeca6b96b | ||
|
|
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 | ||
|
|
494bb83cbf | ||
|
|
9fca5cbe8c | ||
|
|
5f0a02b17f | ||
|
|
da36e8e457 | ||
|
|
3dbfd8ef2a | ||
|
|
4a31b83eb9 | ||
|
|
261c06f1d6 | ||
|
|
130d79f6c2 | ||
|
|
8baf894fc9 | ||
|
|
92bd003122 | ||
|
|
13f116b3b3 | ||
|
|
140e48cbd1 | ||
|
|
7e8f471df4 | ||
|
|
09905eb85a | ||
|
|
e76773e8e4 | ||
|
|
c98c962172 | ||
|
|
b3501d529a |
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
@@ -8,15 +8,21 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${execPath}",
|
"runtimeExecutable": "${execPath}",
|
||||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
||||||
"stopOnEntry": false
|
"stopOnEntry": false,
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Server",
|
"name": "Debugger Server",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"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",
|
||||||
@@ -29,8 +35,8 @@
|
|||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
"name": "Extension + Server",
|
"name": "Extension + Debugger",
|
||||||
"configurations": [ "Launch Extension", "Server" ]
|
"configurations": [ "Launch Extension", "Debugger Server" ]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -1,5 +1,61 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
### version 1.1.0
|
||||||
|
* App launch arguments overriden in a new `amStartArgs` launch configuration property.
|
||||||
|
* A new "attach" launch configuration allows the debugger to attach to running processes.
|
||||||
|
* A "${command:PickAndroidDevice}" value allows a deployment device to be chosen during each launch
|
||||||
|
* Watch and repl expressions now support format specifiers
|
||||||
|
* Small bug fixes and performance improvements
|
||||||
|
|
||||||
|
### version 1.0.0
|
||||||
|
* Update extension to support minimum version of node v10
|
||||||
|
* refactoring and improvement of type-checking using jsdocs
|
||||||
|
|
||||||
|
### version 0.8.0
|
||||||
|
* Try to extract Android manifest directly from APK
|
||||||
|
* Added `manifestFile` launch configuration property
|
||||||
|
* Allow `pm install` arguments to be customised as a launch configuration property
|
||||||
|
* Document `launchActivity` launch configuration property
|
||||||
|
* Fix critical security advisory https://www.npmjs.com/advisories/1118
|
||||||
|
|
||||||
|
### version 0.7.1
|
||||||
|
* Added the [Buy Me A Coffee](https://www.buymeacoffee.com/adelphes) link to the README
|
||||||
|
|
||||||
|
### version 0.7.0
|
||||||
|
* Fix logcat not displaying
|
||||||
|
* Fix breakpoints not triggering on Windows
|
||||||
|
* Added kotlin folder to list of known source locations
|
||||||
|
* Upgraded dependencies to resolve a number of security vulnerabilites
|
||||||
|
* Updated README with info about prelaunch build task
|
||||||
|
* Added MIT license file
|
||||||
|
|
||||||
|
### version 0.6.2
|
||||||
|
* Fix broken logcat command due to missing dependency
|
||||||
|
|
||||||
|
### version 0.6.1
|
||||||
|
* Regenerate package-lock.json to remove event-stream vulnerability - https://github.com/dominictarr/event-stream/issues/116
|
||||||
|
|
||||||
|
### version 0.6.0
|
||||||
|
* Fix issue with breakpoints not enabling correctly
|
||||||
|
* Fix issue with JDWP failure on breakpoint hit
|
||||||
|
* Added support for diagnostic logs using trace configuration option
|
||||||
|
* Updated default apkFile path to match current releases of Android Studio
|
||||||
|
* Updated package dependencies
|
||||||
|
|
||||||
|
### version 0.5.0
|
||||||
|
* Debugger support for Kotlin source files
|
||||||
|
* Exception UI
|
||||||
|
* Fixed some console display issues
|
||||||
|
|
||||||
|
### version 0.4.1
|
||||||
|
* One day I will learn to update the changelog **before** I hit publish
|
||||||
|
* Updated changelog
|
||||||
|
|
||||||
|
### version 0.4.0
|
||||||
|
* Debugger performance improvements
|
||||||
|
* Fixed exception details not being displayed in locals
|
||||||
|
* Fixed some logcat display issues
|
||||||
|
|
||||||
### version 0.3.1
|
### version 0.3.1
|
||||||
* Bug fixes
|
* Bug fixes
|
||||||
* Fix issue with exception breaks crashing debugger
|
* Fix issue with exception breaks crashing debugger
|
||||||
|
|||||||
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.
|
||||||
153
README.md
153
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,30 +29,169 @@ 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": [
|
||||||
{
|
{
|
||||||
// configuration type, request and name. "launch" is used to deploy the app to your device and start a debugging session
|
// configuration type, request and name. "launch" is used to deploy the app
|
||||||
|
// to your device and start a debugging session.
|
||||||
"type": "android",
|
"type": "android",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Launch App",
|
"name": "Launch App",
|
||||||
|
|
||||||
// Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)
|
// Location of the App source files. This value must point to the root of
|
||||||
|
// your App source tree (containing AndroidManifest.xml).
|
||||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||||
|
|
||||||
// Fully qualified path to the built APK (Android Application Package)
|
// Fully qualified path to the built APK (Android Application Package).
|
||||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
|
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
|
||||||
|
|
||||||
// Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037
|
// Port number to connect to the local ADB (Android Debug Bridge) instance.
|
||||||
|
// Default: 5037
|
||||||
"adbPort": 5037,
|
"adbPort": 5037,
|
||||||
|
|
||||||
// Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn
|
// Automatically launch 'adb start-server' if not already started.
|
||||||
|
// Default: true
|
||||||
|
"autoStartADB": true,
|
||||||
|
|
||||||
|
// Launch behaviour if source files have been saved after the APK was built.
|
||||||
|
// One of: [ ignore warn stop ]. Default: warn
|
||||||
"staleBuild": "warn",
|
"staleBuild": "warn",
|
||||||
|
|
||||||
|
// Target Device ID (as indicated by 'adb devices').
|
||||||
|
// Use this to specify which device is used for deployment
|
||||||
|
// when multiple devices are connected.
|
||||||
|
"targetDevice": "",
|
||||||
|
|
||||||
|
// Fully qualified path to the AndroidManifest.xml file compiled into the APK.
|
||||||
|
// Default: "${appSrcRoot}/AndroidManifest.xml"
|
||||||
|
"manifestFile": "${workspaceRoot}/app/src/main/AndroidManifest.xml",
|
||||||
|
|
||||||
|
// Custom arguments passed to the Android package manager to install the app.
|
||||||
|
// Run 'adb shell pm' to show valid arguments. Default: ["-r"]
|
||||||
|
"pmInstallArgs": ["-r"],
|
||||||
|
|
||||||
|
// Custom arguments passed to the Android application manager to start the app.
|
||||||
|
// Run `adb shell am` to show valid arguments.
|
||||||
|
// Note that `-D` is required to enable debugging.
|
||||||
|
"amStartArgs": [
|
||||||
|
"-D",
|
||||||
|
"--activity-brought-to-front",
|
||||||
|
"-a android.intent.action.MAIN",
|
||||||
|
"-c android.intent.category.LAUNCHER",
|
||||||
|
"-n package.name/launch.activity"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Manually specify the activity to run when the app is started. This option is
|
||||||
|
// mutually exclusive with "amStartArgs".
|
||||||
|
"launchActivity": ".MainActivity",
|
||||||
|
|
||||||
|
// Time in milliseconds to wait after launching an app before attempting to attach
|
||||||
|
// the debugger. Default: 1000ms
|
||||||
|
"postLaunchPause": 1000,
|
||||||
|
|
||||||
|
// Set to true to output debugging logs for diagnostics.
|
||||||
|
"trace": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": false
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expression evaluation
|
||||||
|
|
||||||
|
Format specifiers can be appended to watch and repl expressions to change how the evaluated result is displayed.
|
||||||
|
The specifiers work with the same syntax used in Visual Studio.
|
||||||
|
See https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp for examples.
|
||||||
|
|
||||||
|
```
|
||||||
|
123 123
|
||||||
|
123,x 0x0000007b
|
||||||
|
123,xb 0000007b
|
||||||
|
123,X 0x0000007B
|
||||||
|
123,o 000000000173
|
||||||
|
123,b 0b00000000000000000000000001111011
|
||||||
|
123,bb 00000000000000000000000001111011
|
||||||
|
123,c '{'
|
||||||
|
"one\ntwo" "one\ntwo"
|
||||||
|
"one\ntwo",sb one\ntwo
|
||||||
|
"one\ntwo",! one
|
||||||
|
two
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also apply the specifiers to object and array instances to format fields and elements:
|
||||||
|
```
|
||||||
|
arr,x int[3]
|
||||||
|
[0] 0x00000001
|
||||||
|
[1] 0x00000002
|
||||||
|
[1] 0x00000003
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Note: Format specifiers for floating point values (`e`/`g`) and string encoding conversions (`s8`/`su`/`s32`) are not supported.
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|||||||
57
extension.js
57
extension.js
@@ -3,14 +3,8 @@
|
|||||||
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 { selectAndroidProcessID } = require('./src/process-attach');
|
||||||
function getADBPort() {
|
const { selectTargetDevice } = require('./src/utils/device');
|
||||||
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
|
||||||
@@ -19,28 +13,51 @@ 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
|
// add the device picker handler - used to choose a target device
|
||||||
vscode.workspace.onDidChangeConfiguration(e => {
|
vscode.commands.registerCommand('PickAndroidDevice', async (launchConfig) => {
|
||||||
wsproxyserver.setADBPort(getADBPort());
|
// if the config has both PickAndroidDevice and PickAndroidProcess, ignore this
|
||||||
})
|
// request as PickAndroidProcess already includes chooosing a device...
|
||||||
|
if (launchConfig && launchConfig.processId === '${command:PickAndroidProcess}') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const device = await selectTargetDevice(vscode, "Launch", { alwaysShow:true });
|
||||||
|
// the debugger requires a string value to be returned
|
||||||
|
return JSON.stringify(device);
|
||||||
|
}),
|
||||||
|
// add the process picker handler - used to choose a PID to attach to
|
||||||
|
vscode.commands.registerCommand('PickAndroidProcess', async (launchConfig) => {
|
||||||
|
// if the config has a targetDevice specified, use it instead of choosing a device...
|
||||||
|
let target_device = '';
|
||||||
|
if (launchConfig && typeof launchConfig.targetDevice === 'string') {
|
||||||
|
target_device = launchConfig.targetDevice;
|
||||||
|
}
|
||||||
|
const explicit_pick_device = target_device === '${command:PickAndroidDevice}';
|
||||||
|
if (!target_device || explicit_pick_device) {
|
||||||
|
// no targetDevice (or it's set to ${command:PickAndroidDevice})
|
||||||
|
const device = await selectTargetDevice(vscode, 'Attach', { alwaysShow: explicit_pick_device });
|
||||||
|
if (!device) {
|
||||||
|
return JSON.stringify({status: 'cancelled'});
|
||||||
|
}
|
||||||
|
target_device = device.serial;
|
||||||
|
}
|
||||||
|
const o = await selectAndroidProcessID(vscode, target_device);
|
||||||
|
// the debugger requires a string value to be returned
|
||||||
|
return JSON.stringify(o);
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
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
144
package.json
144
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.4.0",
|
"version": "1.1.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"
|
||||||
@@ -18,7 +18,9 @@
|
|||||||
"theme": "dark"
|
"theme": "dark"
|
||||||
},
|
},
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onCommand:android-dev-ext.view_logcat"
|
"onCommand:android-dev-ext.view_logcat",
|
||||||
|
"onCommand:PickAndroidDevice",
|
||||||
|
"onCommand:PickAndroidProcess"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -35,12 +37,15 @@
|
|||||||
"breakpoints": [
|
"breakpoints": [
|
||||||
{
|
{
|
||||||
"language": "java"
|
"language": "java"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "kotlin"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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": {
|
||||||
@@ -51,6 +56,17 @@
|
|||||||
"adbPort"
|
"adbPort"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"amStartArgs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Custom arguments to pass to the Android application manager to start the app. Run `adb shell am` to show valid arguments. Note that `-D` is required to enable debugging.\r\nBe careful using this option - you must specify the correct parameters or the app will not start.\r\n\r\nThis option is incompatible with the `launchActivity` option.",
|
||||||
|
"default": [
|
||||||
|
"-D",
|
||||||
|
"--activity-brought-to-front",
|
||||||
|
"-a android.intent.action.MAIN",
|
||||||
|
"-c android.intent.category.LAUNCHER",
|
||||||
|
"-n package.name/launch.activity"
|
||||||
|
]
|
||||||
|
},
|
||||||
"appSrcRoot": {
|
"appSrcRoot": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
|
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
|
||||||
@@ -59,7 +75,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",
|
||||||
@@ -76,11 +92,33 @@
|
|||||||
"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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"postLaunchPause": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Time in milliseconds to wait after launching an app before attempting to attach the debugger. Default: 1000",
|
||||||
|
"default": 1000
|
||||||
|
},
|
||||||
"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\"",
|
||||||
@@ -89,7 +127,46 @@
|
|||||||
"targetDevice": {
|
"targetDevice": {
|
||||||
"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": "${command:PickAndroidDevice}"
|
||||||
|
},
|
||||||
|
"trace": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Set to true to output debugging logs for diagnostics",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"required": [
|
||||||
|
"appSrcRoot",
|
||||||
|
"adbPort",
|
||||||
|
"processId"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"appSrcRoot": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
|
||||||
|
"default": "${workspaceRoot}/app/src/main"
|
||||||
|
},
|
||||||
|
"adbPort": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037",
|
||||||
|
"default": 5037
|
||||||
|
},
|
||||||
|
"processId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "PID of process to attach to.\n\"${command:PickAndroidProcess}\" will display a list of debuggable PIDs to choose from during launch.",
|
||||||
|
"default": "${command:PickAndroidProcess}"
|
||||||
|
},
|
||||||
|
"targetDevice": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used when multiple devices are connected.",
|
||||||
|
"default": "${command:PickAndroidDevice}"
|
||||||
|
},
|
||||||
|
"trace": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Set to true to output debugging logs for diagnostics",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,25 +174,45 @@
|
|||||||
"initialConfigurations": [
|
"initialConfigurations": [
|
||||||
{
|
{
|
||||||
"type": "android",
|
"type": "android",
|
||||||
"name": "Android Debug",
|
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
"name": "Android 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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Android attach",
|
||||||
|
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||||
|
"adbPort": 5037,
|
||||||
|
"processId": "${command:PickAndroidProcess}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configurationSnippets": [
|
"configurationSnippets": [
|
||||||
{
|
{
|
||||||
"label": "Android: Launch Configuration",
|
"label": "Android: Launch Application",
|
||||||
"description": "A new configuration for launching an Android app debugging session",
|
"description": "A new configuration for launching an Android app debugging session",
|
||||||
"body": {
|
"body": {
|
||||||
"type": "android",
|
"type": "android",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "${2:Launch App}",
|
"name": "${2:Android 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
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Android: Attach to Process",
|
||||||
|
"description": "A new configuration for attaching to a running Android app process",
|
||||||
|
"body": {
|
||||||
|
"type": "android",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "${2:Android Attach}",
|
||||||
|
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
||||||
|
"adbPort": 5037,
|
||||||
|
"processId": "^\"\\${command:PickAndroidProcess}\""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"variables": {}
|
"variables": {}
|
||||||
@@ -123,23 +220,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.15.0",
|
"long": "^4.0.0",
|
||||||
"vscode-debugadapter": "^1.15.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1018
src/adbclient.js
1018
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 = idx - (Math.trunc(idx / align) * 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,
|
||||||
|
}
|
||||||
144
src/apk-file-info.js
Normal file
144
src/apk-file-info.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
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({manifestFile, apkFile, appSrcRoot}) {
|
||||||
|
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} (${err.message})`);
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|||||||
2102
src/debugMain.js
2102
src/debugMain.js
File diff suppressed because it is too large
Load Diff
792
src/debugger-types.js
Normal file
792
src/debugger-types.js
Normal file
@@ -0,0 +1,792 @@
|
|||||||
|
const { ADBClient } = require('./adbclient');
|
||||||
|
const { PackageInfo } = require('./package-searcher');
|
||||||
|
//const { JavaType } = require('./util');
|
||||||
|
const { splitSourcePath } = require('./utils/source-file');
|
||||||
|
|
||||||
|
class BuildInfo {
|
||||||
|
/**
|
||||||
|
* @param {Map<string,PackageInfo>} packages
|
||||||
|
*/
|
||||||
|
constructor(packages) {
|
||||||
|
this.packages = packages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LaunchBuildInfo extends BuildInfo {
|
||||||
|
/**
|
||||||
|
* @param {Map<string,PackageInfo>} packages
|
||||||
|
* @param {string} pkgname
|
||||||
|
* @param {string} launchActivity
|
||||||
|
* @param {string[]} amCommandArgs custom arguments passed to `am start`
|
||||||
|
* @param {number} postLaunchPause amount of time (in ms) to wait after launch before we attempt a debugger connection
|
||||||
|
*/
|
||||||
|
constructor(packages, pkgname, launchActivity, amCommandArgs, postLaunchPause) {
|
||||||
|
super(packages);
|
||||||
|
this.pkgname = pkgname;
|
||||||
|
this.launchActivity = launchActivity;
|
||||||
|
/** the arguments passed to `am start` */
|
||||||
|
this.startCommandArgs = amCommandArgs || [
|
||||||
|
'-D', // enable debugging
|
||||||
|
'--activity-brought-to-front',
|
||||||
|
'-a android.intent.action.MAIN',
|
||||||
|
'-c android.intent.category.LAUNCHER',
|
||||||
|
`-n ${pkgname}/${launchActivity}`,
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* the amount of time (in millis) to wait after 'am start ...' is invoked.
|
||||||
|
* We need this because invoking JDWP too soon causes a hang.
|
||||||
|
*/
|
||||||
|
this.postLaunchPause = ((typeof postLaunchPause === 'number') && (postLaunchPause >= 0)) ? postLaunchPause : 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachBuildInfo extends BuildInfo {
|
||||||
|
/**
|
||||||
|
* @param {Map<string,PackageInfo>} packages
|
||||||
|
*/
|
||||||
|
constructor(packages) {
|
||||||
|
super(packages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 loaded by the runtime
|
||||||
|
* @type {Set<string>}
|
||||||
|
*/
|
||||||
|
this.loadedClasses = 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 {JavaClassType} */
|
||||||
|
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 = {
|
||||||
|
AttachBuildInfo,
|
||||||
|
BreakpointLocation,
|
||||||
|
BreakpointOptions,
|
||||||
|
DebuggerBreakpoint,
|
||||||
|
DebuggerException,
|
||||||
|
DebuggerFrameInfo,
|
||||||
|
DebuggerMethodInfo,
|
||||||
|
DebuggerTypeInfo,
|
||||||
|
DebugSession,
|
||||||
|
DebuggerValue,
|
||||||
|
LaunchBuildInfo,
|
||||||
|
LiteralValue,
|
||||||
|
JavaBreakpointEvent,
|
||||||
|
JavaExceptionEvent,
|
||||||
|
JavaTaggedValue,
|
||||||
|
JavaType,
|
||||||
|
JavaArrayType,
|
||||||
|
JavaClassType,
|
||||||
|
JavaPrimitiveType,
|
||||||
|
JavaThreadInfo,
|
||||||
|
MethodInvokeArgs,
|
||||||
|
SourceLocation,
|
||||||
|
TypeNotAvailable,
|
||||||
|
VariableValue,
|
||||||
|
}
|
||||||
3103
src/debugger.js
3103
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,
|
||||||
|
}
|
||||||
1048
src/expression/evaluate.js
Normal file
1048
src/expression/evaluate.js
Normal file
File diff suppressed because it is too large
Load Diff
336
src/expression/parse.js
Normal file
336
src/expression/parse.js
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ParsedExpression} condition
|
||||||
|
*/
|
||||||
|
constructor(condition) {
|
||||||
|
super();
|
||||||
|
this.condition = condition;
|
||||||
|
/** @type {ParsedExpression} */
|
||||||
|
this.ternary_true = null;
|
||||||
|
/** @type {ParsedExpression} */
|
||||||
|
this.ternary_false = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QualifierExpression extends ParsedExpression {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArrayIndexExpression extends QualifierExpression {
|
||||||
|
/**
|
||||||
|
* @param {ParsedExpression} index_expression
|
||||||
|
*/
|
||||||
|
constructor(index_expression) {
|
||||||
|
super();
|
||||||
|
this.indexExpression = index_expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MethodCallExpression extends QualifierExpression {
|
||||||
|
/** @type {ParsedExpression[]} */
|
||||||
|
arguments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemberExpression extends QualifierExpression {
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
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,71 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// some commonly used Java types in debugger-compatible format
|
|
||||||
const JTYPES = {
|
|
||||||
byte: {typename:'byte',signature:'B'},
|
|
||||||
short: {typename:'short',signature:'S'},
|
|
||||||
int: {typename:'int',signature:'I'},
|
|
||||||
long: {typename:'long',signature:'J'},
|
|
||||||
float: {typename:'float',signature:'F'},
|
|
||||||
double: {typename:'double',signature:'D'},
|
|
||||||
char: {typename:'char',signature:'C'},
|
|
||||||
boolean: {typename:'boolean',signature:'Z'},
|
|
||||||
null: {typename:'null',signature:'Lnull;'}, // null has no type really, but we need something for literals
|
|
||||||
String: {typename:'String',signature:'Ljava/lang/String;'},
|
|
||||||
Object: {typename:'Object',signature:'Ljava/lang/Object;'},
|
|
||||||
isArray(t) { return t.signature[0]==='[' },
|
|
||||||
isObject(t) { return t.signature[0]==='L' },
|
|
||||||
isReference(t) { return /^[L[]/.test(t.signature) },
|
|
||||||
isPrimitive(t) { return !JTYPES.isReference(t.signature) },
|
|
||||||
isInteger(t) { return /^[BCIJS]$/.test(t.signature) },
|
|
||||||
isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) },
|
|
||||||
isString(t) { return t.signature === this.String.signature },
|
|
||||||
isChar(t) { return t.signature === this.char.signature },
|
|
||||||
isBoolean(t) { return t.signature === this.boolean.signature },
|
|
||||||
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
|
|
||||||
}
|
|
||||||
|
|
||||||
// the special name given to exception message fields
|
|
||||||
const exmsg_var_name = ':msg';
|
|
||||||
|
|
||||||
function createJavaString(dbgr, s, opts) {
|
|
||||||
const raw = (opts && opts.israw) ? s : s.slice(1,-1).replace(/\\u[0-9a-fA-F]{4}|\\./,decode_char);
|
|
||||||
// return a deferred, which resolves to a local variable named 'literal'
|
|
||||||
return dbgr.createstring(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decode_char(c) {
|
|
||||||
switch(true) {
|
|
||||||
case /^\\[^u]$/.test(c):
|
|
||||||
// backslash escape
|
|
||||||
var x = {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v','0':String.fromCharCode(0)}[c[1]];
|
|
||||||
return x || c[1];
|
|
||||||
case /^\\u[0-9a-fA-F]{4}$/.test(c):
|
|
||||||
// unicode escape
|
|
||||||
return String.fromCharCode(parseInt(c.slice(2),16));
|
|
||||||
case c.length===1 :
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
throw new Error('Invalid character value');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensure_path_end_slash(p) {
|
|
||||||
return p + (/[\\/]$/.test(p) ? '' : path.sep);
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_subpath_of(fpn, subpath) {
|
|
||||||
if (!subpath || !fpn) return false;
|
|
||||||
subpath = ensure_path_end_slash(''+subpath);
|
|
||||||
return fpn.slice(0,subpath.length) === subpath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function variableRefToThreadId(variablesReference) {
|
|
||||||
return (variablesReference / 1e9)|0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Object.assign(exports, {
|
|
||||||
JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString
|
|
||||||
});
|
|
||||||
122
src/index.d.js
Normal file
122
src/index.d.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* @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 {'ignore'|'warn'|'stop'} StaleBuildSetting
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ADBFileTransferParams
|
||||||
|
* @property {string} pathname
|
||||||
|
* @property {Buffer} data
|
||||||
|
* @property {number} mtime
|
||||||
|
* @property {number} perms
|
||||||
|
*
|
||||||
|
*/
|
||||||
1333
src/jdwp.js
1333
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": [
|
||||||
|
|||||||
375
src/logcat.js
375
src/logcat.js
@@ -1,6 +1,4 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
// vscode stuff
|
|
||||||
const { EventEmitter, Uri } = require('vscode');
|
|
||||||
// node and external modules
|
// node and external modules
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
@@ -9,87 +7,136 @@ 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 { checkADBStarted } = require('./utils/android');
|
||||||
const { D } = require('./util');
|
const { selectTargetDevice } = require('./utils/device');
|
||||||
|
const { D } = require('./utils/print');
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Class to setup and store logcat data
|
* WebSocketServer instance
|
||||||
|
* @type {WebSocketServer}
|
||||||
|
*/
|
||||||
|
let Server = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise resolved once the WebSocketServer is listening
|
||||||
|
* @type {Promise}
|
||||||
|
*/
|
||||||
|
let wss_inited;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hashmap of all LogcatContent instances, keyed on device id
|
||||||
|
* @type {Map<string, LogcatContent>}
|
||||||
|
*/
|
||||||
|
const LogcatInstances = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to manage logcat data transferred between device and a WebView.
|
||||||
|
*
|
||||||
|
* Each LogcatContent instance receives logcat lines via ADB, formats them into
|
||||||
|
* HTML and sends them to a WebSocketClient running within a WebView page.
|
||||||
|
*
|
||||||
|
* The order goes:
|
||||||
|
* - a new LogcatContent instance is created
|
||||||
|
* - if this is the first instance, create the WebSocketServer
|
||||||
|
* - set up handlers to receive logcat messages from ADB
|
||||||
|
* - upon the first get content(), return the templated HTML page - this is designed to bootstrap the view and create a WebSocket client.
|
||||||
|
* - when the client connects, start sending logcat messages over the websocket
|
||||||
*/
|
*/
|
||||||
class LogcatContent {
|
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._htmltemplate = '';
|
|
||||||
this._adbclient = new ADBClient(uri.query);
|
|
||||||
this._initwait = new Promise((resolve, reject) => {
|
|
||||||
this._state = 'connecting';
|
this._state = 'connecting';
|
||||||
LogcatContent.initWebSocketServer()
|
this._htmltemplate = '';
|
||||||
.then(() => {
|
this._adbclient = new ADBClient(deviceid);
|
||||||
return this._adbclient.logcat({
|
this._initwait = this.initialise();
|
||||||
|
LogcatInstances.set(this._logcatid, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the websocket server is initialised and sets up
|
||||||
|
* logcat handlers for ADB.
|
||||||
|
* Once everything is ready, returns the initial HTML bootstrap content
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async initialise() {
|
||||||
|
try {
|
||||||
|
// create the WebSocket server instance
|
||||||
|
await initWebSocketServer();
|
||||||
|
// register handlers for logcat
|
||||||
|
await this._adbclient.startLogcatMonitor({
|
||||||
onlog: this.onLogcatContent.bind(this),
|
onlog: this.onLogcatContent.bind(this),
|
||||||
onclose: this.onLogcatDisconnect.bind(this),
|
onclose: this.onLogcatDisconnect.bind(this),
|
||||||
});
|
});
|
||||||
}).then(x => {
|
|
||||||
this._state = 'connected';
|
this._state = 'connected';
|
||||||
this._initwait = null;
|
this._initwait = null;
|
||||||
resolve(this.content);
|
} catch (err) {
|
||||||
}).fail(e => {
|
return `Logcat initialisation failed. ${err.message}`;
|
||||||
this._state = 'connect_failed';
|
|
||||||
reject(e);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
LogcatContent.byLogcatID[this._logcatid] = this;
|
|
||||||
}
|
}
|
||||||
get content() {
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryReconnect() {
|
||||||
// clear the logs first - if we successfully reconnect, we will be retrieving the entire logcat again
|
// clear the logs first - if we successfully reconnect, we will be retrieving the entire logcat again
|
||||||
this._prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs };
|
const prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs };
|
||||||
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
||||||
this._adbclient.logcat({
|
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(x => {
|
})
|
||||||
// we successfully reconnected
|
// we successfully reconnected
|
||||||
this._state = 'connected';
|
this._state = 'connected';
|
||||||
this._prevlogs = null;
|
|
||||||
this._initwait = null;
|
this._initwait = null;
|
||||||
resolve(this.content);
|
return this.content();
|
||||||
}).fail(e => {
|
} catch(err) {
|
||||||
// reconnection failed - put the logs back and return the cached info
|
// reconnection failed - put the logs back and return the cached info
|
||||||
this._logs = this._prevlogs._logs;
|
this._logs = prevlogs._logs;
|
||||||
this._htmllogs = this._prevlogs._htmllogs;
|
this._htmllogs = prevlogs._htmllogs;
|
||||||
this._oldhtmllogs = this._prevlogs._oldhtmllogs;
|
this._oldhtmllogs = prevlogs._oldhtmllogs;
|
||||||
this._prevlogs = null;
|
|
||||||
this._initwait = null;
|
this._initwait = null;
|
||||||
var cached_content = this.htmlBootstrap({connected:false, status:'Device disconnected',oldlogs: this._oldhtmllogs.join(os.EOL)});
|
const cached_content = this.htmlBootstrap({
|
||||||
resolve(cached_content);
|
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
|
||||||
@@ -99,6 +146,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;
|
||||||
@@ -108,31 +156,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() {
|
||||||
@@ -147,13 +197,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>`);
|
||||||
|
|
||||||
@@ -161,131 +211,144 @@ class LogcatContent {
|
|||||||
this.renotify();
|
this.renotify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLogcatDisconnect(e) {
|
onLogcatDisconnect(/*e*/) {
|
||||||
if (this._state === 'disconnected') return;
|
if (this._state === 'disconnected') return;
|
||||||
this._state = 'disconnected';
|
this._state = 'disconnected';
|
||||||
this.sendDisconnectMsg();
|
this.sendDisconnectMsg();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} port
|
||||||
|
* @param {number} retries
|
||||||
|
* @param {(err,server?) => void} cb
|
||||||
|
*/
|
||||||
|
function tryCreateWebSocketServer(port, retries, cb) {
|
||||||
|
const wsopts = {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
clientTracking: true,
|
||||||
|
};
|
||||||
|
new WebSocketServer(wsopts)
|
||||||
|
.on('listening', function() {
|
||||||
|
cb(null, this);
|
||||||
|
})
|
||||||
|
.on('connection', (client, req) => {
|
||||||
|
onWebSocketClientConnection(client, req);
|
||||||
|
})
|
||||||
|
.on('error', err => {
|
||||||
|
if (retries <= 0) {
|
||||||
|
cb(err);
|
||||||
|
} else {
|
||||||
|
tryCreateWebSocketServer(port + 1, retries - 1, cb);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWebSocketClientConnection(client, req) {
|
||||||
// the client uses the url path to signify which logcat data it wants
|
// the client uses the url path to signify which logcat data it wants
|
||||||
client._logcatid = client.upgradeReq.url.match(/^\/?(.*)$/)[1];
|
client._logcatid = req.url.match(/^\/?(.*)$/)[1];
|
||||||
var lc = LogcatContent.byLogcatID[client._logcatid];
|
const lc = LogcatInstances.get(client._logcatid);
|
||||||
if (lc) lc.onClientConnect(client);
|
if (!lc) {
|
||||||
else client.close();
|
client.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lc.onClientConnect(client);
|
||||||
client.on('message', function(message) {
|
client.on('message', function(message) {
|
||||||
var lc = LogcatContent.byLogcatID[this._logcatid];
|
const lc = LogcatInstances.get(this._logcatid);
|
||||||
if (lc) lc.onClientMessage(this, message);
|
if (lc) {
|
||||||
|
lc.onClientMessage(this, message);
|
||||||
|
}
|
||||||
}.bind(client));
|
}.bind(client));
|
||||||
/*client.on('close', e => {
|
|
||||||
console.log('client close');
|
|
||||||
});*/
|
|
||||||
// try and make sure we don't delay writes
|
// try and make sure we don't delay writes
|
||||||
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
|
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
|
||||||
});
|
|
||||||
this.wss = null;
|
|
||||||
LogcatContent._wssdone.resolveWith(LogcatContent, []);
|
|
||||||
});
|
|
||||||
this.wss.on('error', err => {
|
|
||||||
if (!LogcatContent._wss) {
|
|
||||||
// listen failed -try the next port
|
|
||||||
this.retries++ , this.port++;
|
|
||||||
this.tryCreateWSS();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).tryCreateWSS();
|
|
||||||
return LogcatContent._wssdone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getADBPort() {
|
/**
|
||||||
var defaultPort = 5037;
|
* @param {import('vscode')} vscode
|
||||||
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
* @param {*} target_device
|
||||||
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
*/
|
||||||
return adbPort;
|
function openWebviewLogcatWindow(vscode, target_device) {
|
||||||
return defaultPort;
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'androidlogcat', // Identifies the type of the webview. Used internally
|
||||||
|
`logcat-${target_device.serial}`, // Title of the panel displayed to the user
|
||||||
|
vscode.ViewColumn.Two, // Editor column to show the new webview panel in.
|
||||||
|
{
|
||||||
|
enableScripts: true, // we use embedded scripts to relay logcat info over a websocket
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const logcat = new LogcatContent(target_device.serial);
|
||||||
|
logcat.content().then(html => {
|
||||||
|
panel.webview.html = html;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLogcatWindow(vscode) {
|
/**
|
||||||
new ADBClient().test_adb_connection()
|
* @param {import('vscode')} vscode
|
||||||
.then(err => {
|
* @param {*} target_device
|
||||||
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
*/
|
||||||
var adbport = getADBPort();
|
function openPreviewHtmlLogcatWindow(vscode, target_device) {
|
||||||
var autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
const uri = AndroidContentProvider.getReadLogcatUri(target_device.serial);
|
||||||
if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
|
vscode.commands.executeCommand("vscode.previewHtml", uri, vscode.ViewColumn.Two);
|
||||||
var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
}
|
||||||
var adbargs = ['-P',''+adbport,'start-server'];
|
|
||||||
|
/**
|
||||||
|
* @param {import('vscode')} vscode
|
||||||
|
*/
|
||||||
|
async function openLogcatWindow(vscode) {
|
||||||
try {
|
try {
|
||||||
var stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
// if adb is not running, see if we can start it ourselves
|
||||||
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
||||||
}
|
await checkADBStarted(autoStartADB);
|
||||||
})
|
|
||||||
.then(() => new ADBClient().list_devices())
|
let target_device = await selectTargetDevice(vscode, "Logcat display");
|
||||||
.then(devices => {
|
if (!target_device) {
|
||||||
switch(devices.length) {
|
return;
|
||||||
case 0:
|
|
||||||
vscode.window.showInformationMessage('Logcat cannot be displayed. No Android devices are currently connected');
|
|
||||||
return null;
|
|
||||||
case 1:
|
|
||||||
return devices; // only one device - just show it
|
|
||||||
}
|
|
||||||
var multidevicewait = $.Deferred(), prefix = 'Android: View Logcat - ', all = '[ Display All ]';
|
|
||||||
var devicelist = devices.map(d => prefix + d.serial);
|
|
||||||
//devicelist.push(prefix + all);
|
|
||||||
vscode.window.showQuickPick(devicelist)
|
|
||||||
.then(which => {
|
|
||||||
if (!which) return; // user cancelled
|
|
||||||
which = which.slice(prefix.length);
|
|
||||||
new ADBClient().list_devices()
|
|
||||||
.then(devices => {
|
|
||||||
if (which === all) return multidevicewait.resolveWith(this,[devices]);
|
|
||||||
var found = devices.find(d => d.serial===which);
|
|
||||||
if (found) return multidevicewait.resolveWith(this,[[found]]);
|
|
||||||
vscode.window.showInformationMessage('Logcat cannot be displayed. The device is disconnected');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return multidevicewait;
|
|
||||||
})
|
|
||||||
.then(devices => {
|
|
||||||
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
|
|
||||||
devices.forEach(device => {
|
|
||||||
var uri = AndroidContentProvider.getReadLogcatUri(device.serial);
|
|
||||||
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.fail(e => {
|
|
||||||
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.LogcatContent = LogcatContent;
|
if (vscode.window.createWebviewPanel) {
|
||||||
exports.openLogcatWindow = openLogcatWindow;
|
// newer versions of vscode use WebviewPanels
|
||||||
|
openWebviewLogcatWindow(vscode, target_device);
|
||||||
|
} else {
|
||||||
|
// older versions of vscode use previewHtml
|
||||||
|
openPreviewHtmlLogcatWindow(vscode, target_device);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
vscode.window.showInformationMessage(`Logcat cannot be displayed. ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
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
|
||||||
|
}
|
||||||
77
src/process-attach.js
Normal file
77
src/process-attach.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const os = require('os');
|
||||||
|
const { ADBClient } = require('./adbclient');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('vscode')} vscode
|
||||||
|
* @param {{pid:number,name:string}[]} pids
|
||||||
|
*/
|
||||||
|
async function showPIDPicker(vscode, pids) {
|
||||||
|
// sort by PID (the user can type the package name to search)
|
||||||
|
const sorted_pids = pids.slice().sort((a,b) => a.pid - b.pid);
|
||||||
|
|
||||||
|
/** @type {import('vscode').QuickPickItem[]} */
|
||||||
|
const device_pick_items = sorted_pids
|
||||||
|
.map(x => ({
|
||||||
|
label: `${x.pid}`,
|
||||||
|
description: x.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** @type {import('vscode').QuickPickOptions} */
|
||||||
|
const device_pick_options = {
|
||||||
|
matchOnDescription: true,
|
||||||
|
canPickMany: false,
|
||||||
|
placeHolder: 'Choose the Android process to attach to',
|
||||||
|
};
|
||||||
|
|
||||||
|
const chosen_option = await vscode.window.showQuickPick(device_pick_items, device_pick_options);
|
||||||
|
return sorted_pids[device_pick_items.indexOf(chosen_option)] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('vscode')} vscode
|
||||||
|
* @param {string} device_serial
|
||||||
|
*/
|
||||||
|
async function selectAndroidProcessID(vscode, device_serial) {
|
||||||
|
const res = {
|
||||||
|
/** @type {string|'ok'|'cancelled'|'failed'} */
|
||||||
|
status: 'failed',
|
||||||
|
pid: 0,
|
||||||
|
serial: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
let named_pids;
|
||||||
|
try {
|
||||||
|
named_pids = await new ADBClient(device_serial).named_jdwp_list(5000);
|
||||||
|
} catch {
|
||||||
|
vscode.window.showWarningMessage(`Attach failed. Check the device ${device_serial} is connected.`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
if (named_pids.length === 0) {
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
'Attach failed. No debuggable processes are running on the device.'
|
||||||
|
+ `${os.EOL}${os.EOL}`
|
||||||
|
+ `To allow a debugger to attach, the app must have the "android:debuggable=true" attribute present in AndroidManifest.xml and be running on the device.`
|
||||||
|
+ `${os.EOL}`
|
||||||
|
+ `See https://developer.android.com/guide/topics/manifest/application-element#debug`
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// always show the pid picker - even if there's only one
|
||||||
|
const named_pid = await showPIDPicker(vscode, named_pids);
|
||||||
|
if (named_pid === null) {
|
||||||
|
// user cancelled picker
|
||||||
|
res.status = 'cancelled';
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.pid = named_pid.pid;
|
||||||
|
res.serial = device_serial;
|
||||||
|
res.status = 'ok';
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
selectAndroidProcessID,
|
||||||
|
}
|
||||||
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];
|
|
||||||
}
|
|
||||||
146
src/sockets/adbsocket.js
Normal file
146
src/sockets/adbsocket.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
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
|
||||||
|
* @param {number} timeout_ms
|
||||||
|
* @param {boolean} [until_closed]
|
||||||
|
*/
|
||||||
|
async cmd_and_read_stdout(command, timeout_ms, until_closed) {
|
||||||
|
await this.cmd_and_status(command);
|
||||||
|
const buf = await this.read_stdout(timeout_ms, until_closed);
|
||||||
|
return buf.toString('latin1');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
194
src/sockets/androidsocket.js
Normal file
194
src/sockets/androidsocket.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
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]
|
||||||
|
* @param {number} [timeout_ms]
|
||||||
|
*/
|
||||||
|
async read_bytes(length, format, timeout_ms) {
|
||||||
|
//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(timeout_ms);
|
||||||
|
return this.read_bytes(length, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} [timeout_ms]
|
||||||
|
*/
|
||||||
|
wait_for_socket_data(timeout_ms) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let done = 0, timer = null;
|
||||||
|
let onDataChanged = () => {
|
||||||
|
if ((done += 1) !== 1) return;
|
||||||
|
this.off('socket-ended', onSocketEnded);
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
let onSocketEnded = () => {
|
||||||
|
if ((done += 1) !== 1) return;
|
||||||
|
this.off('data-changed', onDataChanged);
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(new Error(`${this.which} socket read failed. Socket closed.`));
|
||||||
|
}
|
||||||
|
let onTimerExpired = () => {
|
||||||
|
if ((done += 1) !== 1) return;
|
||||||
|
this.off('socket-ended', onSocketEnded);
|
||||||
|
this.off('data-changed', onDataChanged);
|
||||||
|
reject(new Error(`${this.which} socket read failed. Read timeout.`));
|
||||||
|
}
|
||||||
|
this.once('data-changed', onDataChanged);
|
||||||
|
this.once('socket-ended', onSocketEnded);
|
||||||
|
if (typeof timeout_ms === 'number' && timeout_ms >= 0) {
|
||||||
|
timer = setTimeout(onTimerExpired, timeout_ms);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async read_le_length_data(format) {
|
||||||
|
const len = await this.read_bytes(4);
|
||||||
|
return this.read_bytes(len.readUInt32LE(0), format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} [timeout_ms]
|
||||||
|
* @param {boolean} [until_closed]
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
async read_stdout(timeout_ms, until_closed) {
|
||||||
|
let buf = await this.read_bytes(undefined, null, timeout_ms);
|
||||||
|
if (!until_closed) {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
const parts = [buf];
|
||||||
|
try {
|
||||||
|
for (;;) {
|
||||||
|
buf = await this.read_bytes(undefined, null);
|
||||||
|
parts.push(buf);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
return Buffer.concat(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
// @ts-ignore
|
||||||
|
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
|
||||||
|
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, varinfo.display_format));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObjectFields(varinfo) {
|
||||||
|
const supertype = await this.dbgr.getSuperType(varinfo.objvar.type.signature);
|
||||||
|
const fields = await this.dbgr.getFieldValues(varinfo.objvar);
|
||||||
|
// add an extra msg field for exceptions
|
||||||
|
if (varinfo.exception) {
|
||||||
|
const call = await this.dbgr.invokeToString(varinfo.objvar.value, varinfo.threadid, varinfo.objvar.type.signature);
|
||||||
|
call.name = ":message";
|
||||||
|
fields.unshift(call);
|
||||||
|
}
|
||||||
|
// add a ":super" member, unless the super is Object
|
||||||
|
if (supertype && supertype.signature !== JavaType.Object.signature) {
|
||||||
|
fields.unshift(new DebuggerValue('super', supertype, varinfo.objvar.value, true, false, ':super', null));
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getArrayElements(varinfo) {
|
||||||
|
const range = varinfo.range,
|
||||||
|
count = range[1] - range[0];
|
||||||
|
// should always have a +ve count, but just in case...
|
||||||
|
if (count <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// counts over 110 are shown as subranges
|
||||||
|
if (count > 110) {
|
||||||
|
return {
|
||||||
|
isSubrange: true,
|
||||||
|
values: this.getArraySubrange(varinfo.arrvar, count, range),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// get the elements for the specified range
|
||||||
|
const elements = await this.dbgr.getArrayElementValues(varinfo.arrvar, range[0], count);
|
||||||
|
return {
|
||||||
|
isSubrange: false,
|
||||||
|
values: elements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} arrvar
|
||||||
|
* @param {number} count
|
||||||
|
* @param {[number,number]} range
|
||||||
|
*/
|
||||||
|
getArraySubrange(arrvar, count, range) {
|
||||||
|
// create subranges in the sub-power of 10
|
||||||
|
const subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100);
|
||||||
|
/** @type {VariableValue[]} */
|
||||||
|
const variables = [];
|
||||||
|
|
||||||
|
for (let i = range[0]; i < range[1]; i+= subrangelen) {
|
||||||
|
const varinfo = {
|
||||||
|
varref: 0,
|
||||||
|
arrvar,
|
||||||
|
range: [i, Math.min(i+subrangelen, range[1])],
|
||||||
|
};
|
||||||
|
const varref = this._addVariable(varinfo);
|
||||||
|
const variable = new VariableValue(`[${varinfo.range[0]}..${varinfo.range[1]-1}]`, '', null, varref, '');
|
||||||
|
variables.push(variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBigString(varinfo) {
|
||||||
|
const string = await this.dbgr.getStringText(varinfo.bigstring.value);
|
||||||
|
const res = new LiteralValue(JavaType.String, string);
|
||||||
|
res.name = '<value>';
|
||||||
|
res.string = string;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrimitive(varinfo) {
|
||||||
|
/** @type {VariableValue[]} */
|
||||||
|
const variables = [];
|
||||||
|
const bits = {
|
||||||
|
J:64,
|
||||||
|
I:32,
|
||||||
|
S:16,
|
||||||
|
B:8,
|
||||||
|
}[varinfo.signature];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number|hex64} n
|
||||||
|
* @param {number} base
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
function convert(n, base, len) {
|
||||||
|
let converted;
|
||||||
|
if (typeof n === 'string') {
|
||||||
|
converted = {
|
||||||
|
2: () => n.replace(/./g, c => parseInt(c,16).toString(2)),
|
||||||
|
10: () => NumberBaseConverter.hexToDec(n, false),
|
||||||
|
16: () => n,
|
||||||
|
}[base]();
|
||||||
|
} else {
|
||||||
|
converted = n.toString(base);
|
||||||
|
}
|
||||||
|
return converted.padStart(len, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number|hex64} u
|
||||||
|
* @param {8|16|32|64} bits
|
||||||
|
*/
|
||||||
|
function getIntFormats(u, bits) {
|
||||||
|
const bases = [2, 10, 16];
|
||||||
|
const min_lengths = [bits, 1, bits/4];
|
||||||
|
const base_names = ['<binary>', '<decimal>', '<hex>'];
|
||||||
|
return base_names.map((name, i) => new VariableValue(name, convert(u, bases[i], min_lengths[i])));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(varinfo.signature) {
|
||||||
|
case 'Ljava/lang/String;':
|
||||||
|
variables.push(new VariableValue('<length>', varinfo.value.toString()));
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
variables.push(new VariableValue('<charCode>', varinfo.value.charCodeAt(0).toString()));
|
||||||
|
break;
|
||||||
|
case 'J':
|
||||||
|
// because JS cannot handle 64bit ints, we need a bit of extra work
|
||||||
|
const v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,'');
|
||||||
|
variables.push(...getIntFormats(v64hex, 64));
|
||||||
|
break;
|
||||||
|
default:// integer/short/byte value
|
||||||
|
const u = varinfo.value >>> 0;
|
||||||
|
variables.push(...getIntFormats(u, bits));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DebuggerStackFrame,
|
||||||
|
}
|
||||||
11
src/state.js
Normal file
11
src/state.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const vscode = require('vscode');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
var adext = {};
|
||||||
|
try {
|
||||||
|
Object.assign(adext, JSON.parse(fs.readFileSync(path.join(path.dirname(__dirname),'package.json'),'utf8')));
|
||||||
|
} catch (ex) { }
|
||||||
|
|
||||||
|
exports.adext = adext;
|
||||||
254
src/threads.js
254
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) {
|
||||||
}
|
|
||||||
return this.paused.stack_frame_vars[frameId] = stack_frame_var;
|
|
||||||
}
|
|
||||||
|
|
||||||
allocateExceptionScopeReference(frameId) {
|
|
||||||
if (!this.paused) return;
|
|
||||||
if (!this.paused.last_exception) return;
|
|
||||||
this.paused.last_exception.frameId = frameId;
|
|
||||||
this.paused.last_exception.scopeRef = frameId + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVariables(variablesReference) {
|
|
||||||
if (!this.paused)
|
|
||||||
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
|
||||||
|
|
||||||
// is this reference a stack frame
|
|
||||||
var stack_frame_var = this.paused.stack_frame_vars[variablesReference];
|
|
||||||
if (stack_frame_var) {
|
|
||||||
// frame locals request
|
|
||||||
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(varref));
|
|
||||||
}
|
|
||||||
|
|
||||||
// is this refrence an exception scope
|
|
||||||
if (this.paused.last_exception && variablesReference === this.paused.last_exception.scopeRef) {
|
|
||||||
var stack_frame_var = this.paused.stack_frame_vars[this.paused.last_exception.frameId];
|
|
||||||
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(this.paused.last_exception.scopeRef));
|
|
||||||
}
|
|
||||||
|
|
||||||
// work out which stack frame this reference is for
|
|
||||||
var frameId = Math.trunc(variablesReference/1e6) * 1e6;
|
|
||||||
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
|
||||||
|
|
||||||
return stack_frame_var.locals.getVariables(variablesReference);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ensureLocals(varinfo) {
|
|
||||||
if (!this.paused)
|
|
||||||
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
|
||||||
|
|
||||||
// evaluate can call this using frameId as the argument
|
|
||||||
if (typeof varinfo === 'number')
|
|
||||||
return this._ensureLocals(this.paused.stack_frame_vars[varinfo]);
|
|
||||||
|
|
||||||
// if we're currently processing it (or we've finished), just return the promise
|
|
||||||
if (this.paused.locals_done[varinfo.frameId])
|
|
||||||
return this.paused.locals_done[varinfo.frameId];
|
|
||||||
|
|
||||||
// create a new promise
|
|
||||||
var def = this.paused.locals_done[varinfo.frameId] = $.Deferred();
|
|
||||||
|
|
||||||
this.dbgr.getlocals(this.threadid, varinfo.frame, {def:def,varinfo:varinfo})
|
|
||||||
.then((locals,x) => {
|
|
||||||
// make sure we are still paused...
|
|
||||||
if (!this.paused)
|
|
||||||
throw this.threadNotSuspendedError();
|
throw this.threadNotSuspendedError();
|
||||||
|
}
|
||||||
// sort the locals by name, except for 'this' which always goes first
|
const frameId = AndroidThread.makeFrameVariableReference(this.vscode_threadid, call_stack_level) ;
|
||||||
locals.sort((a,b) => {
|
const stack_frame = new DebuggerStackFrame(this.dbgr, frame, frameId);
|
||||||
if (a.name === b.name) return 0;
|
this.paused.stack_frames.set(frameId, stack_frame);
|
||||||
if (a.name === 'this') return -1;
|
return stack_frame;
|
||||||
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]);
|
/**
|
||||||
})
|
* Retrieve the variable manager used to maintain variableReferences for
|
||||||
.fail(e => {
|
* expressions evaluated in the global context for this thread.
|
||||||
x.def.rejectWith(this, [e]);
|
*/
|
||||||
})
|
getGlobalVariableManager() {
|
||||||
return def;
|
if (!this.paused) {
|
||||||
|
throw this.threadNotSuspendedError();
|
||||||
|
}
|
||||||
|
if (!this.paused.global_vars) {
|
||||||
|
const globalFrameId = AndroidThread.makeGlobalVariableReference(this.vscode_threadid) ;
|
||||||
|
this.paused.global_vars = new VariableManager(globalFrameId);
|
||||||
|
}
|
||||||
|
return this.paused.global_vars;
|
||||||
}
|
}
|
||||||
|
|
||||||
setVariableValue(args) {
|
/**
|
||||||
var frameId = Math.trunc(args.variablesReference/1e6) * 1e6;
|
* set a new VSCode thread ID for this thread
|
||||||
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
*/
|
||||||
return this._ensureLocals(stack_frame_var).then(varref => {
|
allocateNewThreadID() {
|
||||||
return this.paused.stack_frame_vars[varref].locals.setVariableValue(args);
|
this.vscode_threadid = (nextVSCodeThreadId += 1);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
clearStepTimeout() {
|
||||||
|
if (this.stepTimeout) {
|
||||||
|
clearTimeout(this.stepTimeout);
|
||||||
|
this.stepTimeout = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.AndroidThread = AndroidThread;
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
631
src/util.js
631
src/util.js
@@ -1,631 +0,0 @@
|
|||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
var nofn=function(){};
|
|
||||||
var D=exports.D=console.log.bind(console);
|
|
||||||
var E=exports.E=console.error.bind(console);
|
|
||||||
var W=exports.W=console.warn.bind(console);
|
|
||||||
var DD=nofn,cl=D,printf=D;
|
|
||||||
var print_jdwp_data = nofn;// _print_jdwp_data;
|
|
||||||
var print_packet = nofn;//_print_packet;
|
|
||||||
|
|
||||||
Array.first = function(arr, fn, defaultvalue) {
|
|
||||||
var idx = Array.indexOfFirst(arr, fn);
|
|
||||||
return idx < 0 ? defaultvalue : arr[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.indexOfFirst = function(arr, fn) {
|
|
||||||
if (!Array.isArray(arr)) return -1;
|
|
||||||
for (var i=0; i < arr.length; i++)
|
|
||||||
if (fn(arr[i], i, arr))
|
|
||||||
return i;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isEmptyObject = exports.isEmptyObject = function(o) {
|
|
||||||
return typeof(o)==='object' && !Object.keys(o).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
var leftpad = exports.leftpad = function(char, len, s) {
|
|
||||||
while (s.length < len)
|
|
||||||
s = char + s;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
var intToHex = exports.intToHex = function(i, minlen) {
|
|
||||||
var s = i.toString(16);
|
|
||||||
if (minlen) s = leftpad('0', minlen, s);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
var intFromHex = exports.intFromHex = function(s, maxlen, defaultvalue) {
|
|
||||||
s = s.slice(0, maxlen);
|
|
||||||
if (!/^[0-9a-fA-F]+$/.test(s)) return defaultvalue;
|
|
||||||
return parseInt(s, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
var fdcache = [];
|
|
||||||
|
|
||||||
var index_of_file_fdn = function(n) {
|
|
||||||
if (n <= 0) return -1;
|
|
||||||
for (var i=0; i < fdcache.length; i++) {
|
|
||||||
if (fdcache[i] && fdcache[i].n === n)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var get_file_fd_from_fdn = function(n) {
|
|
||||||
var idx = index_of_file_fdn(n);
|
|
||||||
if (idx < 0) return null;
|
|
||||||
return fdcache[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
var remove_fd_from_cache = function(fd) {
|
|
||||||
if (!fd) return;
|
|
||||||
var idx = index_of_file_fdn(fd.n);
|
|
||||||
if (idx>=0) fdcache.splice(idx, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add an offset so we don't conflict with tcp socketIds
|
|
||||||
var min_fd_num = 100000;
|
|
||||||
var _new_fd_count = 0;
|
|
||||||
var new_fd = this.new_fd = function(name, raw) {
|
|
||||||
var rwpipe = raw ? new Uint8Array(0) : [];
|
|
||||||
var fd = {
|
|
||||||
name: name,
|
|
||||||
n: min_fd_num + (++_new_fd_count),
|
|
||||||
raw: !!raw,
|
|
||||||
readpipe:rwpipe,
|
|
||||||
writepipe:rwpipe,
|
|
||||||
reader:null,
|
|
||||||
readerlen:0,
|
|
||||||
kickingreader:false,
|
|
||||||
total:{read:0,written:0},
|
|
||||||
duplex: null,
|
|
||||||
closed:'',
|
|
||||||
read:function(cb) {
|
|
||||||
if (this.raw)
|
|
||||||
throw 'Cannot read from raw fd';
|
|
||||||
if (this.reader && this.reader !== cb)
|
|
||||||
throw 'multiple readers?';
|
|
||||||
this.reader = cb;
|
|
||||||
this._kickreader();
|
|
||||||
},
|
|
||||||
write:function(data) {
|
|
||||||
if (this.closed) {
|
|
||||||
D('Ignoring attempt to write to closed file: %o', this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.raw) {
|
|
||||||
D('Ignoring attempt to write object to raw file: %o', this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.writepipe.push(data);
|
|
||||||
if (this.duplex) {
|
|
||||||
this.duplex._kickreader();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
readbytes:function(len, cb) {
|
|
||||||
if (!this.raw)
|
|
||||||
throw 'Cannot readbytes from non-raw fd';
|
|
||||||
if (this.reader)
|
|
||||||
throw 'multiple readers?';
|
|
||||||
this.reader = cb;
|
|
||||||
this.readerlen = len;
|
|
||||||
this._kickreader();
|
|
||||||
},
|
|
||||||
|
|
||||||
writebytes:function(buffer) {
|
|
||||||
if (this.closed) {
|
|
||||||
D('Ignoring attempt to write to closed file: %o', this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.raw) {
|
|
||||||
D('Ignoring attempt to write bytes to non-raw file: %o', this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!buffer || !buffer.byteLength) {
|
|
||||||
// kick the reader when writing 0 bytes
|
|
||||||
this._kickreaders();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.total.written += buffer.byteLength;
|
|
||||||
var newbuf = new Uint8Array(this.writepipe.byteLength + buffer.byteLength);
|
|
||||||
newbuf.set(this.writepipe);
|
|
||||||
newbuf.set(buffer, this.writepipe.byteLength);
|
|
||||||
this.writepipe = newbuf;
|
|
||||||
if (this.duplex)
|
|
||||||
this.duplex.readpipe = newbuf;
|
|
||||||
else
|
|
||||||
this.readpipe = newbuf;
|
|
||||||
D('new buffer size: %d (fd:%d)',this.writepipe.byteLength, this.n);
|
|
||||||
this._kickreaders();
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelread:function(flushfirst) {
|
|
||||||
if (flushfirst)
|
|
||||||
this.flush();
|
|
||||||
this.reader = null;
|
|
||||||
this.readerlen = 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
write_eof:function() {
|
|
||||||
this.flush();
|
|
||||||
// eof is only relevant for read-until-close readers
|
|
||||||
if (this.raw && this.reader && this.readerlen === -1) {
|
|
||||||
this.reader({err:'eof'});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
flush:function() {
|
|
||||||
this._doread();
|
|
||||||
},
|
|
||||||
|
|
||||||
close:function() {
|
|
||||||
if (this.closed)
|
|
||||||
return;
|
|
||||||
console.trace('Closing file %d: %o', this.n, this);
|
|
||||||
this.closed = 'closed';
|
|
||||||
if (this.duplex)
|
|
||||||
this.duplex.close();
|
|
||||||
// last kick to finish off any read-until-close readers
|
|
||||||
this._kickreaders();
|
|
||||||
// remove this entry from the cache
|
|
||||||
remove_fd_from_cache(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
_kickreaders:function() {
|
|
||||||
if (this.duplex)
|
|
||||||
this.duplex._kickreader();
|
|
||||||
else
|
|
||||||
this._kickreader();
|
|
||||||
},
|
|
||||||
|
|
||||||
_kickreader:function() {
|
|
||||||
if (!this.reader) return;
|
|
||||||
if (this.kickingreader) return;
|
|
||||||
var t = this;
|
|
||||||
t.kickingreader = setTimeout(function() {
|
|
||||||
t.kickingreader = false;
|
|
||||||
t._doreadcheckclose();
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
_doreadcheckclose:function() {
|
|
||||||
var cs = this.closed;
|
|
||||||
this._doread();
|
|
||||||
if (cs) {
|
|
||||||
// they've had one last read - no more
|
|
||||||
var rucreader = this.readerlen === -1;
|
|
||||||
var rucreadercb = this.reader;
|
|
||||||
this.reader = null;
|
|
||||||
this.readerlen = 0;
|
|
||||||
if (rucreader && rucreadercb) {
|
|
||||||
// terminate the read-until-close reader
|
|
||||||
D('terminating ruc reader. fd: %o',this);
|
|
||||||
rucreadercb({err:'File closed'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_doread:function() {
|
|
||||||
if (this.raw) {
|
|
||||||
if (!this.reader) return;
|
|
||||||
if (this.readerlen > this.readpipe.byteLength) return;
|
|
||||||
if (this.readerlen && !this.readpipe.byteLength) return;
|
|
||||||
var cb = this.reader, len = this.readerlen;
|
|
||||||
this.reader = null, this.readerlen = 0;
|
|
||||||
var data;
|
|
||||||
if (len) {
|
|
||||||
var readlen = len>0?len:this.readpipe.byteLength;
|
|
||||||
data = this.readpipe.subarray(0, readlen);
|
|
||||||
this.readpipe = this.readpipe.subarray(readlen);
|
|
||||||
if (this.duplex)
|
|
||||||
this.duplex.writepipe = this.readpipe;
|
|
||||||
else
|
|
||||||
this.writepipe = this.readpipe;
|
|
||||||
this.total.read += readlen;
|
|
||||||
} else {
|
|
||||||
data = new Uint8Array(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.asString = function() {
|
|
||||||
return uint8ArrayToString(this);
|
|
||||||
};
|
|
||||||
data.intFromHex = function(len) {
|
|
||||||
len = len||this.byteLength;
|
|
||||||
var x = this.asString().slice(0,len);
|
|
||||||
if (!/^[0-9a-fA-F]+/.test(x)) return -1;
|
|
||||||
return parseInt(x, 16);
|
|
||||||
}
|
|
||||||
cb(null, data);
|
|
||||||
|
|
||||||
if (len < 0) {
|
|
||||||
// reset the reader
|
|
||||||
this.readbytes(len, cb);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.reader && this.readpipe.length) {
|
|
||||||
var cb = this.reader;
|
|
||||||
this.reader = null;
|
|
||||||
cb(this.readpipe.shift());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fdcache.push(fd);
|
|
||||||
return fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
var intToCharString = function(n) {
|
|
||||||
return String.fromCharCode(
|
|
||||||
(n>>0)&255,
|
|
||||||
(n>>8)&255,
|
|
||||||
(n>>16)&255,
|
|
||||||
(n>>24)&255
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var stringToUint8Array = function(s) {
|
|
||||||
var x = new Uint8Array(s.length);
|
|
||||||
for (var i=0; i < s.length; i++)
|
|
||||||
x[i] = s.charCodeAt(i);
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
var uint8ArrayToString = function(a) {
|
|
||||||
var s = new Array(a.byteLength);
|
|
||||||
for (var i=0; i < a.byteLength; i++)
|
|
||||||
s[i] = a[i];
|
|
||||||
return String.fromCharCode.apply(String, s);
|
|
||||||
}
|
|
||||||
|
|
||||||
// asynchronous array iterater
|
|
||||||
var iterate = function(arr, o) {
|
|
||||||
var isrange = typeof(arr)==='number';
|
|
||||||
if (isrange)
|
|
||||||
arr = { length: arr<0?0:arr };
|
|
||||||
var x = {
|
|
||||||
value:arr,
|
|
||||||
isrange:isrange,
|
|
||||||
first:o.first||nofn,
|
|
||||||
each:o.each||(function() { this.next(); }),
|
|
||||||
last:o.last||nofn,
|
|
||||||
success:o.success||nofn,
|
|
||||||
error:o.error||nofn,
|
|
||||||
complete:o.complete||nofn,
|
|
||||||
_idx:0,
|
|
||||||
_donefirst:false,
|
|
||||||
_donelast:false,
|
|
||||||
abort:function(err) {
|
|
||||||
this.error(err);
|
|
||||||
this.complete();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
finish:function(res) {
|
|
||||||
// finish early
|
|
||||||
if (typeof(res)!=='undefined') this.result = res;
|
|
||||||
this.success(res||this.result);
|
|
||||||
this.complete();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
iteratefirst:function() {
|
|
||||||
if (!this.value.length) {
|
|
||||||
this.finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.first(this.value[this._idx],this._idx,this);
|
|
||||||
this.each(this.value[this._idx],this._idx,this);
|
|
||||||
},
|
|
||||||
iteratenext:function() {
|
|
||||||
if (++this._idx >= this.value.length) {
|
|
||||||
this.last(this.value[this._idx],this._idx,this);
|
|
||||||
this.finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.each(this.value[this._idx],this._idx,this);
|
|
||||||
},
|
|
||||||
next:function() {
|
|
||||||
var t = this;
|
|
||||||
setTimeout(function() {
|
|
||||||
t.iteratenext();
|
|
||||||
},0);
|
|
||||||
},
|
|
||||||
nextorabort:function(err) {
|
|
||||||
if (err) this.abort(err);
|
|
||||||
else this.next();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setTimeout(function() { x.iteratefirst(); }, 0);
|
|
||||||
return x;
|
|
||||||
};
|
|
||||||
|
|
||||||
var iterate_repeat = function(arr, count, o, j) {
|
|
||||||
iterate(arr, {
|
|
||||||
each: function(value, i, it) {
|
|
||||||
o.each(value, i, j||0, it);
|
|
||||||
},
|
|
||||||
success: function() {
|
|
||||||
if (!--count) {
|
|
||||||
o.success && o.success();
|
|
||||||
o.complete && o.complete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
iterate_repeat(arr, count, o, (j||0)+1);
|
|
||||||
},
|
|
||||||
error:function(err) {
|
|
||||||
o.error && o.error();
|
|
||||||
o.complete && o.complete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert from an ArrayBuffer to a string.
|
|
||||||
* @param {ArrayBuffer} buffer The array buffer to convert.
|
|
||||||
* @return {string} The textual representation of the array.
|
|
||||||
*/
|
|
||||||
var arrayBufferToString = exports.arrayBufferToString = function(buffer) {
|
|
||||||
var array = new Uint8Array(buffer);
|
|
||||||
var str = '';
|
|
||||||
for (var i = 0; i < array.length; ++i) {
|
|
||||||
str += String.fromCharCode(array[i]);
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert from an UTF-8 array to UTF-8 string.
|
|
||||||
* @param {array} UTF-8 array
|
|
||||||
* @return {string} UTF-8 string
|
|
||||||
*/
|
|
||||||
var ary2utf8 = (function() {
|
|
||||||
|
|
||||||
var patterns = [
|
|
||||||
{pattern: '0xxxxxxx', bytes: 1},
|
|
||||||
{pattern: '110xxxxx', bytes: 2},
|
|
||||||
{pattern: '1110xxxx', bytes: 3},
|
|
||||||
{pattern: '11110xxx', bytes: 4},
|
|
||||||
{pattern: '111110xx', bytes: 5},
|
|
||||||
{pattern: '1111110x', bytes: 6}
|
|
||||||
];
|
|
||||||
patterns.forEach(function(item) {
|
|
||||||
item.header = item.pattern.replace(/[^10]/g, '');
|
|
||||||
item.pattern01 = item.pattern.replace(/[^10]/g, '0');
|
|
||||||
item.pattern01 = parseInt(item.pattern01, 2);
|
|
||||||
item.mask_length = item.header.length;
|
|
||||||
item.data_length = 8 - item.header.length;
|
|
||||||
var mask = '';
|
|
||||||
for (var i = 0, len = item.mask_length; i < len; i++) {
|
|
||||||
mask += '1';
|
|
||||||
}
|
|
||||||
for (var i = 0, len = item.data_length; i < len; i++) {
|
|
||||||
mask += '0';
|
|
||||||
}
|
|
||||||
item.mask = mask;
|
|
||||||
item.mask = parseInt(item.mask, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
return function(ary) {
|
|
||||||
var codes = [];
|
|
||||||
var cur = 0;
|
|
||||||
while(cur < ary.length) {
|
|
||||||
var first = ary[cur];
|
|
||||||
var pattern = null;
|
|
||||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
|
||||||
if ((first & patterns[i].mask) == patterns[i].pattern01) {
|
|
||||||
pattern = patterns[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pattern == null) {
|
|
||||||
throw 'utf-8 decode error';
|
|
||||||
}
|
|
||||||
var rest = ary.slice(cur + 1, cur + pattern.bytes);
|
|
||||||
cur += pattern.bytes;
|
|
||||||
var code = '';
|
|
||||||
code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length);
|
|
||||||
for (var i = 0, len = rest.length; i < len; i++) {
|
|
||||||
code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6);
|
|
||||||
}
|
|
||||||
codes.push(parseInt(code, 2));
|
|
||||||
}
|
|
||||||
return String.fromCharCode.apply(null, codes);
|
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert from an UTF-8 string to UTF-8 array.
|
|
||||||
* @param {string} UTF-8 string
|
|
||||||
* @return {array} UTF-8 array
|
|
||||||
*/
|
|
||||||
var utf82ary = (function() {
|
|
||||||
|
|
||||||
var patterns = [
|
|
||||||
{pattern: '0xxxxxxx', bytes: 1},
|
|
||||||
{pattern: '110xxxxx', bytes: 2},
|
|
||||||
{pattern: '1110xxxx', bytes: 3},
|
|
||||||
{pattern: '11110xxx', bytes: 4},
|
|
||||||
{pattern: '111110xx', bytes: 5},
|
|
||||||
{pattern: '1111110x', bytes: 6}
|
|
||||||
];
|
|
||||||
patterns.forEach(function(item) {
|
|
||||||
item.header = item.pattern.replace(/[^10]/g, '');
|
|
||||||
item.mask_length = item.header.length;
|
|
||||||
item.data_length = 8 - item.header.length;
|
|
||||||
item.max_bit_length = (item.bytes - 1) * 6 + item.data_length;
|
|
||||||
});
|
|
||||||
|
|
||||||
var code2utf8array = function(code) {
|
|
||||||
var pattern = null;
|
|
||||||
var code01 = code.toString(2);
|
|
||||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
|
||||||
if (code01.length <= patterns[i].max_bit_length) {
|
|
||||||
pattern = patterns[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pattern == null) {
|
|
||||||
throw 'utf-8 encode error';
|
|
||||||
}
|
|
||||||
var ary = [];
|
|
||||||
for (var i = 0, len = pattern.bytes - 1; i < len; i++) {
|
|
||||||
ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2));
|
|
||||||
code01 = code01.slice(0, -6);
|
|
||||||
}
|
|
||||||
ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2));
|
|
||||||
return ary;
|
|
||||||
};
|
|
||||||
|
|
||||||
return function(str) {
|
|
||||||
var codes = [];
|
|
||||||
for (var i = 0, len = str.length; i < len; i++) {
|
|
||||||
var code = str.charCodeAt(i);
|
|
||||||
Array.prototype.push.apply(codes, code2utf8array(code));
|
|
||||||
}
|
|
||||||
return codes;
|
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a string to an ArrayBuffer.
|
|
||||||
* @param {string} string The string to convert.
|
|
||||||
* @return {ArrayBuffer} An array buffer whose bytes correspond to the string.
|
|
||||||
*/
|
|
||||||
var stringToArrayBuffer = exports.stringToArrayBuffer = function(string) {
|
|
||||||
var buffer = new ArrayBuffer(string.length);
|
|
||||||
var bufferView = new Uint8Array(buffer);
|
|
||||||
for (var i = 0; i < string.length; i++) {
|
|
||||||
bufferView[i] = string.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return buffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
var str2ab = exports.str2ab = stringToArrayBuffer;
|
|
||||||
var ab2str = exports.ab2str = arrayBufferToString;
|
|
||||||
var str2u8arr = exports.str2u8arr = function(s) {
|
|
||||||
return new Uint8Array(str2ab(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getutf8bytes = function(str) {
|
|
||||||
var utf8 = [];
|
|
||||||
for (var i=0; i < str.length; i++) {
|
|
||||||
var charcode = str.charCodeAt(i);
|
|
||||||
if (charcode < 0x80) utf8.push(charcode);
|
|
||||||
else if (charcode < 0x800) {
|
|
||||||
utf8.push(0xc0 | (charcode >> 6),
|
|
||||||
0x80 | (charcode & 0x3f));
|
|
||||||
}
|
|
||||||
else if (charcode < 0xd800 || charcode >= 0xe000) {
|
|
||||||
utf8.push(0xe0 | (charcode >> 12),
|
|
||||||
0x80 | ((charcode>>6) & 0x3f),
|
|
||||||
0x80 | (charcode & 0x3f));
|
|
||||||
}
|
|
||||||
// surrogate pair
|
|
||||||
else {
|
|
||||||
i++;
|
|
||||||
// UTF-16 encodes 0x10000-0x10FFFF by
|
|
||||||
// subtracting 0x10000 and splitting the
|
|
||||||
// 20 bits of 0x0-0xFFFFF into two halves
|
|
||||||
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
|
|
||||||
| (str.charCodeAt(i) & 0x3ff));
|
|
||||||
utf8.push(0xf0 | (charcode >>18),
|
|
||||||
0x80 | ((charcode>>12) & 0x3f),
|
|
||||||
0x80 | ((charcode>>6) & 0x3f),
|
|
||||||
0x80 | (charcode & 0x3f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return utf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.fromutf8bytes = function(array) {
|
|
||||||
var out, i, len, c;
|
|
||||||
var char2, char3;
|
|
||||||
|
|
||||||
out = "";
|
|
||||||
len = array.length;
|
|
||||||
i = 0;
|
|
||||||
while(i < len) {
|
|
||||||
c = array[i++];
|
|
||||||
switch(c >> 4)
|
|
||||||
{
|
|
||||||
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
|
|
||||||
// 0xxxxxxx
|
|
||||||
out += String.fromCharCode(c);
|
|
||||||
break;
|
|
||||||
case 12: case 13:
|
|
||||||
// 110x xxxx 10xx xxxx
|
|
||||||
char2 = array[i++];
|
|
||||||
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
|
|
||||||
break;
|
|
||||||
case 14:
|
|
||||||
// 1110 xxxx 10xx xxxx 10xx xxxx
|
|
||||||
char2 = array[i++];
|
|
||||||
char3 = array[i++];
|
|
||||||
out += String.fromCharCode(((c & 0x0F) << 12) |
|
|
||||||
((char2 & 0x3F) << 6) |
|
|
||||||
((char3 & 0x3F) << 0));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.arraybuffer_concat = function() {
|
|
||||||
var bufs=[], total=0;
|
|
||||||
for (var i=0; i < arguments.length; i++) {
|
|
||||||
var a = arguments[i];
|
|
||||||
if (!a || !a.byteLength) continue;
|
|
||||||
bufs.push(a);
|
|
||||||
total += a.byteLength;
|
|
||||||
}
|
|
||||||
switch (bufs.length) {
|
|
||||||
case 0: return new Uint8Array(0);
|
|
||||||
case 1: return new Uint8Array(bufs[0]);
|
|
||||||
}
|
|
||||||
var res = new Uint8Array(total);
|
|
||||||
for (var i=0, j=0; i < bufs.length; i++) {
|
|
||||||
res.set(bufs[i], j);
|
|
||||||
j += bufs[i].byteLength;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.remove_from_list = function(arr, item, searchfn) {
|
|
||||||
if (!searchfn) searchfn = function(a,b) { return a===b; };
|
|
||||||
for (var i=0; i < arr.length; i++) {
|
|
||||||
var found = searchfn(arr[i], item);
|
|
||||||
if (found) {
|
|
||||||
return {
|
|
||||||
item: arr.splice(i, 1)[0],
|
|
||||||
index: i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
D('Object %o not removed from list %o', item, arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.dumparr = function(arr, offset, count) {
|
|
||||||
offset=offset||0;
|
|
||||||
count = count||(count===0?0:arr.length);
|
|
||||||
if (count > arr.length-offset)
|
|
||||||
count = arr.length-offset;
|
|
||||||
var s = '';
|
|
||||||
while (count--) {
|
|
||||||
s += ' '+('00'+arr[offset++].toString(16)).slice(-2);
|
|
||||||
}
|
|
||||||
return s.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.btoa = function(arr) {
|
|
||||||
return new Buffer(arr,'binary').toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.atob = function(base64) {
|
|
||||||
return new Buffer(base64, 'base64').toString('binary');
|
|
||||||
}
|
|
||||||
87
src/utils/android.js
Normal file
87
src/utils/android.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const { ADBClient } = require('../adbclient');
|
||||||
|
const ADBSocket = require('../sockets/adbsocket');
|
||||||
|
const { LOG } = require('../utils/print');
|
||||||
|
|
||||||
|
function getAndroidSDKFolder() {
|
||||||
|
// ANDROID_HOME is deprecated
|
||||||
|
return process.env.ANDROID_HOME || process.env.ANDROID_SDK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} api_level
|
||||||
|
* @param {boolean} check_is_dir
|
||||||
|
*/
|
||||||
|
function getAndroidSourcesFolder(api_level, check_is_dir) {
|
||||||
|
const android_sdk = getAndroidSDKFolder();
|
||||||
|
if (!android_sdk) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const sources_path = path.join(android_sdk,'sources',`android-${api_level}`);
|
||||||
|
if (check_is_dir) {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(sources_path);
|
||||||
|
if (!stat || !stat.isDirectory()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getADBPathName() {
|
||||||
|
const android_sdk = getAndroidSDKFolder();
|
||||||
|
if (!android_sdk) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return path.join(android_sdk, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} port
|
||||||
|
*/
|
||||||
|
function startADBServer(port) {
|
||||||
|
if (typeof port !== 'number' || port <= 0 || port >= 65536) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adb_exe_path = getADBPathName();
|
||||||
|
if (!adb_exe_path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const adb_start_server_args = ['-P',`${port}`,'start-server'];
|
||||||
|
try {
|
||||||
|
LOG([adb_exe_path, ...adb_start_server_args].join(' '));
|
||||||
|
const stdout = require('child_process').execFileSync(adb_exe_path, adb_start_server_args, {
|
||||||
|
cwd: getAndroidSDKFolder(),
|
||||||
|
encoding:'utf8',
|
||||||
|
});
|
||||||
|
LOG(stdout);
|
||||||
|
return true;
|
||||||
|
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} auto_start
|
||||||
|
*/
|
||||||
|
async function checkADBStarted(auto_start) {
|
||||||
|
const err = await new ADBClient().test_adb_connection();
|
||||||
|
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
||||||
|
if (err && auto_start) {
|
||||||
|
return startADBServer(ADBSocket.ADBPort);
|
||||||
|
}
|
||||||
|
return !err;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
checkADBStarted,
|
||||||
|
getADBPathName,
|
||||||
|
getAndroidSDKFolder,
|
||||||
|
getAndroidSourcesFolder,
|
||||||
|
startADBServer,
|
||||||
|
}
|
||||||
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,
|
||||||
|
}
|
||||||
62
src/utils/device.js
Normal file
62
src/utils/device.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const { ADBClient } = require('../adbclient');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('vscode')} vscode
|
||||||
|
* @param {{serial:string}[]} devices
|
||||||
|
*/
|
||||||
|
async function showDevicePicker(vscode, devices) {
|
||||||
|
const sorted_devices = devices.slice().sort((a,b) => a.serial.localeCompare(b.serial, undefined, {sensitivity: 'base'}));
|
||||||
|
|
||||||
|
/** @type {import('vscode').QuickPickItem[]} */
|
||||||
|
const quick_pick_items = sorted_devices
|
||||||
|
.map(device => ({
|
||||||
|
label: `${device.serial}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** @type {import('vscode').QuickPickOptions} */
|
||||||
|
const quick_pick_options = {
|
||||||
|
canPickMany: false,
|
||||||
|
placeHolder: 'Choose an Android device',
|
||||||
|
};
|
||||||
|
|
||||||
|
const chosen_option = await vscode.window.showQuickPick(quick_pick_items, quick_pick_options);
|
||||||
|
return sorted_devices[quick_pick_items.indexOf(chosen_option)] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('vscode')} vscode
|
||||||
|
* @param {'Launch'|'Attach'|'Logcat display'} action
|
||||||
|
* @param {{alwaysShow:boolean}} [options]
|
||||||
|
*/
|
||||||
|
async function selectTargetDevice(vscode, action, options) {
|
||||||
|
const devices = await new ADBClient().list_devices();
|
||||||
|
let device;
|
||||||
|
switch(devices.length) {
|
||||||
|
case 0:
|
||||||
|
vscode.window.showWarningMessage(`${action} failed. No Android devices are connected.`);
|
||||||
|
return null;
|
||||||
|
case 1:
|
||||||
|
if (!options || !options.alwaysShow) {
|
||||||
|
return devices[0]; // only one device - just use it
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
device = await showDevicePicker(vscode, devices);
|
||||||
|
if (!device) {
|
||||||
|
return null; // user cancelled
|
||||||
|
}
|
||||||
|
// the user might take a while to choose the device, so once
|
||||||
|
// chosen, recheck it exists
|
||||||
|
const current_devices = await new ADBClient().list_devices();
|
||||||
|
if (!current_devices.find(d => d.serial === device.serial)) {
|
||||||
|
vscode.window.showInformationMessage(`${action} failed. The target device is disconnected.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
selectTargetDevice,
|
||||||
|
showDevicePicker,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
70
src/utils/print.js
Normal file
70
src/utils/print.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let printLogToClient;
|
||||||
|
function initLogToClient(fn) {
|
||||||
|
printLogToClient = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a log message
|
||||||
|
* @param {*} msg
|
||||||
|
*/
|
||||||
|
function LOG(msg) {
|
||||||
|
if (printLogToClient) {
|
||||||
|
printLogToClient(msg);
|
||||||
|
} else {
|
||||||
|
D(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a callback to be called when any message is output
|
||||||
|
* @param {Function} cb
|
||||||
|
*/
|
||||||
|
function onMessagePrint(cb) {
|
||||||
|
messagePrintCallbacks.add(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
D,
|
||||||
|
E,
|
||||||
|
initLogToClient,
|
||||||
|
LOG,
|
||||||
|
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,
|
||||||
|
}
|
||||||
242
src/variable-manager.js
Normal file
242
src/variable-manager.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
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) {
|
||||||
|
|
||||||
|
/** @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve or create a variable reference for a given object instance
|
||||||
|
* @param {JavaType} type
|
||||||
|
* @param {JavaObjectID} instance_id
|
||||||
|
* @param {string} display_format
|
||||||
|
*/
|
||||||
|
_getObjectIdReference(type, instance_id, display_format) {
|
||||||
|
// 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 instance_id)
|
||||||
|
//
|
||||||
|
// display_format is also included to give unique variable references for each display type.
|
||||||
|
// This is because VSCode caches expanded values, so once evaluated in one format, they can
|
||||||
|
// never be changed.
|
||||||
|
const key = `${type.signature}:${instance_id}:${display_format || ''}`;
|
||||||
|
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
|
||||||
|
* @param {string} [display_format]
|
||||||
|
*/
|
||||||
|
makeVariableValue(v, display_format) {
|
||||||
|
let varref = 0;
|
||||||
|
let value = '';
|
||||||
|
const evaluateName = v.fqname || v.name;
|
||||||
|
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:
|
||||||
|
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 {
|
||||||
|
value = formatString(v.string, display_format);
|
||||||
|
}
|
||||||
|
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, display_format);
|
||||||
|
this._setVariable(varref, {
|
||||||
|
varref,
|
||||||
|
arrvar: v,
|
||||||
|
range:[0, v.arraylen],
|
||||||
|
display_format,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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, display_format);
|
||||||
|
this._setVariable(varref, {
|
||||||
|
varref,
|
||||||
|
objvar: v,
|
||||||
|
display_format,
|
||||||
|
});
|
||||||
|
value = v.type.typename;
|
||||||
|
break;
|
||||||
|
case v.type.signature === JavaType.char.signature:
|
||||||
|
// character types have a integer value
|
||||||
|
value = formatChar(v.value, display_format);
|
||||||
|
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 = formatLong(v64hex, display_format);
|
||||||
|
break;
|
||||||
|
case JavaType.isInteger(v.type):
|
||||||
|
value = formatInteger(v.value, v.type.signature, display_format);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// other primitives: boolean, etc
|
||||||
|
value = v.value.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VariableValue(v.name, value, full_typename, varref, evaluateName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmap = {
|
||||||
|
'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t',
|
||||||
|
'\v':'v','\'':'\'','\\':'\\','\0':'0'
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeJavaChar(i) {
|
||||||
|
let value;
|
||||||
|
const char = String.fromCodePoint(i);
|
||||||
|
if (cmap[char]) {
|
||||||
|
value = `'\\${cmap[char]}'`;
|
||||||
|
} else if (i < 32) {
|
||||||
|
value = `'\\u${i.toString(16).padStart(4,'0')}'`;
|
||||||
|
} else value = `'${char}'`;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} c
|
||||||
|
* @param {string} df
|
||||||
|
*/
|
||||||
|
function formatChar(c, df) {
|
||||||
|
if (/[xX]b|o|bb|d/.test(df)) {
|
||||||
|
return formatInteger(c, 'C', df);
|
||||||
|
}
|
||||||
|
return makeJavaChar(c);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} s
|
||||||
|
* @param {string} display_format
|
||||||
|
*/
|
||||||
|
function formatString(s, display_format) {
|
||||||
|
if (display_format === '!') {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
let value = JSON.stringify(s);
|
||||||
|
if (display_format === 'sb') {
|
||||||
|
// remove quotes
|
||||||
|
value = value.slice(1,-1);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {hex64} hex64
|
||||||
|
* @param {string} df
|
||||||
|
*/
|
||||||
|
function formatLong(hex64, df) {
|
||||||
|
let minlength;
|
||||||
|
if (/[xX]b?/.test(df)) {
|
||||||
|
minlength = Math.ceil(64 / 4);
|
||||||
|
let s = `${df[1]?'':'0x'}${hex64.padStart(minlength,'0')}`;
|
||||||
|
return df[0] === 'x' ? s.toLowerCase() : s.toUpperCase();
|
||||||
|
}
|
||||||
|
if (/o/.test(df)) {
|
||||||
|
minlength = Math.ceil(64 / 3);
|
||||||
|
return `${df[1]?'':'0'}${NumberBaseConverter.convertBase(hex64,16,8).padStart(minlength, '0')}`;
|
||||||
|
}
|
||||||
|
if (/bb?/.test(df)) {
|
||||||
|
minlength = 64;
|
||||||
|
return `${df[1]?'':'0b'}${NumberBaseConverter.convertBase(hex64,16,2).padStart(minlength, '0')}`;
|
||||||
|
}
|
||||||
|
if (/c/.test(df)) {
|
||||||
|
return makeJavaChar(parseInt(hex64.slice(-4), 16));
|
||||||
|
}
|
||||||
|
return NumberBaseConverter.convertBase(hex64, 16, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} i
|
||||||
|
* @param {string} signature
|
||||||
|
* @param {string} df
|
||||||
|
*/
|
||||||
|
function formatInteger(i, signature, df) {
|
||||||
|
const bits = { B:8,S:16,I:32,C:16 }[signature];
|
||||||
|
let u = (i & (-1 >>> (32 - bits))) >>> 0;
|
||||||
|
let minlength;
|
||||||
|
if (/[xX]b?/.test(df)) {
|
||||||
|
minlength = Math.ceil(bits / 4);
|
||||||
|
let s = u.toString(16).padStart(minlength,'0');
|
||||||
|
s = df[0] === 'x' ? s.toLowerCase() : s.toUpperCase();
|
||||||
|
return `${df[1]?'':'0x'}${s}`;
|
||||||
|
}
|
||||||
|
if (/o/.test(df)) {
|
||||||
|
minlength = Math.ceil(bits / 3);
|
||||||
|
return `${df[1]?'':'0'}${u.toString(8).padStart(minlength, '0')}`;
|
||||||
|
}
|
||||||
|
if (/bb?/.test(df)) {
|
||||||
|
minlength = bits;
|
||||||
|
return `${df[1]?'':'0b'}${u.toString(2).padStart(minlength, '0')}`;
|
||||||
|
}
|
||||||
|
if (/c/.test(df)) {
|
||||||
|
minlength = bits;
|
||||||
|
return makeJavaChar(u & 0xffff);
|
||||||
|
}
|
||||||
|
return i.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
VariableManager,
|
||||||
|
}
|
||||||
389
src/variables.js
389
src/variables.js
@@ -1,389 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const { JTYPES, exmsg_var_name, createJavaString } = require('./globals');
|
|
||||||
const NumberBaseConverter = require('./nbc');
|
|
||||||
const $ = require('./jq-promise');
|
|
||||||
|
|
||||||
/*
|
|
||||||
Class used to manage stack frame locals and other evaluated expressions
|
|
||||||
*/
|
|
||||||
class AndroidVariables {
|
|
||||||
|
|
||||||
constructor(session, baseId) {
|
|
||||||
this.session = session;
|
|
||||||
this.dbgr = session.dbgr;
|
|
||||||
// the incremental reference id generator for stack frames, locals, etc
|
|
||||||
this.nextId = baseId;
|
|
||||||
// hashmap of variables and frames
|
|
||||||
this.variableHandles = {};
|
|
||||||
// hashmap<objectid, variablesReference>
|
|
||||||
this.objIdCache = {};
|
|
||||||
// allow primitives to be expanded to show more info
|
|
||||||
this._expandable_prims = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
addVariable(varinfo) {
|
|
||||||
var variablesReference = ++this.nextId;
|
|
||||||
this.variableHandles[variablesReference] = varinfo;
|
|
||||||
return variablesReference;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.variableHandles = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
setVariable(variablesReference, varinfo) {
|
|
||||||
this.variableHandles[variablesReference] = varinfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getObjectIdReference(type, objvalue) {
|
|
||||||
// we need the type signature because we must have different id's for
|
|
||||||
// an instance and it's supertype instance (which obviously have the same objvalue)
|
|
||||||
var key = type.signature + objvalue;
|
|
||||||
return this.objIdCache[key] || (this.objIdCache[key] = ++this.nextId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getVariables(variablesReference) {
|
|
||||||
var varinfo = this.variableHandles[variablesReference];
|
|
||||||
if (!varinfo) {
|
|
||||||
return $.Deferred().resolve([]);
|
|
||||||
}
|
|
||||||
else if (varinfo.cached) {
|
|
||||||
return $.Deferred().resolve(this._local_to_variable(varinfo.cached));
|
|
||||||
}
|
|
||||||
else if (varinfo.objvar) {
|
|
||||||
// object fields request
|
|
||||||
return this.dbgr.getsupertype(varinfo.objvar, {varinfo})
|
|
||||||
.then((supertype, x) => {
|
|
||||||
x.supertype = supertype;
|
|
||||||
return this.dbgr.getfieldvalues(x.varinfo.objvar, x);
|
|
||||||
})
|
|
||||||
.then((fields, x) => {
|
|
||||||
// add an extra msg field for exceptions
|
|
||||||
if (!x.varinfo.exception) return;
|
|
||||||
x.fields = fields;
|
|
||||||
return this.dbgr.invokeToString(x.varinfo.objvar.value, x.varinfo.threadid, varinfo.objvar.type.signature, x)
|
|
||||||
.then((call,x) => {
|
|
||||||
call.name = exmsg_var_name;
|
|
||||||
x.fields.unshift(call);
|
|
||||||
return $.Deferred().resolveWith(this, [x.fields, x]);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((fields, x) => {
|
|
||||||
// ignore supertypes of Object
|
|
||||||
x.supertype && x.supertype.signature!=='Ljava/lang/Object;' && fields.unshift({
|
|
||||||
vtype:'super',
|
|
||||||
name:':super',
|
|
||||||
hasnullvalue:false,
|
|
||||||
type: x.supertype,
|
|
||||||
value: x.varinfo.objvar.value,
|
|
||||||
valid:true,
|
|
||||||
});
|
|
||||||
x.varinfo.cached = fields;
|
|
||||||
return this._local_to_variable(fields);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (varinfo.arrvar) {
|
|
||||||
// array elements request
|
|
||||||
var range = varinfo.range, count = range[1] - range[0];
|
|
||||||
// should always have a +ve count, but just in case...
|
|
||||||
if (count <= 0) return $.Deferred().resolve([]);
|
|
||||||
// add some hysteresis
|
|
||||||
if (count > 110) {
|
|
||||||
// create subranges in the sub-power of 10
|
|
||||||
var subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100), variables = [];
|
|
||||||
for (var i=range[0],varref,v; i < range[1]; i+= subrangelen) {
|
|
||||||
varref = ++this.nextId;
|
|
||||||
v = this.variableHandles[varref] = { varref:varref, arrvar:varinfo.arrvar, range:[i, Math.min(i+subrangelen, range[1])] };
|
|
||||||
variables.push({name:`[${v.range[0]}..${v.range[1]-1}]`,type:'',value:'',variablesReference:varref});
|
|
||||||
}
|
|
||||||
return $.Deferred().resolve(variables);
|
|
||||||
}
|
|
||||||
// get the elements for the specified range
|
|
||||||
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count)
|
|
||||||
.then((elements) => {
|
|
||||||
varinfo.cached = elements;
|
|
||||||
return this._local_to_variable(elements);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (varinfo.bigstring) {
|
|
||||||
return this.dbgr.getstringchars(varinfo.bigstring.value)
|
|
||||||
.then((s) => {
|
|
||||||
return this._local_to_variable([{name:'<value>',hasnullvalue:false,string:s,type:JTYPES.String,valid:true}]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (varinfo.primitive) {
|
|
||||||
// convert the primitive value into alternate formats
|
|
||||||
var variables = [], bits = {J:64,I:32,S:16,B:8}[varinfo.signature];
|
|
||||||
const pad = (u,base,len) => ('0000000000000000000000000000000'+u.toString(base)).slice(-len);
|
|
||||||
switch(varinfo.signature) {
|
|
||||||
case 'Ljava/lang/String;':
|
|
||||||
variables.push({name:'<length>',type:'',value:varinfo.value.toString(),variablesReference:0});
|
|
||||||
break;
|
|
||||||
case 'C':
|
|
||||||
variables.push({name:'<charCode>',type:'',value:varinfo.value.charCodeAt(0).toString(),variablesReference:0});
|
|
||||||
break;
|
|
||||||
case 'J':
|
|
||||||
// because JS cannot handle 64bit ints, we need a bit of extra work
|
|
||||||
var v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,'');
|
|
||||||
const s4 = { hi:parseInt(v64hex.slice(0,8),16), lo:parseInt(v64hex.slice(-8),16) };
|
|
||||||
variables.push(
|
|
||||||
{name:'<binary>',type:'',value:pad(s4.hi,2,32)+pad(s4.lo,2,32),variablesReference:0}
|
|
||||||
,{name:'<decimal>',type:'',value:NumberBaseConverter.hexToDec(v64hex,false),variablesReference:0}
|
|
||||||
,{name:'<hex>',type:'',value:pad(s4.hi,16,8)+pad(s4.lo,16,8),variablesReference:0}
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:// integer/short/byte value
|
|
||||||
const u = varinfo.value >>> 0;
|
|
||||||
variables.push(
|
|
||||||
{name:'<binary>',type:'',value:pad(u,2,bits),variablesReference:0}
|
|
||||||
,{name:'<decimal>',type:'',value:u.toString(10),variablesReference:0}
|
|
||||||
,{name:'<hex>',type:'',value:pad(u,16,bits/4),variablesReference:0}
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return $.Deferred().resolve(variables);
|
|
||||||
}
|
|
||||||
else if (varinfo.frame) {
|
|
||||||
// frame locals request - this should be handled by AndroidDebugThread instance
|
|
||||||
return $.Deferred().resolve([]);
|
|
||||||
} else {
|
|
||||||
// something else?
|
|
||||||
return $.Deferred().resolve([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts locals (or other vars) in debugger format into Variable objects used by VSCode
|
|
||||||
*/
|
|
||||||
_local_to_variable(v) {
|
|
||||||
if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._local_to_variable(v));
|
|
||||||
var varref = 0, objvalue, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
|
|
||||||
switch(true) {
|
|
||||||
case v.hasnullvalue && JTYPES.isReference(v.type):
|
|
||||||
// null object or array type
|
|
||||||
objvalue = 'null';
|
|
||||||
break;
|
|
||||||
case v.type.signature === JTYPES.Object.signature:
|
|
||||||
// Object doesn't really have anything worth seeing, so just treat it as unexpandable
|
|
||||||
objvalue = v.type.typename;
|
|
||||||
break;
|
|
||||||
case v.type.signature === JTYPES.String.signature:
|
|
||||||
objvalue = JSON.stringify(v.string);
|
|
||||||
if (v.biglen) {
|
|
||||||
// since this is a big string - make it viewable on expand
|
|
||||||
varref = ++this.nextId;
|
|
||||||
this.variableHandles[varref] = {varref:varref, bigstring:v};
|
|
||||||
objvalue = `String (length:${v.biglen})`;
|
|
||||||
}
|
|
||||||
else if (this._expandable_prims) {
|
|
||||||
// as a courtesy, allow strings to be expanded to see their length
|
|
||||||
varref = ++this.nextId;
|
|
||||||
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.string.length};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case JTYPES.isArray(v.type):
|
|
||||||
// non-null array type - if it's not zero-length add another variable reference so the user can expand
|
|
||||||
if (v.arraylen) {
|
|
||||||
varref = this._getObjectIdReference(v.type, v.value);
|
|
||||||
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
|
|
||||||
}
|
|
||||||
objvalue = v.type.typename.replace(/]$/, v.arraylen+']'); // insert len as the final array bound
|
|
||||||
break;
|
|
||||||
case JTYPES.isObject(v.type):
|
|
||||||
// non-null object instance - add another variable reference so the user can expand
|
|
||||||
varref = this._getObjectIdReference(v.type, v.value);
|
|
||||||
this.variableHandles[varref] = {varref:varref, objvar:v};
|
|
||||||
objvalue = v.type.typename;
|
|
||||||
break;
|
|
||||||
case v.type.signature === 'C':
|
|
||||||
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'};
|
|
||||||
if (cmap[v.char]) {
|
|
||||||
objvalue = `'\\${cmap[v.char]}'`;
|
|
||||||
} else if (v.value < 32) {
|
|
||||||
objvalue = v.value ? `'\\u${('000'+v.value.toString(16)).slice(-4)}'` : "'\\0'";
|
|
||||||
} else objvalue = `'${v.char}'`;
|
|
||||||
break;
|
|
||||||
case v.type.signature === 'J':
|
|
||||||
// because JS cannot handle 64bit ints, we need a bit of extra work
|
|
||||||
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
|
|
||||||
objvalue = NumberBaseConverter.hexToDec(v64hex, true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// other primitives: int, boolean, etc
|
|
||||||
objvalue = v.value.toString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
|
|
||||||
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
|
|
||||||
varref = ++this.nextId;
|
|
||||||
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.value};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: v.name,
|
|
||||||
type: typename,
|
|
||||||
value: objvalue,
|
|
||||||
variablesReference: varref,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setVariableValue(args) {
|
|
||||||
const failSetVariableRequest = reason => $.Deferred().reject(new Error(reason));
|
|
||||||
|
|
||||||
var v = this.variableHandles[args.variablesReference];
|
|
||||||
if (!v || !v.cached) {
|
|
||||||
return failSetVariableRequest(`Variable '${args.name}' not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
var destvar = v.cached.find(v => v.name===args.name);
|
|
||||||
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
|
|
||||||
return failSetVariableRequest(`The value is read-only and cannot be updated.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// be nice and remove any superfluous whitespace
|
|
||||||
var value = args.value.trim();
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
// just ignore blank requests
|
|
||||||
var vsvar = this._local_to_variable(destvar);
|
|
||||||
return $.Deferred().resolve(vsvar);
|
|
||||||
}
|
|
||||||
|
|
||||||
// non-string reference types can only set to null
|
|
||||||
if (/^L/.test(destvar.type.signature) && destvar.type.signature !== JTYPES.String.signature) {
|
|
||||||
if (value !== 'null') {
|
|
||||||
return failSetVariableRequest('Object references can only be set to null');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert the new value into a debugger-compatible object
|
|
||||||
var m, num, data, datadef;
|
|
||||||
switch(true) {
|
|
||||||
case value === 'null':
|
|
||||||
data = {valuetype:'oref',value:null}; // null object reference
|
|
||||||
break;
|
|
||||||
case /^(true|false)$/.test(value):
|
|
||||||
data = {valuetype:'boolean',value:value!=='false'}; // boolean literal
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^[+-]?0x([0-9a-f]+)$/i)):
|
|
||||||
// hex integer- convert to decimal and fall through
|
|
||||||
if (m[1].length < 52/4)
|
|
||||||
value = parseInt(value, 16).toString(10);
|
|
||||||
else
|
|
||||||
value = NumberBaseConverter.hexToDec(value);
|
|
||||||
m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/);
|
|
||||||
// fall-through
|
|
||||||
case !!(m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/)):
|
|
||||||
// decimal integer
|
|
||||||
num = parseFloat(value, 10); // parseInt() can't handle exponents
|
|
||||||
switch(true) {
|
|
||||||
case (num >= -128 && num <= 127): data = {valuetype:'byte',value:num}; break;
|
|
||||||
case (num >= -32768 && num <= 32767): data = {valuetype:'short',value:num}; break;
|
|
||||||
case (num >= -2147483648 && num <= 2147483647): data = {valuetype:'int',value:num}; break;
|
|
||||||
case /inf/i.test(num): return failSetVariableRequest(`Value '${value}' exceeds the maximum number range.`);
|
|
||||||
case /^[FD]$/.test(destvar.type.signature): data = {valuetype:'float',value:num}; break;
|
|
||||||
default:
|
|
||||||
// long (or larger) - need to use the arbitrary precision class
|
|
||||||
data = {valuetype:'long',value:NumberBaseConverter.decToHex(value, 16)};
|
|
||||||
switch(true){
|
|
||||||
case data.value.length > 16:
|
|
||||||
case num > 0 && data.value.length===16 && /[^0-7]/.test(data.value[0]):
|
|
||||||
// number exceeds signed 63 bit - make it a float
|
|
||||||
data = {valuetype:'float',value:num};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^(Float|Double)\s*\.\s*(POSITIVE_INFINITY|NEGATIVE_INFINITY|NaN)$/)):
|
|
||||||
// Java special float constants
|
|
||||||
data = {valuetype:m[1].toLowerCase(),value:{POSITIVE_INFINITY:Infinity,NEGATIVE_INFINITY:-Infinity,NaN:NaN}[m[2]]};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^([+-])?infinity$/i)):// allow js infinity
|
|
||||||
data = {valuetype:'float',value:m[1]!=='-'?Infinity:-Infinity};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^nan$/i)): // allow js nan
|
|
||||||
data = {valuetype:'float',value:NaN};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^[+-]?[0-9]+[eE][-][0-9]+([dDfF])?$/)):
|
|
||||||
case !!(m=value.match(/^[+-]?[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?([dDfF])?$/)):
|
|
||||||
// decimal float
|
|
||||||
num = parseFloat(value);
|
|
||||||
data = {valuetype:/^[dD]$/.test(m[1]) ? 'double': 'float',value:num};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^'(?:\\u([0-9a-fA-F]{4})|\\([bfrntv0'])|(.))'$/)):
|
|
||||||
// character literal
|
|
||||||
var cvalue = m[1] ? String.fromCharCode(parseInt(m[1],16)) :
|
|
||||||
m[2] ? {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v',0:'\0',"'":"'"}[m[2]]
|
|
||||||
: m[3]
|
|
||||||
data = {valuetype:'char',value:cvalue};
|
|
||||||
break;
|
|
||||||
case !!(m=value.match(/^"[^"\\\n]*(\\.[^"\\\n]*)*"$/)):
|
|
||||||
// string literal - we need to get the runtime to create a new string first
|
|
||||||
datadef = createJavaString(this.dbgr, value).then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// invalid literal
|
|
||||||
return failSetVariableRequest(`'${value}' is not a valid Java literal.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!datadef) {
|
|
||||||
// as a nicety, if the destination is a string, stringify any primitive value
|
|
||||||
if (data.valuetype !== 'oref' && destvar.type.signature === JTYPES.String.signature) {
|
|
||||||
datadef = createJavaString(this.dbgr, data.value.toString(), {israw:true})
|
|
||||||
.then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
|
||||||
} else if (destvar.type.signature.length===1) {
|
|
||||||
// if the destination is a primitive, we need to range-check it here
|
|
||||||
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
|
|
||||||
// weirdness if we allow primitives to be set with out-of-range values
|
|
||||||
var validmap = {
|
|
||||||
B:'byte,char', // char may not fit - we special-case this later
|
|
||||||
S:'byte,short,char',
|
|
||||||
I:'byte,short,int,char',
|
|
||||||
J:'byte,short,int,long,char',
|
|
||||||
F:'byte,short,int,long,char,float',
|
|
||||||
D:'byte,short,int,long,char,double,float',
|
|
||||||
C:'byte,short,char',Z:'boolean',
|
|
||||||
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
|
|
||||||
};
|
|
||||||
var is_in_range = (validmap[destvar.type.signature]||'').indexOf(data.valuetype) >= 0;
|
|
||||||
if (destvar.type.signature === 'B' && data.valuetype === 'char')
|
|
||||||
is_in_range = validmap.isCharInRangeForByte(data.value);
|
|
||||||
if (!is_in_range) {
|
|
||||||
return failSetVariableRequest(`'${value}' is not compatible with variable type: ${destvar.type.typename}`);
|
|
||||||
}
|
|
||||||
// check complete - make sure the type matches the destination and use a resolved deferred with the value
|
|
||||||
if (destvar.type.signature!=='C' && data.valuetype === 'char')
|
|
||||||
data.value = data.value.charCodeAt(0); // convert char to it's int value
|
|
||||||
if (destvar.type.signature==='J' && typeof data.value === 'number')
|
|
||||||
data.value = NumberBaseConverter.decToHex(''+data.value,16); // convert ints to hex-string longs
|
|
||||||
data.valuetype = destvar.type.typename;
|
|
||||||
|
|
||||||
datadef = $.Deferred().resolveWith(this,[data]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return datadef.then(data => {
|
|
||||||
// setxxxvalue sets the new value and then returns a new local for the variable
|
|
||||||
switch(destvar.vtype) {
|
|
||||||
case 'field': return this.dbgr.setfieldvalue(destvar, data);
|
|
||||||
case 'local': return this.dbgr.setlocalvalue(destvar, data);
|
|
||||||
case 'arrelem':
|
|
||||||
var idx = parseInt(args.name, 10), count=1;
|
|
||||||
if (idx < 0 || idx >= destvar.data.arrobj.arraylen) throw new Error('Array index out of bounds');
|
|
||||||
return this.dbgr.setarrayvalues(destvar.data.arrobj, idx, count, data);
|
|
||||||
default: throw new Error('Unsupported variable type');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(newlocalvar => {
|
|
||||||
if (destvar.vtype === 'arrelem') newlocalvar = newlocalvar[0];
|
|
||||||
Object.assign(destvar, newlocalvar);
|
|
||||||
var vsvar = this._local_to_variable(destvar);
|
|
||||||
return vsvar;
|
|
||||||
})
|
|
||||||
.fail(e => {
|
|
||||||
return failSetVariableRequest(`Variable update failed. ${e.message||''}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.AndroidVariables = AndroidVariables;
|
|
||||||
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