mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 09:59:25 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45e2dc2fe1 | ||
|
|
30ed5dea3b | ||
|
|
0eb44130a6 | ||
|
|
d1e7c86092 | ||
|
|
690f9dc23a | ||
|
|
27ecd41b68 | ||
|
|
756a1cea29 | ||
|
|
fc2ce97a23 | ||
|
|
de8abc62bc | ||
|
|
8cc31476b3 | ||
|
|
494bb83cbf | ||
|
|
9fca5cbe8c | ||
|
|
5f0a02b17f | ||
|
|
da36e8e457 | ||
|
|
3dbfd8ef2a | ||
|
|
4a31b83eb9 | ||
|
|
261c06f1d6 | ||
|
|
130d79f6c2 | ||
|
|
8baf894fc9 | ||
|
|
92bd003122 | ||
|
|
13f116b3b3 | ||
|
|
140e48cbd1 | ||
|
|
7e8f471df4 | ||
|
|
09905eb85a | ||
|
|
e76773e8e4 | ||
|
|
c98c962172 | ||
|
|
b3501d529a | ||
|
|
6f9b2f7e78 | ||
|
|
eab08cc501 | ||
|
|
0b5be3020a | ||
|
|
6451e38bca | ||
|
|
101611841f | ||
|
|
cc02c1b089 | ||
|
|
fdbd5df16b | ||
|
|
baa3fb6bfd | ||
|
|
3cda2e5f1f | ||
|
|
dfed765d21 | ||
|
|
b60ef65803 | ||
|
|
214a48b1e1 | ||
|
|
d3ff22395e | ||
|
|
ac0b29fb15 | ||
|
|
a47de40088 | ||
|
|
e23fc698a2 | ||
|
|
d324990e28 | ||
|
|
7b670b36ad | ||
|
|
d1ab9a8339 | ||
|
|
ab68b83900 | ||
|
|
a677d636f3 | ||
|
|
71fcf1b760 | ||
|
|
2727dd1b0c | ||
|
|
585b8cca29 | ||
|
|
b27d972613 | ||
|
|
ec38695bcc | ||
|
|
88c20d6470 | ||
|
|
6f3f5eee74 | ||
|
|
6463558034 | ||
|
|
1b7fb3d60a | ||
|
|
2f93ecc16f | ||
|
|
3e1a8dce37 | ||
|
|
6bb8047db6 |
@@ -1,5 +1,6 @@
|
|||||||
.vscode/**
|
.vscode/**
|
||||||
.vscode-test/**
|
.vscode-test/**
|
||||||
|
images/**/*.gif
|
||||||
test/**
|
test/**
|
||||||
.gitignore
|
.gitignore
|
||||||
jsconfig.json
|
jsconfig.json
|
||||||
|
|||||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,5 +1,46 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
### version 0.6.2
|
||||||
|
* Fix broken logcat command due to missing dependency
|
||||||
|
|
||||||
|
### version 0.6.1
|
||||||
|
* Regenerate package-lock.json to remove event-stream vulnerability - https://github.com/dominictarr/event-stream/issues/116
|
||||||
|
|
||||||
|
### version 0.6.0
|
||||||
|
* Fix issue with breakpoints not enabling correctly
|
||||||
|
* Fix issue with JDWP failure on breakpoint hit
|
||||||
|
* Added support for diagnostic logs using trace configuration option
|
||||||
|
* Updated default apkFile path to match current releases of Android Studio
|
||||||
|
* Updated package dependencies
|
||||||
|
|
||||||
|
### version 0.5.0
|
||||||
|
* Debugger support for Kotlin source files
|
||||||
|
* Exception UI
|
||||||
|
* Fixed some console display issues
|
||||||
|
|
||||||
|
### version 0.4.1
|
||||||
|
* One day I will learn to update the changelog **before** I hit publish
|
||||||
|
* Updated changelog
|
||||||
|
|
||||||
|
### version 0.4.0
|
||||||
|
* Debugger performance improvements
|
||||||
|
* Fixed exception details not being displayed in locals
|
||||||
|
* Fixed some logcat display issues
|
||||||
|
|
||||||
|
### version 0.3.1
|
||||||
|
* Bug fixes
|
||||||
|
* Fix issue with exception breaks crashing debugger
|
||||||
|
* Fix issue with Android sources not displaying in VSCode 1.9
|
||||||
|
|
||||||
|
## version 0.3.0
|
||||||
|
* Support for Logcat filtering using regular expressions
|
||||||
|
* Improved expression parsing with support for arithmetic, bitwise, logical and relational operators
|
||||||
|
* Multi-threaded debugging support (experimental)
|
||||||
|
* Hit count breakpoints
|
||||||
|
* Android source breakpoints
|
||||||
|
* Automatic adb server start
|
||||||
|
* Bug fixes
|
||||||
|
|
||||||
## version 0.2.0
|
## version 0.2.0
|
||||||
* Support for Logcat viewing [ Command Palette -> Android: View Logcat ]
|
* Support for Logcat viewing [ Command Palette -> Android: View Logcat ]
|
||||||
* Support for modifying local variables, object fields and array elements (literal values only)
|
* Support for modifying local variables, object fields and array elements (literal values only)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ This is a preview version of the Android for VS Code Extension. The extension al
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
You must have [Android SDK Tools](https://developer.android.com/studio/releases/sdk-tools.html) installed. This extension communicates with your device via the ADB (Android Debug Bridge) interface.
|
You must have [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools.html) installed. This extension communicates with your device via the ADB (Android Debug Bridge) interface.
|
||||||
> You are not required to have Android Studio installed - if you have Android Studio installed, make sure there are no active instances of it when using this extension or you may run into problems with ADB.
|
> You are not required to have Android Studio installed - if you have Android Studio installed, make sure there are no active instances of it when using this extension or you may run into problems with ADB.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
@@ -58,4 +58,4 @@ The following settings are used to configure the debugger:
|
|||||||
|
|
||||||
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
|
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
|
||||||
|
|
||||||

|

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