added support for exception breakpoints

This commit is contained in:
adelphes
2017-01-24 01:05:04 +00:00
parent 544461335a
commit b6591e0bcf
3 changed files with 332 additions and 9 deletions

View File

@@ -139,6 +139,8 @@ class AndroidDebugSession extends DebugSession {
this._locals_done = null; this._locals_done = null;
// the fifo queue of evaluations (watches, hover, etc) // the fifo queue of evaluations (watches, hover, etc)
this._evals_queue = []; 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 // since we want to send breakpoint events, we will assign an id to every event
// so that the frontend can match events with breakpoints. // so that the frontend can match events with breakpoints.
@@ -166,6 +168,12 @@ class AndroidDebugSession extends DebugSession {
// This debug adapter implements the configurationDoneRequest. // This debug adapter implements the configurationDoneRequest.
response.body.supportsConfigurationDoneRequest = true; 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); this.sendResponse(response);
} }
@@ -276,6 +284,7 @@ class AndroidDebugSession extends DebugSession {
this.dbgr.on('bpstatechange', this, this.onBreakpointStateChange) this.dbgr.on('bpstatechange', this, this.onBreakpointStateChange)
.on('bphit', this, this.onBreakpointHit) .on('bphit', this, this.onBreakpointHit)
.on('step', this, this.onStep) .on('step', this, this.onStep)
.on('exception', this, this.onException)
.on('disconnect', this, this.onDebuggerDisconnect); .on('disconnect', this, this.onDebuggerDisconnect);
this.waitForConfigurationDone = $.Deferred(); this.waitForConfigurationDone = $.Deferred();
// - tell the client we're initialised and ready for breakpoint info, etc // - tell the client we're initialised and ready for breakpoint info, etc
@@ -406,6 +415,7 @@ class AndroidDebugSession extends DebugSession {
package: pkgname, package: pkgname,
package_path: fpn, package_path: fpn,
srcroot: path.join(app_root,src_folder), srcroot: path.join(app_root,src_folder),
public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.java$/.test(sf)).map(sf => sf.match(/^(.*)\.java$/)[1])
} }
} }
// add the subfiles to the list to process // add the subfiles to the list to process
@@ -566,6 +576,20 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response); 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*/) { threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
this.dbgr.allthreads(response) this.dbgr.allthreads(response)
@@ -633,9 +657,13 @@ class AndroidDebugSession extends DebugSession {
} }
scopesRequest(response/*: DebugProtocol.ScopesResponse*/, args/*: DebugProtocol.ScopesArguments*/) { 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 = { response.body = {
scopes: [new Scope("Local", args.frameId, false)] scopes: scopes
}; };
this.sendResponse(response); this.sendResponse(response);
} }
@@ -826,6 +854,17 @@ class AndroidDebugSession extends DebugSession {
}; };
this.sendResponse(response); 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 { else {
// frame locals request // frame locals request
this.dbgr.getlocals(varinfo.frame.threadid, varinfo.frame, response) this.dbgr.getlocals(varinfo.frame.threadid, varinfo.frame, response)
@@ -843,6 +882,7 @@ class AndroidDebugSession extends DebugSession {
continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) { continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) {
D('Continue'); D('Continue');
this._variableHandles = {}; this._variableHandles = {};
this._last_exception = null;
// sometimes, the device is so quick that a breakpoint is hit // sometimes, the device is so quick that a breakpoint is hit
// before we've completed the resume promise chain. // before we've completed the resume promise chain.
// so tell the client that we've resumed now and just send a StoppedEvent // 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) { doStep(which, response, args) {
D('step '+which); D('step '+which);
this._variableHandles = {}; this._variableHandles = {};
var threadid = ('000000000000000' + args.threadId.toString(16)).slice(-16); this._last_exception = null;
this.dbgr.step(which, threadid)
.then(() => {
this._running = true; this._running = true;
this._locals_done = $.Deferred(); this._locals_done = $.Deferred();
var threadid = ('000000000000000' + args.threadId.toString(16)).slice(-16);
this.dbgr.step(which, threadid);
this.sendResponse(response); this.sendResponse(response);
});
} }
stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) { stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) {
@@ -900,6 +939,24 @@ class AndroidDebugSession extends DebugSession {
this.doStep('out', response, args); 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 * Called by VSCode to perform watch, console and hover evaluations
*/ */

View File

@@ -11,6 +11,7 @@ function Debugger() {
this.connection = null; this.connection = null;
this.ons = {}; this.ons = {};
this.breakpoints = { all: [], enabled: {}, bysrcloc: {} }; this.breakpoints = { all: [], enabled: {}, bysrcloc: {} };
this.exception_ids = [];
this.JDWP = new _JDWP(); this.JDWP = new _JDWP();
this.session = null; this.session = null;
this.globals = Debugger.globals; 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) { getstringchars: function (stringref, extra) {
return this.session.adbclient.jdwp_command({ return this.session.adbclient.jdwp_command({
ths: this, 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) { _loadclzinfo: function (signature) {
return this.gettypedebuginfo(signature) return this.gettypedebuginfo(signature)
.then(function (classes) { .then(function (classes) {
@@ -1429,6 +1607,8 @@ Debugger.prototype = {
}, },
_findmethodasync: function (classes, location) { _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); var m = this._findmethod(classes, location.cid, location.mid);
if (m) return $.Deferred().resolveWith(this, [m]); if (m) return $.Deferred().resolveWith(this, [m]);
// convert the classid to a type signature // convert the classid to a type signature

View File

@@ -97,7 +97,7 @@ function _JDWP() {
} }
if (this.errorcode != 0) { 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) { if (!this.errorcode && this.command && this.command.replydecodefn) {
@@ -131,6 +131,15 @@ function _JDWP() {
var DataCoder = { var DataCoder = {
_idsizes:null, _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) { decodeString: function(o) {
var rd = o.data; var rd = o.data;
var utf8len=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]); 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 rd = o.data;
var res1=(rd[o.idx++]<<24)+(rd[o.idx++]<<16)+(rd[o.idx++]<<8)+(rd[o.idx++]); 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++]); 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) { decodeInt: function(o) {
var rd = o.data; var rd = o.data;
@@ -241,6 +250,9 @@ 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']);
}, },
decodeTaggedObjectID : function(o) {
return this.decodeValue(o);
},
decodeValue : function(o) { decodeValue : function(o) {
var rd = o.data; var rd = o.data;
return this.tagtodecoder(rd[o.idx++]).call(this, o); return this.tagtodecoder(rd[o.idx++]).call(this, o);
@@ -346,6 +358,13 @@ function _JDWP() {
event.threadid = this.decodeORef(o); event.threadid = this.decodeORef(o);
event.location = this.decodeLocation(o); event.location = this.decodeLocation(o);
break; 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 case 8: // classprepare
event.reqid = this.decodeInt(o); event.reqid = this.decodeInt(o);
event.threadid = this.decodeORef(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) { GetFieldValues:function(objectid, fields) {
return new Command('GetFieldValues:'+objectid, 9, 2, return new Command('GetFieldValues:'+objectid, 9, 2,
function() { 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) { GetArrayLength:function(arrobjid) {
return new Command('GetArrayLength:'+arrobjid, 13, 1, return new Command('GetArrayLength:'+arrobjid, 13, 1,
function() { function() {
@@ -1015,6 +1068,39 @@ function _JDWP() {
onevent 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() { allclasses:function() {
// not supported by android // not supported by android
}, },