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