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