Files
android-dev-ext/src/debugger.js
adelphes 0931d8747b Various fixes.
Fixes for null source location issues
Retrieve locals on a per-frame basis using a common promise for both scope variables and watches
Fix for string char retrieval (removed closure index)
Better support for displaying Android sources
Display device API level during launch
2017-01-26 16:29:02 +00:00

1674 lines
64 KiB
JavaScript

'use strict'
/*
Debugger: thin wrapper around other classes to manage debug connections
*/
const _JDWP = require('./jdwp')._JDWP;
const { ADBClient } = require('./adbclient');
const $ = require('./jq-promise');
const { D } = require('./util');
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;
}
Debugger.globals = {
portrange: { lowest: 31000, highest: 31099 },
inuseports: [],
debuggers: {},
reserveport: function () {
// choose a random port to use each time
for (var i = 0; i < 10000; i++) {
var portidx = ((Math.random() * 100) | 0);
if (this.inuseports.includes(portidx))
continue; // try again
this.inuseports.push(portidx);
return this.portrange.lowest + portidx;
}
},
freeport: function (port) {
var iuidx = this.inuseports.indexOf(port - this.portrange.lowest);
if (iuidx >= 0) this.inuseports.splice(iuidx, 1);
}
};
Debugger.prototype = {
on: function (which, context, data, fn) {
if (!fn && !data && typeof (context) === 'function') {
fn = context; context = data = null;
}
else if (!fn && typeof (data) === 'function') {
fn = data; data = null;
}
if (!this.ons[which]) this.ons[which] = [];
this.ons[which].push({
context: context, data: data, fn: fn
});
return this;
},
_trigger: function (which, e) {
var k = this.ons[which];
if (!k || !k.length) return this;
k = k.slice();
e = e || {};
e.dbgr = this;
for (var i = 0; i < k.length; i++) {
e.data = k[i].data;
try { k[i].fn.call(k[i].context, e) }
catch (ex) {
D('Exception in event trigger: ' + ex.message);
}
}
return this;
},
startDebugSession(build, deviceid, launcherActivity) {
return this.newSession(build, deviceid)
.runapp('debug', launcherActivity, this)
.then(function (deviceid) {
return this.getDebuggablePIDs(this.session.deviceid, this);
})
.then(function (pids, dbgr) {
// choose the last pid in the list
var pid = pids[pids.length - 1];
// after connect(), the caller must call resume() to begin
return dbgr.connect(pid, dbgr);
})
},
runapp(action, launcherActivity) {
// older (<3) versions of Android only allow target components to be specified with -n
var launchcmdparams = ['--activity-brought-to-front', '-a android.intent.action.MAIN', '-c android.intent.category.LAUNCHER', '-n ' + this.session.build.pkgname + '/' + launcherActivity];
if (action === 'debug') {
launchcmdparams.splice(0, 0, '-D');
}
var x = {
dbgr: this,
shell_cmd: {
command: 'am start ' + launchcmdparams.join(' '),
untilclosed: true,
},
retries: {
count: 10, pause: 1000,
},
deviceid: this.session.deviceid,
deferred: $.Deferred(),
};
tryrunapp(x);
function tryrunapp(x) {
var adb = new ADBClient(x.deviceid);
adb.shell_cmd(x.shell_cmd)
.then(function (stdout) {
// failures:
// Error: Activity not started...
var m = stdout.match(/Error:.*/g);
if (m) {
if (--x.retries.count) {
setTimeout(function (o) {
tryrunapp(o);
}, x.retries.pause, x);
return;
}
return x.deferred.reject({ cat: 'cmd', msg: m[0] });
}
// running the JDWP command so soon after launching hangs, so give it a breather before continuing
setTimeout(x => {
x.deferred.resolveWith(x.dbgr, [x.deviceid])
}, 1000, x);
})
.fail(function (err) {
});
}
return x.deferred;
},
newSession: function (build, deviceid) {
this.session = {
build: build,
deviceid: deviceid,
apilevel: 0,
adbclient: null,
stoppedlocation: null,
classes: {},
// classprepare notifier done
cpndone: false,
preparedclasses: [],
}
return this;
},
/* return a list of deviceids available for debugging */
list_devices: function (extra) {
return new ADBClient().list_devices(extra);
},
getDebuggablePIDs: function (deviceid, extra) {
return new ADBClient(deviceid).jdwp_list({
ths: this,
extra: extra,
})
},
getDebuggableProcesses: function (deviceid, extra) {
var info = {
debugger: this,
adbclient: new ADBClient(deviceid),
extra: extra,
};
return info.adbclient.jdwp_list({
ths: this,
extra: info,
})
.then(function (jdwps, info) {
if (!jdwps.length)
return $.Deferred().resolveWith(this, [[], info.extra]);
info.jdwps = jdwps;
// retrieve the ps list from the device
return info.adbclient.shell_cmd({
ths: this,
extra: info,
command: 'ps',
untilclosed: true,
}).then(function (stdout, info) {
// output should look something like...
// USER PID PPID VSIZE RSS WCHAN PC NAME
// u0_a153 32721 1452 1506500 37916 ffffffff 00000000 S com.example.somepkg
// but we cope with variations so long as PID and NAME exist
var lines = stdout.split(/\r?\n|\r/g);
var hdrs = (lines.shift() || '').trim().toUpperCase().split(/\s+/);
var pidindex = hdrs.indexOf('PID');
var nameindex = hdrs.indexOf('NAME');
var result = { deviceid: info.adbclient.deviceid, name: {}, jdwp: {}, all: [] };
if (pidindex < 0 || nameindex < 0)
return $.Deferred().resolveWith(null, [[], info.extra]);
// scan the list looking for matching pids...
for (var i = 0; i < lines.length; i++) {
var entries = lines[i].trim().replace(/ [S] /, ' ').split(/\s+/);
if (entries.length != hdrs.length) continue;
var jdwpidx = info.jdwps.indexOf(entries[pidindex]);
if (jdwpidx < 0) continue;
// we found a match
var entry = {
jdwp: entries[pidindex],
name: entries[nameindex],
};
result.all.push(entry);
result.name[entry.name] = entry;
result.jdwp[entry.jdwp] = entry;
}
return $.Deferred().resolveWith(this, [result, info.extra]);
})
});
},
/* attach to the debuggable pid
Quite a lot happens in this - we setup port forwarding, complete the JDWP handshake,
setup class loader notifications and call anyone waiting for us.
If anything fails, we call disconnect() to return to a sense of normality.
*/
connect: function (jdwpid, extra) {
switch (this.status()) {
case 'connected':
// already connected - just resolve
return $.Deferred().resolveWith(this, [extra]);
case 'connecting':
// wait for the connection to complete (or fail)
var x = { deferred: $.Deferred(), extra: extra };
this.connection.connectingpromises.push(x);
return x.deferred;
default:
if (!jdwpid)
return $.Deferred().rejectWith(this, [new Error('Debugger not connected')]);
break;
}
var info = {
dbgr: this,
extra: extra,
};
// from this point on, we are in the "connecting" state until the JDWP handshake is complete
// (and we mark as connected) or we fail and return to the disconnected state
this.connection = {
jdwp: jdwpid,
localport: this.globals.reserveport(),
portforwarding: false,
connected: false,
connectingpromises: [],
};
// setup port forwarding
return new ADBClient(this.session.deviceid).jdwp_forward({
ths: this,
extra: info,
localport: this.connection.localport,
jdwp: this.connection.jdwp,
})
.then(function (info) {
this.connection.portforwarding = true;
// after this, the client keeps an open connection until
// jdwp_disconnect() is called
this.session.adbclient = new ADBClient(this.session.deviceid);
return this.session.adbclient.jdwp_connect({
ths: this,
extra: info,
localport: this.connection.localport,
onreply: this._onjdwpmessage,
});
})
.then(function (info) {
// handshake has completed
this.connection.connected = true;
// call suspend first - we shouldn't really need to do this (as the debugger
// is already suspended and will not resume until we tell it), but if we
// don't do this, it logs a complaint...
return this.suspend();
})
.then(function () {
return this.session.adbclient.jdwp_command({
ths: this,
cmd: this.JDWP.Commands.idsizes(),
});
})
.then(function (idsizes) {
// set the class loader event notifier so we can set breakpoints...
this.JDWP.setIDSizes(idsizes);
return this._initbreakpoints();
})
.then(function () {
return new ADBClient(this.session.deviceid).shell_cmd({
ths: this,
command: 'getprop ro.build.version.sdk',
});
})
.then(function (apilevel) {
this.session.apilevel = apilevel.trim();
// at this point, we are ready to go - all the caller needs to do is call resume().
// resolve all the connection promises for those waiting on us (usually none)
var cp = this.connection.connectingpromises;
var deferreds = [this, info];
delete this.connection.connectingpromises;
for (var i = 0; i < cp.length; i++) {
deferreds.push(cp[i].deferred);
cp[i].deferred.resolveWith(this, [cp[i].extra]);
}
return $.when.apply($, deferreds).then(function (dbgr, info) {
return $.Deferred().resolveWith(dbgr, [info.extra]);
})
})
.then(function () {
this._trigger('connected');
})
.fail(function (err) {
this.connection.err = err;
// force a return to the disconnected state
this.disconnect();
})
},
_onjdwpmessage: function (data) {
// decodereply will resolve the promise associated with
// any command this reply is in response to.
var reply = this.JDWP.decodereply(this, data);
if (reply.isevent) {
if (reply.decoded.events && reply.decoded.events.length) {
switch (reply.decoded.events[0].kind.value) {
case 100:
// vm disconnected - sent by plugin
this.disconnect();
break;
}
}
}
},
ensureconnected: function (extra) {
// passing null as the jdwpid will cause a fail if the client is not connected (or connecting)
return this.connect(null, extra);
},
status: function () {
if (!this.connection) return "disconnected";
if (this.connection.connected) return "connected";
return "connecting";
},
forcestop: function (extra) {
return this.ensureconnected()
.then(function () {
return new ADBClient(this.session.deviceid).shell_cmd({
command: 'am force-stop ' + this.session.build.pkgname,
});
})
},
disconnect: function (extra) {
// disconnect is called from a variety of failure scenarios
// so it must be fairly robust in how it undoes stuff
const current_state = this.status();
if (!this.connection)
return $.Deferred().resolveWith(this, [current_state, extra]);
var info = {
connection: this.connection,
current_state: current_state,
extra: extra,
};
// from here on in, this instance is in the disconnected state
this.connection = null;
// fail any waiting for the connection to complete
var cp = info.connection.connectingpromises;
if (cp) {
for (var i = 0; i < cp.length; i++) {
cp[i].deferred.rejectWith(this, [info.connection.err]);
}
}
// reset the breakpoint states
this._finitbreakpoints();
this._trigger('disconnect');
// perform the JDWP disconnect
info.jdwpdisconnect = info.connection.connected
? this.session.adbclient.jdwp_disconnect({ ths: this, extra: info })
: $.Deferred().resolveWith(this, [info]);
return info.jdwpdisconnect
.then(function (info) {
this.session.adbclient = null;
// undo the portforwarding
// todo: replace remove_all with remove_port
info.pfremove = info.connection.portforwarding
? new ADBClient(this.session.deviceid).forward_remove_all({ ths: this, extra: info })
: $.Deferred().resolveWith(this, [info]);
return info.pfremove;
})
.then(function (info) {
// mark the port as freed
if (info.connection.portforwarding) {
this.globals.freeport(info.connection.localport)
}
this.session = null;
return $.Deferred().resolveWith(this, [info.current_state, info.extra]);
});
},
allthreads: function (extra) {
return this.ensureconnected(extra)
.then(function (extra) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.allthreads(),
});
});
},
suspend: function (extra) {
return this.ensureconnected(extra)
.then(function (extra) {
this._trigger('suspending');
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.suspend(),
});
})
.then(function () {
this._trigger('suspended');
});
},
resume: function (extra) {
return this.ensureconnected(extra)
.then(function (extra) {
this._trigger('resuming');
this.session.stoppedlocation = null;
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.resume(),
});
})
.then(function (decoded, extra) {
this._trigger('resumed');
return extra;
});
},
_resumesilent: function () {
return this.ensureconnected()
.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) {
var x = { steptype: steptype, threadid: threadid };
return this.ensureconnected(x)
.then(function (x) {
this._trigger('stepping');
return this._setupstepevent(x.steptype, x.threadid);
})
.then(function () {
return this._resumesilent();
});
},
_splitsrcfpn: function (srcfpn) {
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.java$/);
return {
pkg: m[1].replace(/\/+/g, '.'),
type: m[2],
qtype: m[1] + '/' + m[2],
}
},
getbreakpoint: function (srcfpn, line) {
var cls = this._splitsrcfpn(srcfpn);
var bp = this.breakpoints.bysrcloc[cls.qtype + ':' + line];
return bp;
},
getbreakpoints: function (filterfn) {
var x = this.breakpoints.all.reduce(function (x, bp) {
if (x.filterfn(bp))
x.res.push(bp);
return x;
}, { filterfn: filterfn, res: [] });
return x.res;
},
getallbreakpoints: function () {
return this.breakpoints.all.slice();
},
setbreakpoint: function (srcfpn, line) {
var cls = this._splitsrcfpn(srcfpn);
var bid = cls.qtype + ':' + line;
var newbp = this.breakpoints.bysrcloc[bid];
if (newbp) return newbp;
newbp = {
id: bid,
srcfpn: srcfpn,
qtype: cls.qtype,
pkg: cls.pkg,
type: cls.type,
linenum: line,
sigpattern: new RegExp('^L' + cls.qtype + '([$][$a-zA-Z0-9_]+)?;$'),
state: 'set'// set,notloaded,enabled,removed
};
this.breakpoints.all.push(newbp);
this.breakpoints.bysrcloc[bid] = newbp;
// what happens next depends upon what state we are in
switch (this.status()) {
case 'connected':
//this._changebpstate([newbp], 'set');
//this._changebpstate([newbp], 'notloaded');
newbp.state = 'notloaded';
if (this.session.cpndone) {
var bploc = this._findbplocation(this.session.classes, newbp);
if (bploc) {
this._setupbreakpointsevent([bploc]);
}
}
break;
case 'connecting':
case 'disconnected':
default:
//this._changebpstate([newbp], 'set');
newbp.state = 'set';
break;
}
return newbp;
},
clearbreakpoint: function (srcfpn, line) {
var cls = this._splitsrcfpn(srcfpn);
var bp = this.breakpoints.bysrcloc[cls.qtype + ':' + line];
if (!bp) return null;
return this._clearbreakpoints([bp])[0];
},
clearbreakpoints: function (bps) {
if (typeof (bps) === 'function') {
// argument is a filter function
return this.clearbreakpoints(this.getbreakpoints(bps));
}
// sanitise first to remove duplicates, non-existants, nulls, etc
var bpstoclear = [];
var bpkeys = {};
(bps || []).forEach(function (bp) {
if (!bp) return;
if (this.breakpoints.all.indexOf(bp) < 0) return;
var bpkey = bp.cls + ':' + bp.linenum;
if (bpkeys[bpkey]) return;
bpkeys[bpkey] = 1;
bpstoclear.push(bp);
}, this);
return this._clearbreakpoints(bpstoclear);
},
_clearbreakpoints: function (bpstoclear) {
if (!bpstoclear || !bpstoclear.length) return [];
bpstoclear.forEach(function (bp) {
delete this.breakpoints.bysrcloc[bp.qtype + ':' + bp.linenum];
this.breakpoints.all.splice(this.breakpoints.all.indexOf(bp), 1);
}, this);
switch (this.status()) {
case 'connected':
var bpcleareddefs = [{ dbgr: this, bpstoclear: bpstoclear }];
for (var cmlkey in this.breakpoints.enabled) {
var enabledbp = this.breakpoints.enabled[cmlkey].bp;
if (bpstoclear.indexOf(enabledbp) >= 0) {
bpcleareddefs.push(this._clearbreakpointsevent([cmlkey], enabledbp));
}
}
$.when.apply($, bpcleareddefs)
.then(function (x) {
x.dbgr._changebpstate(x.bpstoclear, 'removed');
});
break;
case 'connecting':
case 'disconnected':
default:
this._changebpstate(bpstoclear, 'removed');
break;
}
return bpstoclear;
},
getframes: function (threadid, extra) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.Frames(threadid),
}).then(function (frames, extra) {
var deferreds = [{ dbgr: this, frames: frames, threadid: threadid, extra: extra }];
for (var i = 0; i < frames.length; i++) {
deferreds.push(this._findmethodasync(this.session.classes, frames[i].location));
}
return $.when.apply($, deferreds)
.then(function (x) {
for (var i = 0; i < x.frames.length; i++) {
x.frames[i].method = arguments[i + 1][0];
x.frames[i].threadid = x.threadid;
}
return $.Deferred().resolveWith(x.dbgr, [x.frames, x.extra]);
});
})
},
getlocals: function (threadid, frame, extra) {
var method = this._findmethod(this.session.classes, frame.location.cid, frame.location.mid);
if (!method)
return $.Deferred().resolveWith(this);
return this._ensuremethodvars(method)
.then(function (method) {
function withincodebounds(low, length, idx) {
var i = parseInt(low, 16), j = parseInt(idx, 16);
return (j >= i) && (j < (i + length));
}
var slots = [];
var validslots = [];
var tags = { '[': 76, B: 66, C: 67, L: 76, F: 70, D: 68, I: 73, J: 74, S: 83, V: 86, Z: 90 };
for (var i = 0, k = method.vartable.vars; i < k.length; i++) {
var tag = tags[k[i].type.signature[0]];
if (!tag) continue;
var p = {
slot: k[i].slot,
tag: tag,
valid: withincodebounds(k[i].codeidx, k[i].length, frame.location.idx)
};
slots.push(p);
if (p.valid) validslots.push(p);
}
var x = { method: method, extra: extra, slots: slots };
if (!validslots.length) {
return $.Deferred().resolveWith(this, [[], x]);
}
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetStackValues(threadid, frame.frameid, validslots),
});
})
.then(function (values, x) {
var sv2 = [];
for (var i = 0; i < x.slots.length; i++) {
sv2.push(x.slots[i].valid ? values.shift() : null);
}
return this._mapvalues(
'local',
x.method.vartable.vars,
sv2,
{ frame: frame, slotinfo: null },
x
);
})
.then(function (res, x) {
for (var i = 0; i < res.length; i++)
res[i].data.slotinfo = x.slots[i];
return $.Deferred().resolveWith(this, [res, x.extra]);
});
},
setlocalvalue: function (localvar, data, extra) {
return this.ensureconnected({ localvar: localvar, data: data, extra: extra })
.then(function (x) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.SetStackValue(x.localvar.data.frame.threadid, x.localvar.data.frame.frameid, x.localvar.data.slotinfo.slot, x.data),
});
})
.then(function (success, x) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetStackValues(x.localvar.data.frame.threadid, x.localvar.data.frame.frameid, [x.localvar.data.slotinfo]),
});
})
.then(function (stackvalues, x) {
return this._mapvalues(
'local',
[x.localvar],
stackvalues,
x.localvar.data,
x
);
})
.then(function (res, x) {
return $.Deferred().resolveWith(this, [res[0], x.extra]);
});
},
getsupertype: function (local, extra) {
return this.gettypedebuginfo(local.type.signature, { local: local, extra: extra })
.then(function (dbgtype, x) {
return this._ensuresuper(dbgtype[x.local.type.signature])
})
.then(function (typeinfo) {
return $.Deferred().resolveWith(this, [typeinfo.super, extra]);
});
},
createstring: function (string, extra) {
return this.ensureconnected({ string: string, extra: extra })
.then(function (x) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.CreateStringObject(string),
});
})
.then(function (strobjref, x) {
var keys = [{ name: '', type: this.JDWP.signaturetotype('Ljava/lang/String;') }];
return this._mapvalues('literal', keys, [strobjref], null, x);
})
.then(function (vars, x) {
return $.Deferred().resolveWith(this, [vars[0], x.extra]);
});
},
setstringvalue: function (variable, string, extra) {
return this.createstring(string, { variable: variable, extra: extra })
.then(function (string_variable, x) {
var value = {
value: string_variable.value,
valuetype: 'oref',
};
return this.setvalue(x.variable, value, x.extra);
})
},
setvalue: function (variable, data, extra) {
if (data.stringliteral) {
return this.setstringvalue(variable, data.value, extra);
}
switch (variable.vtype) {
case 'field': return this.setfieldvalue(variable, data, extra);
case 'local': return this.setlocalvalue(variable, data, extra);
case 'arrelem':
return this.setarrayvalues(variable.data.arrobj, parseInt(variable.name), 1, data, extra)
.then(function (res, extra) {
// setarrayvalues returns an array of updated elements - just return the one
return $.Deferred().resolveWith(this, [res[0], extra]);
});
}
},
setfieldvalue: function (fieldvar, data, extra) {
return this.ensureconnected({ fieldvar: fieldvar, data: data, extra: extra })
.then(function (x) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.SetFieldValue(x.fieldvar.data.objvar.value, x.fieldvar.data.field, x.data),
});
})
.then(function (success, x) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetFieldValues(x.fieldvar.data.objvar.value, [x.fieldvar.data.field]),
});
})
.then(function (fieldvalues, x) {
return this._mapvalues('field', [x.fieldvar.data.field], fieldvalues, x.fieldvar.data, x);
})
.then(function (data, x) {
return $.Deferred().resolveWith(this, [data[0], x.extra]);
});
},
getfieldvalues: function (objvar, extra) {
return this.gettypedebuginfo(objvar.type.signature, { objvar: objvar, extra: extra })
.then(function (dbgtype, x) {
return this._ensurefields(dbgtype[x.objvar.type.signature], x);
})
.then(function (typeinfo, x) {
x.typeinfo = typeinfo;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetFieldValues(x.objvar.value, typeinfo.fields),
});
})
.then(function (fieldvalues, x) {
return this._mapvalues('field', x.typeinfo.fields, fieldvalues, { objvar: x.objvar }, x);
})
.then(function (res, x) {
for (var i = 0; i < res.length; i++) {
res[i].data.field = x.typeinfo.fields[i];
}
return $.Deferred().resolveWith(this, [res, x.extra]);
});
},
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,
extra: extra,
cmd: this.JDWP.Commands.GetStringValue(stringref),
});
},
_getstringlen: function (stringref, extra) {
return this.gettypedebuginfo('Ljava/lang/String;', { stringref: stringref, extra: extra })
.then(function (dbgtype, x) {
return this._ensurefields(dbgtype['Ljava/lang/String;'], x);
})
.then(function (typeinfo, x) {
var countfields = typeinfo.fields.filter(f => f.name === 'count');
if (!countfields.length) return -1;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetFieldValues(x.stringref, countfields),
});
})
.then(function (countfields, x) {
var len = (countfields && countfields.length === 1) ? countfields[0] : -1;
return $.Deferred().resolveWith(this, [len, x.extra]);
});
},
getarrayvalues: function (local, start, count, extra) {
return this.gettypedebuginfo(local.type.elementtype.signature, { local: local, start: start, count: count, extra: extra })
.then(function (dbgtype, x) {
x.type = dbgtype[x.local.type.elementtype.signature].type;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetArrayValues(x.local.value, x.start, x.count),
});
})
.then(function (values, x) {
// generate some dummy keys to map against
var keys = [];
for (var i = 0; i < x.count; i++) {
keys.push({ name: '' + (x.start + i), type: x.type });
}
return this._mapvalues('arrelem', keys, values, { arrobj: x.local }, x.extra);
});
},
setarrayvalues: function (arrvar, start, count, data, extra) {
return this.ensureconnected({ arrvar: arrvar, start: start, count: count, data: data, extra: extra })
.then(function (x) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.SetArrayElements(x.arrvar.value, x.start, x.count, x.data),
});
})
.then(function (success, x) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetArrayValues(x.arrvar.value, x.start, x.count),
});
})
.then(function (values, x) {
// generate some dummy keys to map against
var keys = [];
for (var i = 0; i < count; i++) {
keys.push({ name: '' + (x.start + i), type: x.arrvar.type.elementtype });
}
return this._mapvalues('arrelem', keys, values, { arrobj: x.arrvar }, x.extra);
});
},
_mapvalues: function (vtype, keys, values, data, extra) {
var res = [];
var arrayfields = [];
var stringfields = [];
if (values && Array.isArray(values)) {
var v = values.slice(0), i = 0;
while (v.length) {
var info = {
vtype: vtype,
name: keys[i].name,
value: v.shift(),
type: keys[i].type,
hasnullvalue: false,
valid: true,
data: Object.assign({}, data),
};
info.hasnullvalue = /^0+$/.test(info.value);
info.valid = info.value !== null;
res.push(info);
if (keys[i].type.arraydims)
arrayfields.push(info);
else if (keys[i].type.signature === 'Ljava/lang/String;')
stringfields.push(info);
i++;
}
}
var defs = [{ dbgr: this, res: res, extra: extra }];
// for those fields that are (non-null) arrays, retrieve the length
for (var i in arrayfields) {
if (arrayfields[i].hasnullvalue || !arrayfields[i].valid) continue;
var def = this.session.adbclient.jdwp_command({
ths: this,
extra: arrayfields[i],
cmd: this.JDWP.Commands.GetArrayLength(arrayfields[i].value),
})
.then(function (arrlen, arrfield) {
arrfield.arraylen = arrlen;
});
defs.push(def);
}
// for those fields that are strings, retrieve the text
for (var i in stringfields) {
if (stringfields[i].hasnullvalue || !stringfields[i].valid) continue;
var def = this._getstringlen(stringfields[i].value, stringfields[i])
.then(function (len, strfield) {
if (len > 10000)
return $.Deferred().resolveWith(this, [len, strfield]);
// retrieve the actual chars
return this.getstringchars(strfield.value, strfield);
})
.then(function (str, strfield) {
if (typeof (str) === 'number') {
strfield.string = '{string exceeds maximum display length}';
strfield.biglen = str;
} else {
strfield.string = str;
}
});
defs.push(def);
}
return $.when.apply($, defs)
.then(function (x) {
return $.Deferred().resolveWith(x.dbgr, [x.res, x.extra]);
});
},
gettypedebuginfo: function (signature, extra) {
var info = {
signature: signature,
classes: {},
ci: { type: this.JDWP.signaturetotype(signature), },
extra: extra,
deferred: $.Deferred(),
};
if (this.session) {
// see if we've already retrieved the type for this session
var cached = this.session.classes[signature];
if (cached) {
// are we still retrieving it...
if (cached.promise) {
return cached.promise();
}
// return the cached entry
var res = {}; res[signature] = cached;
return $.Deferred().resolveWith(this, [res, extra]);
}
// while we're retrieving it, set a deferred in it's place
this.session.classes[signature] = info.deferred;
}
this.ensureconnected(info)
.then(function (info) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: info,
cmd: this.JDWP.Commands.classinfo(info.ci),
});
})
.then(function (classinfoarr, info) {
if (!classinfoarr || !classinfoarr.length) {
if (this.session)
delete this.session.classes[info.signature];
return info.deferred.resolveWith(this, [{}, info.extra]);
}
info.ci.info = classinfoarr[0];
info.ci.name = info.ci.type.typename;
info.classes[info.ci.type.signature] = info.ci;
// querying the source file for array or primitive types causes the app to crash
return (info.ci.type.signature[0] !== 'L'
? $.Deferred().resolveWith(this, [[null], info])
: this.session.adbclient.jdwp_command({
ths: this,
extra: info,
cmd: this.JDWP.Commands.sourcefile(info.ci),
}))
.then(function (srcinfoarr, info) {
info.ci.src = srcinfoarr[0];
if (this.session) {
Object.assign(this.session.classes, info.classes);
}
return info.deferred.resolveWith(this, [info.classes, info.extra]); // done
});
});
return info.deferred;
},
_ensuresuper: function (typeinfo) {
if (typeinfo.super || typeinfo.super === null) {
if (typeinfo.super && typeinfo.super.promise)
return typeinfo.super.promise();
return $.Deferred().resolveWith(this, [typeinfo]);
}
if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') {
typeinfo.super = null;
return $.Deferred().resolveWith(this, [typeinfo]);
}
typeinfo.super = $.Deferred();
this.session.adbclient.jdwp_command({
ths: this,
extra: typeinfo,
cmd: this.JDWP.Commands.superclass(typeinfo),
})
.then(function (superclassref, typeinfo) {
return this.session.adbclient.jdwp_command({
ths: this,
extra: typeinfo,
cmd: this.JDWP.Commands.signature(superclassref),
});
})
.then(function (supertype, typeinfo) {
var def = typeinfo.super;
typeinfo.super = supertype;
def.resolveWith(this, [typeinfo]);
});
return typeinfo.super.promise();
},
_ensurefields: function (typeinfo, extra) {
if (typeinfo.fields) {
if (typeinfo.fields.promise)
return typeinfo.fields.promise();
return $.Deferred().resolveWith(this, [typeinfo, extra]);
}
typeinfo.fields = $.Deferred();
this.session.adbclient.jdwp_command({
ths: this,
extra: { typeinfo: typeinfo, extra: extra },
cmd: this.JDWP.Commands.fieldsWithGeneric(typeinfo),
})
.then(function (fields, x) {
var def = x.typeinfo.fields;
x.typeinfo.fields = fields;
def.resolveWith(this, [x.typeinfo, x.extra]);
});
return typeinfo.fields.promise();
},
_ensuremethods: function (typeinfo) {
if (typeinfo.methods) {
if (typeinfo.methods.promise)
return typeinfo.methods.promise();
return $.Deferred().resolveWith(this, [typeinfo]);
}
typeinfo.methods = $.Deferred();
this.session.adbclient.jdwp_command({
ths: this,
extra: typeinfo,
cmd: this.JDWP.Commands.methodsWithGeneric(typeinfo),
})
.then(function (methods, typeinfo) {
var def = typeinfo.methods;
typeinfo.methods = {};
for (var i in methods) {
methods[i].owningclass = typeinfo;
typeinfo.methods[methods[i].methodid] = methods[i];
}
def.resolveWith(this, [typeinfo]);
});
return typeinfo.methods.promise();
},
_ensuremethodvars: function (methodinfo) {
if (methodinfo.vartable) {
if (methodinfo.vartable.promise)
return methodinfo.vartable.promise();
return $.Deferred().resolveWith(this, [methodinfo]);
}
methodinfo.vartable = $.Deferred();
this.session.adbclient.jdwp_command({
ths: this,
extra: methodinfo,
cmd: this.JDWP.Commands.VariableTableWithGeneric(methodinfo.owningclass, methodinfo),
})
.then(function (vartable, methodinfo) {
var def = methodinfo.vartable;
methodinfo.vartable = vartable;
def.resolveWith(this, [methodinfo]);
});
return methodinfo.vartable.promise();
},
_ensuremethodlines: function (methodinfo) {
if (methodinfo.linetable) {
if (methodinfo.linetable.promise)
return methodinfo.linetable.promise();
return $.Deferred().resolveWith(this, [methodinfo]);
}
methodinfo.linetable = $.Deferred();
this.session.adbclient.jdwp_command({
ths: this,
extra: methodinfo,
cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo),
})
.then(function (linetable, methodinfo) {
// the linetable does not correlate code indexes with line numbers
// - location searching relies on the table being ordered by code indexes
linetable.lines.sort(function (a, b) {
return (a.linecodeidx === b.linecodeidx) ? 0 : ((a.linecodeidx < b.linecodeidx) ? -1 : +1);
});
var def = methodinfo.linetable;
methodinfo.linetable = linetable;
def.resolveWith(this, [methodinfo]);
});
return methodinfo.linetable.promise();
},
_setupclassprepareevent: function (filter, onprepare) {
var onevent = {
data: {
dbgr: this,
onprepare: onprepare,
},
fn: function (e) {
var x = e.data;
x.onprepare.apply(x.dbgr, [e.event]);
}
};
var cmd = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.OnClassPrepare(filter, onevent),
});
return cmd.promise();
},
_setupstepevent: function (steptype, threadid) {
var onevent = {
data: {
dbgr: this,
},
fn: function (e) {
e.data.dbgr.session.adbclient.jdwp_command({
cmd: e.data.dbgr.JDWP.Commands.ClearStep(e.event.reqid),
})
.then(function () {
var x = e.data;
var loc = e.event.location;
// search the cached classes for a matching source location
x.dbgr._findcmllocation(x.dbgr.session.classes, loc)
.then(function (sloc) {
var stoppedloc = sloc || { qtype: null, linenum: null };
stoppedloc.threadid = e.event.threadid;
var eventdata = {
event: e.event,
stoppedlocation: stoppedloc,
};
x.dbgr.session.stoppedlocation = stoppedloc;
x.dbgr._trigger('step', eventdata);
});
});
}
};
var cmd = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.SetSingleStep(steptype, threadid, onevent),
});
return cmd.promise();
},
_setupbreakpointsevent: function (locations) {
var onevent = {
data: {
dbgr: this,
},
fn: function (e) {
var x = e.data;
var loc = e.event.location;
var cmlkey = loc.cid + ':' + loc.mid + ':' + loc.idx;
var bp = x.dbgr.breakpoints.enabled[cmlkey].bp;
var stoppedloc = {
qtype: bp.qtype,
linenum: bp.linenum,
threadid: e.event.threadid
};
var eventdata = {
event: e.event,
stoppedlocation: stoppedloc,
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
};
x.dbgr.session.stoppedlocation = stoppedloc;
x.dbgr._trigger('bphit', eventdata);
}
};
var bparr = [];
var cmlkeys = [];
var setbpcmds = [{ dbgr: this, bparr: bparr, cmlkeys: cmlkeys }];
for (var i in locations) {
var bploc = locations[i];
// associate, so we can find it when the bp hits...
var cmlkey = bploc.c.info.typeid + ':' + bploc.m.methodid + ':' + bploc.l;
cmlkeys.push(cmlkey);
this.breakpoints.enabled[cmlkey] = {
bp: bploc.bp,
requestid: null,
};
bparr.push(bploc.bp);
var cmd = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, onevent),
});
setbpcmds.push(cmd);
}
return $.when.apply($, setbpcmds)
.then(function (x) {
// save the request ids from the SetBreakpoint commands so we can disable them later
for (var i = 0; i < x.cmlkeys.length; i++) {
x.dbgr.breakpoints.enabled[x.cmlkeys[i]].requestid = arguments[i + 1][0].id;
}
x.dbgr._changebpstate(x.bparr, 'enabled');
return $.Deferred().resolveWith(x.dbgr);
});
},
_clearbreakpointsevent: function (cmlarr, extra) {
var bparr = [];
var clearbpcmds = [{ dbgr: this, extra: extra, bparr: bparr }];
for (var i in cmlarr) {
var enabled = this.breakpoints.enabled[cmlarr[i]];
delete this.breakpoints.enabled[cmlarr[i]];
bparr.push(enabled.bp);
var cmd = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.ClearBreakpoint(enabled.requestid),
});
clearbpcmds.push(cmd);
}
return $.when.apply($, clearbpcmds)
.then(function (x) {
x.dbgr._changebpstate(x.bparr, 'notloaded');
return $.Deferred().resolveWith(x.dbgr, [x.extra]);
});
},
_changebpstate: function (bparr, newstate) {
if (!bparr || !bparr.length || !newstate) return;
for (var i in bparr) {
bparr[i].state = newstate;
}
this._trigger('bpstatechange', { breakpoints: bparr.slice(), newstate: newstate });
},
_initbreakpoints: function () {
var deferreds = [{ dbgr: this }];
var donetypes = {};
// reset any current associations
this.breakpoints.enabled = {};
// set all the breakpoints to the notloaded state
this._changebpstate(this.breakpoints.all, 'notloaded');
// setup class prepare notifications for all the current packages
// when each class is prepared, we initialise any breakpoints for it
for (var pkg in this.session.build.packages) {
try {
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) {
x.dbgr.session.cpndone = true;
return $.Deferred().resolveWith(x.dbgr);
});
function _onclassprepared(preppedclass) {
// if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get
// multiple notifications (which duplicates breakpoints, etc)
if (this.session.preparedclasses.includes(preppedclass.type.signature)) {
return; // we already know about this
}
this.session.preparedclasses.push(preppedclass.type.signature);
D('Prepared: ' + preppedclass.type.signature);
var m = preppedclass.type.signature.match(/^L(.*);$/);
if (!m) {
// unrecognised type - just resume
this._resumesilent();
return;
}
this._loadclzinfo(preppedclass.type.signature)
.then(function (classes) {
var bplocs = [];
for (var idx in this.breakpoints.all) {
var bp = this.breakpoints.all[idx];
var bploc = this._findbplocation(classes, bp);
if (bploc) {
bplocs.push(bploc);
}
}
// set all the breakpoints in one go...
return this._setupbreakpointsevent(bplocs);
})
.then(function () {
// when all the breakpoints for the newly-prepared type have been set...
this._resumesilent();
});
}
},
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 = c ? Object.keys(pkgs).filter(pkgname => pkgs[pkgname].public_classes.length) : [];
var o = {
dbgr: this,
filters: pkgs_to_monitor.map(pkg=>pkg+'.*'),
caught: c,
uncaught: u,
onevent: onevent,
cmds:[],
def: $.Deferred(),
extra: extra,
next() {
var uncaught = false;
if (!this.filters.length) {
if (!this.uncaught) {
this.def.resolveWith(this.dbgr, [this.extra]); // done
return;
}
// setup the uncaught exception break - with no filter
uncaught = true;
this.filters.push(null);
this.caught = this.uncaught = false;
}
// setup next pattern
this.dbgr.session.adbclient.jdwp_command({
cmd: this.dbgr.JDWP.Commands.SetExceptionBreak(this.filters.shift(), this.caught, 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) {
var defs = [{ dbgr: this, classes: classes }];
for (var clz in classes) {
defs.push(this._ensuremethods(classes[clz]));
}
return $.when.apply($, defs).then(function (x) {
return $.Deferred().resolveWith(x.dbgr, [x.classes]);
})
})
.then(function (classes) {
var defs = [{ dbgr: this, classes: classes }];
for (var clz in classes) {
for (var m in classes[clz].methods) {
defs.push(this._ensuremethodlines(classes[clz].methods[m]));
}
}
return $.when.apply($, defs).then(function (x) {
return $.Deferred().resolveWith(x.dbgr, [x.classes]);
})
});
},
_findbplocation: function (classes, bp) {
// search the classes for a method containing the line
for (var i in classes) {
if (!bp.sigpattern.test(classes[i].type.signature))
continue;
for (var j in classes[i].methods) {
var lines = classes[i].methods[j].linetable.lines;
for (var k in lines) {
if (lines[k].linenum === bp.linenum) {
// match - save the info for the command later
var bploc = {
c: classes[i], m: classes[i].methods[j], l: lines[k].linecodeidx,
bp: bp,
};
return bploc;
}
}
}
}
return null;
},
line_idx_to_source_location: function (method, idx) {
if (!method || !method.linetable || !method.linetable.lines || !method.linetable.lines.length)
return null;
var m = method.owningclass.type.signature.match(/^L([^;$]+)[$a-zA-Z0-9_]*;$/);
if (!m)
return null;
var lines = method.linetable.lines, prevk = 0;
for (var k in lines) {
if (lines[k].linecodeidx < idx) {
prevk = k;
continue;
}
// multi-part expressions can return intermediate idx's
// - if the idx is not an exact match, use the previous value
if (lines[k].linecodeidx > idx)
k = prevk;
// convert the class signature to a file location
return {
qtype: m[1],
linenum: lines[k].linenum,
exact: lines[k].linecodeidx === idx,
};
}
// just return the last location in the list
return {
qtype: m[1],
linenum: lines[lines.length-1].linenum,
exact: false,
};
},
_findcmllocation: function (classes, loc) {
// search the classes for a method containing the line
return this._findmethodasync(classes, loc)
.then(function (method) {
if (!method)
return $.Deferred().resolveWith(this, [null]);
return this._ensuremethodlines(method)
.then(function (method) {
var srcloc = this.line_idx_to_source_location(method, loc.idx);
return $.Deferred().resolveWith(this, [srcloc]);
});
});
},
_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
return this.session.adbclient.jdwp_command({
ths: this,
extra: { location: location },
cmd: this.JDWP.Commands.signature(location.cid),
})
.then(function (type, x) {
return this.gettypedebuginfo(type.signature, x);
})
.then(function (classes, x) {
var defs = [{ dbgr: this, classes: classes, x: x }];
for (var clz in classes) {
defs.push(this._ensuremethods(classes[clz]));
}
return $.when.apply($, defs).then(function (x) {
return $.Deferred().resolveWith(x.dbgr, [x.classes, x.x]);
})
})
.then(function (classes, x) {
var m = this._findmethod(classes, x.location.cid, x.location.mid);
return $.Deferred().resolveWith(this, [m]);
});
},
_findmethod: function (classes, classid, methodid) {
for (var i in classes) {
if (classes[i]._isdeferred)
continue;
if (classes[i].info.typeid !== classid)
continue;
for (var j in classes[i].methods) {
if (classes[i].methods[j].methodid !== methodid)
continue;
return classes[i].methods[j];
}
}
return null;
},
_finitbreakpoints: function () {
this._changebpstate(this.breakpoints.all, 'set');
this.breakpoints.enabled = {};
},
};
exports.Debugger = Debugger;