'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 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;