41 Commits

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

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

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

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

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

* add support for WebviewPanel for displaying logcat data

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

* perform a case-insensitve path search for packages

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

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

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

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

* add trace config setting

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

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

View File

@@ -1,5 +1,43 @@
# Change Log # Change Log
### version 0.7.1
* Added the [Buy Me A Coffee](https://www.buymeacoffee.com/adelphes) link to the README
### version 0.7.0
* Fix logcat not displaying
* Fix breakpoints not triggering on Windows
* Added kotlin folder to list of known source locations
* Upgraded dependencies to resolve a number of security vulnerabilites
* Updated README with info about prelaunch build task
* Added MIT license file
### version 0.6.2
* Fix broken logcat command due to missing dependency
### version 0.6.1
* Regenerate package-lock.json to remove event-stream vulnerability - https://github.com/dominictarr/event-stream/issues/116
### version 0.6.0
* Fix issue with breakpoints not enabling correctly
* Fix issue with JDWP failure on breakpoint hit
* Added support for diagnostic logs using trace configuration option
* Updated default apkFile path to match current releases of Android Studio
* Updated package dependencies
### version 0.5.0
* Debugger support for Kotlin source files
* Exception UI
* Fixed some console display issues
### version 0.4.1
* One day I will learn to update the changelog **before** I hit publish
* Updated changelog
### version 0.4.0
* Debugger performance improvements
* Fixed exception details not being displayed in locals
* Fixed some logcat display issues
### version 0.3.1 ### version 0.3.1
* Bug fixes * Bug fixes
* Fix issue with exception breaks crashing debugger * Fix issue with exception breaks crashing debugger

21
LICENSE Normal file
View File

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

View File

@@ -20,7 +20,7 @@ You must have [Android SDK Platform Tools](https://developer.android.com/studio/
* This is a preview version so expect the unexpected. Please log any issues you find on [GitHub](https://github.com/adelphes/android-dev-ext/issues). * This 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,6 +54,52 @@ 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"]
}
]
}
```
## Powered by coffee
The Android Developer Extension is a completely free, fully open-source project. If you've found the extension useful, you
can support it by [buying me a coffee](https://www.buymeacoffee.com/adelphes).
If you use ApplePay or Google Pay, you can scan the code with your phone camera:
![BuyMeACoffee Code](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/bmac-code.png)
Every coffee makes a difference, so thanks for adding your support.
## Questions / Problems ## 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).

View File

@@ -3,6 +3,7 @@
const vscode = require('vscode'); const vscode = require('vscode');
const { AndroidContentProvider } = require('./src/contentprovider'); const { AndroidContentProvider } = require('./src/contentprovider');
const { openLogcatWindow } = require('./src/logcat'); const { openLogcatWindow } = require('./src/logcat');
const state = require('./src/state');
function getADBPort() { function getADBPort() {
var defaultPort = 5037; var defaultPort = 5037;
@@ -38,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

BIN
images/bmac-code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

2695
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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.4.0", "version": "0.7.1",
"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,7 +62,7 @@
"apkFile": { "apkFile": {
"type": "string", "type": "string",
"description": "Fully qualified path to the built APK (Android Application Package)", "description": "Fully qualified path to the built APK (Android Application Package)",
"default": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk" "default": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk"
}, },
"adbPort": { "adbPort": {
"type": "integer", "type": "integer",
@@ -90,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"
} }
} }
} }
@@ -97,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
} }
], ],
@@ -113,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
} }
} }
@@ -123,23 +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",
"long": "^3.2.0", "vscode-debugadapter": "^1.32.0",
"ws": "^1.1.1", "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"
} }
} }

View File

@@ -44,7 +44,7 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
provideLogcatDocumentContent(uri) { provideLogcatDocumentContent(uri) {
// LogcatContent depends upon AndroidContentProvider, so we must delay-load this // LogcatContent depends upon AndroidContentProvider, so we must delay-load this
const { LogcatContent } = require('./logcat'); const { LogcatContent } = require('./logcat');
var doc = this._docs[uri] = new LogcatContent(this, uri); var doc = this._docs[uri] = new LogcatContent(uri.query);
return doc.content; return doc.content;
} }
} }

View File

@@ -1,14 +1,14 @@
'use strict' 'use strict'
const { const {
DebugSession, DebugSession,
ContinuedEvent, InitializedEvent, ExitedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent, Event, InitializedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent,
Thread, StackFrame, Scope, Source, Handles, Breakpoint } = require('vscode-debugadapter'); Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter');
// node and external modules // node and external modules
const crypto = require('crypto'); const crypto = require('crypto');
const dom = require('xmldom').DOMParser; const dom = require('xmldom').DOMParser;
const fs = require('fs'); const fs = require('fs');
const Long = require('long'); const os = require('os');
const path = require('path'); const path = require('path');
const xpath = require('xpath'); const xpath = require('xpath');
@@ -16,12 +16,12 @@ const xpath = require('xpath');
const { ADBClient } = require('./adbclient'); const { ADBClient } = require('./adbclient');
const { Debugger } = require('./debugger'); const { Debugger } = require('./debugger');
const $ = require('./jq-promise'); const $ = require('./jq-promise');
const NumberBaseConverter = require('./nbc');
const { AndroidThread } = require('./threads'); const { AndroidThread } = require('./threads');
const { D, isEmptyObject } = require('./util'); const { D, onMessagePrint, isEmptyObject } = require('./util');
const { AndroidVariables } = require('./variables'); const { AndroidVariables } = require('./variables');
const { evaluate } = require('./expressions');
const ws_proxy = require('./wsproxy').proxy.Server(6037, 5037); const ws_proxy = require('./wsproxy').proxy.Server(6037, 5037);
const { JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString } = require('./globals'); const { exmsg_var_name, signatureToFullyQualifiedType, ensure_path_end_slash,is_subpath_of,variableRefToThreadId } = require('./globals');
class AndroidDebugSession extends DebugSession { class AndroidDebugSession extends DebugSession {
@@ -73,6 +73,9 @@ class AndroidDebugSession extends DebugSession {
// flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests // flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests
this._isDisconnecting = false; this._isDisconnecting = false;
// trace flag for printing diagnostic messages to the client Output Window
this.trace = false;
// this debugger uses one-based lines and columns // this debugger uses one-based lines and columns
this.setDebuggerLinesStartAt1(true); this.setDebuggerLinesStartAt1(true);
this.setDebuggerColumnsStartAt1(true); this.setDebuggerColumnsStartAt1(true);
@@ -82,7 +85,7 @@ class AndroidDebugSession extends DebugSession {
* The 'initialize' request is the first request called by the frontend * The 'initialize' request is the first request called by the frontend
* to interrogate the features the debug adapter provides. * to interrogate the features the debug adapter provides.
*/ */
initializeRequest(response/*: DebugProtocol.InitializeResponse*/, args/*: DebugProtocol.InitializeRequestArguments*/) { initializeRequest(response/*: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments*/) {
// This debug adapter implements the configurationDoneRequest. // This debug adapter implements the configurationDoneRequest.
response.body.supportsConfigurationDoneRequest = true; response.body.supportsConfigurationDoneRequest = true;
@@ -99,22 +102,39 @@ class AndroidDebugSession extends DebugSession {
// we support hit-count conditional breakpoints // we support hit-count conditional breakpoints
response.body.supportsHitConditionalBreakpoints = true; response.body.supportsHitConditionalBreakpoints = true;
// we support the new ExceptionInfoRequest
response.body.supportsExceptionInfoRequest = true;
this.sendResponse(response); this.sendResponse(response);
} }
LOG(msg) { LOG(msg) {
if (!this.trace) {
D(msg); D(msg);
this.sendEvent(new OutputEvent(msg)); }
// VSCode no longer auto-newlines output
this.sendEvent(new OutputEvent(msg + os.EOL));
} }
WARN(msg) { WARN(msg) {
D(msg = 'Warning: '+msg); D(msg = 'Warning: '+msg);
this.sendEvent(new OutputEvent(msg)); // the message will already be sent if trace is enabled
if (!this.trace) {
this.sendEvent(new OutputEvent(msg + os.EOL));
}
} }
failRequest(msg, response) { failRequest(msg, response) {
// yeah, it can happen sometimes... // yeah, it can happen sometimes...
this.WARN(msg); msg && this.WARN(msg);
if (response) {
response.success = false;
this.sendResponse(response);
}
}
cancelRequest(msg, response) {
D(msg); // just log it in debug - don't output it to the client
if (response) { if (response) {
response.success = false; response.success = false;
this.sendResponse(response); this.sendResponse(response);
@@ -129,6 +149,11 @@ class AndroidDebugSession extends DebugSession {
this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response); this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response);
} }
cancelRequestThreadNotSuspended(requestName, threadId, response) {
// now that vscode can resume threads before the locals,callstack,etc are retrieved, we only need to cancel the request
this.cancelRequest(`${requestName} cancelled. Thread ${threadId} is not suspended`, response);
}
getThread(id) { getThread(id) {
var t; var t;
switch(typeof id) { switch(typeof id) {
@@ -206,6 +231,10 @@ class AndroidDebugSession extends DebugSession {
} }
launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) { launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
if (args && args.trace) {
this.trace = args.trace;
onMessagePrint(this.LOG.bind(this));
}
try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {} try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {}
// app_src_root must end in a path-separator for correct validation of sub-paths // app_src_root must end in a path-separator for correct validation of sub-paths
@@ -343,6 +372,9 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response); this.sendResponse(response);
return this.dbgr.resume(); return this.dbgr.resume();
}) })
.then(() => {
this.LOG('Application started');
})
.fail(e => { .fail(e => {
// exceptions use message, adbclient uses msg // exceptions use message, adbclient uses msg
this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available')); this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available'));
@@ -451,18 +483,18 @@ class AndroidDebugSession extends DebugSession {
} }
catch (err) { continue } catch (err) { continue }
// ignore folders not starting with a known top-level Android folder // ignore folders not starting with a known top-level Android folder
if (!/^(assets|res|src|main|java)([\\/]|$)/.test(p)) continue; if (!/^(assets|res|src|main|java|kotlin)([\\/]|$)/.test(p)) continue;
// is this a package folder // is this a package folder
var pkgmatch = p.match(/^(src|main|java)[\\/](.+)/); var pkgmatch = p.match(/^(src|main|java|kotlin)[\\/](.+)/);
if (pkgmatch && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(pkgmatch[2].split(/[\\/]/).pop())) { if (pkgmatch && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(pkgmatch[2].split(/[\\/]/).pop())) {
// looks good - add it to the list // looks good - add it to the list
const src_folder = pkgmatch[1]; // src, main or java const src_folder = pkgmatch[1]; // src, main, java or kotlin
const pkgname = pkgmatch[2].replace(/[\\/]/g,'.'); const pkgname = pkgmatch[2].replace(/[\\/]/g,'.');
src_packages.packages[pkgname] = { src_packages.packages[pkgname] = {
package: pkgname, package: pkgname,
package_path: fpn, package_path: fpn,
srcroot: path.join(app_root,src_folder), srcroot: path.join(app_root,src_folder),
public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.java$/.test(sf)).map(sf => sf.match(/^(.*)\.java$/)[1]) public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.(?:java|kt)$/.test(sf)).map(sf => sf.match(/^(.*)\.(?:java|kt)$/)[1])
} }
} }
// add the subfiles to the list to process // add the subfiles to the list to process
@@ -503,7 +535,8 @@ class AndroidDebugSession extends DebugSession {
}) })
} }
configurationDoneRequest(response, args) { configurationDoneRequest(response/*, args*/) {
D('configurationDoneRequest');
this.waitForConfigurationDone.resolve(); this.waitForConfigurationDone.resolve();
this.sendResponse(response); this.sendResponse(response);
} }
@@ -518,7 +551,7 @@ class AndroidDebugSession extends DebugSession {
} }
} }
disconnectRequest(response, args) { disconnectRequest(response/*, args*/) {
D('disconnectRequest'); D('disconnectRequest');
this._isDisconnecting = true; this._isDisconnecting = true;
// if we're connected, ask ADB to terminate the app // if we're connected, ask ADB to terminate the app
@@ -534,13 +567,14 @@ class AndroidDebugSession extends DebugSession {
} }
onBreakpointStateChange(e) { onBreakpointStateChange(e) {
D('onBreakpointStateChange');
e.breakpoints.forEach(javabp => { e.breakpoints.forEach(javabp => {
// if there's no associated vsbp we're deleting it, so just ignore the update // if there's no associated vsbp we're deleting it, so just ignore the update
if (!javabp.vsbp) return; if (!javabp.vsbp) return;
var verified = !!javabp.state.match(/set|enabled/); var verified = !!javabp.state.match(/set|enabled/);
javabp.vsbp.verified = verified; javabp.vsbp.verified = verified;
javabp.vsbp.message = null; javabp.vsbp.message = null;
this.sendEvent(new BreakpointEvent('updated', javabp.vsbp)); this.sendEvent(new BreakpointEvent('changed', javabp.vsbp));
}); });
} }
@@ -565,6 +599,14 @@ class AndroidDebugSession extends DebugSession {
return bp; return bp;
} }
const sendBPResponse = (response, breakpoints) => {
D('setBreakPointsRequest response ' + JSON.stringify(breakpoints.map(bp => bp.verified)));
response.body = {
breakpoints,
};
this.sendResponse(response);
}
// the file must lie inside one of the source packages we found (and it must be have a .java extension) // the file must lie inside one of the source packages we found (and it must be have a .java extension)
var srcfolder = path.dirname(srcfpn); var srcfolder = path.dirname(srcfpn);
var pkginfo; var pkginfo;
@@ -572,18 +614,23 @@ class AndroidDebugSession extends DebugSession {
if ((pkginfo = this.src_packages.packages[pkg]).package_path === srcfolder) break; if ((pkginfo = this.src_packages.packages[pkg]).package_path === srcfolder) break;
pkginfo = null; pkginfo = null;
} }
// if we didn't find an exact path match, look for a case-insensitive match
if (!pkginfo) {
for (var pkg in this.src_packages.packages) {
if ((pkginfo = this.src_packages.packages[pkg]).package_path.localeCompare(srcfolder, undefined, { sensitivity: 'base' }) === 0) break;
pkginfo = null;
}
}
// if it's not in our source packages, check if it's in the Android source file cache // if it's not in our source packages, check if it's in the Android source file cache
if (!pkginfo && is_subpath_of(srcfpn, this._android_sources_path)) { if (!pkginfo && is_subpath_of(srcfpn, this._android_sources_path)) {
// create a fake pkginfo to use to construct the bp // create a fake pkginfo to use to construct the bp
pkginfo = { srcroot:this._android_sources_path } pkginfo = { srcroot:this._android_sources_path }
} }
if (!pkginfo || !/\.java$/i.test(srcfpn)) { if (!pkginfo || !/\.(java|kt)$/i.test(srcfpn)) {
// source file is not a java file or is outside of the known source packages // source file is not a java file or is outside of the known source packages
// just send back a list of unverified breakpoints // just send back a list of unverified breakpoints
response.body = { sendBPResponse(response, args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid')));
breakpoints: args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid'))
};
this.sendResponse(response);
return; return;
} }
@@ -633,7 +680,7 @@ class AndroidDebugSession extends DebugSession {
javabp.vsbp.order = idx; javabp.vsbp.order = idx;
javabp_arr.push(javabp); javabp_arr.push(javabp);
}). }).
then(javabp => _setup_breakpoints(o, ++idx, javabp_arr)); then((/*javabp*/) => _setup_breakpoints(o, ++idx, javabp_arr));
}; };
if (!this._set_breakpoints_queue) { if (!this._set_breakpoints_queue) {
@@ -650,10 +697,7 @@ class AndroidDebugSession extends DebugSession {
this._setup_breakpoints(this._queue[0]).then(javabp_arr => { this._setup_breakpoints(this._queue[0]).then(javabp_arr => {
// send back the VS Breakpoint instances // send back the VS Breakpoint instances
var response = this._queue[0].response; var response = this._queue[0].response;
response.body = { sendBPResponse(response, javabp_arr.map(javabp => javabp.vsbp));
breakpoints: javabp_arr.map(javabp => javabp.vsbp)
};
this._dbgr.sendResponse(response);
// .. and do the next one // .. and do the next one
this._queue.shift(); this._queue.shift();
this._next(); this._next();
@@ -681,7 +725,7 @@ class AndroidDebugSession extends DebugSession {
threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) { threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
if (this._threads.array.length) { if (this._threads.array.length) {
console.log('threadsRequest: ' + this._threads.array.length); D('threadsRequest: ' + this._threads.array.length);
response.body = { response.body = {
threads: this._threads.array.filter(x=>x).map(t => { threads: this._threads.array.filter(x=>x).map(t => {
var javaid = parseInt(t.threadid, 16); var javaid = parseInt(t.threadid, 16);
@@ -702,7 +746,7 @@ class AndroidDebugSession extends DebugSession {
}; };
this.sendResponse(response); this.sendResponse(response);
}) })
.fail(e => { .fail(() => {
response.success = false; response.success = false;
this.sendResponse(response); this.sendResponse(response);
}); });
@@ -716,7 +760,7 @@ class AndroidDebugSession extends DebugSession {
// debugger threadid's are a padded 64bit hex string // debugger threadid's are a padded 64bit hex string
var thread = this.getThread(args.threadId); var thread = this.getThread(args.threadId);
if (!thread) return this.failRequestNoThread('Stack trace', args.threadId, response); if (!thread) return this.failRequestNoThread('Stack trace', args.threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Stack trace', args.threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Stack trace', args.threadId, response);
// retrieve the (stack) frames from the debugger // retrieve the (stack) frames from the debugger
this.dbgr.getframes(thread.threadid, {response, args, thread}) this.dbgr.getframes(thread.threadid, {response, args, thread})
@@ -732,8 +776,7 @@ class AndroidDebugSession extends DebugSession {
const endFrame = Math.min(startFrame + maxLevels, frames.length); const endFrame = Math.min(startFrame + maxLevels, frames.length);
var stack = [], totalFrames = frames.length, highest_known_source=0; var stack = [], totalFrames = frames.length, highest_known_source=0;
const android_src_path = this._android_sources_path || '{Android SDK}'; const android_src_path = this._android_sources_path || '{Android SDK}';
const device_api_level = this.dbgr.session.apilevel || '25'; for (var i = startFrame; (i < endFrame) && x.thread.paused; i++) {
for (var i= startFrame; i < endFrame; i++) {
// the stack_frame_id must be unique across all threads // the stack_frame_id must be unique across all threads
const stack_frame_id = x.thread.addStackFrameVariable(frames[i], i).frameId; const stack_frame_id = x.thread.addStackFrameVariable(frames[i], i).frameId;
const name = `${frames[i].method.owningclass.name}.${frames[i].method.name}`; const name = `${frames[i].method.owningclass.name}.${frames[i].method.name}`;
@@ -767,7 +810,10 @@ class AndroidDebugSession extends DebugSession {
: null; : null;
const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId); const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId);
pkginfo && (highest_known_source=i); pkginfo && (highest_known_source=i);
stack.push(new StackFrame(stack_frame_id, name, src, linenum, 0)); // we don't support column number when reporting source locations (because JDWP only supports line-granularity)
// but in order to get the Exception UI to show, we must have a non-zero column
const colnum = (!i && x.thread.paused.last_exception && x.thread.paused.reasons[0]==='exception') ? 1 : 0;
stack.push(new StackFrame(stack_frame_id, name, src, linenum, colnum));
} }
// trim the stack to exclude calls above the known sources // trim the stack to exclude calls above the known sources
if (this.callStackDisplaySize > 0) { if (this.callStackDisplaySize > 0) {
@@ -781,7 +827,7 @@ class AndroidDebugSession extends DebugSession {
}; };
this.sendResponse(response); this.sendResponse(response);
}) })
.fail((e,x) => { .fail(() => {
this.failRequest('No call stack is available', response); this.failRequest('No call stack is available', response);
}); });
} }
@@ -790,7 +836,7 @@ class AndroidDebugSession extends DebugSession {
var threadId = variableRefToThreadId(args.frameId); var threadId = variableRefToThreadId(args.frameId);
var thread = this.getThread(threadId); var thread = this.getThread(threadId);
if (!thread) return this.failRequestNoThread('Scopes',threadId, response); if (!thread) return this.failRequestNoThread('Scopes',threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Scopes',threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Scopes', threadId, response);
var scopes = [new Scope("Local", args.frameId, false)]; var scopes = [new Scope("Local", args.frameId, false)];
response.body = { response.body = {
@@ -801,21 +847,30 @@ class AndroidDebugSession extends DebugSession {
if (last_exception && !last_exception.objvar) { if (last_exception && !last_exception.objvar) {
// retrieve the exception object // retrieve the exception object
thread.allocateExceptionScopeReference(args.frameId); thread.allocateExceptionScopeReference(args.frameId);
this.dbgr.getExceptionLocal(last_exception.exception, {response,scopes,last_exception}) this.dbgr.getExceptionLocal(last_exception.exception, {thread,response,scopes,last_exception})
.then((ex_local,x) => { .then((ex_local,x) => {
var {response,scopes,last_exception} = x; x.last_exception.objvar = ex_local;
last_exception.objvar = ex_local; return $.when(x, x.thread.getVariables(x.last_exception.scopeRef));
// put the exception first - otherwise it can get lost if there's a lot of locals
scopes.unshift(new Scope("Exception: "+ex_local.type.typename, last_exception.scopeRef, false));
this.sendResponse(response);
}) })
.fail(e => { this.sendResponse(response); }); .then((x, vars) => {
var {response,scopes,last_exception} = x;
// put the exception first - otherwise it can get lost if there's a lot of locals
scopes.unshift(new Scope("Exception: " + last_exception.objvar.type.typename, last_exception.scopeRef, false));
this.sendResponse(response);
// notify the exceptionInfo who may be waiting on us
if (last_exception.waitForExObject) {
var def = last_exception.waitForExObject;
last_exception.waitForExObject = null;
def.resolveWith(this, []);
}
})
.fail((/*e*/) => { this.sendResponse(response); });
return; return;
} }
this.sendResponse(response); this.sendResponse(response);
} }
sourceRequest(response/*: DebugProtocol.SourceResponse*/, args/*: DebugProtocol.SourceArguments*/) { sourceRequest(response/*: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments*/) {
var content = var content =
`/* `/*
The source for this class is unavailable. The source for this class is unavailable.
@@ -844,7 +899,7 @@ class AndroidDebugSession extends DebugSession {
var threadId = variableRefToThreadId(args.variablesReference); var threadId = variableRefToThreadId(args.variablesReference);
var thread = this.getThread(threadId); var thread = this.getThread(threadId);
if (!thread) return this.failRequestNoThread('Variables',threadId, response); if (!thread) return this.failRequestNoThread('Variables',threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Variables',threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Variables',threadId, response);
thread.getVariables(args.variablesReference) thread.getVariables(args.variablesReference)
.then(vars => { .then(vars => {
@@ -877,7 +932,7 @@ class AndroidDebugSession extends DebugSession {
return; return;
} }
} }
var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid); var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid, thread.paused.last_exception && "Exception thrown");
thread.paused.stoppedEvent = event; thread.paused.stoppedEvent = event;
this.sendEvent(event); this.sendEvent(event);
} }
@@ -926,7 +981,7 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response); this.sendResponse(response);
// we time the step - if it takes more than 2 seconds, we switch to any other threads that are waiting // we time the step - if it takes more than 2 seconds, we switch to any other threads that are waiting
t.stepTimeout = setTimeout(t => { t.stepTimeout = setTimeout(t => {
console.log('Step timeout on thread:'+t.threadid); D('Step timeout on thread:'+t.threadid);
t.stepTimeout = null; t.stepTimeout = null;
this.checkPendingThreadBreaks(); this.checkPendingThreadBreaks();
}, 2000, t); }, 2000, t);
@@ -1070,11 +1125,11 @@ class AndroidDebugSession extends DebugSession {
if (!this._evals_queue.length) { if (!this._evals_queue.length) {
return; return;
} }
var {response, args, getvars, thread} = this._evals_queue[0]; var {response, args, getvars} = this._evals_queue[0];
// wait for any locals in the given context to be retrieved // wait for any locals in the given context to be retrieved
getvars.then((thread, locals, vars) => { getvars.then((thread, locals, vars) => {
return this.evaluate(args.expression, thread, locals, vars); return evaluate(args.expression, thread, locals, vars, this.dbgr);
}) })
.then((value,variablesReference) => { .then((value,variablesReference) => {
response.body = { result:value, variablesReference:variablesReference|0 }; response.body = { result:value, variablesReference:variablesReference|0 };
@@ -1090,479 +1145,52 @@ class AndroidDebugSession extends DebugSession {
}) })
} }
/* exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) {
Asynchronously evaluate an expression var thread = this.getThread(args.threadId);
*/ if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response);
evaluate(expression, thread, locals, vars) { if (!thread.paused) return this.cancelRequestThreadNotSuspended('Exception info', args.threadId, response);
D('evaluate: ' + expression); if (!thread.paused.last_exception) return this.failRequest('No exception available', response);
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]); if (!thread.paused.last_exception.objvar || !thread.paused.last_exception.cached) {
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]); // we must wait for the exception object to be retreived as a local (along with the message field)
if (!thread.paused.last_exception.waitForExObject) {
if (thread && !thread.paused) thread.paused.last_exception.waitForExObject = $.Deferred().then(() => {
return reject_evaluation('not available'); // redo the request
this.exceptionInfoRequest(response, args);
// 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(o);
symbol(e,':');
res.ternary_false = parse_expression(o);
} 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 this.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(this.dbgr, lhs_local.string + rhs_str,{israw:true}));
}
return invalid_operator();
}); });
} }
switch(expr.root_term_type) { return;
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(this.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 this.dbgr.getarrayvalues(arr_local, idx, 1)
}.bind(this,arr_local))
.then(els => els[0])
}
const evaluate_methodcall = (m, obj_local) => {
return reject_evaluation('Error: method calls are not supported');
}
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 = evaluate_number(obj_local.arraylen);
}
// we also special-case :super (for object instances)
else if (JTYPES.isObject(obj_local.type) && m.member === ':super') {
chain = this.dbgr.getsuperinstance(obj_local);
}
// anything else must be a real field
else {
chain = this.dbgr.getFieldValue(obj_local, m.member, true)
} }
var exobj = thread.paused.last_exception.objvar;
var exmsg = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
exmsg = (exmsg && exmsg.string) || '';
return chain.then(local => { response.body = {
if (m.array_or_fncall.arr.length) { /** ID of the exception that was thrown. */
var q = $.Deferred(); exceptionId: exobj.type.typename,
m.array_or_fncall.arr.reduce((q,index_expr) => { /** Descriptive text for the exception provided by the debug adapter. */
return q.then(function(index_expr,local) { return evaluate_array_element(index_expr,local) }.bind(this,index_expr)); description: exmsg,
}, q); /** Mode that caused the exception notification to be raised. */
return q.resolveWith(this, [local]); //'never' | 'always' | 'unhandled' | 'userUnhandled';
} breakMode: 'always',
}); /** Detailed information about the exception. */
} details: {
const evaluate_cast = (type,local) => { /** Message contained in the exception. */
if (type === local.type.typename) return local; message: exmsg,
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`); /** Short type name of the exception object. */
// boolean cannot be converted from anything else typeName: exobj.type.typename,
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast(); /** Fully-qualified type name of the exception object. */
if (local.type.typename === 'long') { fullTypeName: signatureToFullyQualifiedType(exobj.type.signature),
// long to something else /** Optional expression that can be evaluated in the current scope to obtain the exception object. */
var value = Long.fromString(local.value, true, 16); //evaluateName: "evaluateName",
switch(true) { /** Stack trace at the time the exception was thrown. */
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2),16) << 24) >> 24); break; //stackTrace: "stackTrace",
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4),16) << 16) >> 16); break; /** Details of the exception contained by this exception, if any. */
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8),16) | 0)); break; //innerException: [],
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]; this.sendResponse(response);
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');
}
} }

View File

@@ -525,7 +525,7 @@ Debugger.prototype = {
}, },
_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],
@@ -1054,6 +1054,50 @@ Debugger.prototype = {
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,
@@ -1269,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({
@@ -1379,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) {

535
src/expressions.js Normal file
View 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');
}

View File

@@ -27,6 +27,22 @@ const JTYPES = {
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] }, 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 // the special name given to exception message fields
const exmsg_var_name = ':msg'; const exmsg_var_name = ':msg';
@@ -67,5 +83,5 @@ function variableRefToThreadId(variablesReference) {
Object.assign(exports, { Object.assign(exports, {
JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString JTYPES, exmsg_var_name, ensure_path_end_slash, is_subpath_of, decode_char, variableRefToThreadId, createJavaString, signatureToFullyQualifiedType
}); });

View File

@@ -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) {
@@ -537,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 = {

View File

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

View File

@@ -1,6 +1,4 @@
'use strict' 'use strict'
// vscode stuff
const { EventEmitter, Uri } = require('vscode');
// node and external modules // node and external modules
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
@@ -17,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 = [];
@@ -29,7 +25,7 @@ class LogcatContent {
this._refreshRate = 200; // ms this._refreshRate = 200; // ms
this._state = ''; this._state = '';
this._htmltemplate = ''; this._htmltemplate = '';
this._adbclient = new ADBClient(uri.query); 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()
@@ -38,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);
@@ -55,20 +51,20 @@ class LogcatContent {
return this.htmlBootstrap({connected:true, status:'',oldlogs:''}); return this.htmlBootstrap({connected:true, status:'',oldlogs:''});
// if we're in the disconnected state, and this.content is called, it means the user has requested // if we're in the disconnected state, and this.content is called, it means the user has requested
// this logcat again - check if the device has reconnected // this logcat again - check if the device has reconnected
return this._initwait = new Promise((resolve, reject) => { return this._initwait = 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;
@@ -81,8 +77,11 @@ class LogcatContent {
}); });
} }
sendClientMessage(msg) { sendClientMessage(msg) {
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid); LogcatContent._wss.clients.forEach(client => {
clients.forEach(client => client.send(msg+'\n')); // include a newline to try and persuade a buffer write if (client._logcatid === this._logcatid) {
client.send(msg + '\n'); // include a newline to try and persuade a buffer write
}
})
} }
sendDisconnectMsg() { sendDisconnectMsg() {
this.sendClientMessage(':disconnect'); this.sendClientMessage(':disconnect');
@@ -115,7 +114,7 @@ class LogcatContent {
} }
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 class="logblock">' + 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));
@@ -161,7 +160,7 @@ class LogcatContent {
this.renotify(); this.renotify();
} }
} }
onLogcatDisconnect(e) { onLogcatDisconnect(/*e*/) {
if (this._state === 'disconnected') return; if (this._state === 'disconnected') return;
this._state = 'disconnected'; this._state = 'disconnected';
this.sendDisconnectMsg(); this.sendDisconnectMsg();
@@ -191,14 +190,19 @@ LogcatContent.initWebSocketServer = function () {
port: 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._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];
var lc = LogcatContent.byLogcatID[client._logcatid]; var lc = LogcatContent.byLogcatID[client._logcatid];
if (lc) lc.onClientConnect(client); if (lc) lc.onClientConnect(client);
else client.close(); else client.close();
@@ -215,7 +219,7 @@ LogcatContent.initWebSocketServer = function () {
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++;
@@ -245,7 +249,7 @@ function openLogcatWindow(vscode) {
var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb'); var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
var adbargs = ['-P',''+adbport,'start-server']; var adbargs = ['-P',''+adbport,'start-server'];
try { try {
var stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'}); /*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 } catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
} }
}) })
@@ -278,11 +282,26 @@ 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?');
}); });
} }

11
src/state.js Normal file
View File

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

View File

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

View File

@@ -79,6 +79,8 @@ class AndroidVariables {
value: x.varinfo.objvar.value, value: x.varinfo.objvar.value,
valid:true, 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; x.varinfo.cached = fields;
return this._local_to_variable(fields); return this._local_to_variable(fields);
}); });
@@ -100,9 +102,10 @@ class AndroidVariables {
return $.Deferred().resolve(variables); return $.Deferred().resolve(variables);
} }
// get the elements for the specified range // get the elements for the specified range
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count) return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count, {varinfo})
.then((elements) => { .then((elements, x) => {
varinfo.cached = elements; 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); return this._local_to_variable(elements);
}); });
} }
@@ -158,7 +161,7 @@ class AndroidVariables {
*/ */
_local_to_variable(v) { _local_to_variable(v) {
if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._local_to_variable(v)); if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._local_to_variable(v));
var varref = 0, objvalue, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename; 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) { switch(true) {
case v.hasnullvalue && JTYPES.isReference(v.type): case v.hasnullvalue && JTYPES.isReference(v.type):
// null object or array type // null object or array type
@@ -188,7 +191,7 @@ class AndroidVariables {
varref = this._getObjectIdReference(v.type, v.value); varref = this._getObjectIdReference(v.type, v.value);
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] }; this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
} }
objvalue = v.type.typename.replace(/]$/, v.arraylen+']'); // insert len as the final array bound objvalue = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
break; break;
case JTYPES.isObject(v.type): case JTYPES.isObject(v.type):
// non-null object instance - add another variable reference so the user can expand // non-null object instance - add another variable reference so the user can expand
@@ -207,10 +210,27 @@ class AndroidVariables {
case v.type.signature === 'J': case v.type.signature === 'J':
// because JS cannot handle 64bit ints, we need a bit of extra work // because JS cannot handle 64bit ints, we need a bit of extra work
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,''); var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
objvalue = NumberBaseConverter.hexToDec(v64hex, true); 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; break;
default: default:
// other primitives: int, boolean, etc // other primitives: boolean, etc
objvalue = v.value.toString(); objvalue = v.value.toString();
break; break;
} }
@@ -223,6 +243,7 @@ class AndroidVariables {
name: v.name, name: v.name,
type: typename, type: typename,
value: objvalue, value: objvalue,
evaluateName,
variablesReference: varref, variablesReference: varref,
} }
} }