Files
android-dev-ext/src/debugger.js
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

1882 lines
75 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 filters
cpfilters: [],
preparedclasses: [],
stepids: {}, // hashmap<threadid,stepid>
threadsuspends: [], // hashmap<threadid, suspend-count>
invokes: {}, // hashmap<threadid, deferred>
}
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(),
});
});
},
threadinfos: function(thread_ids, extra) {
if (!Array.isArray(thread_ids))
thread_ids = [thread_ids];
var o = {
dbgr: this, thread_ids, extra, threadinfos:[], idx:0,
next() {
var thread_id = this.thread_ids[this.idx];
if (typeof(thread_id) === 'undefined')
return $.Deferred().resolveWith(this.dbgr, [this.threadinfos, this.extra]);
var info = {
threadid: thread_id,
name:'',
status:null,
};
return this.dbgr.session.adbclient.jdwp_command({ ths:this.dbgr, extra:info, cmd:this.dbgr.JDWP.Commands.threadname(info.threadid) })
.then((name,info) => {
info.name = name;
return this.dbgr.session.adbclient.jdwp_command({ ths:this.dbgr, extra:info, cmd:this.dbgr.JDWP.Commands.threadstatus(info.threadid) })
})
.then((status, info) => {
info.status = status;
this.threadinfos.push(info);
})
.always(() => (this.idx++,this.next()))
}
};
return this.ensureconnected(o).then(o => o.next());
},
suspend: function (extra) {
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');
});
},
suspendthread: function (threadid, extra) {
return this.ensureconnected({threadid,extra})
.then(function (x) {
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) + 1;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x.extra,
cmd: this.JDWP.Commands.suspendthread(x.threadid),
});
})
.then((res,extra) => extra);
},
_resume:function(triggers, extra) {
return this.ensureconnected(extra)
.then(function (extra) {
if (triggers) 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) {
if (triggers) this._trigger('resumed');
return extra;
});
},
resume: function (extra) {
return this._resume(true, extra);
},
_resumesilent: function () {
return this._resume(false);
},
resumethread: function (threadid, extra) {
return this.ensureconnected({threadid,extra})
.then(function (x) {
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) - 1;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x.extra,
cmd: this.JDWP.Commands.resumethread(x.threadid),
});
})
.then((res,extra) => extra);
},
step: function (steptype, threadid, extra) {
var x = { steptype, threadid, extra };
return this.ensureconnected(x)
.then(function (x) {
this._trigger('stepping');
return this._setupstepevent(x.steptype, x.threadid, x);
})
.then(x => {
return this.resumethread(x.threadid, x.extra);
});
},
_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, conditions) {
var cls = this._splitsrcfpn(srcfpn);
var bid = cls.qtype + ':' + line;
var newbp = this.breakpoints.bysrcloc[bid];
if (newbp) return $.Deferred().resolveWith(this, [newbp]);
newbp = {
id: bid,
srcfpn: srcfpn,
qtype: cls.qtype,
pkg: cls.pkg,
type: cls.type,
linenum: line,
conditions: Object.assign({},conditions),
sigpattern: new RegExp('^L' + cls.qtype + '([$][$a-zA-Z0-9_]+)?;$'),
state: 'set', // set,notloaded,enabled,removed
hitcount: 0, // number of times this bp was hit during execution
stopcount: 0. // number of times this bp caused a break into the debugger
};
this.breakpoints.all.push(newbp);
this.breakpoints.bysrcloc[bid] = newbp;
// what happens next depends upon what state we are in
switch (this.status()) {
case 'connected':
newbp.state = 'notloaded';
// try and load the class - if the runtime hasn't loaded it yet, this will just return an empty classes object
return this._loadclzinfo('L'+newbp.qtype+';')
.then(classes => {
var bploc = this._findbplocation(classes, newbp);
if (!bploc) {
// the required location may be inside a nested class (anonymous or named)
// Since Android doesn't support the NestedTypes JDWP call (ffs), all we can do here
// is look for existing (cached) loaded types matching inner type signatures
for (var sig in this.session.classes) {
if (newbp.sigpattern.test(sig))
classes[sig] = this.session.classes[sig];
}
// try again
bploc = this._findbplocation(classes, newbp);
}
if (!bploc) {
// we couldn't identify a matching location - either the class is not yet loaded or the
// location doesn't correspond to any code. In case it's the former, make sure we are notified
// when classes in this package are loaded
return this._ensureClassPrepareForPackage(newbp.pkg);
}
// we found a matching location - set the breakpoint event
return this._setupbreakpointsevent([bploc]);
})
.then(() => newbp)
case 'connecting':
case 'disconnected':
default:
newbp.state = 'set';
break;
}
return $.Deferred().resolveWith(this, [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) {
if (local.type.signature==='Ljava/lang/Object;')
return $.Deferred().rejectWith(this,[new Error('java.lang.Object has no super type')]);
return this.gettypedebuginfo(local.type.signature, { local: local, extra: extra })
.then(function (dbgtype, x) {
return this._ensuresuper(dbgtype[x.local.type.signature])
})
.then(function (typeinfo) {
return $.Deferred().resolveWith(this, [typeinfo.super, extra]);
});
},
getsuperinstance: function (local, extra) {
return this.getsupertype(local, {local,extra})
.then(function (supertypeinfo, x) {
var castobj = Object.assign({}, x.local);
castobj.type = supertypeinfo;
return $.Deferred().resolveWith(this, [castobj, x.extra]);
});
},
createstring: function (string, extra) {
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;
// the Android runtime now pointlessly barfs into logcat if an instance value is used
// to retrieve a static field. So, we now split into two calls...
x.splitfields = typeinfo.fields.reduce((z,f) => {
if (f.modbits & 8) z.static.push(f); else z.instance.push(f);
return z;
}, {instance:[],static:[]});
// if there are no instance fields, just resolve with an empty array
if (!x.splitfields.instance.length)
return $.Deferred().resolveWith(this,[[], x]);
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetFieldValues(x.objvar.value, x.splitfields.instance),
});
})
.then(function (instance_fieldvalues, x) {
x.instance_fieldvalues = instance_fieldvalues;
// and now the statics (with a type reference)
if (!x.splitfields.static.length)
return $.Deferred().resolveWith(this,[[], x]);
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetStaticFieldValues(x.splitfields.static[0].typeid, x.splitfields.static),
});
})
.then(function (static_fieldvalues, x) {
x.static_fieldvalues = static_fieldvalues;
// make sure the fields and values match up...
var fields = x.splitfields.instance.concat(x.splitfields.static);
var values = x.instance_fieldvalues.concat(x.static_fieldvalues);
return this._mapvalues('field', fields, values, { objvar: x.objvar }, x);
})
.then(function (res, x) {
for (var i = 0; i < res.length; i++) {
res[i].data.field = x.typeinfo.fields[i];
}
return $.Deferred().resolveWith(this, [res, x.extra]);
});
},
getFieldValue: function(objvar, fieldname, includeInherited, extra) {
const findfield = x => {
return this.getfieldvalues(x.objvar, x)
.then((fields, x) => {
var field = fields.find(f => f.name === x.fieldname);
if (field) return $.Deferred().resolveWith(this,[field,x.extra]);
if (!x.includeInherited || x.objvar.type.signature==='Ljava/lang/Object;') {
var fqtname = [x.reqtype.package,x.reqtype.typename].join('.');
return $.Deferred().rejectWith(this,[new Error(`No such field '${x.fieldname}' in type ${fqtname}`), x.extra]);
}
// search supertype
return this.getsuperinstance(x.objvar, x)
.then((superobjvar,x) => {
x.objvar = superobjvar;
return x.findfield(x);
});
});
}
return findfield({findfield, objvar, fieldname, includeInherited, extra, reqtype:objvar.type});
},
getExceptionLocal: function (ex_ref_value, extra) {
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,
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 => {
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) => {
x.typeinfo = typeinfo;
x.method = method;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, x.typeinfo.info.typeid, x.method.methodid, x.args),
})
})
.then((res, x) => {
// res = {return_value, exception}
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) => {
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) {
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);
else if (keys[i].type.signature === 'C')
info.char = info.valid ? String.fromCodePoint(info.value) : '';
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();
},
_clearLastStepRequest: function (threadid, extra) {
if (!this.session || !this.session.stepids[threadid])
return $.Deferred().resolveWith(this,[extra]);
var clearStepCommand = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.ClearStep(this.session.stepids[threadid]),
extra: extra,
}).then((decoded, extra) => extra);
this.session.stepids[threadid] = 0;
return clearStepCommand;
},
_setupstepevent: function (steptype, threadid, extra) {
var onevent = {
data: {
dbgr: this,
},
fn: function (e) {
e.data.dbgr._clearLastStepRequest(e.event.threadid, e)
.then(function (e) {
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),
extra: extra,
}).then((res,extra) => {
// save the step id so we can manually clear it if an exception break occurs
if (this.session && res && res.id)
this.session.stepids[threadid] = res.id;
return extra;
});
return cmd.promise();
},
_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;
// if this was a conditional breakpoint, it will have been automatically cleared
// - set a new (unconditional) breakpoint in it's place
if (bp.conditions.hitcount) {
bp.hitcount += bp.conditions.hitcount;
delete bp.conditions.hitcount;
var bploc = x.dbgr.breakpoints.enabled[cmlkey].bploc;
x.dbgr.session.adbclient.jdwp_command({
cmd: x.dbgr.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, null, onevent),
});
} else {
bp.hitcount++;
}
bp.stopcount++;
x.dbgr._trigger('bphit', eventdata);
}
};
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,
bploc: {c:bploc.c,m:bploc.m,l:bploc.l},
requestid: null,
};
bparr.push(bploc.bp);
var cmd = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, bploc.bp.conditions.hitcount, 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 }];
// 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 packages associated with breakpoints
// when each class is prepared, we initialise any breakpoints for it
var cpdefs = this.breakpoints.all.map(bp => this._ensureClassPrepareForPackage(bp.pkg));
deferreds = deferreds.concat(cpdefs);
return $.when.apply($, deferreds).then(function (x) {
return $.Deferred().resolveWith(x.dbgr);
});
},
_ensureClassPrepareForPackage: function(pkg) {
var filter = pkg + '.*';
if (this.session.cpfilters.includes(filter))
return $.Deferred().resolveWith(this,[]); // already setup
this.session.cpfilters.push(filter);
return this._setupclassprepareevent(filter, preppedclass => {
// if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get
// 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);
}
}
if (!bplocs.length) return;
// 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) {
// if this exception break occurred during a step request, we must manually clear the event
// or the (device-side) debugger will crash on next step
this._clearLastStepRequest(e.event.threadid, e).then(e => {
this._findcmllocation(this.session.classes, e.event.throwlocation)
.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;
},
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) {
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;