mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 01:48:18 +00:00
added support for exception breakpoints
This commit is contained in:
@@ -139,6 +139,8 @@ class AndroidDebugSession extends DebugSession {
|
||||
this._locals_done = null;
|
||||
// the fifo queue of evaluations (watches, hover, etc)
|
||||
this._evals_queue = [];
|
||||
// the last (current) exception info
|
||||
this._last_exception = null;
|
||||
|
||||
// since we want to send breakpoint events, we will assign an id to every event
|
||||
// so that the frontend can match events with breakpoints.
|
||||
@@ -166,6 +168,12 @@ class AndroidDebugSession extends DebugSession {
|
||||
// This debug adapter implements the configurationDoneRequest.
|
||||
response.body.supportsConfigurationDoneRequest = true;
|
||||
|
||||
// we support some exception options
|
||||
response.body.exceptionBreakpointFilters = [
|
||||
{ label:'All Exceptions', filter:'all', default:false },
|
||||
{ label:'Uncaught Exceptions', filter:'uncaught', default:true },
|
||||
];
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
@@ -276,6 +284,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.dbgr.on('bpstatechange', this, this.onBreakpointStateChange)
|
||||
.on('bphit', this, this.onBreakpointHit)
|
||||
.on('step', this, this.onStep)
|
||||
.on('exception', this, this.onException)
|
||||
.on('disconnect', this, this.onDebuggerDisconnect);
|
||||
this.waitForConfigurationDone = $.Deferred();
|
||||
// - tell the client we're initialised and ready for breakpoint info, etc
|
||||
@@ -406,6 +415,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
package: pkgname,
|
||||
package_path: fpn,
|
||||
srcroot: path.join(app_root,src_folder),
|
||||
public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.java$/.test(sf)).map(sf => sf.match(/^(.*)\.java$/)[1])
|
||||
}
|
||||
}
|
||||
// add the subfiles to the list to process
|
||||
@@ -566,6 +576,20 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) {
|
||||
this.dbgr.clearBreakOnExceptions({response,args})
|
||||
.then(x => {
|
||||
if (x.args.filters.includes('all')) {
|
||||
x.set = this.dbgr.setBreakOnExceptions('both', x);
|
||||
} else if (x.args.filters.includes('uncaught')) {
|
||||
x.set = this.dbgr.setBreakOnExceptions('uncaught', x);
|
||||
} else {
|
||||
x.set = $.Deferred().resolveWith(this, [x]);
|
||||
}
|
||||
x.set.then(x => this.sendResponse(x.response));
|
||||
});
|
||||
}
|
||||
|
||||
threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
|
||||
|
||||
this.dbgr.allthreads(response)
|
||||
@@ -633,9 +657,13 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
scopesRequest(response/*: DebugProtocol.ScopesResponse*/, args/*: DebugProtocol.ScopesArguments*/) {
|
||||
var scopes = [new Scope("Local", args.frameId, false)];
|
||||
if (this._last_exception) {
|
||||
scopes.push(new Scope("Exception", this._last_exception.varref, false));
|
||||
}
|
||||
|
||||
response.body = {
|
||||
scopes: [new Scope("Local", args.frameId, false)]
|
||||
scopes: scopes
|
||||
};
|
||||
this.sendResponse(response);
|
||||
}
|
||||
@@ -826,6 +854,17 @@ class AndroidDebugSession extends DebugSession {
|
||||
};
|
||||
this.sendResponse(response);
|
||||
}
|
||||
else if (varinfo.exception) {
|
||||
this.dbgr.getExceptionLocal(varinfo.exception, {varinfo,response})
|
||||
.then((ex_local,x) => {
|
||||
x.ex_local = ex_local;
|
||||
return this.dbgr.invokeToString(ex_local.value, x.varinfo.threadid, ex_local.type.signature, x);
|
||||
})
|
||||
.then((call,x) => {
|
||||
call.name = '{msg}';
|
||||
return_mapped_vars(x.varinfo.cached = [call,x.ex_local], x.response);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// frame locals request
|
||||
this.dbgr.getlocals(varinfo.frame.threadid, varinfo.frame, response)
|
||||
@@ -843,6 +882,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) {
|
||||
D('Continue');
|
||||
this._variableHandles = {};
|
||||
this._last_exception = null;
|
||||
// sometimes, the device is so quick that a breakpoint is hit
|
||||
// before we've completed the resume promise chain.
|
||||
// so tell the client that we've resumed now and just send a StoppedEvent
|
||||
@@ -879,13 +919,12 @@ class AndroidDebugSession extends DebugSession {
|
||||
doStep(which, response, args) {
|
||||
D('step '+which);
|
||||
this._variableHandles = {};
|
||||
var threadid = ('000000000000000' + args.threadId.toString(16)).slice(-16);
|
||||
this.dbgr.step(which, threadid)
|
||||
.then(() => {
|
||||
this._last_exception = null;
|
||||
this._running = true;
|
||||
this._locals_done = $.Deferred();
|
||||
var threadid = ('000000000000000' + args.threadId.toString(16)).slice(-16);
|
||||
this.dbgr.step(which, threadid);
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) {
|
||||
@@ -900,6 +939,24 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.doStep('out', response, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the debugger if an exception event is triggered
|
||||
*/
|
||||
onException(e) {
|
||||
D('exception hit: ' + JSON.stringify(e.throwlocation));
|
||||
// it's possible for the debugger to send multiple exception notifications, depending on the package filters
|
||||
// , so just ignore them if we've already stopped
|
||||
if (!this._running) return;
|
||||
this._running = false;
|
||||
this._last_exception = {
|
||||
exception: e.event.exception,
|
||||
threadid: e.throwlocation.threadid,
|
||||
varref: ++this._nextObjVarRef,
|
||||
};
|
||||
this._variableHandles[this._last_exception.varref] = this._last_exception;
|
||||
this.sendEvent(new StoppedEvent("exception", parseInt(e.throwlocation.threadid,16)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by VSCode to perform watch, console and hover evaluations
|
||||
*/
|
||||
|
||||
180
src/debugger.js
180
src/debugger.js
@@ -11,6 +11,7 @@ function Debugger() {
|
||||
this.connection = null;
|
||||
this.ons = {};
|
||||
this.breakpoints = { all: [], enabled: {}, bysrcloc: {} };
|
||||
this.exception_ids = [];
|
||||
this.JDWP = new _JDWP();
|
||||
this.session = null;
|
||||
this.globals = Debugger.globals;
|
||||
@@ -810,6 +811,100 @@ Debugger.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
getExceptionLocal: function (ex_ref_value, extra) {
|
||||
var x = {
|
||||
ex_ref_value: ex_ref_value,
|
||||
extra: extra
|
||||
};
|
||||
return this.session.adbclient.jdwp_command({
|
||||
ths: this,
|
||||
extra: x,
|
||||
cmd: this.JDWP.Commands.GetObjectType(ex_ref_value),
|
||||
})
|
||||
.then((typeref, x) => this.session.adbclient.jdwp_command({
|
||||
ths: this,
|
||||
extra: x,
|
||||
cmd: this.JDWP.Commands.signature(typeref)
|
||||
}))
|
||||
.then((type, x) => {
|
||||
x.type = type;
|
||||
return this.gettypedebuginfo(type.signature, x)
|
||||
})
|
||||
.then((dbgtype, x) => {
|
||||
return this._ensurefields(dbgtype[x.type.signature], x)
|
||||
})
|
||||
.then((typeinfo, x) => {
|
||||
return this._mapvalues('exception', [{ name: '{ex}', type: x.type }], [x.ex_ref_value], {}, x);
|
||||
})
|
||||
.then((res, x) => {
|
||||
return $.Deferred().resolveWith(this, [res[0], x.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 };
|
||||
x.return_type_signature = method_sig.match(/\)(.*)/)[1];
|
||||
return this.gettypedebuginfo(x.return_type_signature)
|
||||
.then(dbgtypes => {
|
||||
x.return_type = dbgtypes[x.return_type_signature].type;
|
||||
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
|
||||
var o = {
|
||||
dbgr:this,
|
||||
def:$.Deferred(),
|
||||
x: x,
|
||||
find_method(typeinfo) {
|
||||
for (var mid in typeinfo.methods) {
|
||||
var m = typeinfo.methods[mid];
|
||||
if ((m.name === this.x.method_name) && ((m.genericsig||m.sig) === this.x.method_sig)) {
|
||||
this.def.resolveWith(this, [typeinfo, m, this.x]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// search the supertype
|
||||
if (typeinfo.type.signature==='Ljava/lang/Object;') {
|
||||
this.def.rejectWith(this, [new Error('No such method: ' + this.x.method_name + ' ' + this.x.method_sig)]);
|
||||
return;
|
||||
}
|
||||
|
||||
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_method(typeinfo)
|
||||
});
|
||||
}
|
||||
}
|
||||
o.find_method(typeinfo);
|
||||
return o.def;
|
||||
})
|
||||
.then((typeinfo, method, x) => {
|
||||
return this.session.adbclient.jdwp_command({
|
||||
ths: this,
|
||||
extra: x,
|
||||
cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, typeinfo.info.typeid, method.methodid, x.args),
|
||||
});
|
||||
})
|
||||
.then((res, x) => {
|
||||
if (/^0+$/.test(res.exception))
|
||||
return this._mapvalues('return', [{ name:'{return}', type:x.return_type }], [res.return_value], {}, x);
|
||||
// todo - handle reutrn exceptions
|
||||
})
|
||||
.then((res, x) => $.Deferred().resolveWith(this, [res[0], x.extra])); // res = {return_value, exception}
|
||||
},
|
||||
|
||||
invokeToString(objectid, threadid, type_signature, extra) {
|
||||
return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra);
|
||||
},
|
||||
|
||||
getstringchars: function (stringref, extra) {
|
||||
return this.session.adbclient.jdwp_command({
|
||||
ths: this,
|
||||
@@ -1343,6 +1438,89 @@ Debugger.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
clearBreakOnExceptions: function(extra) {
|
||||
var o = {
|
||||
dbgr: this,
|
||||
def: $.Deferred(),
|
||||
extra: extra,
|
||||
next() {
|
||||
if (!this.dbgr.exception_ids.length) {
|
||||
return this.def.resolveWith(this.dbgr, [this.extra]); // done
|
||||
}
|
||||
// clear next pattern
|
||||
this.dbgr.session.adbclient.jdwp_command({
|
||||
cmd: this.dbgr.JDWP.Commands.ClearExceptionBreak(this.dbgr.exception_ids.pop())
|
||||
})
|
||||
.then(() => this.next())
|
||||
.fail(e => this.def.rejectWith(this, [e]))
|
||||
}
|
||||
};
|
||||
o.next();
|
||||
return o.def;
|
||||
},
|
||||
|
||||
setBreakOnExceptions: function(which, extra) {
|
||||
var onevent = {
|
||||
data: {
|
||||
dbgr: this,
|
||||
},
|
||||
fn: function (e) {
|
||||
this._findcmllocation(this.session.classes, e.event.throwlocation)
|
||||
.then(tloc => {
|
||||
this._findcmllocation(this.session.classes, e.event.catchlocation)
|
||||
.then(cloc => {
|
||||
var eventdata = {
|
||||
event: e.event,
|
||||
throwlocation: Object.assign({ threadid: e.event.threadid }, tloc),
|
||||
catchlocation: Object.assign({ threadid: e.event.threadid }, cloc),
|
||||
};
|
||||
this.session.stoppedlocation = Object.assign({}, eventdata.throwlocation);
|
||||
this._trigger('exception', eventdata);
|
||||
})
|
||||
})
|
||||
}.bind(this)
|
||||
};
|
||||
|
||||
var c = false, u = false;
|
||||
switch (which) {
|
||||
case 'caught': c = true; break;
|
||||
case 'uncaught': u = true; break;
|
||||
case 'both': c = u = true; break;
|
||||
default: throw new Error('Invalid exception option');
|
||||
}
|
||||
// when setting up the exceptions, we filter by packages containing public classes in the current session
|
||||
// - each filter needs a separate call (I think), so we do this as an asynchronous list
|
||||
var pkgs = this.session.build.packages;
|
||||
var pkgs_to_monitor = Object.keys(pkgs).filter(pkgname => pkgs[pkgname].public_classes.length);
|
||||
var o = {
|
||||
dbgr: this,
|
||||
pkgs: pkgs_to_monitor,
|
||||
caught: c,
|
||||
uncaught: u,
|
||||
onevent: onevent,
|
||||
cmds:[],
|
||||
def: $.Deferred(),
|
||||
extra: extra,
|
||||
next() {
|
||||
if (!this.pkgs.length) {
|
||||
this.def.resolveWith(this.dbgr, [this.extra]); // done
|
||||
return;
|
||||
}
|
||||
// setup next pattern
|
||||
this.dbgr.session.adbclient.jdwp_command({
|
||||
cmd: this.dbgr.JDWP.Commands.SetExceptionBreak(this.pkgs.shift() + '.*', this.caught, this.uncaught, this.onevent),
|
||||
})
|
||||
.then(x => {
|
||||
this.dbgr.exception_ids.push(x.id);
|
||||
this.next();
|
||||
})
|
||||
.fail(e => this.def.rejectWith(this, [e]))
|
||||
}
|
||||
};
|
||||
o.next();
|
||||
return o.def;
|
||||
},
|
||||
|
||||
_loadclzinfo: function (signature) {
|
||||
return this.gettypedebuginfo(signature)
|
||||
.then(function (classes) {
|
||||
@@ -1429,6 +1607,8 @@ Debugger.prototype = {
|
||||
},
|
||||
|
||||
_findmethodasync: function (classes, location) {
|
||||
// some locations are null (which causes the jdwp command to fail)
|
||||
if (/^0+$/.test(location.cid)) return $.Deferred().resolveWith(this, [null]);
|
||||
var m = this._findmethod(classes, location.cid, location.mid);
|
||||
if (m) return $.Deferred().resolveWith(this, [m]);
|
||||
// convert the classid to a type signature
|
||||
|
||||
90
src/jdwp.js
90
src/jdwp.js
@@ -97,7 +97,7 @@ function _JDWP() {
|
||||
}
|
||||
|
||||
if (this.errorcode != 0) {
|
||||
console.error("Command failed: error " + this.errorcode);
|
||||
console.error("Command failed: error " + this.errorcode, this);
|
||||
}
|
||||
|
||||
if (!this.errorcode && this.command && this.command.replydecodefn) {
|
||||
@@ -131,6 +131,15 @@ function _JDWP() {
|
||||
var DataCoder = {
|
||||
_idsizes:null,
|
||||
|
||||
nullRefValue: function() {
|
||||
if (!this._idsizes._nullreftypeid) {
|
||||
var x = '00', len = this._idsizes.reftypeidsize * 2; // each byte needs 2 chars
|
||||
while (x.length < len) x += x;
|
||||
this._idsizes._nullreftypeid = x.slice(0, len); // should be power of 2, but just in case...
|
||||
}
|
||||
return this._idsizes._nullreftypeid;
|
||||
},
|
||||
|
||||
decodeString: function(o) {
|
||||
var rd = o.data;
|
||||
var utf8len=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]);
|
||||
@@ -144,7 +153,7 @@ function _JDWP() {
|
||||
var rd = o.data;
|
||||
var res1=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]);
|
||||
var res2=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]);
|
||||
return intToHex(res1,8)+intToHex(res2,8);
|
||||
return intToHex(res1>>>0,8)+intToHex(res2>>>0,8); // >>> 0 ensures +ve value
|
||||
},
|
||||
decodeInt: function(o) {
|
||||
var rd = o.data;
|
||||
@@ -241,6 +250,9 @@ function _JDWP() {
|
||||
decodeStatus : function(o) {
|
||||
return this.mapflags(this.decodeInt(o), ['verified','prepared','initialized','error']);
|
||||
},
|
||||
decodeTaggedObjectID : function(o) {
|
||||
return this.decodeValue(o);
|
||||
},
|
||||
decodeValue : function(o) {
|
||||
var rd = o.data;
|
||||
return this.tagtodecoder(rd[o.idx++]).call(this, o);
|
||||
@@ -346,6 +358,13 @@ function _JDWP() {
|
||||
event.threadid = this.decodeORef(o);
|
||||
event.location = this.decodeLocation(o);
|
||||
break;
|
||||
case 4: // exception
|
||||
event.reqid = this.decodeInt(o);
|
||||
event.threadid = this.decodeORef(o);
|
||||
event.throwlocation = this.decodeLocation(o);
|
||||
event.exception = this.decodeTaggedObjectID(o);
|
||||
event.catchlocation = this.decodeLocation(o); // 0 = uncaught
|
||||
break;
|
||||
case 8: // classprepare
|
||||
event.reqid = this.decodeInt(o);
|
||||
event.threadid = this.decodeORef(o);
|
||||
@@ -804,6 +823,19 @@ function _JDWP() {
|
||||
}
|
||||
);
|
||||
},
|
||||
GetObjectType:function(objectid) {
|
||||
return new Command('GetObjectType:'+objectid, 9, 1,
|
||||
function() {
|
||||
var res=[];
|
||||
DataCoder.encodeRef(res, objectid);
|
||||
return res;
|
||||
},
|
||||
function(o) {
|
||||
DataCoder.decodeRefType(o);
|
||||
return DataCoder.decodeTRef(o);
|
||||
}
|
||||
);
|
||||
},
|
||||
GetFieldValues:function(objectid, fields) {
|
||||
return new Command('GetFieldValues:'+objectid, 9, 2,
|
||||
function() {
|
||||
@@ -842,6 +874,27 @@ function _JDWP() {
|
||||
}
|
||||
);
|
||||
},
|
||||
InvokeMethod:function(objectid, threadid, classid, methodid, args) {
|
||||
return new Command('InvokeMethod:'+[objectid, threadid, classid, methodid, args].join(','), 9, 6,
|
||||
function() {
|
||||
var res=[];
|
||||
DataCoder.encodeRef(res, objectid);
|
||||
DataCoder.encodeRef(res, threadid);
|
||||
DataCoder.encodeRef(res, classid);
|
||||
DataCoder.encodeRef(res, methodid);
|
||||
DataCoder.encodeInt(res, args.length);
|
||||
args.forEach(arg => DataCoder.encodeValue(res, arg.type, arg.value));
|
||||
DataCoder.encodeInt(res, 1); // INVOKE_SINGLE_THREADED
|
||||
return res;
|
||||
},
|
||||
function(o) {
|
||||
return {
|
||||
return_value: DataCoder.decodeValue(o),
|
||||
exception: DataCoder.decodeTaggedObjectID(o),
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
GetArrayLength:function(arrobjid) {
|
||||
return new Command('GetArrayLength:'+arrobjid, 13, 1,
|
||||
function() {
|
||||
@@ -1015,6 +1068,39 @@ function _JDWP() {
|
||||
onevent
|
||||
);
|
||||
},
|
||||
ClearExceptionBreak:function(requestid) {
|
||||
// kind(4=exception)
|
||||
return this.ClearEvent("exception",4,requestid);
|
||||
},
|
||||
SetExceptionBreak:function(pattern, caught, uncaught, onevent) {
|
||||
// a wrapper around SetEventRequest
|
||||
var mods = [{
|
||||
modkind:8, // exceptiononly
|
||||
reftypeid: DataCoder.nullRefValue(), // exception class
|
||||
caught: caught,
|
||||
uncaught: uncaught,
|
||||
}];
|
||||
pattern && mods.unshift({
|
||||
modkind:5, // classmatch
|
||||
pattern: pattern,
|
||||
});
|
||||
// kind(4=exception)
|
||||
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||
return this.SetEventRequest("exception",4,2,mods,
|
||||
function(m, i, res) {
|
||||
res.push(m.modkind);
|
||||
switch(m.modkind) {
|
||||
case 5: DataCoder.encodeString(res, m.pattern); break;
|
||||
case 8:
|
||||
DataCoder.encodeRef(res, m.reftypeid);
|
||||
DataCoder.encodeBoolean(res, m.caught);
|
||||
DataCoder.encodeBoolean(res, m.uncaught);
|
||||
break;
|
||||
}
|
||||
},
|
||||
onevent
|
||||
);
|
||||
},
|
||||
allclasses:function() {
|
||||
// not supported by android
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user