11 Commits

Author SHA1 Message Date
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
13 changed files with 1017 additions and 714 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,10 @@
# Change Log # Change Log
### 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 ## version 0.3.0
* Support for Logcat filtering using regular expressions * Support for Logcat filtering using regular expressions
* Improved expression parsing with support for arithmetic, bitwise, logical and relational operators * Improved expression parsing with support for arithmetic, bitwise, logical and relational operators

View File

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

@@ -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.3.0", "version": "0.4.0",
"publisher": "adelphes", "publisher": "adelphes",
"preview": true, "preview": true,
"license": "MIT", "license": "MIT",
@@ -71,6 +71,11 @@
"description": "Automatically launch 'adb start-server' if not already started. Default: true", "description": "Automatically launch 'adb start-server' if not already started. Default: true",
"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": { "logcatPort": {
"type": "integer", "type": "integer",
"description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038", "description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038",
@@ -124,8 +129,8 @@
"dependencies": { "dependencies": {
"vscode-debugprotocol": "^1.15.0", "vscode-debugprotocol": "^1.15.0",
"vscode-debugadapter": "^1.15.0", "vscode-debugadapter": "^1.15.0",
"long":"^3.2.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

@@ -403,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,
} }

File diff suppressed because it is too large Load Diff

View File

@@ -141,7 +141,8 @@ Debugger.prototype = {
cpfilters: [], cpfilters: [],
preparedclasses: [], preparedclasses: [],
stepids: {}, // hashmap<threadid,stepid> stepids: {}, // hashmap<threadid,stepid>
suspendcount: 0, // refcount of suspend-all-threads threadsuspends: [], // hashmap<threadid, suspend-count>
invokes: {}, // hashmap<threadid, deferred>
} }
return this; return this;
}, },
@@ -456,18 +457,18 @@ Debugger.prototype = {
}); });
}) })
.then(function () { .then(function () {
this.session.suspendcount++;
this._trigger('suspended'); this._trigger('suspended');
}); });
}, },
suspendthread: function (threadid, extra) { suspendthread: function (threadid, extra) {
return this.ensureconnected(extra) return this.ensureconnected({threadid,extra})
.then(function (extra) { .then(function (x) {
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) + 1;
return this.session.adbclient.jdwp_command({ return this.session.adbclient.jdwp_command({
ths: this, ths: this,
extra: extra, extra: x.extra,
cmd: this.JDWP.Commands.suspendthread(threadid), cmd: this.JDWP.Commands.suspendthread(x.threadid),
}); });
}) })
.then((res,extra) => extra); .then((res,extra) => extra);
@@ -477,21 +478,12 @@ Debugger.prototype = {
return this.ensureconnected(extra) return this.ensureconnected(extra)
.then(function (extra) { .then(function (extra) {
if (triggers) this._trigger('resuming'); if (triggers) this._trigger('resuming');
const resume_cmd = (decoded,extra) => { this.session.stoppedlocation = null;
return this.session.adbclient.jdwp_command({ return this.session.adbclient.jdwp_command({
ths: this, ths: this,
extra: extra, extra: extra,
cmd: this.JDWP.Commands.resume(), cmd: this.JDWP.Commands.resume(),
}); });
}
// we must resume with the same number of suspends
var def = resume_cmd(null, extra);
for (var i=1; i < this.session.suspendcount; i++) {
def = def.then(resume_cmd);
}
this.session.stoppedlocation = null;
this.session.suspendcount = 0;
return def;
}) })
.then(function (decoded, extra) { .then(function (decoded, extra) {
if (triggers) this._trigger('resumed'); if (triggers) this._trigger('resumed');
@@ -508,26 +500,27 @@ Debugger.prototype = {
}, },
resumethread: function (threadid, extra) { resumethread: function (threadid, extra) {
return this.ensureconnected(extra) return this.ensureconnected({threadid,extra})
.then(function (extra) { .then(function (x) {
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) - 1;
return this.session.adbclient.jdwp_command({ return this.session.adbclient.jdwp_command({
ths: this, ths: this,
extra: extra, extra: x.extra,
cmd: this.JDWP.Commands.resumethread(threadid), cmd: this.JDWP.Commands.resumethread(x.threadid),
}); });
}) })
.then((res,extra) => extra); .then((res,extra) => extra);
}, },
step: function (steptype, threadid) { step: function (steptype, threadid, extra) {
var x = { steptype: steptype, threadid: threadid }; 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);
}); });
}, },
@@ -977,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);
@@ -1022,18 +1026,28 @@ 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) {
@@ -1386,8 +1400,6 @@ Debugger.prototype = {
}, },
fn: function (e) { fn: function (e) {
var x = e.data; var x = e.data;
// each class prepare contributes a global suspend
x.dbgr.session.suspendcount++;
x.onprepare.apply(x.dbgr, [e.event]); x.onprepare.apply(x.dbgr, [e.event]);
} }
}; };
@@ -1410,14 +1422,12 @@ Debugger.prototype = {
return clearStepCommand; return clearStepCommand;
}, },
_setupstepevent: function (steptype, threadid) { _setupstepevent: function (steptype, threadid, extra) {
var onevent = { var onevent = {
data: { data: {
dbgr: this, dbgr: this,
}, },
fn: function (e) { fn: function (e) {
// each step hit contributes a global suspend
e.data.dbgr.session.suspendcount++;
e.data.dbgr._clearLastStepRequest(e.event.threadid, e) e.data.dbgr._clearLastStepRequest(e.event.threadid, e)
.then(function (e) { .then(function (e) {
var x = e.data; var x = e.data;
@@ -1441,10 +1451,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),
}).then(res => { extra: extra,
}).then((res,extra) => {
// save the step id so we can manually clear it if an exception break occurs // save the step id so we can manually clear it if an exception break occurs
if (this.session && res && res.id) if (this.session && res && res.id)
this.session.stepids[threadid] = res.id; this.session.stepids[threadid] = res.id;
return extra;
}); });
return cmd.promise(); return cmd.promise();
@@ -1471,8 +1483,6 @@ Debugger.prototype = {
bp: x.dbgr.breakpoints.enabled[cmlkey].bp, bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
}; };
x.dbgr.session.stoppedlocation = stoppedloc; x.dbgr.session.stoppedlocation = stoppedloc;
// each breakpoint hit contributes a global suspend
x.dbgr.session.suspendcount++;
// if this was a conditional breakpoint, it will have been automatically cleared // if this was a conditional breakpoint, it will have been automatically cleared
// - set a new (unconditional) breakpoint in it's place // - set a new (unconditional) breakpoint in it's place
if (bp.conditions.hitcount) { if (bp.conditions.hitcount) {
@@ -1635,8 +1645,6 @@ Debugger.prototype = {
dbgr: this, dbgr: this,
}, },
fn: function (e) { fn: function (e) {
// each exception hit contributes a global suspend
x.dbgr.session.suspendcount++;
// if this exception break occurred during a step request, we must manually clear the event // 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 // or the (device-side) debugger will crash on next step
this._clearLastStepRequest(e.event.threadid, e).then(e => { this._clearLastStepRequest(e.event.threadid, e).then(e => {
@@ -1704,6 +1712,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) {

71
src/globals.js Normal file
View File

@@ -0,0 +1,71 @@
'use strict'
const path = require('path');
// some commonly used Java types in debugger-compatible format
const JTYPES = {
byte: {typename:'byte',signature:'B'},
short: {typename:'short',signature:'S'},
int: {typename:'int',signature:'I'},
long: {typename:'long',signature:'J'},
float: {typename:'float',signature:'F'},
double: {typename:'double',signature:'D'},
char: {typename:'char',signature:'C'},
boolean: {typename:'boolean',signature:'Z'},
null: {typename:'null',signature:'Lnull;'}, // null has no type really, but we need something for literals
String: {typename:'String',signature:'Ljava/lang/String;'},
Object: {typename:'Object',signature:'Ljava/lang/Object;'},
isArray(t) { return t.signature[0]==='[' },
isObject(t) { return t.signature[0]==='L' },
isReference(t) { return /^[L[]/.test(t.signature) },
isPrimitive(t) { return !JTYPES.isReference(t.signature) },
isInteger(t) { return /^[BCIJS]$/.test(t.signature) },
isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) },
isString(t) { return t.signature === this.String.signature },
isChar(t) { return t.signature === this.char.signature },
isBoolean(t) { return t.signature === this.boolean.signature },
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
}
// the special name given to exception message fields
const exmsg_var_name = ':msg';
function createJavaString(dbgr, s, opts) {
const raw = (opts && opts.israw) ? s : s.slice(1,-1).replace(/\\u[0-9a-fA-F]{4}|\\./,decode_char);
// return a deferred, which resolves to a local variable named 'literal'
return dbgr.createstring(raw);
}
function decode_char(c) {
switch(true) {
case /^\\[^u]$/.test(c):
// backslash escape
var x = {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v','0':String.fromCharCode(0)}[c[1]];
return x || c[1];
case /^\\u[0-9a-fA-F]{4}$/.test(c):
// unicode escape
return String.fromCharCode(parseInt(c.slice(2),16));
case c.length===1 :
return c;
}
throw new Error('Invalid character value');
}
function ensure_path_end_slash(p) {
return p + (/[\\/]$/.test(p) ? '' : path.sep);
}
function is_subpath_of(fpn, subpath) {
if (!subpath || !fpn) return false;
subpath = ensure_path_end_slash(''+subpath);
return fpn.slice(0,subpath.length) === subpath;
}
function variableRefToThreadId(variablesReference) {
return (variablesReference / 1e9)|0;
}
Object.assign(exports, {
JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString
});

View File

@@ -371,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);
@@ -1048,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);
@@ -1084,7 +1090,7 @@ function _JDWP() {
} }
// 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(m, i, res) { function(m, i, res) {
m.encode(res,i); m.encode(res,i);
}, },
@@ -1099,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 = [{
@@ -1133,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) {

View File

@@ -117,7 +117,7 @@ class LogcatContent {
// no point in formatting the data if there are no connected clients // no point in formatting the data if there are no connected clients
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid); var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
if (clients.length) { if (clients.length) {
var lines = '<div class="logblock">' + this._htmllogs.join(os.EOL) + '</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

View File

@@ -10,7 +10,7 @@
.E {color:#f88} .vscode-light .E {color:#f55} .vscode-high-contrast .E {color:#f00} .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} .F {color:#f66} .vscode-light .F {color:#f00} .vscode-high-contrast .F {color:#f00}
.hide {display:none} .hide {display:none}
.logblock {display:inline-block} .logblock {display:block}
.a {display:flex;flex-direction:column;position:absolute;top:0;bottom:0;left:0;right:0;} .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} .b {flex:0 0 auto;border-bottom: 1px solid rgba(128,128,128,.2); padding-bottom: .2em}
.vscode-high-contrast .b {border-color: #0cc} .vscode-high-contrast .b {border-color: #0cc}
@@ -56,7 +56,7 @@
const setStatus = (x) => { getId('status').textContent = ''+x; } const setStatus = (x) => { getId('status').textContent = ''+x; }
const start = () => { const start = () => {
var rows = getId('rows'), filter = getId('q'); var rows = getId('rows'), filter = getId('q');
var last_known_scroll_position=0, selectall=0, logcount=0, currfilter,ws; var last_known_scroll_position=0, selectall=0, logcount=0, prevlc=0, currfilter,ws;
var selecttext = (rows) => { var selecttext = (rows) => {
if (!rows) return window.getSelection().empty(); if (!rows) return window.getSelection().empty();
var range = document.createRange(); var range = document.createRange();
@@ -74,8 +74,17 @@
} }
}; };
updateLogCountDisplay = () => { updateLogCountDisplay = () => {
var msg = currfilter ? `${currfilter.matchCounts.true}/${logcount}` : logcount var diff = logcount - prevlc;
if (diff <= 0 || diff > 100) {
prevlc = logcount;
var msg = currfilter ? `${currfilter.matchCounts.true}/${logcount}` : logcount;
getId('lcount').textContent = msg; getId('lcount').textContent = msg;
return;
}
prevlc++;
var msg = currfilter ? `${currfilter.matchCounts.true}/${prevlc}` : prevlc;
getId('lcount').textContent = msg;
setTimeout(updateLogCountDisplay, 1);
} }
showFilterErr = (msg) => { showFilterErr = (msg) => {
filter.style['border-color'] = 'red'; filter.style['border-color'] = 'red';
@@ -151,7 +160,7 @@
if (/^:logcat_cleared$/.test(rawlogs)) { if (/^:logcat_cleared$/.test(rawlogs)) {
rows.innerHTML = ''; rows.innerHTML = '';
rows.insertAdjacentHTML('afterbegin','<div>---- log cleared ----</div>'); rows.insertAdjacentHTML('afterbegin','<div>---- log cleared ----</div>');
logcount = 0; logcount = prevlc = 0;
if (currfilter) currfilter.matchCounts = {true:0,false:0}; if (currfilter) currfilter.matchCounts = {true:0,false:0};
updateLogCountDisplay(); updateLogCountDisplay();
return; return;

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;

389
src/variables.js Normal file
View File

@@ -0,0 +1,389 @@
'use strict'
const { JTYPES, exmsg_var_name, createJavaString } = require('./globals');
const NumberBaseConverter = require('./nbc');
const $ = require('./jq-promise');
/*
Class used to manage stack frame locals and other evaluated expressions
*/
class AndroidVariables {
constructor(session, baseId) {
this.session = session;
this.dbgr = session.dbgr;
// the incremental reference id generator for stack frames, locals, etc
this.nextId = baseId;
// hashmap of variables and frames
this.variableHandles = {};
// hashmap<objectid, variablesReference>
this.objIdCache = {};
// allow primitives to be expanded to show more info
this._expandable_prims = false;
}
addVariable(varinfo) {
var variablesReference = ++this.nextId;
this.variableHandles[variablesReference] = varinfo;
return variablesReference;
}
clear() {
this.variableHandles = {};
}
setVariable(variablesReference, varinfo) {
this.variableHandles[variablesReference] = varinfo;
}
_getObjectIdReference(type, objvalue) {
// we need the type signature because we must have different id's for
// an instance and it's supertype instance (which obviously have the same objvalue)
var key = type.signature + objvalue;
return this.objIdCache[key] || (this.objIdCache[key] = ++this.nextId);
}
getVariables(variablesReference) {
var varinfo = this.variableHandles[variablesReference];
if (!varinfo) {
return $.Deferred().resolve([]);
}
else if (varinfo.cached) {
return $.Deferred().resolve(this._local_to_variable(varinfo.cached));
}
else if (varinfo.objvar) {
// object fields request
return this.dbgr.getsupertype(varinfo.objvar, {varinfo})
.then((supertype, x) => {
x.supertype = supertype;
return this.dbgr.getfieldvalues(x.varinfo.objvar, x);
})
.then((fields, x) => {
// add an extra msg field for exceptions
if (!x.varinfo.exception) return;
x.fields = fields;
return this.dbgr.invokeToString(x.varinfo.objvar.value, x.varinfo.threadid, varinfo.objvar.type.signature, x)
.then((call,x) => {
call.name = exmsg_var_name;
x.fields.unshift(call);
return $.Deferred().resolveWith(this, [x.fields, x]);
});
})
.then((fields, x) => {
// ignore supertypes of Object
x.supertype && x.supertype.signature!=='Ljava/lang/Object;' && fields.unshift({
vtype:'super',
name:':super',
hasnullvalue:false,
type: x.supertype,
value: x.varinfo.objvar.value,
valid:true,
});
x.varinfo.cached = fields;
return this._local_to_variable(fields);
});
}
else if (varinfo.arrvar) {
// array elements request
var range = varinfo.range, count = range[1] - range[0];
// should always have a +ve count, but just in case...
if (count <= 0) return $.Deferred().resolve([]);
// add some hysteresis
if (count > 110) {
// create subranges in the sub-power of 10
var subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100), variables = [];
for (var i=range[0],varref,v; i < range[1]; i+= subrangelen) {
varref = ++this.nextId;
v = this.variableHandles[varref] = { varref:varref, arrvar:varinfo.arrvar, range:[i, Math.min(i+subrangelen, range[1])] };
variables.push({name:`[${v.range[0]}..${v.range[1]-1}]`,type:'',value:'',variablesReference:varref});
}
return $.Deferred().resolve(variables);
}
// get the elements for the specified range
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count)
.then((elements) => {
varinfo.cached = elements;
return this._local_to_variable(elements);
});
}
else if (varinfo.bigstring) {
return this.dbgr.getstringchars(varinfo.bigstring.value)
.then((s) => {
return this._local_to_variable([{name:'<value>',hasnullvalue:false,string:s,type:JTYPES.String,valid:true}]);
});
}
else if (varinfo.primitive) {
// convert the primitive value into alternate formats
var variables = [], bits = {J:64,I:32,S:16,B:8}[varinfo.signature];
const pad = (u,base,len) => ('0000000000000000000000000000000'+u.toString(base)).slice(-len);
switch(varinfo.signature) {
case 'Ljava/lang/String;':
variables.push({name:'<length>',type:'',value:varinfo.value.toString(),variablesReference:0});
break;
case 'C':
variables.push({name:'<charCode>',type:'',value:varinfo.value.charCodeAt(0).toString(),variablesReference:0});
break;
case 'J':
// because JS cannot handle 64bit ints, we need a bit of extra work
var v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,'');
const s4 = { hi:parseInt(v64hex.slice(0,8),16), lo:parseInt(v64hex.slice(-8),16) };
variables.push(
{name:'<binary>',type:'',value:pad(s4.hi,2,32)+pad(s4.lo,2,32),variablesReference:0}
,{name:'<decimal>',type:'',value:NumberBaseConverter.hexToDec(v64hex,false),variablesReference:0}
,{name:'<hex>',type:'',value:pad(s4.hi,16,8)+pad(s4.lo,16,8),variablesReference:0}
);
break;
default:// integer/short/byte value
const u = varinfo.value >>> 0;
variables.push(
{name:'<binary>',type:'',value:pad(u,2,bits),variablesReference:0}
,{name:'<decimal>',type:'',value:u.toString(10),variablesReference:0}
,{name:'<hex>',type:'',value:pad(u,16,bits/4),variablesReference:0}
);
break;
}
return $.Deferred().resolve(variables);
}
else if (varinfo.frame) {
// frame locals request - this should be handled by AndroidDebugThread instance
return $.Deferred().resolve([]);
} else {
// something else?
return $.Deferred().resolve([]);
}
}
/**
* Converts locals (or other vars) in debugger format into Variable objects used by VSCode
*/
_local_to_variable(v) {
if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._local_to_variable(v));
var varref = 0, objvalue, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
switch(true) {
case v.hasnullvalue && JTYPES.isReference(v.type):
// null object or array type
objvalue = 'null';
break;
case v.type.signature === JTYPES.Object.signature:
// Object doesn't really have anything worth seeing, so just treat it as unexpandable
objvalue = v.type.typename;
break;
case v.type.signature === JTYPES.String.signature:
objvalue = JSON.stringify(v.string);
if (v.biglen) {
// since this is a big string - make it viewable on expand
varref = ++this.nextId;
this.variableHandles[varref] = {varref:varref, bigstring:v};
objvalue = `String (length:${v.biglen})`;
}
else if (this._expandable_prims) {
// as a courtesy, allow strings to be expanded to see their length
varref = ++this.nextId;
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.string.length};
}
break;
case JTYPES.isArray(v.type):
// non-null array type - if it's not zero-length add another variable reference so the user can expand
if (v.arraylen) {
varref = this._getObjectIdReference(v.type, v.value);
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
}
objvalue = v.type.typename.replace(/]$/, v.arraylen+']'); // insert len as the final array bound
break;
case JTYPES.isObject(v.type):
// non-null object instance - add another variable reference so the user can expand
varref = this._getObjectIdReference(v.type, v.value);
this.variableHandles[varref] = {varref:varref, objvar:v};
objvalue = v.type.typename;
break;
case v.type.signature === 'C':
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'};
if (cmap[v.char]) {
objvalue = `'\\${cmap[v.char]}'`;
} else if (v.value < 32) {
objvalue = v.value ? `'\\u${('000'+v.value.toString(16)).slice(-4)}'` : "'\\0'";
} else objvalue = `'${v.char}'`;
break;
case v.type.signature === 'J':
// because JS cannot handle 64bit ints, we need a bit of extra work
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
objvalue = NumberBaseConverter.hexToDec(v64hex, true);
break;
default:
// other primitives: int, boolean, etc
objvalue = v.value.toString();
break;
}
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
varref = ++this.nextId;
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.value};
}
return {
name: v.name,
type: typename,
value: objvalue,
variablesReference: varref,
}
}
setVariableValue(args) {
const failSetVariableRequest = reason => $.Deferred().reject(new Error(reason));
var v = this.variableHandles[args.variablesReference];
if (!v || !v.cached) {
return failSetVariableRequest(`Variable '${args.name}' not found`);
}
var destvar = v.cached.find(v => v.name===args.name);
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
return failSetVariableRequest(`The value is read-only and cannot be updated.`);
}
// be nice and remove any superfluous whitespace
var value = args.value.trim();
if (!value) {
// just ignore blank requests
var vsvar = this._local_to_variable(destvar);
return $.Deferred().resolve(vsvar);
}
// non-string reference types can only set to null
if (/^L/.test(destvar.type.signature) && destvar.type.signature !== JTYPES.String.signature) {
if (value !== 'null') {
return failSetVariableRequest('Object references can only be set to null');
}
}
// convert the new value into a debugger-compatible object
var m, num, data, datadef;
switch(true) {
case value === 'null':
data = {valuetype:'oref',value:null}; // null object reference
break;
case /^(true|false)$/.test(value):
data = {valuetype:'boolean',value:value!=='false'}; // boolean literal
break;
case !!(m=value.match(/^[+-]?0x([0-9a-f]+)$/i)):
// hex integer- convert to decimal and fall through
if (m[1].length < 52/4)
value = parseInt(value, 16).toString(10);
else
value = NumberBaseConverter.hexToDec(value);
m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/);
// fall-through
case !!(m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/)):
// decimal integer
num = parseFloat(value, 10); // parseInt() can't handle exponents
switch(true) {
case (num >= -128 && num <= 127): data = {valuetype:'byte',value:num}; break;
case (num >= -32768 && num <= 32767): data = {valuetype:'short',value:num}; break;
case (num >= -2147483648 && num <= 2147483647): data = {valuetype:'int',value:num}; break;
case /inf/i.test(num): return failSetVariableRequest(`Value '${value}' exceeds the maximum number range.`);
case /^[FD]$/.test(destvar.type.signature): data = {valuetype:'float',value:num}; break;
default:
// long (or larger) - need to use the arbitrary precision class
data = {valuetype:'long',value:NumberBaseConverter.decToHex(value, 16)};
switch(true){
case data.value.length > 16:
case num > 0 && data.value.length===16 && /[^0-7]/.test(data.value[0]):
// number exceeds signed 63 bit - make it a float
data = {valuetype:'float',value:num};
break;
}
}
break;
case !!(m=value.match(/^(Float|Double)\s*\.\s*(POSITIVE_INFINITY|NEGATIVE_INFINITY|NaN)$/)):
// Java special float constants
data = {valuetype:m[1].toLowerCase(),value:{POSITIVE_INFINITY:Infinity,NEGATIVE_INFINITY:-Infinity,NaN:NaN}[m[2]]};
break;
case !!(m=value.match(/^([+-])?infinity$/i)):// allow js infinity
data = {valuetype:'float',value:m[1]!=='-'?Infinity:-Infinity};
break;
case !!(m=value.match(/^nan$/i)): // allow js nan
data = {valuetype:'float',value:NaN};
break;
case !!(m=value.match(/^[+-]?[0-9]+[eE][-][0-9]+([dDfF])?$/)):
case !!(m=value.match(/^[+-]?[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?([dDfF])?$/)):
// decimal float
num = parseFloat(value);
data = {valuetype:/^[dD]$/.test(m[1]) ? 'double': 'float',value:num};
break;
case !!(m=value.match(/^'(?:\\u([0-9a-fA-F]{4})|\\([bfrntv0'])|(.))'$/)):
// character literal
var cvalue = m[1] ? String.fromCharCode(parseInt(m[1],16)) :
m[2] ? {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v',0:'\0',"'":"'"}[m[2]]
: m[3]
data = {valuetype:'char',value:cvalue};
break;
case !!(m=value.match(/^"[^"\\\n]*(\\.[^"\\\n]*)*"$/)):
// string literal - we need to get the runtime to create a new string first
datadef = createJavaString(this.dbgr, value).then(stringlit => ({valuetype:'oref', value:stringlit.value}));
break;
default:
// invalid literal
return failSetVariableRequest(`'${value}' is not a valid Java literal.`);
}
if (!datadef) {
// as a nicety, if the destination is a string, stringify any primitive value
if (data.valuetype !== 'oref' && destvar.type.signature === JTYPES.String.signature) {
datadef = createJavaString(this.dbgr, data.value.toString(), {israw:true})
.then(stringlit => ({valuetype:'oref', value:stringlit.value}));
} else if (destvar.type.signature.length===1) {
// if the destination is a primitive, we need to range-check it here
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
// weirdness if we allow primitives to be set with out-of-range values
var validmap = {
B:'byte,char', // char may not fit - we special-case this later
S:'byte,short,char',
I:'byte,short,int,char',
J:'byte,short,int,long,char',
F:'byte,short,int,long,char,float',
D:'byte,short,int,long,char,double,float',
C:'byte,short,char',Z:'boolean',
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
};
var is_in_range = (validmap[destvar.type.signature]||'').indexOf(data.valuetype) >= 0;
if (destvar.type.signature === 'B' && data.valuetype === 'char')
is_in_range = validmap.isCharInRangeForByte(data.value);
if (!is_in_range) {
return failSetVariableRequest(`'${value}' is not compatible with variable type: ${destvar.type.typename}`);
}
// check complete - make sure the type matches the destination and use a resolved deferred with the value
if (destvar.type.signature!=='C' && data.valuetype === 'char')
data.value = data.value.charCodeAt(0); // convert char to it's int value
if (destvar.type.signature==='J' && typeof data.value === 'number')
data.value = NumberBaseConverter.decToHex(''+data.value,16); // convert ints to hex-string longs
data.valuetype = destvar.type.typename;
datadef = $.Deferred().resolveWith(this,[data]);
}
}
return datadef.then(data => {
// setxxxvalue sets the new value and then returns a new local for the variable
switch(destvar.vtype) {
case 'field': return this.dbgr.setfieldvalue(destvar, data);
case 'local': return this.dbgr.setlocalvalue(destvar, data);
case 'arrelem':
var idx = parseInt(args.name, 10), count=1;
if (idx < 0 || idx >= destvar.data.arrobj.arraylen) throw new Error('Array index out of bounds');
return this.dbgr.setarrayvalues(destvar.data.arrobj, idx, count, data);
default: throw new Error('Unsupported variable type');
}
})
.then(newlocalvar => {
if (destvar.vtype === 'arrelem') newlocalvar = newlocalvar[0];
Object.assign(destvar, newlocalvar);
var vsvar = this._local_to_variable(destvar);
return vsvar;
})
.fail(e => {
return failSetVariableRequest(`Variable update failed. ${e.message||''}`);
});
}
}
exports.AndroidVariables = AndroidVariables;