mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 09:59:25 +00:00
Support attaching to running app (#85)
* add support for timeout on adb socket reads * add debugger support for attaching to a process * add new launch configuration and support for picking an Android process ID * initial support for attaching to android process * display enhanced quick pick list with pids and names * add flag to prevent disconnect messages when not connected * Retrieve all loaded classes during startup. This allows us to identify breakpoints in anonymous classes that are already loaded. * correct name of process picker command * make PickAndroidProcess command private * selectAndroidProcessID always returns an object * make breakpoint setup a loop instead of recursive * tidy some labels and error messages * use a more consistent command for retrieving process names * show pid list sorted by pid instead of name * refactor some Android and ADB-specific functions Check ANDROID_SDK as replacement for ANDROID_HOME * tidy up logcat launch and refactor target device selection * fix logcat not displaying * filter duplicates and blanks from logcat output
This commit is contained in:
135
src/debugger.js
135
src/debugger.js
@@ -75,11 +75,17 @@ class Debugger extends EventEmitter {
|
||||
* @param {string} deviceid
|
||||
*/
|
||||
async startDebugSession(build, deviceid) {
|
||||
if (this.status() !== 'disconnected') {
|
||||
throw new Error('startDebugSession: session already active');
|
||||
}
|
||||
this.session = new DebugSession(build, deviceid);
|
||||
const stdout = await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause);
|
||||
|
||||
// retrieve the list of debuggable processes
|
||||
const pids = await this.getDebuggablePIDs(this.session.deviceid);
|
||||
const pids = await Debugger.getDebuggablePIDs(this.session.deviceid, 10e3);
|
||||
if (pids.length === 0) {
|
||||
throw new Error(`startDebugSession: No debuggable processes after app launch.`);
|
||||
}
|
||||
// choose the last pid in the list
|
||||
const pid = pids[pids.length - 1];
|
||||
// after connect(), the caller must call resume() to begin
|
||||
@@ -87,6 +93,20 @@ class Debugger extends EventEmitter {
|
||||
return stdout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BuildInfo} build
|
||||
* @param {number} pid process ID to connect to
|
||||
* @param {string} deviceid device ID to connect to
|
||||
*/
|
||||
async attachToProcess(build, pid, deviceid) {
|
||||
if (this.status() !== 'disconnected') {
|
||||
throw new Error('attachToProcess: session already active')
|
||||
}
|
||||
this.session = new DebugSession(build, deviceid);
|
||||
// after connect(), the caller must call resume() to begin
|
||||
await this.connect(pid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} deviceid Device ID to connect to
|
||||
* @param {string[]} launch_cmd_args Array of arguments to pass to 'am start'
|
||||
@@ -127,54 +147,21 @@ class Debugger extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Retrieve a list of debuggable process IDs from a device
|
||||
* @param {string} deviceid
|
||||
* @param {number} timeout_ms
|
||||
*/
|
||||
getDebuggablePIDs(deviceid) {
|
||||
return new ADBClient(deviceid).jdwp_list();
|
||||
static getDebuggablePIDs(deviceid, timeout_ms) {
|
||||
return new ADBClient(deviceid).jdwp_list(timeout_ms);
|
||||
}
|
||||
|
||||
async getDebuggableProcesses(deviceid) {
|
||||
const adbclient = new ADBClient(deviceid);
|
||||
const info = {
|
||||
debugger: this,
|
||||
jdwps: null,
|
||||
};
|
||||
const jdwps = await info.adbclient.jdwp_list();
|
||||
if (!jdwps.length)
|
||||
return null;
|
||||
info.jdwps = jdwps;
|
||||
// retrieve the ps list from the device
|
||||
const stdout = await adbclient.shell_cmd({
|
||||
command: 'ps',
|
||||
});
|
||||
// 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
|
||||
const lines = stdout.split(/\r?\n|\r/g);
|
||||
const hdrs = (lines.shift() || '').trim().toUpperCase().split(/\s+/);
|
||||
const pidindex = hdrs.indexOf('PID');
|
||||
const nameindex = hdrs.indexOf('NAME');
|
||||
if (pidindex < 0 || nameindex < 0)
|
||||
return [];
|
||||
const result = [];
|
||||
// scan the list looking for matching pids...
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const entries = lines[i].trim().replace(/ [S] /, ' ').split(/\s+/);
|
||||
if (entries.length !== hdrs.length) {
|
||||
continue;
|
||||
}
|
||||
const jdwpidx = info.jdwps.indexOf(entries[pidindex]);
|
||||
if (jdwpidx < 0) {
|
||||
continue;
|
||||
}
|
||||
// we found a match
|
||||
const entry = {
|
||||
jdwp: entries[pidindex],
|
||||
name: entries[nameindex],
|
||||
};
|
||||
result.push(entry);
|
||||
}
|
||||
return result;
|
||||
/**
|
||||
* Retrieve a list of debuggable process IDs with process names from a device.
|
||||
* For Android, the process name is usually the package name.
|
||||
* @param {string} deviceid
|
||||
* @param {number} timeout_ms
|
||||
*/
|
||||
static getDebuggableProcesses(deviceid, timeout_ms) {
|
||||
return new ADBClient(deviceid).named_jdwp_list(timeout_ms);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,6 +214,7 @@ class Debugger extends EventEmitter {
|
||||
|
||||
async performConnectionTasks() {
|
||||
// setup port forwarding
|
||||
// note that this call generally succeeds - even if the JDWP pid is invalid
|
||||
await new ADBClient(this.session.deviceid).jdwp_forward({
|
||||
localport: this.connection.localport,
|
||||
jdwp: this.connection.jdwp,
|
||||
@@ -236,13 +224,21 @@ class Debugger extends EventEmitter {
|
||||
// after this, the client keeps an open connection until
|
||||
// jdwp_disconnect() is called
|
||||
this.session.adbclient = new ADBClient(this.session.deviceid);
|
||||
await this.session.adbclient.jdwp_connect({
|
||||
localport: this.connection.localport,
|
||||
onreply: data => this._onJDWPMessage(data),
|
||||
ondisconnect: () => this._onJDWPDisconnect(),
|
||||
});
|
||||
try {
|
||||
// if the JDWP pid is invalid (doesn't exist, not debuggable, etc) ,this
|
||||
// is where it will fail...
|
||||
await this.session.adbclient.jdwp_connect({
|
||||
localport: this.connection.localport,
|
||||
onreply: data => this._onJDWPMessage(data),
|
||||
ondisconnect: () => this._onJDWPDisconnect(),
|
||||
});
|
||||
} catch (e) {
|
||||
// provide a slightly more meaningful message than a socket error
|
||||
throw new Error(`A debugger connection to pid ${this.connection.jdwp} could not be established. ${e.message}`)
|
||||
}
|
||||
// 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...
|
||||
@@ -258,6 +254,12 @@ class Debugger extends EventEmitter {
|
||||
// set the class loader event notifier so we can enable breakpoints when the
|
||||
// runtime loads the classes
|
||||
await this.initClassPrepareForBreakpoints();
|
||||
|
||||
// some types have already been loaded (so we won't receive class-prepare notifications).
|
||||
// we can't map breakpoint source locations to already-loaded anonymous types, so we just retrieve
|
||||
// a list of all classes for now.
|
||||
const all_classes = await this.getAllClasses();
|
||||
this.session.loadedClasses = new Set(all_classes.map(x => x.signature));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,17 +339,20 @@ class Debugger extends EventEmitter {
|
||||
|
||||
// reset the breakpoint states
|
||||
this.resetBreakpoints();
|
||||
this.emit('disconnect');
|
||||
|
||||
// clear the session
|
||||
const adbclient = this.session.adbclient;
|
||||
this.session = null;
|
||||
|
||||
// perform the JDWP disconnect
|
||||
if (connection.connected) {
|
||||
await this.session.adbclient.jdwp_disconnect();
|
||||
await adbclient.jdwp_disconnect();
|
||||
}
|
||||
|
||||
// undo the portforwarding
|
||||
// todo: replace remove_all with remove_port
|
||||
if (connection.portforwarding) {
|
||||
await new ADBClient(this.session.deviceid).forward_remove_all();
|
||||
await adbclient.forward_remove_all();
|
||||
}
|
||||
|
||||
// mark the port as freed
|
||||
@@ -355,8 +360,7 @@ class Debugger extends EventEmitter {
|
||||
Debugger.portManager.freeport(connection.localport);
|
||||
}
|
||||
|
||||
// clear the session
|
||||
this.session = null;
|
||||
this.emit('disconnect');
|
||||
return previous_state;
|
||||
}
|
||||
|
||||
@@ -539,17 +543,12 @@ class Debugger extends EventEmitter {
|
||||
*/
|
||||
async initialiseBreakpoint(bp) {
|
||||
// try and load the class - if the runtime hasn't loaded it yet, this will just return a TypeNotAvailable instance
|
||||
let classes = [await this.loadClassInfo(`L${bp.qtype};`)];
|
||||
let classes = await Promise.all(
|
||||
[...this.session.loadedClasses]
|
||||
.filter(signature => bp.sigpattern.test(signature))
|
||||
.map(signature => this.loadClassInfo(signature))
|
||||
);
|
||||
let bploc = Debugger.findBreakpointLocation(classes, bp);
|
||||
if (!bploc) {
|
||||
// the required location may be inside a nested class (anonymous or named)
|
||||
// Since Android doesn't support the NestedTypes JDWP call (ffs), all we can do here
|
||||
// is look for existing (cached) loaded types matching inner type signatures
|
||||
classes = this.session.classList
|
||||
.filter(c => bp.sigpattern.test(c.type.signature));
|
||||
// try again
|
||||
bploc = Debugger.findBreakpointLocation(classes, bp);
|
||||
}
|
||||
if (!bploc) {
|
||||
// we couldn't identify a matching location - either the class is not yet loaded or the
|
||||
// location doesn't correspond to any code. In case it's the former, make sure we are notified
|
||||
@@ -1492,10 +1491,10 @@ class Debugger extends EventEmitter {
|
||||
// if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get
|
||||
// multiple notifications (which duplicates breakpoints, etc)
|
||||
const signature = prepared_class.type.signature;
|
||||
if (this.session.preparedClasses.has(signature)) {
|
||||
if (this.session.loadedClasses.has(signature)) {
|
||||
return; // we already know about this
|
||||
}
|
||||
this.session.preparedClasses.add(signature);
|
||||
this.session.loadedClasses.add(signature);
|
||||
D('Prepared: ' + signature);
|
||||
if (!/^L(.*);$/.test(signature)) {
|
||||
// unrecognised type signature - ignore it
|
||||
|
||||
Reference in New Issue
Block a user