mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
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
1882 lines
75 KiB
JavaScript
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;
|