mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 01:48:18 +00:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6f9b2f7e78 | ||
|
|
eab08cc501 | ||
|
|
0b5be3020a | ||
|
|
6451e38bca | ||
|
|
101611841f | ||
|
|
cc02c1b089 | ||
|
|
fdbd5df16b | ||
|
|
baa3fb6bfd | ||
|
|
3cda2e5f1f | ||
|
|
dfed765d21 | ||
|
|
b60ef65803 | ||
|
|
214a48b1e1 | ||
|
|
d3ff22395e | ||
|
|
ac0b29fb15 | ||
|
|
a47de40088 | ||
|
|
e23fc698a2 | ||
|
|
d324990e28 | ||
|
|
7b670b36ad | ||
|
|
d1ab9a8339 | ||
|
|
ab68b83900 | ||
|
|
a677d636f3 | ||
|
|
71fcf1b760 | ||
|
|
2727dd1b0c | ||
|
|
585b8cca29 | ||
|
|
b27d972613 | ||
|
|
ec38695bcc | ||
|
|
88c20d6470 | ||
|
|
6f3f5eee74 | ||
|
|
6463558034 | ||
|
|
1b7fb3d60a | ||
|
|
2f93ecc16f | ||
|
|
3e1a8dce37 | ||
|
|
6bb8047db6 |
@@ -1,5 +1,6 @@
|
|||||||
.vscode/**
|
.vscode/**
|
||||||
.vscode-test/**
|
.vscode-test/**
|
||||||
|
images/**/*.gif
|
||||||
test/**
|
test/**
|
||||||
.gitignore
|
.gitignore
|
||||||
jsconfig.json
|
jsconfig.json
|
||||||
|
|||||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,5 +1,54 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
### 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
|
||||||
|
* Bug fixes
|
||||||
|
* Fix issue with exception breaks crashing debugger
|
||||||
|
* Fix issue with Android sources not displaying in VSCode 1.9
|
||||||
|
|
||||||
|
## version 0.3.0
|
||||||
|
* Support for Logcat filtering using regular expressions
|
||||||
|
* Improved expression parsing with support for arithmetic, bitwise, logical and relational operators
|
||||||
|
* Multi-threaded debugging support (experimental)
|
||||||
|
* Hit count breakpoints
|
||||||
|
* Android source breakpoints
|
||||||
|
* Automatic adb server start
|
||||||
|
* Bug fixes
|
||||||
|
|
||||||
## version 0.2.0
|
## version 0.2.0
|
||||||
* Support for Logcat viewing [ Command Palette -> Android: View Logcat ]
|
* Support for Logcat viewing [ Command Palette -> Android: View Logcat ]
|
||||||
* Support for modifying local variables, object fields and array elements (literal values only)
|
* Support for modifying local variables, object fields and array elements (literal values only)
|
||||||
|
|||||||
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.
|
||||||
42
README.md
42
README.md
@@ -12,7 +12,7 @@ This is a preview version of the Android for VS Code Extension. The extension al
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
You must have [Android SDK Tools](https://developer.android.com/studio/releases/sdk-tools.html) installed. This extension communicates with your device via the ADB (Android Debug Bridge) interface.
|
You must have [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools.html) installed. This extension communicates with your device via the ADB (Android Debug Bridge) interface.
|
||||||
> You are not required to have Android Studio installed - if you have Android Studio installed, make sure there are no active instances of it when using this extension or you may run into problems with ADB.
|
> You are not required to have Android Studio installed - if you have Android Studio installed, make sure there are no active instances of it when using this extension or you may run into problems with ADB.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
@@ -20,7 +20,7 @@ You must have [Android SDK Tools](https://developer.android.com/studio/releases/
|
|||||||
* 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.
|
||||||
@@ -54,8 +54,44 @@ The following settings are used to configure the debugger:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## 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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Questions / Problems
|
## Questions / Problems
|
||||||
|
|
||||||
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
|
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
19
extension.js
19
extension.js
@@ -1,20 +1,16 @@
|
|||||||
// The module 'vscode' contains the VS Code extensibility API
|
// The module 'vscode' contains the VS Code extensibility API
|
||||||
// Import the module and reference it with the alias vscode in your code below
|
// Import the module and reference it with the alias vscode in your code below
|
||||||
const vscode = require('vscode');
|
const vscode = require('vscode');
|
||||||
const { AndroidContentProvider, openLogcatWindow } = require('./src/logcat');
|
const { AndroidContentProvider } = require('./src/contentprovider');
|
||||||
|
const { openLogcatWindow } = require('./src/logcat');
|
||||||
|
const state = require('./src/state');
|
||||||
|
|
||||||
function getADBPort() {
|
function getADBPort() {
|
||||||
var adbPort = 5037;
|
var defaultPort = 5037;
|
||||||
// there's surely got to be a better way than this...
|
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
||||||
var configs = vscode.workspace.getConfiguration('launch.configurations');
|
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
||||||
for (var i=0,config; config=configs.get(''+i); i++) {
|
|
||||||
if (config.type!=='android') continue;
|
|
||||||
if (config.request!=='launch') continue;
|
|
||||||
if (typeof config.adbPort === 'number' && config.adbPort === (config.adbPort|0))
|
|
||||||
adbPort = config.adbPort;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return adbPort;
|
return adbPort;
|
||||||
|
return defaultPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this method is called when your extension is activated
|
// this method is called when your extension is activated
|
||||||
@@ -43,6 +39,7 @@ function activate(context) {
|
|||||||
var spliceparams = [context.subscriptions.length,0].concat(disposables);
|
var spliceparams = [context.subscriptions.length,0].concat(disposables);
|
||||||
Array.prototype.splice.apply(context.subscriptions,spliceparams);
|
Array.prototype.splice.apply(context.subscriptions,spliceparams);
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.activate = activate;
|
exports.activate = activate;
|
||||||
|
|
||||||
// this method is called when your extension is deactivated
|
// this method is called when your extension is deactivated
|
||||||
|
|||||||
2695
package-lock.json
generated
Normal file
2695
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@@ -2,7 +2,7 @@
|
|||||||
"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.2.0",
|
"version": "0.7.0",
|
||||||
"publisher": "adelphes",
|
"publisher": "adelphes",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -35,12 +35,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": {
|
||||||
@@ -59,13 +62,28 @@
|
|||||||
"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",
|
||||||
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037",
|
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037",
|
||||||
"default": 5037
|
"default": 5037
|
||||||
},
|
},
|
||||||
|
"autoStartADB": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Automatically launch 'adb start-server' if not already started. Default: true",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"callStackDisplaySize": {
|
||||||
|
"type": "integer",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"logcatPort": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038",
|
||||||
|
"default": 7038
|
||||||
|
},
|
||||||
"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\"",
|
||||||
@@ -75,6 +93,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
|
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
|
||||||
"default": ""
|
"default": ""
|
||||||
|
},
|
||||||
|
"trace": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Set to true to output debugging logs for diagnostics",
|
||||||
|
"default": "false"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,10 +105,10 @@
|
|||||||
"initialConfigurations": [
|
"initialConfigurations": [
|
||||||
{
|
{
|
||||||
"type": "android",
|
"type": "android",
|
||||||
"name": "Android Debug",
|
"name": "Android",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
|
"apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk",
|
||||||
"adbPort": 5037
|
"adbPort": 5037
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -98,7 +121,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "${2:Launch App}",
|
"name": "${2:Launch App}",
|
||||||
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
||||||
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/app-debug.apk\"",
|
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk\"",
|
||||||
"adbPort": 5037
|
"adbPort": 5037
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,22 +131,24 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
"prepare": "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",
|
"uuid": "^3.3.2",
|
||||||
"ws":"^1.1.1",
|
"vscode-debugadapter": "^1.32.0",
|
||||||
|
"vscode-debugprotocol": "^1.32.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",
|
"eslint": "^5.9.0",
|
||||||
"eslint": "^3.6.0",
|
"mocha": "^5.2.0",
|
||||||
"@types/node": "^6.0.40",
|
"typescript": "^3.1.6",
|
||||||
"@types/mocha": "^2.2.32"
|
"vscode": "^1.1.26"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,6 +87,29 @@ ADBClient.prototype = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
test_adb_connection : function(o) {
|
||||||
|
var x = {o:o||{},deferred:$.Deferred()};
|
||||||
|
this.proxy_connect()
|
||||||
|
.then(function() {
|
||||||
|
return this.dexcmd('cn');
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
this.fd = data;
|
||||||
|
return this.dexcmd('dc', this.fd);
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return this.proxy_disconnect();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
x.deferred.resolveWith(x.o.ths||this, [null, x.o.extra]);
|
||||||
|
})
|
||||||
|
.fail(function(err) {
|
||||||
|
// if we fail, still resolve the deferred, passing the error
|
||||||
|
x.deferred.resolveWith(x.o.ths||this, [err, x.o.extra]);
|
||||||
|
});
|
||||||
|
return x.deferred;
|
||||||
|
},
|
||||||
|
|
||||||
list_devices : function(o) {
|
list_devices : function(o) {
|
||||||
var x = {o:o||{},deferred:$.Deferred()};
|
var x = {o:o||{},deferred:$.Deferred()};
|
||||||
this.proxy_connect()
|
this.proxy_connect()
|
||||||
@@ -380,9 +403,9 @@ ADBClient.prototype = {
|
|||||||
this.logcatinfo = {
|
this.logcatinfo = {
|
||||||
deferred: x.deferred,
|
deferred: x.deferred,
|
||||||
buffer: '',
|
buffer: '',
|
||||||
onlog: o.onlog||$.noop,
|
onlog: o.onlog||(()=>{}),
|
||||||
onlogdata: o.data,
|
onlogdata: o.data,
|
||||||
onclose: o.onclose||$.noop,
|
onclose: o.onclose||(()=>{}),
|
||||||
fd: this.fd,
|
fd: this.fd,
|
||||||
waitfn:_waitfornextlogcat,
|
waitfn:_waitfornextlogcat,
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/contentprovider.js
Normal file
78
src/contentprovider.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
'use strict'
|
||||||
|
// vscode stuff
|
||||||
|
const { workspace, EventEmitter, Uri } = require('vscode');
|
||||||
|
|
||||||
|
class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._docs = {}; // hashmap<url, LogcatContent>
|
||||||
|
this._onDidChange = new EventEmitter();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this._onDidChange.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event to signal a resource has changed.
|
||||||
|
*/
|
||||||
|
get onDidChange() {
|
||||||
|
return this._onDidChange.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide textual content for a given uri.
|
||||||
|
*
|
||||||
|
* The editor will use the returned string-content to create a readonly
|
||||||
|
* [document](TextDocument). Resources allocated should be released when
|
||||||
|
* the corresponding document has been [closed](#workspace.onDidCloseTextDocument).
|
||||||
|
*
|
||||||
|
* @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
|
||||||
|
* @param token A cancellation token.
|
||||||
|
* @return A string or a thenable that resolves to such.
|
||||||
|
*/
|
||||||
|
provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable<string>;*/ {
|
||||||
|
var doc = this._docs[uri];
|
||||||
|
if (doc) return this._docs[uri].content;
|
||||||
|
switch (uri.authority) {
|
||||||
|
// android-dev-ext://logcat/read?<deviceid>
|
||||||
|
case 'logcat': return this.provideLogcatDocumentContent(uri);
|
||||||
|
}
|
||||||
|
throw new Error('Document Uri not recognised');
|
||||||
|
}
|
||||||
|
|
||||||
|
provideLogcatDocumentContent(uri) {
|
||||||
|
// LogcatContent depends upon AndroidContentProvider, so we must delay-load this
|
||||||
|
const { LogcatContent } = require('./logcat');
|
||||||
|
var doc = this._docs[uri] = new LogcatContent(uri.query);
|
||||||
|
return doc.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the statics
|
||||||
|
AndroidContentProvider.SCHEME = 'android-dev-ext';
|
||||||
|
AndroidContentProvider.register = (ctx, workspace) => {
|
||||||
|
var provider = new AndroidContentProvider();
|
||||||
|
var registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
|
||||||
|
ctx.subscriptions.push(registration);
|
||||||
|
ctx.subscriptions.push(provider);
|
||||||
|
}
|
||||||
|
AndroidContentProvider.getReadLogcatUri = (deviceId) => {
|
||||||
|
var uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`);
|
||||||
|
return uri.with({
|
||||||
|
query: deviceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
AndroidContentProvider.getLaunchConfigSetting = (name, defvalue) => {
|
||||||
|
// there's surely got to be a better way than this...
|
||||||
|
var configs = workspace.getConfiguration('launch.configurations');
|
||||||
|
for (var i=0,config; config=configs.get(''+i); i++) {
|
||||||
|
if (config.type!=='android') continue;
|
||||||
|
if (config.request!=='launch') continue;
|
||||||
|
if (config[name]) return config[name];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return defvalue;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.AndroidContentProvider = AndroidContentProvider;
|
||||||
1387
src/debugMain.js
1387
src/debugMain.js
File diff suppressed because it is too large
Load Diff
387
src/debugger.js
387
src/debugger.js
@@ -137,9 +137,12 @@ Debugger.prototype = {
|
|||||||
adbclient: null,
|
adbclient: null,
|
||||||
stoppedlocation: null,
|
stoppedlocation: null,
|
||||||
classes: {},
|
classes: {},
|
||||||
// classprepare notifier done
|
// classprepare filters
|
||||||
cpndone: false,
|
cpfilters: [],
|
||||||
preparedclasses: [],
|
preparedclasses: [],
|
||||||
|
stepids: {}, // hashmap<threadid,stepid>
|
||||||
|
threadsuspends: [], // hashmap<threadid, suspend-count>
|
||||||
|
invokes: {}, // hashmap<threadid, deferred>
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@@ -414,6 +417,35 @@ Debugger.prototype = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
threadinfos: function(thread_ids, extra) {
|
||||||
|
if (!Array.isArray(thread_ids))
|
||||||
|
thread_ids = [thread_ids];
|
||||||
|
var o = {
|
||||||
|
dbgr: this, thread_ids, extra, threadinfos:[], idx:0,
|
||||||
|
next() {
|
||||||
|
var thread_id = this.thread_ids[this.idx];
|
||||||
|
if (typeof(thread_id) === 'undefined')
|
||||||
|
return $.Deferred().resolveWith(this.dbgr, [this.threadinfos, this.extra]);
|
||||||
|
var info = {
|
||||||
|
threadid: thread_id,
|
||||||
|
name:'',
|
||||||
|
status:null,
|
||||||
|
};
|
||||||
|
return this.dbgr.session.adbclient.jdwp_command({ ths:this.dbgr, extra:info, cmd:this.dbgr.JDWP.Commands.threadname(info.threadid) })
|
||||||
|
.then((name,info) => {
|
||||||
|
info.name = name;
|
||||||
|
return this.dbgr.session.adbclient.jdwp_command({ ths:this.dbgr, extra:info, cmd:this.dbgr.JDWP.Commands.threadstatus(info.threadid) })
|
||||||
|
})
|
||||||
|
.then((status, info) => {
|
||||||
|
info.status = status;
|
||||||
|
this.threadinfos.push(info);
|
||||||
|
})
|
||||||
|
.always(() => (this.idx++,this.next()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return this.ensureconnected(o).then(o => o.next());
|
||||||
|
},
|
||||||
|
|
||||||
suspend: function (extra) {
|
suspend: function (extra) {
|
||||||
return this.ensureconnected(extra)
|
return this.ensureconnected(extra)
|
||||||
.then(function (extra) {
|
.then(function (extra) {
|
||||||
@@ -429,10 +461,23 @@ Debugger.prototype = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
resume: function (extra) {
|
suspendthread: function (threadid, extra) {
|
||||||
|
return this.ensureconnected({threadid,extra})
|
||||||
|
.then(function (x) {
|
||||||
|
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) + 1;
|
||||||
|
return this.session.adbclient.jdwp_command({
|
||||||
|
ths: this,
|
||||||
|
extra: x.extra,
|
||||||
|
cmd: this.JDWP.Commands.suspendthread(x.threadid),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((res,extra) => extra);
|
||||||
|
},
|
||||||
|
|
||||||
|
_resume:function(triggers, extra) {
|
||||||
return this.ensureconnected(extra)
|
return this.ensureconnected(extra)
|
||||||
.then(function (extra) {
|
.then(function (extra) {
|
||||||
this._trigger('resuming');
|
if (triggers) this._trigger('resuming');
|
||||||
this.session.stoppedlocation = null;
|
this.session.stoppedlocation = null;
|
||||||
return this.session.adbclient.jdwp_command({
|
return this.session.adbclient.jdwp_command({
|
||||||
ths: this,
|
ths: this,
|
||||||
@@ -441,37 +486,46 @@ Debugger.prototype = {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(function (decoded, extra) {
|
.then(function (decoded, extra) {
|
||||||
this._trigger('resumed');
|
if (triggers) this._trigger('resumed');
|
||||||
return extra;
|
return extra;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_resumesilent: function () {
|
resume: function (extra) {
|
||||||
return this.ensureconnected()
|
return this._resume(true, extra);
|
||||||
.then(function () {
|
|
||||||
this.session.stoppedlocation = null;
|
|
||||||
return this.session.adbclient.jdwp_command({
|
|
||||||
ths: this,
|
|
||||||
//extra: extra,
|
|
||||||
cmd: this.JDWP.Commands.resume(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
step: function (steptype, threadid) {
|
_resumesilent: function () {
|
||||||
var x = { steptype: steptype, threadid: threadid };
|
return this._resume(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
resumethread: function (threadid, extra) {
|
||||||
|
return this.ensureconnected({threadid,extra})
|
||||||
|
.then(function (x) {
|
||||||
|
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) - 1;
|
||||||
|
return this.session.adbclient.jdwp_command({
|
||||||
|
ths: this,
|
||||||
|
extra: x.extra,
|
||||||
|
cmd: this.JDWP.Commands.resumethread(x.threadid),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((res,extra) => extra);
|
||||||
|
},
|
||||||
|
|
||||||
|
step: function (steptype, threadid, extra) {
|
||||||
|
var x = { steptype, threadid, extra };
|
||||||
return this.ensureconnected(x)
|
return this.ensureconnected(x)
|
||||||
.then(function (x) {
|
.then(function (x) {
|
||||||
this._trigger('stepping');
|
this._trigger('stepping');
|
||||||
return this._setupstepevent(x.steptype, x.threadid);
|
return this._setupstepevent(x.steptype, x.threadid, x);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(x => {
|
||||||
return this._resumesilent();
|
return this.resumethread(x.threadid, x.extra);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_splitsrcfpn: function (srcfpn) {
|
_splitsrcfpn: function (srcfpn) {
|
||||||
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.java$/);
|
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.(java|kt)$/);
|
||||||
return {
|
return {
|
||||||
pkg: m[1].replace(/\/+/g, '.'),
|
pkg: m[1].replace(/\/+/g, '.'),
|
||||||
type: m[2],
|
type: m[2],
|
||||||
@@ -498,11 +552,11 @@ Debugger.prototype = {
|
|||||||
return this.breakpoints.all.slice();
|
return this.breakpoints.all.slice();
|
||||||
},
|
},
|
||||||
|
|
||||||
setbreakpoint: function (srcfpn, line) {
|
setbreakpoint: function (srcfpn, line, conditions) {
|
||||||
var cls = this._splitsrcfpn(srcfpn);
|
var cls = this._splitsrcfpn(srcfpn);
|
||||||
var bid = cls.qtype + ':' + line;
|
var bid = cls.qtype + ':' + line;
|
||||||
var newbp = this.breakpoints.bysrcloc[bid];
|
var newbp = this.breakpoints.bysrcloc[bid];
|
||||||
if (newbp) return newbp;
|
if (newbp) return $.Deferred().resolveWith(this, [newbp]);
|
||||||
newbp = {
|
newbp = {
|
||||||
id: bid,
|
id: bid,
|
||||||
srcfpn: srcfpn,
|
srcfpn: srcfpn,
|
||||||
@@ -510,8 +564,11 @@ Debugger.prototype = {
|
|||||||
pkg: cls.pkg,
|
pkg: cls.pkg,
|
||||||
type: cls.type,
|
type: cls.type,
|
||||||
linenum: line,
|
linenum: line,
|
||||||
|
conditions: Object.assign({},conditions),
|
||||||
sigpattern: new RegExp('^L' + cls.qtype + '([$][$a-zA-Z0-9_]+)?;$'),
|
sigpattern: new RegExp('^L' + cls.qtype + '([$][$a-zA-Z0-9_]+)?;$'),
|
||||||
state: 'set'// set,notloaded,enabled,removed
|
state: 'set', // set,notloaded,enabled,removed
|
||||||
|
hitcount: 0, // number of times this bp was hit during execution
|
||||||
|
stopcount: 0. // number of times this bp caused a break into the debugger
|
||||||
};
|
};
|
||||||
this.breakpoints.all.push(newbp);
|
this.breakpoints.all.push(newbp);
|
||||||
this.breakpoints.bysrcloc[bid] = newbp;
|
this.breakpoints.bysrcloc[bid] = newbp;
|
||||||
@@ -519,25 +576,40 @@ Debugger.prototype = {
|
|||||||
// what happens next depends upon what state we are in
|
// what happens next depends upon what state we are in
|
||||||
switch (this.status()) {
|
switch (this.status()) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
//this._changebpstate([newbp], 'set');
|
|
||||||
//this._changebpstate([newbp], 'notloaded');
|
|
||||||
newbp.state = 'notloaded';
|
newbp.state = 'notloaded';
|
||||||
if (this.session.cpndone) {
|
// try and load the class - if the runtime hasn't loaded it yet, this will just return an empty classes object
|
||||||
var bploc = this._findbplocation(this.session.classes, newbp);
|
return this._loadclzinfo('L'+newbp.qtype+';')
|
||||||
if (bploc) {
|
.then(classes => {
|
||||||
this._setupbreakpointsevent([bploc]);
|
var bploc = this._findbplocation(classes, newbp);
|
||||||
|
if (!bploc) {
|
||||||
|
// the required location may be inside a nested class (anonymous or named)
|
||||||
|
// Since Android doesn't support the NestedTypes JDWP call (ffs), all we can do here
|
||||||
|
// is look for existing (cached) loaded types matching inner type signatures
|
||||||
|
for (var sig in this.session.classes) {
|
||||||
|
if (newbp.sigpattern.test(sig))
|
||||||
|
classes[sig] = this.session.classes[sig];
|
||||||
}
|
}
|
||||||
|
// try again
|
||||||
|
bploc = this._findbplocation(classes, newbp);
|
||||||
}
|
}
|
||||||
break;
|
if (!bploc) {
|
||||||
|
// we couldn't identify a matching location - either the class is not yet loaded or the
|
||||||
|
// location doesn't correspond to any code. In case it's the former, make sure we are notified
|
||||||
|
// when classes in this package are loaded
|
||||||
|
return this._ensureClassPrepareForPackage(newbp.pkg);
|
||||||
|
}
|
||||||
|
// we found a matching location - set the breakpoint event
|
||||||
|
return this._setupbreakpointsevent([bploc]);
|
||||||
|
})
|
||||||
|
.then(() => newbp)
|
||||||
case 'connecting':
|
case 'connecting':
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
default:
|
default:
|
||||||
//this._changebpstate([newbp], 'set');
|
|
||||||
newbp.state = 'set';
|
newbp.state = 'set';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newbp;
|
return $.Deferred().resolveWith(this, [newbp]);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearbreakpoint: function (srcfpn, line) {
|
clearbreakpoint: function (srcfpn, line) {
|
||||||
@@ -709,6 +781,8 @@ Debugger.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getsupertype: function (local, extra) {
|
getsupertype: function (local, extra) {
|
||||||
|
if (local.type.signature==='Ljava/lang/Object;')
|
||||||
|
return $.Deferred().rejectWith(this,[new Error('java.lang.Object has no super type')]);
|
||||||
return this.gettypedebuginfo(local.type.signature, { local: local, extra: extra })
|
return this.gettypedebuginfo(local.type.signature, { local: local, extra: extra })
|
||||||
.then(function (dbgtype, x) {
|
.then(function (dbgtype, x) {
|
||||||
return this._ensuresuper(dbgtype[x.local.type.signature])
|
return this._ensuresuper(dbgtype[x.local.type.signature])
|
||||||
@@ -718,6 +792,15 @@ Debugger.prototype = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getsuperinstance: function (local, extra) {
|
||||||
|
return this.getsupertype(local, {local,extra})
|
||||||
|
.then(function (supertypeinfo, x) {
|
||||||
|
var castobj = Object.assign({}, x.local);
|
||||||
|
castobj.type = supertypeinfo;
|
||||||
|
return $.Deferred().resolveWith(this, [castobj, x.extra]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
createstring: function (string, extra) {
|
createstring: function (string, extra) {
|
||||||
return this.ensureconnected({ string: string, extra: extra })
|
return this.ensureconnected({ string: string, extra: extra })
|
||||||
.then(function (x) {
|
.then(function (x) {
|
||||||
@@ -794,14 +877,38 @@ Debugger.prototype = {
|
|||||||
})
|
})
|
||||||
.then(function (typeinfo, x) {
|
.then(function (typeinfo, x) {
|
||||||
x.typeinfo = typeinfo;
|
x.typeinfo = typeinfo;
|
||||||
|
// the Android runtime now pointlessly barfs into logcat if an instance value is used
|
||||||
|
// to retrieve a static field. So, we now split into two calls...
|
||||||
|
x.splitfields = typeinfo.fields.reduce((z,f) => {
|
||||||
|
if (f.modbits & 8) z.static.push(f); else z.instance.push(f);
|
||||||
|
return z;
|
||||||
|
}, {instance:[],static:[]});
|
||||||
|
// if there are no instance fields, just resolve with an empty array
|
||||||
|
if (!x.splitfields.instance.length)
|
||||||
|
return $.Deferred().resolveWith(this,[[], x]);
|
||||||
return this.session.adbclient.jdwp_command({
|
return this.session.adbclient.jdwp_command({
|
||||||
ths: this,
|
ths: this,
|
||||||
extra: x,
|
extra: x,
|
||||||
cmd: this.JDWP.Commands.GetFieldValues(x.objvar.value, typeinfo.fields),
|
cmd: this.JDWP.Commands.GetFieldValues(x.objvar.value, x.splitfields.instance),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(function (fieldvalues, x) {
|
.then(function (instance_fieldvalues, x) {
|
||||||
return this._mapvalues('field', x.typeinfo.fields, fieldvalues, { objvar: x.objvar }, x);
|
x.instance_fieldvalues = instance_fieldvalues;
|
||||||
|
// and now the statics (with a type reference)
|
||||||
|
if (!x.splitfields.static.length)
|
||||||
|
return $.Deferred().resolveWith(this,[[], x]);
|
||||||
|
return this.session.adbclient.jdwp_command({
|
||||||
|
ths: this,
|
||||||
|
extra: x,
|
||||||
|
cmd: this.JDWP.Commands.GetStaticFieldValues(x.splitfields.static[0].typeid, x.splitfields.static),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function (static_fieldvalues, x) {
|
||||||
|
x.static_fieldvalues = static_fieldvalues;
|
||||||
|
// make sure the fields and values match up...
|
||||||
|
var fields = x.splitfields.instance.concat(x.splitfields.static);
|
||||||
|
var values = x.instance_fieldvalues.concat(x.static_fieldvalues);
|
||||||
|
return this._mapvalues('field', fields, values, { objvar: x.objvar }, x);
|
||||||
})
|
})
|
||||||
.then(function (res, x) {
|
.then(function (res, x) {
|
||||||
for (var i = 0; i < res.length; i++) {
|
for (var i = 0; i < res.length; i++) {
|
||||||
@@ -811,6 +918,27 @@ Debugger.prototype = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFieldValue: function(objvar, fieldname, includeInherited, extra) {
|
||||||
|
const findfield = x => {
|
||||||
|
return this.getfieldvalues(x.objvar, x)
|
||||||
|
.then((fields, x) => {
|
||||||
|
var field = fields.find(f => f.name === x.fieldname);
|
||||||
|
if (field) return $.Deferred().resolveWith(this,[field,x.extra]);
|
||||||
|
if (!x.includeInherited || x.objvar.type.signature==='Ljava/lang/Object;') {
|
||||||
|
var fqtname = [x.reqtype.package,x.reqtype.typename].join('.');
|
||||||
|
return $.Deferred().rejectWith(this,[new Error(`No such field '${x.fieldname}' in type ${fqtname}`), x.extra]);
|
||||||
|
}
|
||||||
|
// search supertype
|
||||||
|
return this.getsuperinstance(x.objvar, x)
|
||||||
|
.then((superobjvar,x) => {
|
||||||
|
x.objvar = superobjvar;
|
||||||
|
return x.findfield(x);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return findfield({findfield, objvar, fieldname, includeInherited, extra, reqtype:objvar.type});
|
||||||
|
},
|
||||||
|
|
||||||
getExceptionLocal: function (ex_ref_value, extra) {
|
getExceptionLocal: function (ex_ref_value, extra) {
|
||||||
var x = {
|
var x = {
|
||||||
ex_ref_value: ex_ref_value,
|
ex_ref_value: ex_ref_value,
|
||||||
@@ -842,9 +970,20 @@ Debugger.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
invokeMethod: function (objectid, threadid, type_signature, method_name, method_sig, args, extra) {
|
invokeMethod: function (objectid, threadid, type_signature, method_name, method_sig, args, extra) {
|
||||||
var x = { objectid, threadid, type_signature, method_name, method_sig, args, extra };
|
var x = {
|
||||||
x.return_type_signature = method_sig.match(/\)(.*)/)[1];
|
objectid, threadid, type_signature, method_name, method_sig, args, extra,
|
||||||
return this.gettypedebuginfo(x.return_type_signature)
|
return_type_signature: method_sig.match(/\)(.*)/)[1],
|
||||||
|
def: $.Deferred()
|
||||||
|
};
|
||||||
|
// we must wait until any previous invokes on the same thread have completed
|
||||||
|
var invokes = this.session.invokes[threadid] = (this.session.invokes[threadid] || []);
|
||||||
|
if (invokes.push(x) === 1)
|
||||||
|
this._doInvokeMethod(x);
|
||||||
|
return x.def;
|
||||||
|
},
|
||||||
|
|
||||||
|
_doInvokeMethod: function (x) {
|
||||||
|
this.gettypedebuginfo(x.return_type_signature)
|
||||||
.then(dbgtypes => {
|
.then(dbgtypes => {
|
||||||
x.return_type = dbgtypes[x.return_type_signature].type;
|
x.return_type = dbgtypes[x.return_type_signature].type;
|
||||||
return this.gettypedebuginfo(x.type_signature);
|
return this.gettypedebuginfo(x.type_signature);
|
||||||
@@ -887,24 +1026,78 @@ Debugger.prototype = {
|
|||||||
return o.def;
|
return o.def;
|
||||||
})
|
})
|
||||||
.then((typeinfo, method, x) => {
|
.then((typeinfo, method, x) => {
|
||||||
|
x.typeinfo = typeinfo;
|
||||||
|
x.method = method;
|
||||||
return this.session.adbclient.jdwp_command({
|
return this.session.adbclient.jdwp_command({
|
||||||
ths: this,
|
ths: this,
|
||||||
extra: x,
|
extra: x,
|
||||||
cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, typeinfo.info.typeid, method.methodid, x.args),
|
cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, x.typeinfo.info.typeid, x.method.methodid, x.args),
|
||||||
});
|
})
|
||||||
})
|
})
|
||||||
.then((res, x) => {
|
.then((res, x) => {
|
||||||
|
// res = {return_value, exception}
|
||||||
if (/^0+$/.test(res.exception))
|
if (/^0+$/.test(res.exception))
|
||||||
return this._mapvalues('return', [{ name:'{return}', type:x.return_type }], [res.return_value], {}, x);
|
return this._mapvalues('return', [{ name:'{return}', type:x.return_type }], [res.return_value], {}, x);
|
||||||
// todo - handle reutrn exceptions
|
// todo - handle reutrn exceptions
|
||||||
})
|
})
|
||||||
.then((res, x) => $.Deferred().resolveWith(this, [res[0], x.extra])); // res = {return_value, exception}
|
.then((res, x) => {
|
||||||
|
x.def.resolveWith(this, [res[0], x.extra]);
|
||||||
|
})
|
||||||
|
.always(function(invokes) {
|
||||||
|
invokes.shift();
|
||||||
|
if (invokes.length)
|
||||||
|
this._doInvokeMethod(invokes[0]);
|
||||||
|
}.bind(this,this.session.invokes[x.threadid]));
|
||||||
},
|
},
|
||||||
|
|
||||||
invokeToString(objectid, threadid, type_signature, extra) {
|
invokeToString(objectid, threadid, type_signature, extra) {
|
||||||
return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra);
|
return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
findNamedMethods(type_signature, name, method_signature) {
|
||||||
|
var x = { type_signature, name, method_signature }
|
||||||
|
const ismatch = function(x, y) {
|
||||||
|
if (!x || (x === y)) return true;
|
||||||
|
return (x instanceof RegExp) && x.test(y);
|
||||||
|
}
|
||||||
|
return this.gettypedebuginfo(x.type_signature)
|
||||||
|
.then(dbgtype => this._ensuremethods(dbgtype[x.type_signature]))
|
||||||
|
.then(typeinfo => ({
|
||||||
|
// resolving the methods only resolves the non-inherited methods
|
||||||
|
// if we can't find a matching method, we need to search the super types
|
||||||
|
dbgr: this,
|
||||||
|
def: $.Deferred(),
|
||||||
|
matches:[],
|
||||||
|
find_methods(typeinfo) {
|
||||||
|
for (var mid in typeinfo.methods) {
|
||||||
|
var m = typeinfo.methods[mid];
|
||||||
|
// does the name match
|
||||||
|
if (!ismatch(x.name, m.name)) continue;
|
||||||
|
// does the signature match
|
||||||
|
if (!ismatch(x.method_signature, m.genericsig || m.sig)) continue;
|
||||||
|
// add it to the results
|
||||||
|
this.matches.push(m);
|
||||||
|
}
|
||||||
|
// search the supertype
|
||||||
|
if (typeinfo.type.signature === 'Ljava/lang/Object;') {
|
||||||
|
this.def.resolveWith(this.dbgr, [this.matches]);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
this.dbgr._ensuresuper(typeinfo)
|
||||||
|
.then(typeinfo => {
|
||||||
|
return this.dbgr.gettypedebuginfo(typeinfo.super.signature, typeinfo.super.signature)
|
||||||
|
})
|
||||||
|
.then((dbgtype, sig) => {
|
||||||
|
return this.dbgr._ensuremethods(dbgtype[sig])
|
||||||
|
})
|
||||||
|
.then(typeinfo => {
|
||||||
|
this.find_methods(typeinfo)
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}).find_methods(typeinfo).def)
|
||||||
|
},
|
||||||
|
|
||||||
getstringchars: function (stringref, extra) {
|
getstringchars: function (stringref, extra) {
|
||||||
return this.session.adbclient.jdwp_command({
|
return this.session.adbclient.jdwp_command({
|
||||||
ths: this,
|
ths: this,
|
||||||
@@ -1003,6 +1196,8 @@ Debugger.prototype = {
|
|||||||
arrayfields.push(info);
|
arrayfields.push(info);
|
||||||
else if (keys[i].type.signature === 'Ljava/lang/String;')
|
else if (keys[i].type.signature === 'Ljava/lang/String;')
|
||||||
stringfields.push(info);
|
stringfields.push(info);
|
||||||
|
else if (keys[i].type.signature === 'C')
|
||||||
|
info.char = info.valid ? String.fromCodePoint(info.value) : '';
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1118,9 +1313,11 @@ Debugger.prototype = {
|
|||||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||||
}
|
}
|
||||||
if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') {
|
if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') {
|
||||||
|
if (typeinfo.info.reftype.string !== 'array') {
|
||||||
typeinfo.super = null;
|
typeinfo.super = null;
|
||||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typeinfo.super = $.Deferred();
|
typeinfo.super = $.Deferred();
|
||||||
this.session.adbclient.jdwp_command({
|
this.session.adbclient.jdwp_command({
|
||||||
@@ -1228,6 +1425,15 @@ Debugger.prototype = {
|
|||||||
cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo),
|
cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo),
|
||||||
})
|
})
|
||||||
.then(function (linetable, methodinfo) {
|
.then(function (linetable, methodinfo) {
|
||||||
|
// if the request failed, just return a blank table
|
||||||
|
if (linetable.errorcode) {
|
||||||
|
linetable = {
|
||||||
|
errorcode: linetable.errorcode,
|
||||||
|
start: '00000000000000000000000000000000',
|
||||||
|
end: '00000000000000000000000000000000',
|
||||||
|
lines:[],
|
||||||
|
}
|
||||||
|
}
|
||||||
// the linetable does not correlate code indexes with line numbers
|
// the linetable does not correlate code indexes with line numbers
|
||||||
// - location searching relies on the table being ordered by code indexes
|
// - location searching relies on the table being ordered by code indexes
|
||||||
linetable.lines.sort(function (a, b) {
|
linetable.lines.sort(function (a, b) {
|
||||||
@@ -1259,16 +1465,26 @@ Debugger.prototype = {
|
|||||||
return cmd.promise();
|
return cmd.promise();
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupstepevent: function (steptype, threadid) {
|
_clearLastStepRequest: function (threadid, extra) {
|
||||||
|
if (!this.session || !this.session.stepids[threadid])
|
||||||
|
return $.Deferred().resolveWith(this,[extra]);
|
||||||
|
|
||||||
|
var clearStepCommand = this.session.adbclient.jdwp_command({
|
||||||
|
cmd: this.JDWP.Commands.ClearStep(this.session.stepids[threadid]),
|
||||||
|
extra: extra,
|
||||||
|
}).then((decoded, extra) => extra);
|
||||||
|
this.session.stepids[threadid] = 0;
|
||||||
|
return clearStepCommand;
|
||||||
|
},
|
||||||
|
|
||||||
|
_setupstepevent: function (steptype, threadid, extra) {
|
||||||
var onevent = {
|
var onevent = {
|
||||||
data: {
|
data: {
|
||||||
dbgr: this,
|
dbgr: this,
|
||||||
},
|
},
|
||||||
fn: function (e) {
|
fn: function (e) {
|
||||||
e.data.dbgr.session.adbclient.jdwp_command({
|
e.data.dbgr._clearLastStepRequest(e.event.threadid, e)
|
||||||
cmd: e.data.dbgr.JDWP.Commands.ClearStep(e.event.reqid),
|
.then(function (e) {
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
var x = e.data;
|
var x = e.data;
|
||||||
var loc = e.event.location;
|
var loc = e.event.location;
|
||||||
|
|
||||||
@@ -1290,6 +1506,12 @@ Debugger.prototype = {
|
|||||||
};
|
};
|
||||||
var cmd = this.session.adbclient.jdwp_command({
|
var cmd = this.session.adbclient.jdwp_command({
|
||||||
cmd: this.JDWP.Commands.SetSingleStep(steptype, threadid, onevent),
|
cmd: this.JDWP.Commands.SetSingleStep(steptype, threadid, onevent),
|
||||||
|
extra: extra,
|
||||||
|
}).then((res,extra) => {
|
||||||
|
// save the step id so we can manually clear it if an exception break occurs
|
||||||
|
if (this.session && res && res.id)
|
||||||
|
this.session.stepids[threadid] = res.id;
|
||||||
|
return extra;
|
||||||
});
|
});
|
||||||
|
|
||||||
return cmd.promise();
|
return cmd.promise();
|
||||||
@@ -1310,13 +1532,25 @@ Debugger.prototype = {
|
|||||||
linenum: bp.linenum,
|
linenum: bp.linenum,
|
||||||
threadid: e.event.threadid
|
threadid: e.event.threadid
|
||||||
};
|
};
|
||||||
|
|
||||||
var eventdata = {
|
var eventdata = {
|
||||||
event: e.event,
|
event: e.event,
|
||||||
stoppedlocation: stoppedloc,
|
stoppedlocation: stoppedloc,
|
||||||
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
|
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
|
||||||
};
|
};
|
||||||
x.dbgr.session.stoppedlocation = stoppedloc;
|
x.dbgr.session.stoppedlocation = stoppedloc;
|
||||||
|
// if this was a conditional breakpoint, it will have been automatically cleared
|
||||||
|
// - set a new (unconditional) breakpoint in it's place
|
||||||
|
if (bp.conditions.hitcount) {
|
||||||
|
bp.hitcount += bp.conditions.hitcount;
|
||||||
|
delete bp.conditions.hitcount;
|
||||||
|
var bploc = x.dbgr.breakpoints.enabled[cmlkey].bploc;
|
||||||
|
x.dbgr.session.adbclient.jdwp_command({
|
||||||
|
cmd: x.dbgr.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, null, onevent),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
bp.hitcount++;
|
||||||
|
}
|
||||||
|
bp.stopcount++;
|
||||||
x.dbgr._trigger('bphit', eventdata);
|
x.dbgr._trigger('bphit', eventdata);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1331,11 +1565,12 @@ Debugger.prototype = {
|
|||||||
cmlkeys.push(cmlkey);
|
cmlkeys.push(cmlkey);
|
||||||
this.breakpoints.enabled[cmlkey] = {
|
this.breakpoints.enabled[cmlkey] = {
|
||||||
bp: bploc.bp,
|
bp: bploc.bp,
|
||||||
|
bploc: {c:bploc.c,m:bploc.m,l:bploc.l},
|
||||||
requestid: null,
|
requestid: null,
|
||||||
};
|
};
|
||||||
bparr.push(bploc.bp);
|
bparr.push(bploc.bp);
|
||||||
var cmd = this.session.adbclient.jdwp_command({
|
var cmd = this.session.adbclient.jdwp_command({
|
||||||
cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, onevent),
|
cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, bploc.bp.conditions.hitcount, onevent),
|
||||||
});
|
});
|
||||||
setbpcmds.push(cmd);
|
setbpcmds.push(cmd);
|
||||||
}
|
}
|
||||||
@@ -1382,29 +1617,28 @@ Debugger.prototype = {
|
|||||||
|
|
||||||
_initbreakpoints: function () {
|
_initbreakpoints: function () {
|
||||||
var deferreds = [{ dbgr: this }];
|
var deferreds = [{ dbgr: this }];
|
||||||
var donetypes = {};
|
|
||||||
// reset any current associations
|
// reset any current associations
|
||||||
this.breakpoints.enabled = {};
|
this.breakpoints.enabled = {};
|
||||||
// set all the breakpoints to the notloaded state
|
// set all the breakpoints to the notloaded state
|
||||||
this._changebpstate(this.breakpoints.all, 'notloaded');
|
this._changebpstate(this.breakpoints.all, 'notloaded');
|
||||||
|
|
||||||
// setup class prepare notifications for all the current packages
|
// setup class prepare notifications for all the packages associated with breakpoints
|
||||||
// when each class is prepared, we initialise any breakpoints for it
|
// when each class is prepared, we initialise any breakpoints for it
|
||||||
for (var pkg in this.session.build.packages) {
|
var cpdefs = this.breakpoints.all.map(bp => this._ensureClassPrepareForPackage(bp.pkg));
|
||||||
try {
|
deferreds = deferreds.concat(cpdefs);
|
||||||
var def = this._setupclassprepareevent(pkg + '.*', _onclassprepared);
|
|
||||||
deferreds.push(def);
|
|
||||||
} catch (e) {
|
|
||||||
D('Ignoring additional class prepared notification for: ' + preppedclass.type.signature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $.when.apply($, deferreds).then(function (x) {
|
return $.when.apply($, deferreds).then(function (x) {
|
||||||
x.dbgr.session.cpndone = true;
|
|
||||||
return $.Deferred().resolveWith(x.dbgr);
|
return $.Deferred().resolveWith(x.dbgr);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
function _onclassprepared(preppedclass) {
|
_ensureClassPrepareForPackage: function(pkg) {
|
||||||
|
var filter = pkg + '.*';
|
||||||
|
if (this.session.cpfilters.includes(filter))
|
||||||
|
return $.Deferred().resolveWith(this,[]); // already setup
|
||||||
|
|
||||||
|
this.session.cpfilters.push(filter);
|
||||||
|
return this._setupclassprepareevent(filter, preppedclass => {
|
||||||
// if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get
|
// if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get
|
||||||
// multiple notifications (which duplicates breakpoints, etc)
|
// multiple notifications (which duplicates breakpoints, etc)
|
||||||
if (this.session.preparedclasses.includes(preppedclass.type.signature)) {
|
if (this.session.preparedclasses.includes(preppedclass.type.signature)) {
|
||||||
@@ -1428,6 +1662,7 @@ Debugger.prototype = {
|
|||||||
bplocs.push(bploc);
|
bplocs.push(bploc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!bplocs.length) return;
|
||||||
// set all the breakpoints in one go...
|
// set all the breakpoints in one go...
|
||||||
return this._setupbreakpointsevent(bplocs);
|
return this._setupbreakpointsevent(bplocs);
|
||||||
})
|
})
|
||||||
@@ -1435,7 +1670,7 @@ Debugger.prototype = {
|
|||||||
// when all the breakpoints for the newly-prepared type have been set...
|
// when all the breakpoints for the newly-prepared type have been set...
|
||||||
this._resumesilent();
|
this._resumesilent();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
clearBreakOnExceptions: function(extra) {
|
clearBreakOnExceptions: function(extra) {
|
||||||
@@ -1465,6 +1700,9 @@ Debugger.prototype = {
|
|||||||
dbgr: this,
|
dbgr: this,
|
||||||
},
|
},
|
||||||
fn: function (e) {
|
fn: function (e) {
|
||||||
|
// if this exception break occurred during a step request, we must manually clear the event
|
||||||
|
// or the (device-side) debugger will crash on next step
|
||||||
|
this._clearLastStepRequest(e.event.threadid, e).then(e => {
|
||||||
this._findcmllocation(this.session.classes, e.event.throwlocation)
|
this._findcmllocation(this.session.classes, e.event.throwlocation)
|
||||||
.then(tloc => {
|
.then(tloc => {
|
||||||
this._findcmllocation(this.session.classes, e.event.catchlocation)
|
this._findcmllocation(this.session.classes, e.event.catchlocation)
|
||||||
@@ -1478,6 +1716,7 @@ Debugger.prototype = {
|
|||||||
this._trigger('exception', eventdata);
|
this._trigger('exception', eventdata);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
});
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1528,6 +1767,30 @@ Debugger.prototype = {
|
|||||||
return o.def;
|
return o.def;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setThreadNotify: function(extra) {
|
||||||
|
var onevent = {
|
||||||
|
data: {
|
||||||
|
dbgr: this,
|
||||||
|
},
|
||||||
|
fn: function (e) {
|
||||||
|
// the thread notifiers don't give any location information
|
||||||
|
//this.session.stoppedlocation = ...
|
||||||
|
this._trigger('threadchange', {state:e.event.state, threadid:e.event.threadid});
|
||||||
|
}.bind(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.ensureconnected(extra)
|
||||||
|
.then((extra) => this.session.adbclient.jdwp_command({
|
||||||
|
cmd: this.JDWP.Commands.ThreadStartNotify(onevent),
|
||||||
|
extra:extra,
|
||||||
|
}))
|
||||||
|
.then((res,extra) => this.session.adbclient.jdwp_command({
|
||||||
|
cmd: this.JDWP.Commands.ThreadEndNotify(onevent),
|
||||||
|
extra:extra,
|
||||||
|
}))
|
||||||
|
.then((res, extra) => extra);
|
||||||
|
},
|
||||||
|
|
||||||
_loadclzinfo: function (signature) {
|
_loadclzinfo: function (signature) {
|
||||||
return this.gettypedebuginfo(signature)
|
return this.gettypedebuginfo(signature)
|
||||||
.then(function (classes) {
|
.then(function (classes) {
|
||||||
|
|||||||
535
src/expressions.js
Normal file
535
src/expressions.js
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
'use strict'
|
||||||
|
const Long = require('long');
|
||||||
|
const $ = require('./jq-promise');
|
||||||
|
const { D } = require('./util');
|
||||||
|
const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./globals');
|
||||||
|
|
||||||
|
/*
|
||||||
|
Asynchronously evaluate an expression
|
||||||
|
*/
|
||||||
|
exports.evaluate = function(expression, thread, locals, vars, dbgr) {
|
||||||
|
D('evaluate: ' + expression);
|
||||||
|
|
||||||
|
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
|
||||||
|
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]);
|
||||||
|
|
||||||
|
if (thread && !thread.paused)
|
||||||
|
return reject_evaluation('not available');
|
||||||
|
|
||||||
|
// special case for evaluating exception messages
|
||||||
|
// - this is called if the user tries to evaluate ':msg' from the locals
|
||||||
|
if (expression === exmsg_var_name) {
|
||||||
|
if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) {
|
||||||
|
var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
|
||||||
|
if (msglocal) {
|
||||||
|
return resolve_evaluation(vars._local_to_variable(msglocal).value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject_evaluation('not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parse_array_or_fncall = function (e) {
|
||||||
|
var arg, res = { arr: [], call: null };
|
||||||
|
// pre-call array indexes
|
||||||
|
while (e.expr[0] === '[') {
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
if ((arg = parse_expression(e)) === null) return null;
|
||||||
|
res.arr.push(arg);
|
||||||
|
if (e.expr[0] !== ']') return null;
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
}
|
||||||
|
if (res.arr.length) return res;
|
||||||
|
// method call
|
||||||
|
if (e.expr[0] === '(') {
|
||||||
|
res.call = []; e.expr = e.expr.slice(1).trim();
|
||||||
|
if (e.expr[0] !== ')') {
|
||||||
|
for (; ;) {
|
||||||
|
if ((arg = parse_expression(e)) === null) return null;
|
||||||
|
res.call.push(arg);
|
||||||
|
if (e.expr[0] === ')') break;
|
||||||
|
if (e.expr[0] !== ',') return null;
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
// post-call array indexes
|
||||||
|
while (e.expr[0] === '[') {
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
if ((arg = parse_expression(e)) === null) return null;
|
||||||
|
res.arr.push(arg);
|
||||||
|
if (e.expr[0] !== ']') return null;
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
const parse_expression_term = function (e) {
|
||||||
|
if (e.expr[0] === '(') {
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
var subexpr = { expr: e.expr };
|
||||||
|
var res = parse_expression(subexpr);
|
||||||
|
if (res) {
|
||||||
|
if (subexpr.expr[0] !== ')') return null;
|
||||||
|
e.expr = subexpr.expr.slice(1).trim();
|
||||||
|
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) {
|
||||||
|
// primitive typecast
|
||||||
|
var castexpr = parse_expression_term(e);
|
||||||
|
if (castexpr) castexpr.typecast = res.root_term;
|
||||||
|
res = castexpr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
|
||||||
|
if (unop) {
|
||||||
|
var op = unop[0].replace(/\s/g, '');
|
||||||
|
e.expr = e.expr.slice(unop[0].length).trim();
|
||||||
|
var res = parse_expression_term(e);
|
||||||
|
if (res) {
|
||||||
|
for (var i = op.length - 1; i >= 0; --i)
|
||||||
|
res = { operator: op[i], rhs: res };
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
||||||
|
if (!root_term) return null;
|
||||||
|
var res = {
|
||||||
|
root_term: root_term[0],
|
||||||
|
root_term_type: ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'][[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1],
|
||||||
|
array_or_fncall: null,
|
||||||
|
members: [],
|
||||||
|
typecast: ''
|
||||||
|
}
|
||||||
|
e.expr = e.expr.slice(res.root_term.length).trim();
|
||||||
|
if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
||||||
|
// the root term is not allowed to be a method call
|
||||||
|
if (res.array_or_fncall.call) return null;
|
||||||
|
while (e.expr[0] === '.') {
|
||||||
|
// member expression
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
|
||||||
|
if (!member_name) return null;
|
||||||
|
res.members.push(m = { member: member_name[0], array_or_fncall: null })
|
||||||
|
e.expr = e.expr.slice(m.member.length).trim();
|
||||||
|
if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
const prec = {
|
||||||
|
'*': 1, '%': 1, '/': 1,
|
||||||
|
'+': 2, '-': 2,
|
||||||
|
'<<': 3, '>>': 3, '>>>': 3,
|
||||||
|
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
|
||||||
|
'==': 5, '!=': 5,
|
||||||
|
'&': 6, '^': 7, '|': 8, '&&': 9, '||': 10, '?': 11,
|
||||||
|
}
|
||||||
|
const parse_expression = function (e) {
|
||||||
|
var res = parse_expression_term(e);
|
||||||
|
|
||||||
|
if (!e.currprec) e.currprec = [12];
|
||||||
|
for (; ;) {
|
||||||
|
var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
|
||||||
|
if (!binary_operator) break;
|
||||||
|
var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0];
|
||||||
|
if (precdiff > 0) {
|
||||||
|
// bigger number -> lower precendence -> end of (sub)expression
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (precdiff === 0 && binary_operator[0] !== '?') {
|
||||||
|
// equal precedence, ltr evaluation
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// higher or equal precendence
|
||||||
|
e.currprec.unshift(e.currprec[0] + precdiff);
|
||||||
|
e.expr = e.expr.slice(binary_operator[0].length).trim();
|
||||||
|
// current or higher precendence
|
||||||
|
if (binary_operator[0] === '?') {
|
||||||
|
res = { condition: res, operator: binary_operator[0], ternary_true: null, ternary_false: null };
|
||||||
|
res.ternary_true = parse_expression(e);
|
||||||
|
if (e.expr[0] === ':') {
|
||||||
|
e.expr = e.expr.slice(1).trim();
|
||||||
|
res.ternary_false = parse_expression(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res = { lhs: res, operator: binary_operator[0], rhs: parse_expression(e) };
|
||||||
|
}
|
||||||
|
e.currprec.shift();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16);
|
||||||
|
const evaluate_number = (n) => {
|
||||||
|
n += '';
|
||||||
|
var numtype, m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base = 10;
|
||||||
|
if (m) {
|
||||||
|
switch (m[2]) {
|
||||||
|
case 'b': base = 2; n = m[1] + m[3]; break;
|
||||||
|
case 'x': base = 16; n = m[1] + m[3]; break;
|
||||||
|
default: base = 8; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (base !== 16 && /[fFdD]$/.test(n)) {
|
||||||
|
numtype = /[fF]$/.test(n) ? 'float' : 'double';
|
||||||
|
n = n.slice(0, -1);
|
||||||
|
} else if (/[lL]$/.test(n)) {
|
||||||
|
numtype = 'long'
|
||||||
|
n = n.slice(0, -1);
|
||||||
|
} else {
|
||||||
|
numtype = /\./.test(n) ? 'double' : 'int';
|
||||||
|
}
|
||||||
|
if (numtype === 'long') n = hex_long(Long.fromString(n, false, base));
|
||||||
|
else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base);
|
||||||
|
else n = parseInt(n, base) | 0;
|
||||||
|
|
||||||
|
const iszero = /^[+-]?0+(\.0*)?$/.test(n);
|
||||||
|
return { vtype: 'literal', name: '', hasnullvalue: iszero, type: JTYPES[numtype], value: n, valid: true };
|
||||||
|
}
|
||||||
|
const evaluate_char = (char) => {
|
||||||
|
return { vtype: 'literal', name: '', char: char, hasnullvalue: false, type: JTYPES.char, value: char.charCodeAt(0), valid: true };
|
||||||
|
}
|
||||||
|
const numberify = (local) => {
|
||||||
|
//if (local.type.signature==='C') return local.char.charCodeAt(0);
|
||||||
|
if (/^[FD]$/.test(local.type.signature))
|
||||||
|
return parseFloat(local.value);
|
||||||
|
if (local.type.signature === 'J')
|
||||||
|
return parseInt(local.value, 16);
|
||||||
|
return parseInt(local.value, 10);
|
||||||
|
}
|
||||||
|
const stringify = (local) => {
|
||||||
|
var s;
|
||||||
|
if (JTYPES.isString(local.type)) s = local.string;
|
||||||
|
else if (JTYPES.isChar(local.type)) s = local.char;
|
||||||
|
else if (JTYPES.isPrimitive(local.type)) s = '' + local.value;
|
||||||
|
else if (local.hasnullvalue) s = '(null)';
|
||||||
|
if (typeof s === 'string')
|
||||||
|
return $.Deferred().resolveWith(this, [s]);
|
||||||
|
return dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature)
|
||||||
|
.then(s => s.string);
|
||||||
|
}
|
||||||
|
const evaluate_expression = (expr) => {
|
||||||
|
var q = $.Deferred(), local;
|
||||||
|
if (expr.operator) {
|
||||||
|
const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary ? 'type' : 'types'} for operator '${expr.operator}'`),
|
||||||
|
divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero');
|
||||||
|
var lhs_local;
|
||||||
|
return !expr.lhs
|
||||||
|
? // unary operator
|
||||||
|
evaluate_expression(expr.rhs)
|
||||||
|
.then(rhs_local => {
|
||||||
|
if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) {
|
||||||
|
rhs_local.value = !rhs_local.value;
|
||||||
|
return rhs_local;
|
||||||
|
}
|
||||||
|
else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) {
|
||||||
|
switch (rhs_local.type.typename) {
|
||||||
|
case 'long': rhs_local.value = rhs_local.value.replace(/./g, c => (15 - parseInt(c, 16)).toString(16)); break;
|
||||||
|
default: rhs_local = evaluate_number('' + ~rhs_local.value); break;
|
||||||
|
}
|
||||||
|
return rhs_local;
|
||||||
|
}
|
||||||
|
else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) {
|
||||||
|
if (expr.operator === '+') return rhs_local;
|
||||||
|
switch (rhs_local.type.typename) {
|
||||||
|
case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value, false, 16).neg()); break;
|
||||||
|
default: rhs_local = evaluate_number('' + (-rhs_local.value)); break;
|
||||||
|
}
|
||||||
|
return rhs_local;
|
||||||
|
}
|
||||||
|
return invalid_operator('unary');
|
||||||
|
})
|
||||||
|
: // binary operator
|
||||||
|
evaluate_expression(expr.lhs)
|
||||||
|
.then(x => (lhs_local = x) && evaluate_expression(expr.rhs))
|
||||||
|
.then(rhs_local => {
|
||||||
|
if ((lhs_local.type.signature === 'J' && JTYPES.isInteger(rhs_local.type))
|
||||||
|
|| (rhs_local.type.signature === 'J' && JTYPES.isInteger(lhs_local.type))) {
|
||||||
|
// one operand is a long, the other is an integer -> the result is a long
|
||||||
|
var a, b, lbase, rbase;
|
||||||
|
lbase = lhs_local.type.signature === 'J' ? 16 : 10;
|
||||||
|
rbase = rhs_local.type.signature === 'J' ? 16 : 10;
|
||||||
|
a = Long.fromString('' + lhs_local.value, false, lbase);
|
||||||
|
b = Long.fromString('' + rhs_local.value, false, rbase);
|
||||||
|
switch (expr.operator) {
|
||||||
|
case '+': a = a.add(b); break;
|
||||||
|
case '-': a = a.subtract(b); break;
|
||||||
|
case '*': a = a.multiply(b); break;
|
||||||
|
case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero();
|
||||||
|
case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero();
|
||||||
|
case '<<': a = a.shl(b); break;
|
||||||
|
case '>>': a = a.shr(b); break;
|
||||||
|
case '>>>': a = a.shru(b); break;
|
||||||
|
case '&': a = a.and(b); break;
|
||||||
|
case '|': a = a.or(b); break;
|
||||||
|
case '^': a = a.xor(b); break;
|
||||||
|
case '==': a = a.eq(b); break;
|
||||||
|
case '!=': a = !a.eq(b); break;
|
||||||
|
case '<': a = a.lt(b); break;
|
||||||
|
case '<=': a = a.lte(b); break;
|
||||||
|
case '>': a = a.gt(b); break;
|
||||||
|
case '>=': a = a.gte(b); break;
|
||||||
|
default: return invalid_operator();
|
||||||
|
}
|
||||||
|
if (typeof a === 'boolean')
|
||||||
|
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||||
|
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.long, value: hex_long(a), valid: true };
|
||||||
|
}
|
||||||
|
else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) {
|
||||||
|
// both are (non-long) integer types
|
||||||
|
var a = numberify(lhs_local), b = numberify(rhs_local);
|
||||||
|
switch (expr.operator) {
|
||||||
|
case '+': a += b; break;
|
||||||
|
case '-': a -= b; break;
|
||||||
|
case '*': a *= b; break;
|
||||||
|
case '/': if (b) { a = Math.trunc(a / b); break } return divide_by_zero();
|
||||||
|
case '%': if (b) { a %= b; break; } return divide_by_zero();
|
||||||
|
case '<<': a <<= b; break;
|
||||||
|
case '>>': a >>= b; break;
|
||||||
|
case '>>>': a >>>= b; break;
|
||||||
|
case '&': a &= b; break;
|
||||||
|
case '|': a |= b; break;
|
||||||
|
case '^': a ^= b; break;
|
||||||
|
case '==': a = a === b; break;
|
||||||
|
case '!=': a = a !== b; break;
|
||||||
|
case '<': a = a < b; break;
|
||||||
|
case '<=': a = a <= b; break;
|
||||||
|
case '>': a = a > b; break;
|
||||||
|
case '>=': a = a >= b; break;
|
||||||
|
default: return invalid_operator();
|
||||||
|
}
|
||||||
|
if (typeof a === 'boolean')
|
||||||
|
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||||
|
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.int, value: '' + a, valid: true };
|
||||||
|
}
|
||||||
|
else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) {
|
||||||
|
var a = numberify(lhs_local), b = numberify(rhs_local);
|
||||||
|
switch (expr.operator) {
|
||||||
|
case '+': a += b; break;
|
||||||
|
case '-': a -= b; break;
|
||||||
|
case '*': a *= b; break;
|
||||||
|
case '/': a /= b; break;
|
||||||
|
case '==': a = a === b; break;
|
||||||
|
case '!=': a = a !== b; break;
|
||||||
|
case '<': a = a < b; break;
|
||||||
|
case '<=': a = a <= b; break;
|
||||||
|
case '>': a = a > b; break;
|
||||||
|
case '>=': a = a >= b; break;
|
||||||
|
default: return invalid_operator();
|
||||||
|
}
|
||||||
|
if (typeof a === 'boolean')
|
||||||
|
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||||
|
// one of them must be a float or double
|
||||||
|
var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))];
|
||||||
|
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES[result_type], value: '' + a, valid: true };
|
||||||
|
}
|
||||||
|
else if (lhs_local.type.signature === 'Z' && rhs_local.type.signature === 'Z') {
|
||||||
|
// boolean operands
|
||||||
|
var a = lhs_local.value, b = rhs_local.value;
|
||||||
|
switch (expr.operator) {
|
||||||
|
case '&': case '&&': a = a && b; break;
|
||||||
|
case '|': case '||': a = a || b; break;
|
||||||
|
case '^': a = !!(a ^ b); break;
|
||||||
|
case '==': a = a === b; break;
|
||||||
|
case '!=': a = a !== b; break;
|
||||||
|
default: return invalid_operator();
|
||||||
|
}
|
||||||
|
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||||
|
}
|
||||||
|
else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) {
|
||||||
|
return stringify(rhs_local).then(rhs_str => createJavaString(dbgr, lhs_local.string + rhs_str, { israw: true }));
|
||||||
|
}
|
||||||
|
return invalid_operator();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
switch (expr.root_term_type) {
|
||||||
|
case 'boolean':
|
||||||
|
local = { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: expr.root_term !== 'false', valid: true };
|
||||||
|
break;
|
||||||
|
case 'null':
|
||||||
|
const nullvalue = '0000000000000000'; // null reference value
|
||||||
|
local = { vtype: 'literal', name: '', hasnullvalue: true, type: JTYPES.null, value: nullvalue, valid: true };
|
||||||
|
break;
|
||||||
|
case 'ident':
|
||||||
|
local = locals && locals.find(l => l.name === expr.root_term);
|
||||||
|
break;
|
||||||
|
case 'hexint':
|
||||||
|
case 'octint':
|
||||||
|
case 'decint':
|
||||||
|
case 'decfloat':
|
||||||
|
local = evaluate_number(expr.root_term);
|
||||||
|
break;
|
||||||
|
case 'char':
|
||||||
|
case 'echar':
|
||||||
|
case 'uchar':
|
||||||
|
local = evaluate_char(decode_char(expr.root_term.slice(1, -1)))
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
// we must get the runtime to create string instances
|
||||||
|
q = createJavaString(dbgr, expr.root_term);
|
||||||
|
local = { valid: true }; // make sure we don't fail the evaluation
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!local || !local.valid) return reject_evaluation('not available');
|
||||||
|
// we've got the root term variable - work out the rest
|
||||||
|
q = expr.array_or_fncall.arr.reduce((q, index_expr) => {
|
||||||
|
return q.then(function (index_expr, local) { return evaluate_array_element.call(this, index_expr, local) }.bind(this, index_expr));
|
||||||
|
}, q);
|
||||||
|
q = expr.members.reduce((q, m) => {
|
||||||
|
return q.then(function (m, local) { return evaluate_member.call(this, m, local) }.bind(this, m));
|
||||||
|
}, q);
|
||||||
|
if (expr.typecast) {
|
||||||
|
q = q.then(function (type, local) { return evaluate_cast.call(this, type, local) }.bind(this, expr.typecast))
|
||||||
|
}
|
||||||
|
// if it's a string literal, we are already waiting for the runtime to create the string
|
||||||
|
// - otherwise, start the evalaution...
|
||||||
|
if (expr.root_term_type !== 'string')
|
||||||
|
q.resolveWith(this, [local]);
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
const evaluate_array_element = (index_expr, arr_local) => {
|
||||||
|
if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
|
||||||
|
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||||
|
return evaluate_expression(index_expr)
|
||||||
|
.then(function (arr_local, idx_local) {
|
||||||
|
if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value');
|
||||||
|
var idx = numberify(idx_local);
|
||||||
|
if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`);
|
||||||
|
return dbgr.getarrayvalues(arr_local, idx, 1)
|
||||||
|
}.bind(this, arr_local))
|
||||||
|
.then(els => els[0])
|
||||||
|
}
|
||||||
|
const evaluate_methodcall = (m, obj_local) => {
|
||||||
|
// until we can figure out why method invokes with parameters crash the debugger, disallow parameterised calls
|
||||||
|
if (m.array_or_fncall.call.length)
|
||||||
|
return reject_evaluation('Error: method calls with parameter values are not supported');
|
||||||
|
|
||||||
|
// find any methods matching the member name with any parameters in the signature
|
||||||
|
return dbgr.findNamedMethods(obj_local.type.signature, m.member, /^/)
|
||||||
|
.then(methods => {
|
||||||
|
if (!methods[0])
|
||||||
|
return reject_evaluation(`Error: method '${m.member}()' not found`);
|
||||||
|
// evaluate any parameters (and wait for the results)
|
||||||
|
return $.when({methods},...m.array_or_fncall.call.map(evaluate_expression));
|
||||||
|
})
|
||||||
|
.then((x,...paramValues) => {
|
||||||
|
// filter the method based upon the types of parameters - note that null types and integer literals can match multiple types
|
||||||
|
paramValues = paramValues = paramValues.map(p => p[0]);
|
||||||
|
var matchers = paramValues.map(p => {
|
||||||
|
switch(true) {
|
||||||
|
case p.type.signature === 'I':
|
||||||
|
// match bytes/shorts/ints/longs/floats/doubles within range
|
||||||
|
if (p.value >= -128 && p.value <= 127) return /^[BSIJFD]$/
|
||||||
|
if (p.value >= -32768 && p.value <= 32767) return /^[SIJFD]$/
|
||||||
|
return /^[IJFD]$/;
|
||||||
|
case p.type.signature === 'F':
|
||||||
|
return /^[FD]$/;
|
||||||
|
case p.type.signature === 'Lnull;':
|
||||||
|
return /^[LT\[]/; // any reference type
|
||||||
|
default:
|
||||||
|
// anything else must be an exact signature match (for now - in reality we should allow subclassed type)
|
||||||
|
return new RegExp(`^${p.type.signature.replace(/[$]/g,x=>'\\'+x)}$`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var methods = x.methods.filter(m => {
|
||||||
|
// extract a list of parameter types
|
||||||
|
var paramtypere = /\[*([BSIJFDCZ]|([LT][^;]+;))/g;
|
||||||
|
for (var x, ptypes=[]; x = paramtypere.exec(m.sig); ) {
|
||||||
|
ptypes.push(x[0]);
|
||||||
|
}
|
||||||
|
// the last paramter type is the return value
|
||||||
|
ptypes.pop();
|
||||||
|
// check if they match
|
||||||
|
if (ptypes.length !== paramValues.length)
|
||||||
|
return;
|
||||||
|
return matchers.filter(m => {
|
||||||
|
return !m.test(ptypes.shift())
|
||||||
|
}).length === 0;
|
||||||
|
});
|
||||||
|
if (!methods[0])
|
||||||
|
return reject_evaluation(`Error: incompatible parameters for method '${m.member}'`);
|
||||||
|
// convert the parameters to exact debugger-compatible values
|
||||||
|
paramValues = paramValues.map(p => {
|
||||||
|
if (p.type.signature.length === 1)
|
||||||
|
return { type: p.type.typename, value: p.value};
|
||||||
|
return { type: 'oref', value: p.value };
|
||||||
|
})
|
||||||
|
return dbgr.invokeMethod(obj_local.value, thread.threadid, obj_local.type.signature, m.member, methods[0].genericsig || methods[0].sig, paramValues, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const evaluate_member = (m, obj_local) => {
|
||||||
|
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
|
||||||
|
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||||
|
var chain;
|
||||||
|
if (m.array_or_fncall.call) {
|
||||||
|
chain = evaluate_methodcall(m, obj_local);
|
||||||
|
}
|
||||||
|
// length is a 'fake' field of arrays, so special-case it
|
||||||
|
else if (JTYPES.isArray(obj_local.type) && m.member === 'length') {
|
||||||
|
chain = $.Deferred().resolve(evaluate_number(obj_local.arraylen));
|
||||||
|
}
|
||||||
|
// we also special-case :super (for object instances)
|
||||||
|
else if (JTYPES.isObject(obj_local.type) && m.member === ':super') {
|
||||||
|
chain = dbgr.getsuperinstance(obj_local);
|
||||||
|
}
|
||||||
|
// anything else must be a real field
|
||||||
|
else {
|
||||||
|
chain = dbgr.getFieldValue(obj_local, m.member, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.then(local => {
|
||||||
|
if (m.array_or_fncall.arr.length) {
|
||||||
|
var q = $.Deferred();
|
||||||
|
m.array_or_fncall.arr.reduce((q, index_expr) => {
|
||||||
|
return q.then(function (index_expr, local) { return evaluate_array_element(index_expr, local) }.bind(this, index_expr));
|
||||||
|
}, q);
|
||||||
|
return q.resolveWith(this, [local]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const evaluate_cast = (type, local) => {
|
||||||
|
if (type === local.type.typename) return local;
|
||||||
|
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`);
|
||||||
|
// boolean cannot be converted from anything else
|
||||||
|
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast();
|
||||||
|
if (local.type.typename === 'long') {
|
||||||
|
// long to something else
|
||||||
|
var value = Long.fromString(local.value, true, 16);
|
||||||
|
switch (true) {
|
||||||
|
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2), 16) << 24) >> 24); break;
|
||||||
|
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4), 16) << 16) >> 16); break;
|
||||||
|
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8), 16) | 0)); break;
|
||||||
|
case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4), 16))); break;
|
||||||
|
case (type === 'float'): local = evaluate_number(value.toSigned().toNumber() + 'F'); break;
|
||||||
|
case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break;
|
||||||
|
default: return incompatible_cast();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (true) {
|
||||||
|
case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break;
|
||||||
|
case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break;
|
||||||
|
case (type === 'int'): local = evaluate_number((local.value | 0)); break;
|
||||||
|
case (type === 'long'): local = evaluate_number(local.value + 'L'); break;
|
||||||
|
case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value | 0)); break;
|
||||||
|
case (type === 'float'): break;
|
||||||
|
case (type === 'double'): break;
|
||||||
|
default: return incompatible_cast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local.type = JTYPES[type];
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
var e = { expr: expression.trim() };
|
||||||
|
var parsed_expression = parse_expression(e);
|
||||||
|
// if there's anything left, it's an error
|
||||||
|
if (parsed_expression && !e.expr) {
|
||||||
|
// the expression is well-formed - start the (asynchronous) evaluation
|
||||||
|
return evaluate_expression(parsed_expression)
|
||||||
|
.then(local => {
|
||||||
|
var v = vars._local_to_variable(local);
|
||||||
|
return resolve_evaluation(v.value, v.variablesReference);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// the expression is not well-formed
|
||||||
|
return reject_evaluation('not available');
|
||||||
|
}
|
||||||
87
src/globals.js
Normal file
87
src/globals.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// some commonly used Java types in debugger-compatible format
|
||||||
|
const JTYPES = {
|
||||||
|
byte: {typename:'byte',signature:'B'},
|
||||||
|
short: {typename:'short',signature:'S'},
|
||||||
|
int: {typename:'int',signature:'I'},
|
||||||
|
long: {typename:'long',signature:'J'},
|
||||||
|
float: {typename:'float',signature:'F'},
|
||||||
|
double: {typename:'double',signature:'D'},
|
||||||
|
char: {typename:'char',signature:'C'},
|
||||||
|
boolean: {typename:'boolean',signature:'Z'},
|
||||||
|
null: {typename:'null',signature:'Lnull;'}, // null has no type really, but we need something for literals
|
||||||
|
String: {typename:'String',signature:'Ljava/lang/String;'},
|
||||||
|
Object: {typename:'Object',signature:'Ljava/lang/Object;'},
|
||||||
|
isArray(t) { return t.signature[0]==='[' },
|
||||||
|
isObject(t) { return t.signature[0]==='L' },
|
||||||
|
isReference(t) { return /^[L[]/.test(t.signature) },
|
||||||
|
isPrimitive(t) { return !JTYPES.isReference(t.signature) },
|
||||||
|
isInteger(t) { return /^[BCIJS]$/.test(t.signature) },
|
||||||
|
isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) },
|
||||||
|
isString(t) { return t.signature === this.String.signature },
|
||||||
|
isChar(t) { return t.signature === this.char.signature },
|
||||||
|
isBoolean(t) { return t.signature === this.boolean.signature },
|
||||||
|
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
|
||||||
|
}
|
||||||
|
|
||||||
|
function signatureToFullyQualifiedType(sig) {
|
||||||
|
var arr = sig.match(/^\[+/) || '';
|
||||||
|
if (arr) {
|
||||||
|
arr = '[]'.repeat(arr[0].length);
|
||||||
|
sig = sig.slice(0, arr.length/2);
|
||||||
|
}
|
||||||
|
var m = sig.match(/^((L([^<;]+).)|T([^;]+).|.)/);
|
||||||
|
if (!m) return '';
|
||||||
|
if (m[3]) {
|
||||||
|
return m[3].replace(/[/$]/g,'.') + arr;
|
||||||
|
} else if (m[4]) {
|
||||||
|
return m[4].replace(/[/$]/g, '.') + arr;
|
||||||
|
}
|
||||||
|
return JTYPES.fromPrimSig(sig[0]) + arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the special name given to exception message fields
|
||||||
|
const exmsg_var_name = ':msg';
|
||||||
|
|
||||||
|
function createJavaString(dbgr, s, opts) {
|
||||||
|
const raw = (opts && opts.israw) ? s : s.slice(1,-1).replace(/\\u[0-9a-fA-F]{4}|\\./,decode_char);
|
||||||
|
// return a deferred, which resolves to a local variable named 'literal'
|
||||||
|
return dbgr.createstring(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decode_char(c) {
|
||||||
|
switch(true) {
|
||||||
|
case /^\\[^u]$/.test(c):
|
||||||
|
// backslash escape
|
||||||
|
var x = {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v','0':String.fromCharCode(0)}[c[1]];
|
||||||
|
return x || c[1];
|
||||||
|
case /^\\u[0-9a-fA-F]{4}$/.test(c):
|
||||||
|
// unicode escape
|
||||||
|
return String.fromCharCode(parseInt(c.slice(2),16));
|
||||||
|
case c.length===1 :
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
throw new Error('Invalid character value');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensure_path_end_slash(p) {
|
||||||
|
return p + (/[\\/]$/.test(p) ? '' : path.sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_subpath_of(fpn, subpath) {
|
||||||
|
if (!subpath || !fpn) return false;
|
||||||
|
subpath = ensure_path_end_slash(''+subpath);
|
||||||
|
return fpn.slice(0,subpath.length) === subpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function variableRefToThreadId(variablesReference) {
|
||||||
|
return (variablesReference / 1e9)|0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Object.assign(exports, {
|
||||||
|
JTYPES, exmsg_var_name, ensure_path_end_slash, is_subpath_of, decode_char, variableRefToThreadId, createJavaString, signatureToFullyQualifiedType
|
||||||
|
});
|
||||||
161
src/jdwp.js
161
src/jdwp.js
@@ -1,5 +1,5 @@
|
|||||||
const $ = require('./jq-promise');
|
const $ = require('./jq-promise');
|
||||||
const { atob,btoa,D,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
|
const { btoa,D,E,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
|
||||||
/*
|
/*
|
||||||
JDWP - The Java Debug Wire Protocol
|
JDWP - The Java Debug Wire Protocol
|
||||||
*/
|
*/
|
||||||
@@ -96,8 +96,8 @@ function _JDWP() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.errorcode != 0) {
|
if (this.errorcode !== 0) {
|
||||||
console.error("Command failed: error " + this.errorcode, this);
|
E(`JDWP command failed '${this.command.name}'. Error ${this.errorcode}`, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.errorcode && this.command && this.command.replydecodefn) {
|
if (!this.errorcode && this.command && this.command.replydecodefn) {
|
||||||
@@ -109,7 +109,10 @@ function _JDWP() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.decoded = {empty:true};
|
this.decoded = {
|
||||||
|
empty: true,
|
||||||
|
errorcode: this.errorcode,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.decodereply = function(ths,s) {
|
this.decodereply = function(ths,s) {
|
||||||
@@ -169,7 +172,7 @@ function _JDWP() {
|
|||||||
return i<32768?i:i-65536;
|
return i<32768?i:i-65536;
|
||||||
},
|
},
|
||||||
decodeChar: function(o) {
|
decodeChar: function(o) {
|
||||||
return String.fromCharCode((o.data[o.idx++]<<8)+o.data[o.idx++]);
|
return (o.data[o.idx++]<<8)+o.data[o.idx++]; // uint16
|
||||||
},
|
},
|
||||||
decodeBoolean: function(o) {
|
decodeBoolean: function(o) {
|
||||||
return o.data[o.idx++] != 0;
|
return o.data[o.idx++] != 0;
|
||||||
@@ -250,6 +253,12 @@ function _JDWP() {
|
|||||||
decodeStatus : function(o) {
|
decodeStatus : function(o) {
|
||||||
return this.mapflags(this.decodeInt(o), ['verified','prepared','initialized','error']);
|
return this.mapflags(this.decodeInt(o), ['verified','prepared','initialized','error']);
|
||||||
},
|
},
|
||||||
|
decodeThreadStatus : function(o) {
|
||||||
|
return ['zombie','running','sleeping','monitor','wait'][this.decodeInt(o)] || '';
|
||||||
|
},
|
||||||
|
decodeSuspendStatus : function(o) {
|
||||||
|
return this.decodeInt(o) ? 'suspended': '';
|
||||||
|
},
|
||||||
decodeTaggedObjectID : function(o) {
|
decodeTaggedObjectID : function(o) {
|
||||||
return this.decodeValue(o);
|
return this.decodeValue(o);
|
||||||
},
|
},
|
||||||
@@ -365,6 +374,12 @@ function _JDWP() {
|
|||||||
event.exception = this.decodeTaggedObjectID(o);
|
event.exception = this.decodeTaggedObjectID(o);
|
||||||
event.catchlocation = this.decodeLocation(o); // 0 = uncaught
|
event.catchlocation = this.decodeLocation(o); // 0 = uncaught
|
||||||
break;
|
break;
|
||||||
|
case 6: // thread start
|
||||||
|
case 7: // thread end
|
||||||
|
event.reqid = this.decodeInt(o);
|
||||||
|
event.threadid = this.decodeORef(o);
|
||||||
|
event.state = event.kind.value === 6 ? 'start' : 'end';
|
||||||
|
break;
|
||||||
case 8: // classprepare
|
case 8: // classprepare
|
||||||
event.reqid = this.decodeInt(o);
|
event.reqid = this.decodeInt(o);
|
||||||
event.threadid = this.decodeORef(o);
|
event.threadid = this.decodeORef(o);
|
||||||
@@ -525,12 +540,12 @@ function _JDWP() {
|
|||||||
}
|
}
|
||||||
m = signature.match(/^(\[+)(.+)$/);
|
m = signature.match(/^(\[+)(.+)$/);
|
||||||
if (m) {
|
if (m) {
|
||||||
var elementtype = this.signaturetotype(m[2]);
|
var elementtype = this.signaturetotype(m[1].slice(0,-1) + m[2]);
|
||||||
return {
|
return {
|
||||||
signature:signature,
|
signature:signature,
|
||||||
arraydims:m[1].length,
|
arraydims:m[1].length,
|
||||||
elementtype: elementtype,
|
elementtype: elementtype,
|
||||||
typename:elementtype.typename+m[1].replace(/\[/g,'[]'),
|
typename:elementtype.typename+'[]',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var primitivetypes = {
|
var primitivetypes = {
|
||||||
@@ -627,6 +642,28 @@ function _JDWP() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
GetStaticFieldValues:function(typeid, fields) {
|
||||||
|
return new Command('GetStaticFieldValues:'+typeid, 2, 6,
|
||||||
|
function() {
|
||||||
|
var res=[];
|
||||||
|
DataCoder.encodeRef(res, typeid);
|
||||||
|
DataCoder.encodeInt(res, fields.length);
|
||||||
|
for (var i in fields) {
|
||||||
|
DataCoder.encodeRef(res, fields[i].fieldid);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
function(o) {
|
||||||
|
var res = [];
|
||||||
|
var arrlen = DataCoder.decodeInt(o);
|
||||||
|
while (--arrlen>=0) {
|
||||||
|
var v = DataCoder.decodeValue(o);
|
||||||
|
res.push(v);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
sourcefile:function(ci) {
|
sourcefile:function(ci) {
|
||||||
return new Command('SourceFile:'+ci.name, 2, 7,
|
return new Command('SourceFile:'+ci.name, 2, 7,
|
||||||
function() {
|
function() {
|
||||||
@@ -650,7 +687,9 @@ function _JDWP() {
|
|||||||
var arrlen = DataCoder.decodeInt(o);
|
var arrlen = DataCoder.decodeInt(o);
|
||||||
var res = [];
|
var res = [];
|
||||||
while (--arrlen>=0) {
|
while (--arrlen>=0) {
|
||||||
res.push(DataCoder.decodeList(o, [{fieldid:'fref'},{name:'string'},{type:'signature'},{genericsig:'string'},{modbits:'int'}]));
|
var field = DataCoder.decodeList(o, [{fieldid:'fref'},{name:'string'},{type:'signature'},{genericsig:'string'},{modbits:'int'}]);
|
||||||
|
field.typeid = ci.info.typeid;
|
||||||
|
res.push(field);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -697,6 +736,7 @@ function _JDWP() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// nestedTypes is not implemented on android
|
||||||
nestedTypes:function(ci) {
|
nestedTypes:function(ci) {
|
||||||
return new Command('NestedTypes:'+ci.name, 2, 8,
|
return new Command('NestedTypes:'+ci.name, 2, 8,
|
||||||
function() {
|
function() {
|
||||||
@@ -709,7 +749,7 @@ function _JDWP() {
|
|||||||
var arrlen = DataCoder.decodeInt(o);
|
var arrlen = DataCoder.decodeInt(o);
|
||||||
while (--arrlen>=0) {
|
while (--arrlen>=0) {
|
||||||
var v = DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'}]);
|
var v = DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'}]);
|
||||||
res.vars.push(v);
|
res.push(v);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -1017,7 +1057,7 @@ function _JDWP() {
|
|||||||
}];
|
}];
|
||||||
// kind(1=singlestep)
|
// kind(1=singlestep)
|
||||||
// suspendpolicy(0=none,1=event-thread,2=all)
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
return this.SetEventRequest("step",1,2,mods,
|
return this.SetEventRequest("step",1,1,mods,
|
||||||
function(m1, i, res) {
|
function(m1, i, res) {
|
||||||
res.push(m1.modkind);
|
res.push(m1.modkind);
|
||||||
DataCoder.encodeRef(res, m1.threadid);
|
DataCoder.encodeRef(res, m1.threadid);
|
||||||
@@ -1027,21 +1067,35 @@ function _JDWP() {
|
|||||||
onevent
|
onevent
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
SetBreakpoint:function(ci, mi, idx, onevent) {
|
SetBreakpoint:function(ci, mi, idx, hitcount, onevent) {
|
||||||
// a wrapper around SetEventRequest
|
// a wrapper around SetEventRequest
|
||||||
var mods = [{
|
var mods = [{
|
||||||
modkind:7, // location
|
modkind:7, // location
|
||||||
loc:{ type:ci.info.reftype.value, cid:ci.info.typeid, mid:mi.methodid, idx:idx }
|
loc:{ type:ci.info.reftype.value, cid:ci.info.typeid, mid:mi.methodid, idx:idx },
|
||||||
|
encode(res) {
|
||||||
|
res.push(this.modkind);
|
||||||
|
res.push(this.loc.type);
|
||||||
|
DataCoder.encodeRef(res, this.loc.cid);
|
||||||
|
DataCoder.encodeRef(res, this.loc.mid);
|
||||||
|
DataCoder.encodeLong(res, this.loc.idx);
|
||||||
|
}
|
||||||
}];
|
}];
|
||||||
|
if (hitcount > 0) {
|
||||||
|
// remember when setting a hitcount, the event is automatically cancelled after being fired
|
||||||
|
mods.unshift({
|
||||||
|
modkind:1,
|
||||||
|
count: hitcount,
|
||||||
|
encode(res) {
|
||||||
|
res.push(this.modkind);
|
||||||
|
DataCoder.encodeInt(res, this.count);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
// kind(2=breakpoint)
|
// kind(2=breakpoint)
|
||||||
// suspendpolicy(0=none,1=event-thread,2=all)
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
return this.SetEventRequest("breakpoint",2,2,mods,
|
return this.SetEventRequest("breakpoint",2,1,mods,
|
||||||
function(m1, i, res) {
|
function(m, i, res) {
|
||||||
res.push(m1.modkind);
|
m.encode(res,i);
|
||||||
res.push(m1.loc.type);
|
|
||||||
DataCoder.encodeRef(res, m1.loc.cid);
|
|
||||||
DataCoder.encodeRef(res, m1.loc.mid);
|
|
||||||
DataCoder.encodeLong(res, m1.loc.idx);
|
|
||||||
},
|
},
|
||||||
onevent
|
onevent
|
||||||
);
|
);
|
||||||
@@ -1054,6 +1108,26 @@ function _JDWP() {
|
|||||||
// kind(2=breakpoint)
|
// kind(2=breakpoint)
|
||||||
return this.ClearEvent("breakpoint",2,requestid);
|
return this.ClearEvent("breakpoint",2,requestid);
|
||||||
},
|
},
|
||||||
|
ThreadStartNotify:function(onevent) {
|
||||||
|
// a wrapper around SetEventRequest
|
||||||
|
var mods = [];
|
||||||
|
// kind(6=threadstart)
|
||||||
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
|
return this.SetEventRequest("threadstart",6,1,mods,
|
||||||
|
function() {},
|
||||||
|
onevent
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ThreadEndNotify:function(onevent) {
|
||||||
|
// a wrapper around SetEventRequest
|
||||||
|
var mods = [];
|
||||||
|
// kind(7=threadend)
|
||||||
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
|
return this.SetEventRequest("threadend",7,1,mods,
|
||||||
|
function() {},
|
||||||
|
onevent
|
||||||
|
);
|
||||||
|
},
|
||||||
OnClassPrepare:function(pattern, onevent) {
|
OnClassPrepare:function(pattern, onevent) {
|
||||||
// a wrapper around SetEventRequest
|
// a wrapper around SetEventRequest
|
||||||
var mods = [{
|
var mods = [{
|
||||||
@@ -1088,7 +1162,7 @@ function _JDWP() {
|
|||||||
});
|
});
|
||||||
// kind(4=exception)
|
// kind(4=exception)
|
||||||
// suspendpolicy(0=none,1=event-thread,2=all)
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
return this.SetEventRequest("exception",4,2,mods,
|
return this.SetEventRequest("exception",4,1,mods,
|
||||||
function(m, i, res) {
|
function(m, i, res) {
|
||||||
res.push(m.modkind);
|
res.push(m.modkind);
|
||||||
switch(m.modkind) {
|
switch(m.modkind) {
|
||||||
@@ -1125,6 +1199,26 @@ function _JDWP() {
|
|||||||
resume:function() {
|
resume:function() {
|
||||||
return new Command('resume',1, 9, null, null);
|
return new Command('resume',1, 9, null, null);
|
||||||
},
|
},
|
||||||
|
suspendthread:function(threadid) {
|
||||||
|
return new Command('suspendthread:'+threadid,11, 2,
|
||||||
|
function() {
|
||||||
|
var res = [];
|
||||||
|
DataCoder.encodeRef(res, this);
|
||||||
|
return res;
|
||||||
|
}.bind(threadid),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
resumethread:function(threadid) {
|
||||||
|
return new Command('resumethread:'+threadid,11, 3,
|
||||||
|
function() {
|
||||||
|
var res = [];
|
||||||
|
DataCoder.encodeRef(res, this);
|
||||||
|
return res;
|
||||||
|
}.bind(threadid),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
},
|
||||||
allthreads:function() {
|
allthreads:function() {
|
||||||
return new Command('allthreads',1, 4,
|
return new Command('allthreads',1, 4,
|
||||||
null,
|
null,
|
||||||
@@ -1137,7 +1231,34 @@ function _JDWP() {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
threadname:function(threadid) {
|
||||||
|
return new Command('threadname',11,1,
|
||||||
|
function() {
|
||||||
|
var res=[];
|
||||||
|
DataCoder.encodeRef(res, this);
|
||||||
|
return res;
|
||||||
|
}.bind(threadid),
|
||||||
|
function(o) {
|
||||||
|
return DataCoder.decodeString(o);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
threadstatus:function(threadid) {
|
||||||
|
return new Command('threadstatus',11,4,
|
||||||
|
function() {
|
||||||
|
var res=[];
|
||||||
|
DataCoder.encodeRef(res, this);
|
||||||
|
return res;
|
||||||
|
}.bind(threadid),
|
||||||
|
function(o) {
|
||||||
|
return {
|
||||||
|
thread: DataCoder.decodeThreadStatus(o),
|
||||||
|
suspend: DataCoder.decodeSuspendStatus(o),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var Deferred = exports.Deferred = function(p, parent) {
|
|||||||
var thendef = this.then(fn);
|
var thendef = this.then(fn);
|
||||||
this.fail(function() {
|
this.fail(function() {
|
||||||
// we cannot bind thendef to the function because we need the caller's this to resolve the thendef
|
// we cannot bind thendef to the function because we need the caller's this to resolve the thendef
|
||||||
thendef.resolveWith(this, Array.prototype.map.call(arguments,x=>x));
|
return thendef.resolveWith(this, Array.prototype.map.call(arguments,x=>x))._promise;
|
||||||
});
|
});
|
||||||
return thendef;
|
return thendef;
|
||||||
},
|
},
|
||||||
@@ -40,7 +40,7 @@ var Deferred = exports.Deferred = function(p, parent) {
|
|||||||
var faildef = $.Deferred(null, this);
|
var faildef = $.Deferred(null, this);
|
||||||
var p = this._promise.catch(function(a) {
|
var p = this._promise.catch(function(a) {
|
||||||
if (a.stack) {
|
if (a.stack) {
|
||||||
console.error(a.stack);
|
util.E(a.stack);
|
||||||
a = [a];
|
a = [a];
|
||||||
}
|
}
|
||||||
if (this.def._context === null && this.def._parent)
|
if (this.def._context === null && this.def._parent)
|
||||||
@@ -50,6 +50,8 @@ var Deferred = exports.Deferred = function(p, parent) {
|
|||||||
var res = this.fn.apply(this.def._context,a);
|
var res = this.fn.apply(this.def._context,a);
|
||||||
if (res === undefined)
|
if (res === undefined)
|
||||||
return a;
|
return a;
|
||||||
|
if (res && res._isdeferred)
|
||||||
|
return res._promise;
|
||||||
return res;
|
return res;
|
||||||
}.bind({def:faildef,fn:fn}));
|
}.bind({def:faildef,fn:fn}));
|
||||||
faildef._promise = faildef._original = p;
|
faildef._promise = faildef._original = p;
|
||||||
@@ -106,6 +108,9 @@ var Deferred = exports.Deferred = function(p, parent) {
|
|||||||
// $.when() is jQuery's version of Promise.all()
|
// $.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
|
// - 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() {
|
var when = exports.when = function() {
|
||||||
|
if (arguments.length === 1 && Array.isArray(arguments[0])) {
|
||||||
|
return when.apply(this,...arguments).then(() => [...arguments]);
|
||||||
|
}
|
||||||
var x = {
|
var x = {
|
||||||
def: $.Deferred(),
|
def: $.Deferred(),
|
||||||
args: Array.prototype.map.call(arguments,x=>x),
|
args: Array.prototype.map.call(arguments,x=>x),
|
||||||
|
|||||||
303
src/logcat.js
303
src/logcat.js
@@ -1,12 +1,12 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
// vscode stuff
|
|
||||||
const { EventEmitter, Uri } = require('vscode');
|
|
||||||
// node and external modules
|
// node and external modules
|
||||||
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const WebSocketServer = require('ws').Server;
|
const WebSocketServer = require('ws').Server;
|
||||||
// our stuff
|
// our stuff
|
||||||
const { ADBClient } = require('./adbclient');
|
const { ADBClient } = require('./adbclient');
|
||||||
|
const { AndroidContentProvider } = require('./contentprovider');
|
||||||
const $ = require('./jq-promise');
|
const $ = require('./jq-promise');
|
||||||
const { D } = require('./util');
|
const { D } = require('./util');
|
||||||
|
|
||||||
@@ -15,10 +15,8 @@ const { D } = require('./util');
|
|||||||
*/
|
*/
|
||||||
class LogcatContent {
|
class LogcatContent {
|
||||||
|
|
||||||
constructor(provider/*: AndroidContentProvider*/, uri/*: Uri*/) {
|
constructor(deviceid) {
|
||||||
this._provider = provider;
|
this._logcatid = deviceid;
|
||||||
this._uri = uri;
|
|
||||||
this._logcatid = uri.query;
|
|
||||||
this._logs = [];
|
this._logs = [];
|
||||||
this._htmllogs = [];
|
this._htmllogs = [];
|
||||||
this._oldhtmllogs = [];
|
this._oldhtmllogs = [];
|
||||||
@@ -26,7 +24,8 @@ class LogcatContent {
|
|||||||
this._notifying = 0;
|
this._notifying = 0;
|
||||||
this._refreshRate = 200; // ms
|
this._refreshRate = 200; // ms
|
||||||
this._state = '';
|
this._state = '';
|
||||||
this._adbclient = new ADBClient(uri.query);
|
this._htmltemplate = '';
|
||||||
|
this._adbclient = new ADBClient(deviceid);
|
||||||
this._initwait = new Promise((resolve, reject) => {
|
this._initwait = new Promise((resolve, reject) => {
|
||||||
this._state = 'connecting';
|
this._state = 'connecting';
|
||||||
LogcatContent.initWebSocketServer()
|
LogcatContent.initWebSocketServer()
|
||||||
@@ -35,7 +34,7 @@ class LogcatContent {
|
|||||||
onlog: this.onLogcatContent.bind(this),
|
onlog: this.onLogcatContent.bind(this),
|
||||||
onclose: this.onLogcatDisconnect.bind(this),
|
onclose: this.onLogcatDisconnect.bind(this),
|
||||||
});
|
});
|
||||||
}).then(x => {
|
}).then(() => {
|
||||||
this._state = 'connected';
|
this._state = 'connected';
|
||||||
this._initwait = null;
|
this._initwait = null;
|
||||||
resolve(this.content);
|
resolve(this.content);
|
||||||
@@ -44,121 +43,96 @@ class LogcatContent {
|
|||||||
reject(e);
|
reject(e);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
LogcatContent.byLogcatID[this._logcatid] = this;
|
||||||
}
|
}
|
||||||
get content() {
|
get content() {
|
||||||
if (this._initwait) return this._initwait;
|
if (this._initwait) return this._initwait;
|
||||||
if (this._state !== 'disconnected')
|
if (this._state !== 'disconnected')
|
||||||
return this.htmlBootstrap(true, '');
|
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 = new Promise((resolve/*, reject*/) => {
|
||||||
// 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 };
|
this._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({
|
this._adbclient.logcat({
|
||||||
onlog: this.onLogcatContent.bind(this),
|
onlog: this.onLogcatContent.bind(this),
|
||||||
onclose: this.onLogcatDisconnect.bind(this),
|
onclose: this.onLogcatDisconnect.bind(this),
|
||||||
}).then(x => {
|
}).then(() => {
|
||||||
// we successfully reconnected
|
// we successfully reconnected
|
||||||
this._state = 'connected';
|
this._state = 'connected';
|
||||||
this._prevlogs = null;
|
this._prevlogs = null;
|
||||||
this._initwait = null;
|
this._initwait = null;
|
||||||
resolve(this.content);
|
resolve(this.content);
|
||||||
}).fail(e => {
|
}).fail((/*e*/) => {
|
||||||
// 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 = this._prevlogs._logs;
|
||||||
this._htmllogs = this._prevlogs._htmllogs;
|
this._htmllogs = this._prevlogs._htmllogs;
|
||||||
this._oldhtmllogs = this._prevlogs._oldhtmllogs;
|
this._oldhtmllogs = this._prevlogs._oldhtmllogs;
|
||||||
this._prevlogs = null;
|
this._prevlogs = null;
|
||||||
this._initwait = null;
|
this._initwait = null;
|
||||||
var cached_content = this.htmlBootstrap(false, 'Device disconnected');
|
var cached_content = this.htmlBootstrap({connected:false, status:'Device disconnected',oldlogs: this._oldhtmllogs.join(os.EOL)});
|
||||||
resolve(cached_content);
|
resolve(cached_content);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
sendClientMessage(msg) {
|
||||||
|
LogcatContent._wss.clients.forEach(client => {
|
||||||
|
if (client._logcatid === this._logcatid) {
|
||||||
|
client.send(msg + '\n'); // include a newline to try and persuade a buffer write
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
sendDisconnectMsg() {
|
sendDisconnectMsg() {
|
||||||
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
|
this.sendClientMessage(':disconnect');
|
||||||
clients.forEach(client => client.send(':disconnect'));
|
}
|
||||||
|
onClientConnect(client) {
|
||||||
|
if (this._oldhtmllogs.length) {
|
||||||
|
var lines = '<div class="logblock">' + this._oldhtmllogs.join(os.EOL) + '</div>';
|
||||||
|
client.send(lines);
|
||||||
|
}
|
||||||
|
// if the window is tabbed away and then returned to, vscode assumes the content
|
||||||
|
// has not changed from the original bootstrap. So it proceeds to load the html page (with no data),
|
||||||
|
// causing a connection to the WSServer as if the connection is still valid (which it was, originally).
|
||||||
|
// If it's not, tell the client (again) that the device has disconnected
|
||||||
|
if (this._state === 'disconnected')
|
||||||
|
this.sendDisconnectMsg();
|
||||||
|
}
|
||||||
|
onClientMessage(client, message) {
|
||||||
|
if (message === 'cmd:clear_logcat') {
|
||||||
|
if (this._state !== 'connected') return;
|
||||||
|
new ADBClient(this._adbclient.deviceid).shell_cmd({command:'logcat -c'})
|
||||||
|
.then(() => {
|
||||||
|
// clear everything and tell the clients
|
||||||
|
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
||||||
|
this.sendClientMessage(':logcat_cleared');
|
||||||
|
})
|
||||||
|
.fail(e => {
|
||||||
|
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);
|
var clients = [...LogcatContent._wss.clients].filter(client => client._logcatid === this._logcatid);
|
||||||
if (clients.length) {
|
if (clients.length) {
|
||||||
var lines = '<div style="display:inline-block">' + this._htmllogs.join('') + '</div>';
|
var 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, 5000);
|
this._oldhtmllogs = this._htmllogs.concat(this._oldhtmllogs).slice(0, 10000);
|
||||||
this._htmllogs = [], this._logs = [];
|
this._htmllogs = [], this._logs = [];
|
||||||
}
|
}
|
||||||
htmlBootstrap(connected, statusmsg) {
|
htmlBootstrap(vars) {
|
||||||
return `<!DOCTYPE html>
|
if (!this._htmltemplate)
|
||||||
<html><head>
|
this._htmltemplate = fs.readFileSync(path.join(__dirname,'res/logcat.html'), 'utf8');
|
||||||
<style type="text/css">
|
vars = Object.assign({
|
||||||
.V {color:#999}
|
logcatid: this._logcatid,
|
||||||
.D {color:#519B4F}
|
wssport: LogcatContent._wssport,
|
||||||
.I {color:#CCC0D3}
|
}, vars);
|
||||||
.W {color:#BD955C}
|
// simple value replacement using !{name} as the placeholder
|
||||||
.E {color:#f88}
|
var html = this._htmltemplate.replace(/!\{(.*?)\}/g, (match,expr) => ''+(vars[expr.trim()]||''));
|
||||||
.F {color:#f66}
|
return html;
|
||||||
.hide {display:none}
|
|
||||||
.unhide {display:inline-block}
|
|
||||||
</style></head>
|
|
||||||
<body style="color:#fff;font-size:.9em">
|
|
||||||
<div id="status" style="color:#888">${statusmsg}</div>
|
|
||||||
<div id="rows">${this._oldhtmllogs.join(os.EOL)}</div>
|
|
||||||
<script>
|
|
||||||
function start() {
|
|
||||||
var rows = document.getElementById('rows');
|
|
||||||
var last_known_scroll_position=0, selectall=0;
|
|
||||||
var selecttext = (rows) => {
|
|
||||||
if (!rows) return window.getSelection().empty();
|
|
||||||
var range = document.createRange();
|
|
||||||
range.selectNode(rows);
|
|
||||||
window.getSelection().addRange(range);
|
|
||||||
}
|
|
||||||
window.addEventListener('scroll', function(e) {
|
|
||||||
if ((last_known_scroll_position = window.scrollY)===0) {
|
|
||||||
var hidden = document.getElementsByClassName('hide');
|
|
||||||
for (var i=hidden.length-1; i>=0; i--)
|
|
||||||
hidden[i].className='unhide';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.addEventListener('keypress', function(e) {
|
|
||||||
if (e.ctrlKey && /[aA]/.test(e.key) && !selectall) {
|
|
||||||
selectall = 1;
|
|
||||||
selecttext(rows);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.addEventListener('keyup', function(e) {
|
|
||||||
selectall = 0;
|
|
||||||
/^escape$/i.test(e.key) && selecttext(null);
|
|
||||||
});
|
|
||||||
var setStatus = (x) => { document.getElementById('status').textContent = x; }
|
|
||||||
var connect = () => {
|
|
||||||
try {
|
|
||||||
setStatus('Connecting...');
|
|
||||||
var x = new WebSocket('ws://127.0.0.1:${LogcatContent._wssport}/${this._logcatid}');
|
|
||||||
x.onopen = e => { setStatus('') };x.onclose = e => { };x.onerror = e => { setStatus('Connection error') }
|
|
||||||
x.onmessage = e => {
|
|
||||||
var logs = e.data;
|
|
||||||
if (/^:disconnect$/.test(logs)) {
|
|
||||||
x.close(),setStatus('Device disconnected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (last_known_scroll_position > 0)
|
|
||||||
logs = '<div class="hide">'+logs+'</div>';
|
|
||||||
rows && rows.insertAdjacentHTML('afterbegin',logs);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch(e) { setStatus('Connection exception') }
|
|
||||||
}
|
|
||||||
${connected ? '' : '//'} connect();
|
|
||||||
}
|
|
||||||
setTimeout(start, 100);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
}
|
||||||
renotify() {
|
renotify() {
|
||||||
if (++this._notifying > 1) return;
|
if (++this._notifying > 1) return;
|
||||||
@@ -172,56 +146,80 @@ class LogcatContent {
|
|||||||
}
|
}
|
||||||
onLogcatContent(e) {
|
onLogcatContent(e) {
|
||||||
if (e.logs.length) {
|
if (e.logs.length) {
|
||||||
var mrfirst = e.logs.slice().reverse();
|
var mrlast = e.logs.slice();
|
||||||
this._logs = mrfirst.concat(this._logs);
|
this._logs = this._logs.concat(mrlast);
|
||||||
mrfirst.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+?(.)/);
|
var m = log.match(/^\d\d-\d\d\s+?\d\d:\d\d:\d\d\.\d+?\s+?(.)/);
|
||||||
var style = (m && m[1]) || '';
|
var style = (m && m[1]) || '';
|
||||||
log = log.replace(/[&"'<>]/g, c => ({ '&': '&', '"': '"', "'": ''', '<': '<', '>': '>' }[c]));
|
log = log.replace(/[&"'<>]/g, c => ({ '&': '&', '"': '"', "'": ''', '<': '<', '>': '>' }[c]));
|
||||||
this._htmllogs.unshift(`<div class="${style}">${log}</div>`);
|
this._htmllogs.unshift(`<div class="log ${style}">${log}</div>`);
|
||||||
})
|
|
||||||
|
});
|
||||||
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
|
||||||
|
LogcatContent.byLogcatID = {};
|
||||||
|
|
||||||
LogcatContent.initWebSocketServer = function () {
|
LogcatContent.initWebSocketServer = function () {
|
||||||
|
|
||||||
if (LogcatContent._wssdone) {
|
if (LogcatContent._wssdone) {
|
||||||
// already inited
|
// already inited
|
||||||
return LogcatContent._wssdone;
|
return LogcatContent._wssdone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// retrieve the logcat websocket port
|
||||||
|
var default_wssport = 7038;
|
||||||
|
var wssport = AndroidContentProvider.getLaunchConfigSetting('logcatPort', default_wssport);
|
||||||
|
if (typeof wssport !== 'number' || wssport <= 0 || wssport >= 65536 || wssport !== (wssport|0))
|
||||||
|
wssport = default_wssport;
|
||||||
|
|
||||||
LogcatContent._wssdone = $.Deferred();
|
LogcatContent._wssdone = $.Deferred();
|
||||||
({
|
({
|
||||||
wss: null,
|
wss: null,
|
||||||
port: 31100,
|
startport: wssport,
|
||||||
|
port: wssport,
|
||||||
retries: 0,
|
retries: 0,
|
||||||
tryCreateWSS() {
|
tryCreateWSS() {
|
||||||
this.wss = new WebSocketServer({ host: '127.0.0.1', port: this.port }, () => {
|
const wsopts = {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: this.port,
|
||||||
|
clientTracking: true,
|
||||||
|
};
|
||||||
|
this.wss = new WebSocketServer(wsopts, () => {
|
||||||
// success - save the info and resolve the deferred
|
// success - save the info and resolve the deferred
|
||||||
LogcatContent._wssport = this.port;
|
LogcatContent._wssport = this.port;
|
||||||
|
LogcatContent._wssstartport = this.startport;
|
||||||
LogcatContent._wss = this.wss;
|
LogcatContent._wss = this.wss;
|
||||||
this.wss.on('connection', client => {
|
this.wss.on('connection', (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];
|
||||||
// we're not really interested in anything the client sends
|
var lc = LogcatContent.byLogcatID[client._logcatid];
|
||||||
/*client.on('message', message => {
|
if (lc) lc.onClientConnect(client);
|
||||||
console.log('ws received: %s', message);
|
else client.close();
|
||||||
});
|
client.on('message', function(message) {
|
||||||
client.on('close', e => {
|
var lc = LogcatContent.byLogcatID[this._logcatid];
|
||||||
console.log('ws close');
|
if (lc) lc.onClientMessage(this, message);
|
||||||
|
}.bind(client));
|
||||||
|
/*client.on('close', e => {
|
||||||
|
console.log('client close');
|
||||||
});*/
|
});*/
|
||||||
|
// try and make sure we don't delay writes
|
||||||
|
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
|
||||||
});
|
});
|
||||||
this.wss = null;
|
this.wss = null;
|
||||||
LogcatContent._wssdone.resolveWith(LogcatContent, []);
|
LogcatContent._wssdone.resolveWith(LogcatContent, []);
|
||||||
});
|
});
|
||||||
this.wss.on('error', err => {
|
this.wss.on('error', (/*err*/) => {
|
||||||
if (!LogcatContent._wss) {
|
if (!LogcatContent._wss) {
|
||||||
// listen failed -try the next port
|
// listen failed -try the next port
|
||||||
this.retries++ , this.port++;
|
this.retries++ , this.port++;
|
||||||
@@ -233,68 +231,30 @@ LogcatContent.initWebSocketServer = function () {
|
|||||||
return LogcatContent._wssdone;
|
return LogcatContent._wssdone;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
function getADBPort() {
|
||||||
|
var defaultPort = 5037;
|
||||||
constructor() {
|
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
||||||
this._logs = {}; // hashmap<url, LogcatContent>
|
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
||||||
this._onDidChange = new EventEmitter();
|
return adbPort;
|
||||||
}
|
return defaultPort;
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this._onDidChange.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An event to signal a resource has changed.
|
|
||||||
*/
|
|
||||||
get onDidChange() {
|
|
||||||
return this._onDidChange.event;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide textual content for a given uri.
|
|
||||||
*
|
|
||||||
* The editor will use the returned string-content to create a readonly
|
|
||||||
* [document](TextDocument). Resources allocated should be released when
|
|
||||||
* the corresponding document has been [closed](#workspace.onDidCloseTextDocument).
|
|
||||||
*
|
|
||||||
* @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
|
|
||||||
* @param token A cancellation token.
|
|
||||||
* @return A string or a thenable that resolves to such.
|
|
||||||
*/
|
|
||||||
provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable<string>;*/ {
|
|
||||||
var doc = this._logs[uri];
|
|
||||||
if (doc) return this._logs[uri].content;
|
|
||||||
switch (uri.authority) {
|
|
||||||
// android-dev-ext://logcat/read?<deviceid>
|
|
||||||
case 'logcat': return this.provideLogcatDocumentContent(uri);
|
|
||||||
}
|
|
||||||
throw new Error('Document Uri not recognised');
|
|
||||||
}
|
|
||||||
|
|
||||||
provideLogcatDocumentContent(uri) {
|
|
||||||
var doc = this._logs[uri] = new LogcatContent(this, uri);
|
|
||||||
return doc.content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the statics
|
|
||||||
AndroidContentProvider.SCHEME = 'android-dev-ext';
|
|
||||||
AndroidContentProvider.register = (ctx, workspace) => {
|
|
||||||
var provider = new AndroidContentProvider();
|
|
||||||
var registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
|
|
||||||
ctx.subscriptions.push(registration);
|
|
||||||
ctx.subscriptions.push(provider);
|
|
||||||
}
|
|
||||||
AndroidContentProvider.getReadLogcatUri = (deviceId) => {
|
|
||||||
var uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`);
|
|
||||||
return uri.with({
|
|
||||||
query: deviceId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLogcatWindow(vscode) {
|
function openLogcatWindow(vscode) {
|
||||||
new ADBClient().list_devices().then(devices => {
|
new ADBClient().test_adb_connection()
|
||||||
|
.then(err => {
|
||||||
|
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
||||||
|
var adbport = getADBPort();
|
||||||
|
var autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
||||||
|
if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
|
||||||
|
var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||||
|
var adbargs = ['-P',''+adbport,'start-server'];
|
||||||
|
try {
|
||||||
|
/*var stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
||||||
|
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => new ADBClient().list_devices())
|
||||||
|
.then(devices => {
|
||||||
switch(devices.length) {
|
switch(devices.length) {
|
||||||
case 0:
|
case 0:
|
||||||
vscode.window.showInformationMessage('Logcat cannot be displayed. No Android devices are currently connected');
|
vscode.window.showInformationMessage('Logcat cannot be displayed. No Android devices are currently connected');
|
||||||
@@ -322,14 +282,29 @@ function openLogcatWindow(vscode) {
|
|||||||
.then(devices => {
|
.then(devices => {
|
||||||
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
|
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
|
||||||
devices.forEach(device => {
|
devices.forEach(device => {
|
||||||
|
if (vscode.window.createWebviewPanel) {
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'androidlogcat', // Identifies the type of the webview. Used internally
|
||||||
|
`logcat-${device.serial}`, // Title of the panel displayed to the user
|
||||||
|
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
|
||||||
|
{
|
||||||
|
enableScripts: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const logcat = new LogcatContent(device.serial);
|
||||||
|
logcat.content.then(html => {
|
||||||
|
panel.webview.html = html;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
var uri = AndroidContentProvider.getReadLogcatUri(device.serial);
|
var uri = AndroidContentProvider.getReadLogcatUri(device.serial);
|
||||||
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
|
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.fail(e => {
|
.fail((/*e*/) => {
|
||||||
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
|
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.AndroidContentProvider = AndroidContentProvider;
|
exports.LogcatContent = LogcatContent;
|
||||||
exports.openLogcatWindow = openLogcatWindow;
|
exports.openLogcatWindow = openLogcatWindow;
|
||||||
|
|||||||
89
src/nbc.js
Normal file
89
src/nbc.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
// arbitrary precision helper class for 64 bit numbers
|
||||||
|
const NumberBaseConverter = {
|
||||||
|
// Adds two arrays for the given base (10 or 16), returning the result.
|
||||||
|
add(x, y, base) {
|
||||||
|
var z = [], n = Math.max(x.length, y.length), carry = 0, i = 0;
|
||||||
|
while (i < n || carry) {
|
||||||
|
var xi = i < x.length ? x[i] : 0;
|
||||||
|
var yi = i < y.length ? y[i] : 0;
|
||||||
|
var zi = carry + xi + yi;
|
||||||
|
z.push(zi % base);
|
||||||
|
carry = Math.floor(zi / base);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return z;
|
||||||
|
},
|
||||||
|
// Returns a*x, where x is an array of decimal digits and a is an ordinary
|
||||||
|
// JavaScript number. base is the number base of the array x.
|
||||||
|
multiplyByNumber(num, x, base) {
|
||||||
|
if (num < 0) return null;
|
||||||
|
if (num == 0) return [];
|
||||||
|
var result = [], power = x;
|
||||||
|
for(;;) {
|
||||||
|
if (num & 1) {
|
||||||
|
result = this.add(result, power, base);
|
||||||
|
}
|
||||||
|
num = num >> 1;
|
||||||
|
if (num === 0) return result;
|
||||||
|
power = this.add(power, power, base);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
twosComplement(str, base) {
|
||||||
|
const invdigits = str.split('').map(c => base - 1 - parseInt(c,base)).reverse();
|
||||||
|
const negdigits = this.add(invdigits, [1], base).slice(0,str.length);
|
||||||
|
return negdigits.reverse().map(d => d.toString(base)).join('');
|
||||||
|
},
|
||||||
|
convertBase(str, fromBase, toBase) {
|
||||||
|
if (fromBase === 10 && /[eE]/.test(str)) {
|
||||||
|
// convert exponents to a string of zeros
|
||||||
|
var s = str.split(/[eE]/);
|
||||||
|
str = s[0] + '0'.repeat(parseInt(s[1],10)); // works for 0/+ve exponent,-ve throws
|
||||||
|
}
|
||||||
|
var digits = str.split('').map(d => parseInt(d,fromBase)).reverse();
|
||||||
|
var outArray = [], power = [1];
|
||||||
|
for (var i = 0; i < digits.length; i++) {
|
||||||
|
if (digits[i]) {
|
||||||
|
outArray = this.add(outArray, this.multiplyByNumber(digits[i], power, toBase), toBase);
|
||||||
|
}
|
||||||
|
power = this.multiplyByNumber(fromBase, power, toBase);
|
||||||
|
}
|
||||||
|
return outArray.reverse().map(d => d.toString(toBase)).join('');
|
||||||
|
},
|
||||||
|
decToHex(decstr, minlen) {
|
||||||
|
var res, isneg = decstr[0] === '-';
|
||||||
|
if (isneg) decstr = decstr.slice(1)
|
||||||
|
decstr = decstr.match(/^0*(.+)$/)[1]; // strip leading zeros
|
||||||
|
if (decstr.length < 16 && !/[eE]/.test(decstr)) { // 16 = Math.pow(2,52).toString(10).length
|
||||||
|
// less than 52 bits - just use parseInt
|
||||||
|
res = parseInt(decstr, 10).toString(16);
|
||||||
|
} else {
|
||||||
|
res = NumberBaseConverter.convertBase(decstr, 10, 16);
|
||||||
|
}
|
||||||
|
if (isneg) {
|
||||||
|
res = NumberBaseConverter.twosComplement(res, 16);
|
||||||
|
if (/^[0-7]/.test(res)) res = 'f'+res; //msb must be set for -ve numbers
|
||||||
|
} else if (/^[^0-7]/.test(res))
|
||||||
|
res = '0' + res; // msb must not be set for +ve numbers
|
||||||
|
if (minlen && res.length < minlen) {
|
||||||
|
res = (isneg?'f':'0').repeat(minlen - res.length) + res;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
hexToDec(hexstr, signed) {
|
||||||
|
var res, isneg = /^[^0-7]/.test(hexstr);
|
||||||
|
if (hexstr.match(/^0*(.+)$/)[1].length*4 < 52) {
|
||||||
|
// less than 52 bits - just use parseInt
|
||||||
|
res = parseInt(hexstr, 16);
|
||||||
|
if (signed && isneg) res = -res;
|
||||||
|
return res.toString(10);
|
||||||
|
}
|
||||||
|
if (isneg) {
|
||||||
|
hexstr = NumberBaseConverter.twosComplement(hexstr, 16);
|
||||||
|
}
|
||||||
|
res = (isneg ? '-' : '') + NumberBaseConverter.convertBase(hexstr, 16, 10);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(exports, NumberBaseConverter);
|
||||||
195
src/res/logcat.html
Normal file
195
src/res/logcat.html
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<style type="text/css">
|
||||||
|
.V {color:#777} .vscode-light .V {color:#999} .vscode-high-contrast .V {color:#fff}
|
||||||
|
.D {color:#8b8} .vscode-light .D {color:#292} .vscode-high-contrast .D {color:#0a0}
|
||||||
|
.I {color:#99a} .vscode-light .I {color:#557} .vscode-high-contrast .I {color:#aaf}
|
||||||
|
.W {color:#C84} .vscode-light .W {color:#F80} .vscode-high-contrast .W {color:#f80}
|
||||||
|
.E {color:#f88} .vscode-light .E {color:#f55} .vscode-high-contrast .E {color:#f00}
|
||||||
|
.F {color:#f66} .vscode-light .F {color:#f00} .vscode-high-contrast .F {color:#f00}
|
||||||
|
.hide {display:none}
|
||||||
|
.logblock {display:block}
|
||||||
|
.a {display:flex;flex-direction:column;position:absolute;top:0;bottom:0;left:0;right:0;}
|
||||||
|
.b {flex:0 0 auto;border-bottom: 1px solid rgba(128,128,128,.2); padding-bottom: .2em}
|
||||||
|
.vscode-high-contrast .b {border-color: #0cc}
|
||||||
|
.c {flex: 1 1 auto;overflow:auto;padding-top: .2em}
|
||||||
|
.g {margin:.6em .2em 0 .2em;background: none;border: 1px solid #444; color:#888;padding:.3em .8em;font-size: .9em}
|
||||||
|
.g:hover:enabled {color:#ccc;cursor: pointer} .g:focus:enabled,.g:focus:hover:enabled {color:#eee;cursor: pointer}
|
||||||
|
.vscode-light .g:enabled {color: #666;}
|
||||||
|
.vscode-light .g:hover:enabled,.vscode-light .g:focus:enabled,.vscode-light .g:focus:hover:enabled {color: #333;background: #eee;}
|
||||||
|
.vscode-high-contrast .g:enabled {color: #fff;border-color: #0cc;background:none;}
|
||||||
|
.vscode-high-contrast .g:hover:enabled,.vscode-high-contrast .g:focus:enabled {border-color: darkorange;}
|
||||||
|
.h{display: flex;align-items: center;flex-wrap: wrap;}
|
||||||
|
.log { white-space:nowrap }
|
||||||
|
.filter { display:none }
|
||||||
|
body {font-size:.9em}
|
||||||
|
#q {margin:.6em .2em 0 .2em;padding:.3em;width:20em;outline:none;border:solid 1px #444;color: #eee;background:rgba(128,128,128,.05);}
|
||||||
|
#q:hover { border-color: #464; }
|
||||||
|
#q:focus,#q:focus:hover { border-color: #4a4; }
|
||||||
|
.vscode-light #q {color: #333;border-color: #444;background:none;}
|
||||||
|
.vscode-light #q:hover { border-color: #464; }
|
||||||
|
.vscode-light #q:focus,.vscode-light #q:focus:hover { border-color: #4a4; }
|
||||||
|
.vscode-high-contrast #q {color: #fff;border-color: #0cc;background:none;}
|
||||||
|
.vscode-high-contrast #q:hover {border-color: darkorange;}
|
||||||
|
.vscode-high-contrast #q:focus,.vscode-high-contrast #q:focus:hover {border-color: darkorange;}
|
||||||
|
#lcount {font-family:monospace;font-size:1em;margin-top:.4em}
|
||||||
|
.vscode-dark #lcount {color:#484;} .vscode-light #lcount {color:#484;} .vscode-high-contrast #lcount {color:#0d0;}
|
||||||
|
.vscode-dark #status {color:#eee;} .vscode-light #status {color:#333;} .vscode-high-contrast #status {color:#fff;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="a">
|
||||||
|
<div class="b">
|
||||||
|
<div class="h"><input id="q" placeholder="Filter regex"/><button id="clearlcbtn" class="g" disabled="true">Clear logcat</button></div>
|
||||||
|
<div id="lcount"></div>
|
||||||
|
</div>
|
||||||
|
<div id="rc" class="c">
|
||||||
|
<div id="status">!{status}</div>
|
||||||
|
<div id="rows">!{oldlogs}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const getId = document.getElementById.bind(document);
|
||||||
|
const setStatus = (x) => { getId('status').textContent = ''+x; }
|
||||||
|
const start = () => {
|
||||||
|
var rows = getId('rows'), filter = getId('q');
|
||||||
|
var last_known_scroll_position=0, selectall=0, logcount=0, prevlc=0, currfilter,ws;
|
||||||
|
var selecttext = (rows) => {
|
||||||
|
if (!rows) return window.getSelection().empty();
|
||||||
|
var range = document.createRange();
|
||||||
|
range.selectNode(rows);
|
||||||
|
window.getSelection().addRange(range);
|
||||||
|
};
|
||||||
|
getId('clearlcbtn').onclick = (e) => {
|
||||||
|
ws && ws.send('cmd:clear_logcat');
|
||||||
|
}
|
||||||
|
getId('rc').onscroll = (e) => {
|
||||||
|
if ((last_known_scroll_position = e.target.scrollTop)===0) {
|
||||||
|
var hidden = document.getElementsByClassName('hide');
|
||||||
|
for (var i=hidden.length-1; i>=0; i--)
|
||||||
|
hidden[i].className = hidden[i].className.replace(/\bhide\b/g,'').trim();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateLogCountDisplay = () => {
|
||||||
|
var diff = logcount - prevlc;
|
||||||
|
if (diff <= 0 || diff > 100) {
|
||||||
|
prevlc = logcount;
|
||||||
|
var msg = currfilter ? `${currfilter.matchCounts.true}/${logcount}` : logcount;
|
||||||
|
getId('lcount').textContent = msg;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevlc++;
|
||||||
|
var msg = currfilter ? `${currfilter.matchCounts.true}/${prevlc}` : prevlc;
|
||||||
|
getId('lcount').textContent = msg;
|
||||||
|
setTimeout(updateLogCountDisplay, 1);
|
||||||
|
}
|
||||||
|
showFilterErr = (msg) => {
|
||||||
|
filter.style['border-color'] = 'red';
|
||||||
|
}
|
||||||
|
updateFilter = (new_filter_source) => {
|
||||||
|
if (currfilter && currfilter.source === new_filter_source) return; // nothing's changed
|
||||||
|
var newfilter = null;
|
||||||
|
if (new_filter_source) {
|
||||||
|
try {
|
||||||
|
newfilter = new RegExp(new_filter_source, 'i');
|
||||||
|
newfilter.matchCounts = {true:0,false:0};
|
||||||
|
}
|
||||||
|
catch(err) { return showFilterErr('Invalid regular expression') }
|
||||||
|
}
|
||||||
|
// reset the filtered elements
|
||||||
|
var logs = document.getElementsByClassName('log');
|
||||||
|
for (var i=logs.length-1,m; i>=0; i--) {
|
||||||
|
m = newfilter ? newfilter.test(logs[i].textContent) : 1;
|
||||||
|
logs[i].className = logs[i].className.replace(/\bfilter\b|$/g,m?'':' filter').trim();
|
||||||
|
newfilter && newfilter.matchCounts[!!m]++;
|
||||||
|
}
|
||||||
|
currfilter = newfilter;
|
||||||
|
updateLogCountDisplay();
|
||||||
|
}
|
||||||
|
var filter_pause = 0;
|
||||||
|
filter.oninput = (e) => {
|
||||||
|
if (filter_pause++) return;
|
||||||
|
filter.style['border-color'] = '';
|
||||||
|
({
|
||||||
|
wait() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (filter_pause === 1)
|
||||||
|
return filter_pause=0,updateFilter(filter.value);
|
||||||
|
filter_pause = 1;
|
||||||
|
this.wait();
|
||||||
|
},250);
|
||||||
|
}
|
||||||
|
}).wait();
|
||||||
|
};
|
||||||
|
filter.onkeyup = (e) => {
|
||||||
|
// when enter/escape is pressed - lose focus
|
||||||
|
/^(escape|enter)$/i.test(e.key) && filter.blur();
|
||||||
|
}
|
||||||
|
window.addEventListener('keypress', function(e) {
|
||||||
|
if (e.ctrlKey && /[aA]/.test(e.key) && !selectall) {
|
||||||
|
selectall = 1;
|
||||||
|
selecttext(rows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('keyup', function(e) {
|
||||||
|
selectall = 0;
|
||||||
|
/^escape$/i.test(e.key) && selecttext(null);
|
||||||
|
});
|
||||||
|
var connect = () => {
|
||||||
|
try {
|
||||||
|
setStatus('Connecting...');
|
||||||
|
var x = new WebSocket('ws://127.0.0.1:!{wssport}/!{logcatid}');
|
||||||
|
x.onopen = e => {
|
||||||
|
setStatus('');getId('clearlcbtn').disabled = false;ws = x;
|
||||||
|
};
|
||||||
|
x.onclose = e => {
|
||||||
|
ws = null;
|
||||||
|
getId('clearlcbtn').disabled = true;
|
||||||
|
};
|
||||||
|
x.onerror = e => { setStatus('Connection error') }
|
||||||
|
x.onmessage = e => {
|
||||||
|
if (!rows) return;
|
||||||
|
var rawlogs = e.data.trim();
|
||||||
|
if (/^:disconnect$/.test(rawlogs)) {
|
||||||
|
x.close(),setStatus('Device disconnected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/^:logcat_cleared$/.test(rawlogs)) {
|
||||||
|
rows.innerHTML = '';
|
||||||
|
rows.insertAdjacentHTML('afterbegin','<div>---- log cleared ----</div>');
|
||||||
|
logcount = prevlc = 0;
|
||||||
|
if (currfilter) currfilter.matchCounts = {true:0,false:0};
|
||||||
|
updateLogCountDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (last_known_scroll_position > 0)
|
||||||
|
rawlogs = '<div class="hide">'+rawlogs+'</div>';
|
||||||
|
rows.insertAdjacentHTML('afterbegin',rawlogs);
|
||||||
|
var logs = rows.firstElementChild.getElementsByClassName('log');
|
||||||
|
logcount += logs.length;
|
||||||
|
// apply the filter to the newly insert elements
|
||||||
|
if (currfilter) {
|
||||||
|
for (var i=logs.length-1,m; i>=0; i--) {
|
||||||
|
m = currfilter.test(logs[i].textContent);
|
||||||
|
if (!m) logs[i].className += ' filter';
|
||||||
|
currfilter.matchCounts[!!m]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateLogCountDisplay();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch(e) { setStatus('Connection exception') }
|
||||||
|
}
|
||||||
|
!{connected} && connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", function(event) {
|
||||||
|
try { start(); } catch(e) {setStatus('start exception: '+e.message);}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
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;
|
||||||
129
src/threads.js
Normal file
129
src/threads.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { AndroidVariables } = require('./variables');
|
||||||
|
const $ = require('./jq-promise');
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class used to manage a single thread reported by JDWP
|
||||||
|
*/
|
||||||
|
class AndroidThread {
|
||||||
|
constructor(session, threadid, vscode_threadid) {
|
||||||
|
// the AndroidDebugSession instance
|
||||||
|
this.session = session;
|
||||||
|
// the Android debugger instance
|
||||||
|
this.dbgr = session.dbgr;
|
||||||
|
// the java thread id (hex string)
|
||||||
|
this.threadid = threadid;
|
||||||
|
// the vscode thread id (number)
|
||||||
|
this.vscode_threadid = vscode_threadid;
|
||||||
|
// the (Java) name of the thread
|
||||||
|
this.name = null;
|
||||||
|
// the thread break info
|
||||||
|
this.paused = null;
|
||||||
|
// the timeout during a step which, if it expires, we allow other threads to break
|
||||||
|
this.stepTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
threadNotSuspendedError() {
|
||||||
|
return new Error(`Thread ${this.vscode_threadid} not suspended`);
|
||||||
|
}
|
||||||
|
|
||||||
|
addStackFrameVariable(frame, level) {
|
||||||
|
if (!this.paused) throw this.threadNotSuspendedError();
|
||||||
|
var frameId = (this.vscode_threadid * 1e9) + (level * 1e6);
|
||||||
|
var stack_frame_var = {
|
||||||
|
frame, frameId,
|
||||||
|
locals: null,
|
||||||
|
}
|
||||||
|
return this.paused.stack_frame_vars[frameId] = stack_frame_var;
|
||||||
|
}
|
||||||
|
|
||||||
|
allocateExceptionScopeReference(frameId) {
|
||||||
|
if (!this.paused) return;
|
||||||
|
if (!this.paused.last_exception) return;
|
||||||
|
this.paused.last_exception.frameId = frameId;
|
||||||
|
this.paused.last_exception.scopeRef = frameId + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariables(variablesReference) {
|
||||||
|
if (!this.paused)
|
||||||
|
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
||||||
|
|
||||||
|
// is this reference a stack frame
|
||||||
|
var stack_frame_var = this.paused.stack_frame_vars[variablesReference];
|
||||||
|
if (stack_frame_var) {
|
||||||
|
// frame locals request
|
||||||
|
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(varref));
|
||||||
|
}
|
||||||
|
|
||||||
|
// is this refrence an exception scope
|
||||||
|
if (this.paused.last_exception && variablesReference === this.paused.last_exception.scopeRef) {
|
||||||
|
var stack_frame_var = this.paused.stack_frame_vars[this.paused.last_exception.frameId];
|
||||||
|
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(this.paused.last_exception.scopeRef));
|
||||||
|
}
|
||||||
|
|
||||||
|
// work out which stack frame this reference is for
|
||||||
|
var frameId = Math.trunc(variablesReference/1e6) * 1e6;
|
||||||
|
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
||||||
|
|
||||||
|
return stack_frame_var.locals.getVariables(variablesReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureLocals(varinfo) {
|
||||||
|
if (!this.paused)
|
||||||
|
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
||||||
|
|
||||||
|
// evaluate can call this using frameId as the argument
|
||||||
|
if (typeof varinfo === 'number')
|
||||||
|
return this._ensureLocals(this.paused.stack_frame_vars[varinfo]);
|
||||||
|
|
||||||
|
// if we're currently processing it (or we've finished), just return the promise
|
||||||
|
if (this.paused.locals_done[varinfo.frameId])
|
||||||
|
return this.paused.locals_done[varinfo.frameId];
|
||||||
|
|
||||||
|
// create a new promise
|
||||||
|
var def = this.paused.locals_done[varinfo.frameId] = $.Deferred();
|
||||||
|
|
||||||
|
this.dbgr.getlocals(this.threadid, varinfo.frame, {def:def,varinfo:varinfo})
|
||||||
|
.then((locals,x) => {
|
||||||
|
// make sure we are still paused...
|
||||||
|
if (!this.paused)
|
||||||
|
throw this.threadNotSuspendedError();
|
||||||
|
|
||||||
|
// sort the locals by name, except for 'this' which always goes first
|
||||||
|
locals.sort((a,b) => {
|
||||||
|
if (a.name === b.name) return 0;
|
||||||
|
if (a.name === 'this') return -1;
|
||||||
|
if (b.name === 'this') return +1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})
|
||||||
|
|
||||||
|
// create a new local variable with the results and resolve the promise
|
||||||
|
var varinfo = x.varinfo;
|
||||||
|
varinfo.cached = locals;
|
||||||
|
x.varinfo.locals = new AndroidVariables(this.session, x.varinfo.frameId + 2); // 0 = stack frame, 1 = exception, 2... others
|
||||||
|
x.varinfo.locals.setVariable(varinfo.frameId, varinfo);
|
||||||
|
|
||||||
|
var last_exception = this.paused.last_exception;
|
||||||
|
if (last_exception) {
|
||||||
|
x.varinfo.locals.setVariable(last_exception.scopeRef, last_exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
x.def.resolveWith(this, [varinfo.frameId]);
|
||||||
|
})
|
||||||
|
.fail(e => {
|
||||||
|
x.def.rejectWith(this, [e]);
|
||||||
|
})
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariableValue(args) {
|
||||||
|
var frameId = Math.trunc(args.variablesReference/1e6) * 1e6;
|
||||||
|
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
||||||
|
return this._ensureLocals(stack_frame_var).then(varref => {
|
||||||
|
return this.paused.stack_frame_vars[varref].locals.setVariableValue(args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.AndroidThread = AndroidThread;
|
||||||
18
src/util.js
18
src/util.js
@@ -1,13 +1,18 @@
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
var nofn = function () { };
|
var nofn = function () { };
|
||||||
var D=exports.D=console.log.bind(console);
|
const messagePrintCallbacks = new Set();
|
||||||
var E=exports.E=console.error.bind(console);
|
var D = exports.D = (...args) => (console.log(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||||
var W=exports.W=console.warn.bind(console);
|
var E = exports.E = (...args) => (console.error(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||||
|
var W = exports.W = (...args) => (console.warn(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||||
var DD = nofn, cl = D, printf = D;
|
var DD = nofn, cl = D, printf = D;
|
||||||
var print_jdwp_data = nofn;// _print_jdwp_data;
|
var print_jdwp_data = nofn;// _print_jdwp_data;
|
||||||
var print_packet = nofn;//_print_packet;
|
var print_packet = nofn;//_print_packet;
|
||||||
|
|
||||||
|
exports.onMessagePrint = function(cb) {
|
||||||
|
messagePrintCallbacks.add(cb);
|
||||||
|
}
|
||||||
|
|
||||||
Array.first = function (arr, fn, defaultvalue) {
|
Array.first = function (arr, fn, defaultvalue) {
|
||||||
var idx = Array.indexOfFirst(arr, fn);
|
var idx = Array.indexOfFirst(arr, fn);
|
||||||
return idx < 0 ? defaultvalue : arr[idx];
|
return idx < 0 ? defaultvalue : arr[idx];
|
||||||
@@ -551,8 +556,7 @@ exports.fromutf8bytes = function(array) {
|
|||||||
i = 0;
|
i = 0;
|
||||||
while (i < len) {
|
while (i < len) {
|
||||||
c = array[i++];
|
c = array[i++];
|
||||||
switch(c >> 4)
|
switch (c >> 4) {
|
||||||
{
|
|
||||||
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
|
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
|
||||||
// 0xxxxxxx
|
// 0xxxxxxx
|
||||||
out += String.fromCharCode(c);
|
out += String.fromCharCode(c);
|
||||||
@@ -623,9 +627,9 @@ exports.dumparr = function(arr, offset, count) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.btoa = function (arr) {
|
exports.btoa = function (arr) {
|
||||||
return new Buffer(arr,'binary').toString('base64');
|
return Buffer.from(arr, 'binary').toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.atob = function (base64) {
|
exports.atob = function (base64) {
|
||||||
return new Buffer(base64, 'base64').toString('binary');
|
return Buffer.from(base64, 'base64').toString('binary');
|
||||||
}
|
}
|
||||||
|
|||||||
410
src/variables.js
Normal file
410
src/variables.js
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { JTYPES, exmsg_var_name, createJavaString } = require('./globals');
|
||||||
|
const NumberBaseConverter = require('./nbc');
|
||||||
|
const $ = require('./jq-promise');
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class used to manage stack frame locals and other evaluated expressions
|
||||||
|
*/
|
||||||
|
class AndroidVariables {
|
||||||
|
|
||||||
|
constructor(session, baseId) {
|
||||||
|
this.session = session;
|
||||||
|
this.dbgr = session.dbgr;
|
||||||
|
// the incremental reference id generator for stack frames, locals, etc
|
||||||
|
this.nextId = baseId;
|
||||||
|
// hashmap of variables and frames
|
||||||
|
this.variableHandles = {};
|
||||||
|
// hashmap<objectid, variablesReference>
|
||||||
|
this.objIdCache = {};
|
||||||
|
// allow primitives to be expanded to show more info
|
||||||
|
this._expandable_prims = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVariable(varinfo) {
|
||||||
|
var variablesReference = ++this.nextId;
|
||||||
|
this.variableHandles[variablesReference] = varinfo;
|
||||||
|
return variablesReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.variableHandles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariable(variablesReference, varinfo) {
|
||||||
|
this.variableHandles[variablesReference] = varinfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getObjectIdReference(type, objvalue) {
|
||||||
|
// we need the type signature because we must have different id's for
|
||||||
|
// an instance and it's supertype instance (which obviously have the same objvalue)
|
||||||
|
var key = type.signature + objvalue;
|
||||||
|
return this.objIdCache[key] || (this.objIdCache[key] = ++this.nextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariables(variablesReference) {
|
||||||
|
var varinfo = this.variableHandles[variablesReference];
|
||||||
|
if (!varinfo) {
|
||||||
|
return $.Deferred().resolve([]);
|
||||||
|
}
|
||||||
|
else if (varinfo.cached) {
|
||||||
|
return $.Deferred().resolve(this._local_to_variable(varinfo.cached));
|
||||||
|
}
|
||||||
|
else if (varinfo.objvar) {
|
||||||
|
// object fields request
|
||||||
|
return this.dbgr.getsupertype(varinfo.objvar, {varinfo})
|
||||||
|
.then((supertype, x) => {
|
||||||
|
x.supertype = supertype;
|
||||||
|
return this.dbgr.getfieldvalues(x.varinfo.objvar, x);
|
||||||
|
})
|
||||||
|
.then((fields, x) => {
|
||||||
|
// add an extra msg field for exceptions
|
||||||
|
if (!x.varinfo.exception) return;
|
||||||
|
x.fields = fields;
|
||||||
|
return this.dbgr.invokeToString(x.varinfo.objvar.value, x.varinfo.threadid, varinfo.objvar.type.signature, x)
|
||||||
|
.then((call,x) => {
|
||||||
|
call.name = exmsg_var_name;
|
||||||
|
x.fields.unshift(call);
|
||||||
|
return $.Deferred().resolveWith(this, [x.fields, x]);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((fields, x) => {
|
||||||
|
// ignore supertypes of Object
|
||||||
|
x.supertype && x.supertype.signature!=='Ljava/lang/Object;' && fields.unshift({
|
||||||
|
vtype:'super',
|
||||||
|
name:':super',
|
||||||
|
hasnullvalue:false,
|
||||||
|
type: x.supertype,
|
||||||
|
value: x.varinfo.objvar.value,
|
||||||
|
valid:true,
|
||||||
|
});
|
||||||
|
// create the fully qualified names to use for evaluation
|
||||||
|
fields.forEach(f => f.fqname = `${x.varinfo.objvar.fqname || x.varinfo.objvar.name}.${f.name}`);
|
||||||
|
x.varinfo.cached = fields;
|
||||||
|
return this._local_to_variable(fields);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (varinfo.arrvar) {
|
||||||
|
// array elements request
|
||||||
|
var range = varinfo.range, count = range[1] - range[0];
|
||||||
|
// should always have a +ve count, but just in case...
|
||||||
|
if (count <= 0) return $.Deferred().resolve([]);
|
||||||
|
// add some hysteresis
|
||||||
|
if (count > 110) {
|
||||||
|
// create subranges in the sub-power of 10
|
||||||
|
var subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100), variables = [];
|
||||||
|
for (var i=range[0],varref,v; i < range[1]; i+= subrangelen) {
|
||||||
|
varref = ++this.nextId;
|
||||||
|
v = this.variableHandles[varref] = { varref:varref, arrvar:varinfo.arrvar, range:[i, Math.min(i+subrangelen, range[1])] };
|
||||||
|
variables.push({name:`[${v.range[0]}..${v.range[1]-1}]`,type:'',value:'',variablesReference:varref});
|
||||||
|
}
|
||||||
|
return $.Deferred().resolve(variables);
|
||||||
|
}
|
||||||
|
// get the elements for the specified range
|
||||||
|
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count, {varinfo})
|
||||||
|
.then((elements, x) => {
|
||||||
|
elements.forEach(el => el.fqname = `${x.varinfo.arrvar.fqname || x.varinfo.arrvar.name}[${el.name}]`);
|
||||||
|
x.varinfo.cached = elements;
|
||||||
|
return this._local_to_variable(elements);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (varinfo.bigstring) {
|
||||||
|
return this.dbgr.getstringchars(varinfo.bigstring.value)
|
||||||
|
.then((s) => {
|
||||||
|
return this._local_to_variable([{name:'<value>',hasnullvalue:false,string:s,type:JTYPES.String,valid:true}]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (varinfo.primitive) {
|
||||||
|
// convert the primitive value into alternate formats
|
||||||
|
var variables = [], bits = {J:64,I:32,S:16,B:8}[varinfo.signature];
|
||||||
|
const pad = (u,base,len) => ('0000000000000000000000000000000'+u.toString(base)).slice(-len);
|
||||||
|
switch(varinfo.signature) {
|
||||||
|
case 'Ljava/lang/String;':
|
||||||
|
variables.push({name:'<length>',type:'',value:varinfo.value.toString(),variablesReference:0});
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
variables.push({name:'<charCode>',type:'',value:varinfo.value.charCodeAt(0).toString(),variablesReference:0});
|
||||||
|
break;
|
||||||
|
case 'J':
|
||||||
|
// because JS cannot handle 64bit ints, we need a bit of extra work
|
||||||
|
var v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,'');
|
||||||
|
const s4 = { hi:parseInt(v64hex.slice(0,8),16), lo:parseInt(v64hex.slice(-8),16) };
|
||||||
|
variables.push(
|
||||||
|
{name:'<binary>',type:'',value:pad(s4.hi,2,32)+pad(s4.lo,2,32),variablesReference:0}
|
||||||
|
,{name:'<decimal>',type:'',value:NumberBaseConverter.hexToDec(v64hex,false),variablesReference:0}
|
||||||
|
,{name:'<hex>',type:'',value:pad(s4.hi,16,8)+pad(s4.lo,16,8),variablesReference:0}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:// integer/short/byte value
|
||||||
|
const u = varinfo.value >>> 0;
|
||||||
|
variables.push(
|
||||||
|
{name:'<binary>',type:'',value:pad(u,2,bits),variablesReference:0}
|
||||||
|
,{name:'<decimal>',type:'',value:u.toString(10),variablesReference:0}
|
||||||
|
,{name:'<hex>',type:'',value:pad(u,16,bits/4),variablesReference:0}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $.Deferred().resolve(variables);
|
||||||
|
}
|
||||||
|
else if (varinfo.frame) {
|
||||||
|
// frame locals request - this should be handled by AndroidDebugThread instance
|
||||||
|
return $.Deferred().resolve([]);
|
||||||
|
} else {
|
||||||
|
// something else?
|
||||||
|
return $.Deferred().resolve([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts locals (or other vars) in debugger format into Variable objects used by VSCode
|
||||||
|
*/
|
||||||
|
_local_to_variable(v) {
|
||||||
|
if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._local_to_variable(v));
|
||||||
|
var varref = 0, objvalue, evaluateName = v.fqname || v.name, formats = {}, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
|
||||||
|
switch(true) {
|
||||||
|
case v.hasnullvalue && JTYPES.isReference(v.type):
|
||||||
|
// null object or array type
|
||||||
|
objvalue = 'null';
|
||||||
|
break;
|
||||||
|
case v.type.signature === JTYPES.Object.signature:
|
||||||
|
// Object doesn't really have anything worth seeing, so just treat it as unexpandable
|
||||||
|
objvalue = v.type.typename;
|
||||||
|
break;
|
||||||
|
case v.type.signature === JTYPES.String.signature:
|
||||||
|
objvalue = JSON.stringify(v.string);
|
||||||
|
if (v.biglen) {
|
||||||
|
// since this is a big string - make it viewable on expand
|
||||||
|
varref = ++this.nextId;
|
||||||
|
this.variableHandles[varref] = {varref:varref, bigstring:v};
|
||||||
|
objvalue = `String (length:${v.biglen})`;
|
||||||
|
}
|
||||||
|
else if (this._expandable_prims) {
|
||||||
|
// as a courtesy, allow strings to be expanded to see their length
|
||||||
|
varref = ++this.nextId;
|
||||||
|
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.string.length};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case JTYPES.isArray(v.type):
|
||||||
|
// non-null array type - if it's not zero-length add another variable reference so the user can expand
|
||||||
|
if (v.arraylen) {
|
||||||
|
varref = this._getObjectIdReference(v.type, v.value);
|
||||||
|
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
|
||||||
|
}
|
||||||
|
objvalue = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
|
||||||
|
break;
|
||||||
|
case JTYPES.isObject(v.type):
|
||||||
|
// non-null object instance - add another variable reference so the user can expand
|
||||||
|
varref = this._getObjectIdReference(v.type, v.value);
|
||||||
|
this.variableHandles[varref] = {varref:varref, objvar:v};
|
||||||
|
objvalue = v.type.typename;
|
||||||
|
break;
|
||||||
|
case v.type.signature === 'C':
|
||||||
|
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'};
|
||||||
|
if (cmap[v.char]) {
|
||||||
|
objvalue = `'\\${cmap[v.char]}'`;
|
||||||
|
} else if (v.value < 32) {
|
||||||
|
objvalue = v.value ? `'\\u${('000'+v.value.toString(16)).slice(-4)}'` : "'\\0'";
|
||||||
|
} else objvalue = `'${v.char}'`;
|
||||||
|
break;
|
||||||
|
case v.type.signature === 'J':
|
||||||
|
// because JS cannot handle 64bit ints, we need a bit of extra work
|
||||||
|
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
|
||||||
|
objvalue = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
|
||||||
|
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
|
||||||
|
formats.oct = formats.bin = '';
|
||||||
|
// 24 bit chunks...
|
||||||
|
for (var s=v64hex,uint; s; s = s.slice(0,-6)) {
|
||||||
|
uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
|
||||||
|
formats.oct = uint.toString(8) + formats.oct;
|
||||||
|
formats.bin = uint.toString(2) + formats.bin;
|
||||||
|
}
|
||||||
|
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
|
||||||
|
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
|
||||||
|
break;
|
||||||
|
case /^[BIS]$/.test(v.type.signature):
|
||||||
|
objvalue = formats.dec = v.value.toString();
|
||||||
|
var uint = (v.value >>> 0);
|
||||||
|
formats.hex = '0x' + uint.toString(16);
|
||||||
|
formats.oct = '0c' + uint.toString(8);
|
||||||
|
formats.bin = '0b' + uint.toString(2);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// other primitives: boolean, etc
|
||||||
|
objvalue = v.value.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
|
||||||
|
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
|
||||||
|
varref = ++this.nextId;
|
||||||
|
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.value};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: v.name,
|
||||||
|
type: typename,
|
||||||
|
value: objvalue,
|
||||||
|
evaluateName,
|
||||||
|
variablesReference: varref,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariableValue(args) {
|
||||||
|
const failSetVariableRequest = reason => $.Deferred().reject(new Error(reason));
|
||||||
|
|
||||||
|
var v = this.variableHandles[args.variablesReference];
|
||||||
|
if (!v || !v.cached) {
|
||||||
|
return failSetVariableRequest(`Variable '${args.name}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
var destvar = v.cached.find(v => v.name===args.name);
|
||||||
|
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
|
||||||
|
return failSetVariableRequest(`The value is read-only and cannot be updated.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// be nice and remove any superfluous whitespace
|
||||||
|
var value = args.value.trim();
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
// just ignore blank requests
|
||||||
|
var vsvar = this._local_to_variable(destvar);
|
||||||
|
return $.Deferred().resolve(vsvar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-string reference types can only set to null
|
||||||
|
if (/^L/.test(destvar.type.signature) && destvar.type.signature !== JTYPES.String.signature) {
|
||||||
|
if (value !== 'null') {
|
||||||
|
return failSetVariableRequest('Object references can only be set to null');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert the new value into a debugger-compatible object
|
||||||
|
var m, num, data, datadef;
|
||||||
|
switch(true) {
|
||||||
|
case value === 'null':
|
||||||
|
data = {valuetype:'oref',value:null}; // null object reference
|
||||||
|
break;
|
||||||
|
case /^(true|false)$/.test(value):
|
||||||
|
data = {valuetype:'boolean',value:value!=='false'}; // boolean literal
|
||||||
|
break;
|
||||||
|
case !!(m=value.match(/^[+-]?0x([0-9a-f]+)$/i)):
|
||||||
|
// hex integer- convert to decimal and fall through
|
||||||
|
if (m[1].length < 52/4)
|
||||||
|
value = parseInt(value, 16).toString(10);
|
||||||
|
else
|
||||||
|
value = NumberBaseConverter.hexToDec(value);
|
||||||
|
m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/);
|
||||||
|
// fall-through
|
||||||
|
case !!(m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/)):
|
||||||
|
// decimal integer
|
||||||
|
num = parseFloat(value, 10); // parseInt() can't handle exponents
|
||||||
|
switch(true) {
|
||||||
|
case (num >= -128 && num <= 127): data = {valuetype:'byte',value:num}; break;
|
||||||
|
case (num >= -32768 && num <= 32767): data = {valuetype:'short',value:num}; break;
|
||||||
|
case (num >= -2147483648 && num <= 2147483647): data = {valuetype:'int',value:num}; break;
|
||||||
|
case /inf/i.test(num): return failSetVariableRequest(`Value '${value}' exceeds the maximum number range.`);
|
||||||
|
case /^[FD]$/.test(destvar.type.signature): data = {valuetype:'float',value:num}; break;
|
||||||
|
default:
|
||||||
|
// long (or larger) - need to use the arbitrary precision class
|
||||||
|
data = {valuetype:'long',value:NumberBaseConverter.decToHex(value, 16)};
|
||||||
|
switch(true){
|
||||||
|
case data.value.length > 16:
|
||||||
|
case num > 0 && data.value.length===16 && /[^0-7]/.test(data.value[0]):
|
||||||
|
// number exceeds signed 63 bit - make it a float
|
||||||
|
data = {valuetype:'float',value:num};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case !!(m=value.match(/^(Float|Double)\s*\.\s*(POSITIVE_INFINITY|NEGATIVE_INFINITY|NaN)$/)):
|
||||||
|
// Java special float constants
|
||||||
|
data = {valuetype:m[1].toLowerCase(),value:{POSITIVE_INFINITY:Infinity,NEGATIVE_INFINITY:-Infinity,NaN:NaN}[m[2]]};
|
||||||
|
break;
|
||||||
|
case !!(m=value.match(/^([+-])?infinity$/i)):// allow js infinity
|
||||||
|
data = {valuetype:'float',value:m[1]!=='-'?Infinity:-Infinity};
|
||||||
|
break;
|
||||||
|
case !!(m=value.match(/^nan$/i)): // allow js nan
|
||||||
|
data = {valuetype:'float',value:NaN};
|
||||||
|
break;
|
||||||
|
case !!(m=value.match(/^[+-]?[0-9]+[eE][-][0-9]+([dDfF])?$/)):
|
||||||
|
case !!(m=value.match(/^[+-]?[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?([dDfF])?$/)):
|
||||||
|
// decimal float
|
||||||
|
num = parseFloat(value);
|
||||||
|
data = {valuetype:/^[dD]$/.test(m[1]) ? 'double': 'float',value:num};
|
||||||
|
break;
|
||||||
|
case !!(m=value.match(/^'(?:\\u([0-9a-fA-F]{4})|\\([bfrntv0'])|(.))'$/)):
|
||||||
|
// character literal
|
||||||
|
var cvalue = m[1] ? String.fromCharCode(parseInt(m[1],16)) :
|
||||||
|
m[2] ? {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v',0:'\0',"'":"'"}[m[2]]
|
||||||
|
: m[3]
|
||||||
|
data = {valuetype:'char',value:cvalue};
|
||||||
|
break;
|
||||||
|
case !!(m=value.match(/^"[^"\\\n]*(\\.[^"\\\n]*)*"$/)):
|
||||||
|
// string literal - we need to get the runtime to create a new string first
|
||||||
|
datadef = createJavaString(this.dbgr, value).then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// invalid literal
|
||||||
|
return failSetVariableRequest(`'${value}' is not a valid Java literal.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!datadef) {
|
||||||
|
// as a nicety, if the destination is a string, stringify any primitive value
|
||||||
|
if (data.valuetype !== 'oref' && destvar.type.signature === JTYPES.String.signature) {
|
||||||
|
datadef = createJavaString(this.dbgr, data.value.toString(), {israw:true})
|
||||||
|
.then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
||||||
|
} else if (destvar.type.signature.length===1) {
|
||||||
|
// if the destination is a primitive, we need to range-check it here
|
||||||
|
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
|
||||||
|
// weirdness if we allow primitives to be set with out-of-range values
|
||||||
|
var validmap = {
|
||||||
|
B:'byte,char', // char may not fit - we special-case this later
|
||||||
|
S:'byte,short,char',
|
||||||
|
I:'byte,short,int,char',
|
||||||
|
J:'byte,short,int,long,char',
|
||||||
|
F:'byte,short,int,long,char,float',
|
||||||
|
D:'byte,short,int,long,char,double,float',
|
||||||
|
C:'byte,short,char',Z:'boolean',
|
||||||
|
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
|
||||||
|
};
|
||||||
|
var is_in_range = (validmap[destvar.type.signature]||'').indexOf(data.valuetype) >= 0;
|
||||||
|
if (destvar.type.signature === 'B' && data.valuetype === 'char')
|
||||||
|
is_in_range = validmap.isCharInRangeForByte(data.value);
|
||||||
|
if (!is_in_range) {
|
||||||
|
return failSetVariableRequest(`'${value}' is not compatible with variable type: ${destvar.type.typename}`);
|
||||||
|
}
|
||||||
|
// check complete - make sure the type matches the destination and use a resolved deferred with the value
|
||||||
|
if (destvar.type.signature!=='C' && data.valuetype === 'char')
|
||||||
|
data.value = data.value.charCodeAt(0); // convert char to it's int value
|
||||||
|
if (destvar.type.signature==='J' && typeof data.value === 'number')
|
||||||
|
data.value = NumberBaseConverter.decToHex(''+data.value,16); // convert ints to hex-string longs
|
||||||
|
data.valuetype = destvar.type.typename;
|
||||||
|
|
||||||
|
datadef = $.Deferred().resolveWith(this,[data]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return datadef.then(data => {
|
||||||
|
// setxxxvalue sets the new value and then returns a new local for the variable
|
||||||
|
switch(destvar.vtype) {
|
||||||
|
case 'field': return this.dbgr.setfieldvalue(destvar, data);
|
||||||
|
case 'local': return this.dbgr.setlocalvalue(destvar, data);
|
||||||
|
case 'arrelem':
|
||||||
|
var idx = parseInt(args.name, 10), count=1;
|
||||||
|
if (idx < 0 || idx >= destvar.data.arrobj.arraylen) throw new Error('Array index out of bounds');
|
||||||
|
return this.dbgr.setarrayvalues(destvar.data.arrobj, idx, count, data);
|
||||||
|
default: throw new Error('Unsupported variable type');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(newlocalvar => {
|
||||||
|
if (destvar.vtype === 'arrelem') newlocalvar = newlocalvar[0];
|
||||||
|
Object.assign(destvar, newlocalvar);
|
||||||
|
var vsvar = this._local_to_variable(destvar);
|
||||||
|
return vsvar;
|
||||||
|
})
|
||||||
|
.fail(e => {
|
||||||
|
return failSetVariableRequest(`Variable update failed. ${e.message||''}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.AndroidVariables = AndroidVariables;
|
||||||
Reference in New Issue
Block a user