50 Commits

Author SHA1 Message Date
adelphes
494bb83cbf version 0.5.0 2018-05-06 20:32:57 +01:00
Dave Holoway
9fca5cbe8c Merge pull request #28 from adelphes/kotlin-support
add basic support for kotlin source files
2018-05-06 20:13:24 +01:00
adelphes
5f0a02b17f add basic support for kotlin source files 2018-05-06 20:02:31 +01:00
adelphes
da36e8e457 added extension package state info 2017-06-24 19:53:22 +01:00
adelphes
3dbfd8ef2a Improved array handling. Better multidimensional array support. 2017-06-24 16:18:22 +01:00
adelphes
4a31b83eb9 add support for evaluateName so Add To Watch works again 2017-06-24 13:16:36 +01:00
adelphes
261c06f1d6 Ensure the cached fields are populated before showing the Exception UI 2017-06-24 12:20:21 +01:00
adelphes
130d79f6c2 add initial support for method call expressions 2017-06-24 12:02:36 +01:00
adelphes
8baf894fc9 add basic support for Exception UI 2017-06-20 13:40:57 +01:00
adelphes
92bd003122 Refactor expression evaluation into its own file 2017-06-18 13:56:04 +01:00
adelphes
13f116b3b3 format integers into other bases 2017-06-18 13:40:13 +01:00
adelphes
140e48cbd1 Add newlines to output
Cancel certain requests if the thread has been resumed
2017-06-18 12:57:38 +01:00
adelphes
7e8f471df4 revert util cleanup until dependencies are sorted 2017-06-14 16:10:58 +01:00
adelphes
09905eb85a Fix output newlines 2017-06-14 15:57:17 +01:00
adelphes
e76773e8e4 code tidy - fix lint warnings for unused fns, params and locals 2017-05-12 14:45:59 +01:00
adelphes
c98c962172 version 0.4.1 2017-03-02 17:30:04 +00:00
adelphes
b3501d529a version 0.4.0 changelog 2017-03-02 17:21:56 +00:00
adelphes
6f9b2f7e78 version 0.4.0 2017-03-02 17:17:46 +00:00
adelphes
eab08cc501 Switch local demo gif link to github 2017-03-02 17:05:18 +00:00
adelphes
0b5be3020a Fixed exception details not being displayed
Make sure the exception scoperef is allocated before locals are retrieved.
2017-03-02 16:12:27 +00:00
adelphes
6451e38bca remove reference to $.noop 2017-02-12 13:43:39 +00:00
adelphes
101611841f Fix logcat lines issue and added count up display. 2017-02-05 23:28:02 +00:00
adelphes
cc02c1b089 added callStackDisplaySize config setting 2017-02-05 21:36:28 +00:00
adelphes
fdbd5df16b Improvements to multi-threaded debugging
Separate out thread-specific parts
Only pause event thread for step, bp and thread events
Continue now resumes the specified thread instead of all threads
Prioritise stepping thread to prevent context switching during step
Monitor thread starts/ends
2017-02-05 19:34:12 +00:00
adelphes
baa3fb6bfd version 0.3.1
bug fix release
2017-02-02 20:13:37 +00:00
adelphes
3cda2e5f1f Fix issue with exception breaks hanging
Temporarily resume the thread used to invoke toString() and re-suspend after.
2017-02-02 20:04:04 +00:00
adelphes
dfed765d21 Fix issue with crash on exception break 2017-02-02 19:19:20 +00:00
adelphes
b60ef65803 Fix issue with Android source not displaying
VSCode 1.9 prioritises srcref over path - we now set the ref to 0 if a source path is set.
2017-02-02 19:18:54 +00:00
adelphes
214a48b1e1 version 0.3 2017-02-01 18:51:19 +00:00
adelphes
d3ff22395e Fix breakpoint failures on windows
Make sure relative_fpn uses forward slashes as path delimiters
2017-02-01 18:41:25 +00:00
adelphes
ac0b29fb15 Auto-start ADB for logcat views 2017-02-01 12:44:36 +00:00
adelphes
a47de40088 More configuration settings
Added autoStartADB and logcatPort  launch config settings
Moved AndroidContentProvider into it's own file.
2017-02-01 12:22:25 +00:00
adelphes
e23fc698a2 auto-start ADB if it's not running 2017-02-01 11:04:40 +00:00
adelphes
d324990e28 Improved multi-threaded debug support
Include thread names in display
Use a thread id mapping for vscode to fix problems with Android reusing thread ids
Stepping only resumes the paused thread, Continue resumes all.
2017-02-01 10:18:04 +00:00
adelphes
7b670b36ad Improve breakpoint searching
Search for inner types when identifying breakpoint locations
2017-02-01 10:13:02 +00:00
adelphes
d1ab9a8339 Android debugger class enhancements
Support for thread info - name and status
Reference count global suspends to allow correct resuming when multiple events are triggered.
Added methods for suspend/resume individual threads
2017-02-01 10:10:22 +00:00
adelphes
ab68b83900 jq promise fixes
return promise in always()
allow when() to accept a single array of arguments
2017-02-01 09:59:46 +00:00
adelphes
a677d636f3 Fix #17
Clear last step request on exception break
2017-01-31 15:00:45 +00:00
adelphes
71fcf1b760 Code tidy
Prevent multiple StoppedEvents being sent when debugger reports multiple events (bp,step,exception) at the same location.
2017-01-31 14:18:19 +00:00
adelphes
2727dd1b0c Refactored setBreakPointsRequest
Breakpoints are now updated using an async queue
Allow source locations inside ANDROID_HOME
Fixes #14
2017-01-31 13:40:54 +00:00
adelphes
585b8cca29 Improve logcat support
Move html into a separate resource file.
Implement regex filtering and logcat clear.
Fixes #7
2017-01-30 18:51:03 +00:00
adelphes
b27d972613 Improve evaluation support
Allow arithmetic, bitwise, logical and relational operators and primitive casts.
2017-01-29 20:25:21 +00:00
adelphes
ec38695bcc include type info when reporting missing fields
Store character primitives as a uint16 integer with a separate char field.
2017-01-29 19:06:32 +00:00
adelphes
88c20d6470 implement a more reliable evaluation queue 2017-01-28 19:12:35 +00:00
adelphes
6f3f5eee74 Improve expression parsing
Allow ':super' to be used as a member.
Search super types when locating field members.
Fixes #16
2017-01-28 15:54:26 +00:00
adelphes
6463558034 Fix issues with expressions not completing
Moved NumberBaseConverter into its own file
2017-01-28 14:12:11 +00:00
adelphes
1b7fb3d60a first pass at hit-count breakpoints 2017-01-27 13:59:36 +00:00
adelphes
2f93ecc16f We only need platform-tools for adb 2017-01-27 10:19:50 +00:00
adelphes
3e1a8dce37 bump version 2017-01-26 17:59:01 +00:00
adelphes
6bb8047db6 Stop the Android runtime from barfing into logcat
Split field retrieval into statics and instances
2017-01-26 17:58:37 +00:00
20 changed files with 3032 additions and 1430 deletions

View File

@@ -1,5 +1,6 @@
.vscode/** .vscode/**
.vscode-test/** .vscode-test/**
images/**/*.gif
test/** test/**
.gitignore .gitignore
jsconfig.json jsconfig.json

View File

@@ -1,5 +1,33 @@
# Change Log # Change Log
### 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)

View File

@@ -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).
![Launch Android App](images/demo.gif) ![Launch Android App](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/demo.gif)

View File

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

View File

@@ -2,7 +2,7 @@
"name": "android-dev-ext", "name": "android-dev-ext",
"displayName": "Android", "displayName": "Android",
"description": "Android debugging support for VS Code", "description": "Android debugging support for VS Code",
"version": "0.2.0", "version": "0.5.0",
"publisher": "adelphes", "publisher": "adelphes",
"preview": true, "preview": true,
"license": "MIT", "license": "MIT",
@@ -35,6 +35,9 @@
"breakpoints": [ "breakpoints": [
{ {
"language": "java" "language": "java"
},
{
"language": "kotlin"
} }
], ],
"debuggers": [ "debuggers": [
@@ -66,6 +69,21 @@
"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\"",
@@ -112,8 +130,9 @@
"test": "node ./node_modules/vscode/bin/test" "test": "node ./node_modules/vscode/bin/test"
}, },
"dependencies": { "dependencies": {
"vscode-debugprotocol": "^1.15.0", "vscode-debugprotocol": "^1.20.0",
"vscode-debugadapter": "^1.15.0", "vscode-debugadapter": "^1.20.0",
"long": "^3.2.0",
"ws": "^1.1.1", "ws": "^1.1.1",
"xmldom": "^0.1.27", "xmldom": "^0.1.27",
"xpath": "^0.0.23" "xpath": "^0.0.23"

View File

@@ -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
View 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;

File diff suppressed because it is too large Load Diff

View File

@@ -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({
@@ -1259,16 +1456,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 +1497,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 +1523,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 +1556,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 +1608,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 +1653,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 +1661,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 +1691,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 +1707,7 @@ Debugger.prototype = {
this._trigger('exception', eventdata); this._trigger('exception', eventdata);
}) })
}) })
});
}.bind(this) }.bind(this)
}; };
@@ -1528,6 +1758,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
View File

@@ -0,0 +1,535 @@
'use strict'
const Long = require('long');
const $ = require('./jq-promise');
const { D } = require('./util');
const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./globals');
/*
Asynchronously evaluate an expression
*/
exports.evaluate = function(expression, thread, locals, vars, dbgr) {
D('evaluate: ' + expression);
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]);
if (thread && !thread.paused)
return reject_evaluation('not available');
// special case for evaluating exception messages
// - this is called if the user tries to evaluate ':msg' from the locals
if (expression === exmsg_var_name) {
if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) {
var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
if (msglocal) {
return resolve_evaluation(vars._local_to_variable(msglocal).value);
}
}
return reject_evaluation('not available');
}
const parse_array_or_fncall = function (e) {
var arg, res = { arr: [], call: null };
// pre-call array indexes
while (e.expr[0] === '[') {
e.expr = e.expr.slice(1).trim();
if ((arg = parse_expression(e)) === null) return null;
res.arr.push(arg);
if (e.expr[0] !== ']') return null;
e.expr = e.expr.slice(1).trim();
}
if (res.arr.length) return res;
// method call
if (e.expr[0] === '(') {
res.call = []; e.expr = e.expr.slice(1).trim();
if (e.expr[0] !== ')') {
for (; ;) {
if ((arg = parse_expression(e)) === null) return null;
res.call.push(arg);
if (e.expr[0] === ')') break;
if (e.expr[0] !== ',') return null;
e.expr = e.expr.slice(1).trim();
}
}
e.expr = e.expr.slice(1).trim();
// post-call array indexes
while (e.expr[0] === '[') {
e.expr = e.expr.slice(1).trim();
if ((arg = parse_expression(e)) === null) return null;
res.arr.push(arg);
if (e.expr[0] !== ']') return null;
e.expr = e.expr.slice(1).trim();
}
}
return res;
}
const parse_expression_term = function (e) {
if (e.expr[0] === '(') {
e.expr = e.expr.slice(1).trim();
var subexpr = { expr: e.expr };
var res = parse_expression(subexpr);
if (res) {
if (subexpr.expr[0] !== ')') return null;
e.expr = subexpr.expr.slice(1).trim();
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) {
// primitive typecast
var castexpr = parse_expression_term(e);
if (castexpr) castexpr.typecast = res.root_term;
res = castexpr;
}
}
return res;
}
var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
if (unop) {
var op = unop[0].replace(/\s/g, '');
e.expr = e.expr.slice(unop[0].length).trim();
var res = parse_expression_term(e);
if (res) {
for (var i = op.length - 1; i >= 0; --i)
res = { operator: op[i], rhs: res };
}
return res;
}
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
if (!root_term) return null;
var res = {
root_term: root_term[0],
root_term_type: ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'][[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1],
array_or_fncall: null,
members: [],
typecast: ''
}
e.expr = e.expr.slice(res.root_term.length).trim();
if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
// the root term is not allowed to be a method call
if (res.array_or_fncall.call) return null;
while (e.expr[0] === '.') {
// member expression
e.expr = e.expr.slice(1).trim();
var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
if (!member_name) return null;
res.members.push(m = { member: member_name[0], array_or_fncall: null })
e.expr = e.expr.slice(m.member.length).trim();
if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
}
return res;
}
const prec = {
'*': 1, '%': 1, '/': 1,
'+': 2, '-': 2,
'<<': 3, '>>': 3, '>>>': 3,
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
'==': 5, '!=': 5,
'&': 6, '^': 7, '|': 8, '&&': 9, '||': 10, '?': 11,
}
const parse_expression = function (e) {
var res = parse_expression_term(e);
if (!e.currprec) e.currprec = [12];
for (; ;) {
var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
if (!binary_operator) break;
var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0];
if (precdiff > 0) {
// bigger number -> lower precendence -> end of (sub)expression
break;
}
if (precdiff === 0 && binary_operator[0] !== '?') {
// equal precedence, ltr evaluation
break;
}
// higher or equal precendence
e.currprec.unshift(e.currprec[0] + precdiff);
e.expr = e.expr.slice(binary_operator[0].length).trim();
// current or higher precendence
if (binary_operator[0] === '?') {
res = { condition: res, operator: binary_operator[0], ternary_true: null, ternary_false: null };
res.ternary_true = parse_expression(e);
if (e.expr[0] === ':') {
e.expr = e.expr.slice(1).trim();
res.ternary_false = parse_expression(e);
}
} else {
res = { lhs: res, operator: binary_operator[0], rhs: parse_expression(e) };
}
e.currprec.shift();
}
return res;
}
const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16);
const evaluate_number = (n) => {
n += '';
var numtype, m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base = 10;
if (m) {
switch (m[2]) {
case 'b': base = 2; n = m[1] + m[3]; break;
case 'x': base = 16; n = m[1] + m[3]; break;
default: base = 8; break;
}
}
if (base !== 16 && /[fFdD]$/.test(n)) {
numtype = /[fF]$/.test(n) ? 'float' : 'double';
n = n.slice(0, -1);
} else if (/[lL]$/.test(n)) {
numtype = 'long'
n = n.slice(0, -1);
} else {
numtype = /\./.test(n) ? 'double' : 'int';
}
if (numtype === 'long') n = hex_long(Long.fromString(n, false, base));
else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base);
else n = parseInt(n, base) | 0;
const iszero = /^[+-]?0+(\.0*)?$/.test(n);
return { vtype: 'literal', name: '', hasnullvalue: iszero, type: JTYPES[numtype], value: n, valid: true };
}
const evaluate_char = (char) => {
return { vtype: 'literal', name: '', char: char, hasnullvalue: false, type: JTYPES.char, value: char.charCodeAt(0), valid: true };
}
const numberify = (local) => {
//if (local.type.signature==='C') return local.char.charCodeAt(0);
if (/^[FD]$/.test(local.type.signature))
return parseFloat(local.value);
if (local.type.signature === 'J')
return parseInt(local.value, 16);
return parseInt(local.value, 10);
}
const stringify = (local) => {
var s;
if (JTYPES.isString(local.type)) s = local.string;
else if (JTYPES.isChar(local.type)) s = local.char;
else if (JTYPES.isPrimitive(local.type)) s = '' + local.value;
else if (local.hasnullvalue) s = '(null)';
if (typeof s === 'string')
return $.Deferred().resolveWith(this, [s]);
return dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature)
.then(s => s.string);
}
const evaluate_expression = (expr) => {
var q = $.Deferred(), local;
if (expr.operator) {
const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary ? 'type' : 'types'} for operator '${expr.operator}'`),
divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero');
var lhs_local;
return !expr.lhs
? // unary operator
evaluate_expression(expr.rhs)
.then(rhs_local => {
if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) {
rhs_local.value = !rhs_local.value;
return rhs_local;
}
else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) {
switch (rhs_local.type.typename) {
case 'long': rhs_local.value = rhs_local.value.replace(/./g, c => (15 - parseInt(c, 16)).toString(16)); break;
default: rhs_local = evaluate_number('' + ~rhs_local.value); break;
}
return rhs_local;
}
else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) {
if (expr.operator === '+') return rhs_local;
switch (rhs_local.type.typename) {
case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value, false, 16).neg()); break;
default: rhs_local = evaluate_number('' + (-rhs_local.value)); break;
}
return rhs_local;
}
return invalid_operator('unary');
})
: // binary operator
evaluate_expression(expr.lhs)
.then(x => (lhs_local = x) && evaluate_expression(expr.rhs))
.then(rhs_local => {
if ((lhs_local.type.signature === 'J' && JTYPES.isInteger(rhs_local.type))
|| (rhs_local.type.signature === 'J' && JTYPES.isInteger(lhs_local.type))) {
// one operand is a long, the other is an integer -> the result is a long
var a, b, lbase, rbase;
lbase = lhs_local.type.signature === 'J' ? 16 : 10;
rbase = rhs_local.type.signature === 'J' ? 16 : 10;
a = Long.fromString('' + lhs_local.value, false, lbase);
b = Long.fromString('' + rhs_local.value, false, rbase);
switch (expr.operator) {
case '+': a = a.add(b); break;
case '-': a = a.subtract(b); break;
case '*': a = a.multiply(b); break;
case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero();
case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero();
case '<<': a = a.shl(b); break;
case '>>': a = a.shr(b); break;
case '>>>': a = a.shru(b); break;
case '&': a = a.and(b); break;
case '|': a = a.or(b); break;
case '^': a = a.xor(b); break;
case '==': a = a.eq(b); break;
case '!=': a = !a.eq(b); break;
case '<': a = a.lt(b); break;
case '<=': a = a.lte(b); break;
case '>': a = a.gt(b); break;
case '>=': a = a.gte(b); break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.long, value: hex_long(a), valid: true };
}
else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) {
// both are (non-long) integer types
var a = numberify(lhs_local), b = numberify(rhs_local);
switch (expr.operator) {
case '+': a += b; break;
case '-': a -= b; break;
case '*': a *= b; break;
case '/': if (b) { a = Math.trunc(a / b); break } return divide_by_zero();
case '%': if (b) { a %= b; break; } return divide_by_zero();
case '<<': a <<= b; break;
case '>>': a >>= b; break;
case '>>>': a >>>= b; break;
case '&': a &= b; break;
case '|': a |= b; break;
case '^': a ^= b; break;
case '==': a = a === b; break;
case '!=': a = a !== b; break;
case '<': a = a < b; break;
case '<=': a = a <= b; break;
case '>': a = a > b; break;
case '>=': a = a >= b; break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.int, value: '' + a, valid: true };
}
else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) {
var a = numberify(lhs_local), b = numberify(rhs_local);
switch (expr.operator) {
case '+': a += b; break;
case '-': a -= b; break;
case '*': a *= b; break;
case '/': a /= b; break;
case '==': a = a === b; break;
case '!=': a = a !== b; break;
case '<': a = a < b; break;
case '<=': a = a <= b; break;
case '>': a = a > b; break;
case '>=': a = a >= b; break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
// one of them must be a float or double
var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))];
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES[result_type], value: '' + a, valid: true };
}
else if (lhs_local.type.signature === 'Z' && rhs_local.type.signature === 'Z') {
// boolean operands
var a = lhs_local.value, b = rhs_local.value;
switch (expr.operator) {
case '&': case '&&': a = a && b; break;
case '|': case '||': a = a || b; break;
case '^': a = !!(a ^ b); break;
case '==': a = a === b; break;
case '!=': a = a !== b; break;
default: return invalid_operator();
}
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
}
else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) {
return stringify(rhs_local).then(rhs_str => createJavaString(dbgr, lhs_local.string + rhs_str, { israw: true }));
}
return invalid_operator();
});
}
switch (expr.root_term_type) {
case 'boolean':
local = { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: expr.root_term !== 'false', valid: true };
break;
case 'null':
const nullvalue = '0000000000000000'; // null reference value
local = { vtype: 'literal', name: '', hasnullvalue: true, type: JTYPES.null, value: nullvalue, valid: true };
break;
case 'ident':
local = locals && locals.find(l => l.name === expr.root_term);
break;
case 'hexint':
case 'octint':
case 'decint':
case 'decfloat':
local = evaluate_number(expr.root_term);
break;
case 'char':
case 'echar':
case 'uchar':
local = evaluate_char(decode_char(expr.root_term.slice(1, -1)))
break;
case 'string':
// we must get the runtime to create string instances
q = createJavaString(dbgr, expr.root_term);
local = { valid: true }; // make sure we don't fail the evaluation
break;
}
if (!local || !local.valid) return reject_evaluation('not available');
// we've got the root term variable - work out the rest
q = expr.array_or_fncall.arr.reduce((q, index_expr) => {
return q.then(function (index_expr, local) { return evaluate_array_element.call(this, index_expr, local) }.bind(this, index_expr));
}, q);
q = expr.members.reduce((q, m) => {
return q.then(function (m, local) { return evaluate_member.call(this, m, local) }.bind(this, m));
}, q);
if (expr.typecast) {
q = q.then(function (type, local) { return evaluate_cast.call(this, type, local) }.bind(this, expr.typecast))
}
// if it's a string literal, we are already waiting for the runtime to create the string
// - otherwise, start the evalaution...
if (expr.root_term_type !== 'string')
q.resolveWith(this, [local]);
return q;
}
const evaluate_array_element = (index_expr, arr_local) => {
if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
return evaluate_expression(index_expr)
.then(function (arr_local, idx_local) {
if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value');
var idx = numberify(idx_local);
if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`);
return dbgr.getarrayvalues(arr_local, idx, 1)
}.bind(this, arr_local))
.then(els => els[0])
}
const evaluate_methodcall = (m, obj_local) => {
// until we can figure out why method invokes with parameters crash the debugger, disallow parameterised calls
if (m.array_or_fncall.call.length)
return reject_evaluation('Error: method calls with parameter values are not supported');
// find any methods matching the member name with any parameters in the signature
return dbgr.findNamedMethods(obj_local.type.signature, m.member, /^/)
.then(methods => {
if (!methods[0])
return reject_evaluation(`Error: method '${m.member}()' not found`);
// evaluate any parameters (and wait for the results)
return $.when({methods},...m.array_or_fncall.call.map(evaluate_expression));
})
.then((x,...paramValues) => {
// filter the method based upon the types of parameters - note that null types and integer literals can match multiple types
paramValues = paramValues = paramValues.map(p => p[0]);
var matchers = paramValues.map(p => {
switch(true) {
case p.type.signature === 'I':
// match bytes/shorts/ints/longs/floats/doubles within range
if (p.value >= -128 && p.value <= 127) return /^[BSIJFD]$/
if (p.value >= -32768 && p.value <= 32767) return /^[SIJFD]$/
return /^[IJFD]$/;
case p.type.signature === 'F':
return /^[FD]$/;
case p.type.signature === 'Lnull;':
return /^[LT\[]/; // any reference type
default:
// anything else must be an exact signature match (for now - in reality we should allow subclassed type)
return new RegExp(`^${p.type.signature.replace(/[$]/g,x=>'\\'+x)}$`);
}
});
var methods = x.methods.filter(m => {
// extract a list of parameter types
var paramtypere = /\[*([BSIJFDCZ]|([LT][^;]+;))/g;
for (var x, ptypes=[]; x = paramtypere.exec(m.sig); ) {
ptypes.push(x[0]);
}
// the last paramter type is the return value
ptypes.pop();
// check if they match
if (ptypes.length !== paramValues.length)
return;
return matchers.filter(m => {
return !m.test(ptypes.shift())
}).length === 0;
});
if (!methods[0])
return reject_evaluation(`Error: incompatible parameters for method '${m.member}'`);
// convert the parameters to exact debugger-compatible values
paramValues = paramValues.map(p => {
if (p.type.signature.length === 1)
return { type: p.type.typename, value: p.value};
return { type: 'oref', value: p.value };
})
return dbgr.invokeMethod(obj_local.value, thread.threadid, obj_local.type.signature, m.member, methods[0].genericsig || methods[0].sig, paramValues, {});
});
}
const evaluate_member = (m, obj_local) => {
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
var chain;
if (m.array_or_fncall.call) {
chain = evaluate_methodcall(m, obj_local);
}
// length is a 'fake' field of arrays, so special-case it
else if (JTYPES.isArray(obj_local.type) && m.member === 'length') {
chain = $.Deferred().resolve(evaluate_number(obj_local.arraylen));
}
// we also special-case :super (for object instances)
else if (JTYPES.isObject(obj_local.type) && m.member === ':super') {
chain = dbgr.getsuperinstance(obj_local);
}
// anything else must be a real field
else {
chain = dbgr.getFieldValue(obj_local, m.member, true)
}
return chain.then(local => {
if (m.array_or_fncall.arr.length) {
var q = $.Deferred();
m.array_or_fncall.arr.reduce((q, index_expr) => {
return q.then(function (index_expr, local) { return evaluate_array_element(index_expr, local) }.bind(this, index_expr));
}, q);
return q.resolveWith(this, [local]);
}
});
}
const evaluate_cast = (type, local) => {
if (type === local.type.typename) return local;
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`);
// boolean cannot be converted from anything else
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast();
if (local.type.typename === 'long') {
// long to something else
var value = Long.fromString(local.value, true, 16);
switch (true) {
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2), 16) << 24) >> 24); break;
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4), 16) << 16) >> 16); break;
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8), 16) | 0)); break;
case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4), 16))); break;
case (type === 'float'): local = evaluate_number(value.toSigned().toNumber() + 'F'); break;
case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break;
default: return incompatible_cast();
}
} else {
switch (true) {
case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break;
case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break;
case (type === 'int'): local = evaluate_number((local.value | 0)); break;
case (type === 'long'): local = evaluate_number(local.value + 'L'); break;
case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value | 0)); break;
case (type === 'float'): break;
case (type === 'double'): break;
default: return incompatible_cast();
}
}
local.type = JTYPES[type];
return local;
}
var e = { expr: expression.trim() };
var parsed_expression = parse_expression(e);
// if there's anything left, it's an error
if (parsed_expression && !e.expr) {
// the expression is well-formed - start the (asynchronous) evaluation
return evaluate_expression(parsed_expression)
.then(local => {
var v = vars._local_to_variable(local);
return resolve_evaluation(v.value, v.variablesReference);
});
}
// the expression is not well-formed
return reject_evaluation('not available');
}

87
src/globals.js Normal file
View 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
});

View File

@@ -169,7 +169,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 +250,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 +371,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 +537,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 +639,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 +684,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 +733,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 +746,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 +1054,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 +1064,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 +1105,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 +1159,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 +1196,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 +1228,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),
}
}
);
},
}; };
} }

View File

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

View File

@@ -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 => ({ '&': '&amp;', '"': '&quot;', "'": '&#39;', '<': '&lt;', '>': '&gt;' }[c])); log = log.replace(/[&"'<>]/g, c => ({ '&': '&amp;', '"': '&quot;', "'": '&#39;', '<': '&lt;', '>': '&gt;' }[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
View 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
View 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
View File

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

129
src/threads.js Normal file
View 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;

View File

@@ -551,8 +551,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
View 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;