mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +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:
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -14,7 +14,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Server",
|
||||
"name": "Debugger Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceRoot}",
|
||||
@@ -35,8 +35,8 @@
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Extension + Server",
|
||||
"configurations": [ "Launch Extension", "Server" ]
|
||||
"name": "Extension + Debugger",
|
||||
"configurations": [ "Launch Extension", "Debugger Server" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
const vscode = require('vscode');
|
||||
const { AndroidContentProvider } = require('./src/contentprovider');
|
||||
const { openLogcatWindow } = require('./src/logcat');
|
||||
const { selectAndroidProcessID } = require('./src/process-attach');
|
||||
|
||||
// this method is called when your extension is activated
|
||||
// your extension is activated the very first time the command is executed
|
||||
@@ -17,6 +18,12 @@ function activate(context) {
|
||||
vscode.commands.registerCommand('android-dev-ext.view_logcat', () => {
|
||||
openLogcatWindow(vscode);
|
||||
}),
|
||||
// add the process picker handler - used to choose a PID to attach to
|
||||
vscode.commands.registerCommand('PickAndroidProcess', async () => {
|
||||
const o = await selectAndroidProcessID(vscode);
|
||||
// the debugger requires a string value to be returned
|
||||
return JSON.stringify(o);
|
||||
}),
|
||||
];
|
||||
|
||||
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);
|
||||
|
||||
63
package.json
63
package.json
@@ -18,7 +18,8 @@
|
||||
"theme": "dark"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:android-dev-ext.view_logcat"
|
||||
"onCommand:android-dev-ext.view_logcat",
|
||||
"onCommand:PickAndroidProcess"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -128,30 +129,84 @@
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"attach": {
|
||||
"required": [
|
||||
"appSrcRoot",
|
||||
"adbPort",
|
||||
"processId"
|
||||
],
|
||||
"properties": {
|
||||
"appSrcRoot": {
|
||||
"type": "string",
|
||||
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
|
||||
"default": "${workspaceRoot}/app/src/main"
|
||||
},
|
||||
"adbPort": {
|
||||
"type": "integer",
|
||||
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037",
|
||||
"default": 5037
|
||||
},
|
||||
"processId": {
|
||||
"type": "string",
|
||||
"description": "PID of process to attach to.\n\"${command:PickAndroidProcess}\" will display a list of debuggable PIDs to choose from during launch.",
|
||||
"default": "${command:PickAndroidProcess}"
|
||||
},
|
||||
"targetDevice": {
|
||||
"type": "string",
|
||||
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used when multiple devices are connected.",
|
||||
"default": ""
|
||||
},
|
||||
"trace": {
|
||||
"type": "boolean",
|
||||
"description": "Set to true to output debugging logs for diagnostics",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "android",
|
||||
"name": "Android",
|
||||
"request": "launch",
|
||||
"name": "Android launch",
|
||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk",
|
||||
"adbPort": 5037
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"request": "attach",
|
||||
"name": "Android attach",
|
||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||
"adbPort": 5037,
|
||||
"processId": "${command:PickAndroidProcess}"
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "Android: Launch Configuration",
|
||||
"label": "Android: Launch Application",
|
||||
"description": "A new configuration for launching an Android app debugging session",
|
||||
"body": {
|
||||
"type": "android",
|
||||
"request": "launch",
|
||||
"name": "${2:Launch App}",
|
||||
"name": "${2:Android Launch}",
|
||||
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
||||
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk\"",
|
||||
"adbPort": 5037
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Android: Attach to Process",
|
||||
"description": "A new configuration for attaching to a running Android app process",
|
||||
"body": {
|
||||
"type": "android",
|
||||
"request": "attach",
|
||||
"name": "${2:Android Attach}",
|
||||
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
||||
"adbPort": 5037,
|
||||
"processId": "^\"\\${command:PickAndroidProcess}\""
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": {}
|
||||
|
||||
@@ -68,14 +68,63 @@ class ADBClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of debuggable pids from the device
|
||||
* Return a list of debuggable pids from the device.
|
||||
*
|
||||
* The `adb jdwp` command never terminates - it just posts each debuggable PID
|
||||
* as it comes online. Normally we just perform a single read of stdout
|
||||
* and terminate the connection, but if there are no pids available, the command
|
||||
* will wait forever.
|
||||
* @param {number} [timeout_ms] time to wait before we abort reading (and return an empty list).
|
||||
*/
|
||||
async jdwp_list() {
|
||||
async jdwp_list(timeout_ms) {
|
||||
await this.connect_to_adb();
|
||||
await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`);
|
||||
const stdout = await this.adbsocket.cmd_and_read_stdout('jdwp');
|
||||
/** @type {string} */
|
||||
let stdout;
|
||||
try {
|
||||
stdout = await this.adbsocket.cmd_and_read_stdout('jdwp', timeout_ms);
|
||||
} catch {
|
||||
// timeout or socket closed
|
||||
stdout = '';
|
||||
}
|
||||
await this.disconnect_from_adb();
|
||||
return stdout.trim().split(/\r?\n|\r/);
|
||||
// do not sort the pid list - the debugger needs to pick the last one in the list.
|
||||
return stdout.trim().split(/\s+/).filter(x => x).map(s => parseInt(s, 10));
|
||||
}
|
||||
|
||||
async named_jdwp_list(timeout_ms) {
|
||||
const named_pids = (await this.jdwp_list(timeout_ms))
|
||||
.map(pid => ({
|
||||
pid,
|
||||
name: '',
|
||||
}))
|
||||
if (!named_pids.length)
|
||||
return [];
|
||||
|
||||
// retrieve the list of process names from the device
|
||||
const command = `for pid in ${named_pids.map(np => np.pid).join(' ')}; do cat /proc/$pid/cmdline;echo " $pid"; done`;
|
||||
const stdout = await this.shell_cmd({
|
||||
command,
|
||||
untilclosed: true,
|
||||
});
|
||||
// output should look something like...
|
||||
// com.example.somepkg 32721
|
||||
const lines = stdout.replace(/\0+/g,'').split(/\r?\n|\r/g);
|
||||
|
||||
// scan the list looking for pids to match names with...
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let entries = lines[i].match(/^\s*(.*)\s+(\d+)$/);
|
||||
if (!entries) {
|
||||
continue;
|
||||
}
|
||||
const pid = parseInt(entries[2], 10);
|
||||
const named_pid = named_pids.find(x => x.pid === pid);
|
||||
if (named_pid) {
|
||||
named_pid.name = entries[1];
|
||||
}
|
||||
}
|
||||
|
||||
return named_pids;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,12 +181,14 @@ class ADBClient {
|
||||
|
||||
/**
|
||||
* Run a shell command on the connected device
|
||||
* @param {{command:string}} o
|
||||
* @param {{command:string, untilclosed?:boolean}} o
|
||||
* @param {number} [timeout_ms]
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async shell_cmd(o) {
|
||||
async shell_cmd(o, timeout_ms) {
|
||||
await this.connect_to_adb();
|
||||
await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`);
|
||||
const stdout = await this.adbsocket.cmd_and_read_stdout(`shell:${o.command}`);
|
||||
const stdout = await this.adbsocket.cmd_and_read_stdout(`shell:${o.command}`, timeout_ms, o.untilclosed);
|
||||
await this.disconnect_from_adb();
|
||||
return stdout;
|
||||
}
|
||||
@@ -145,7 +196,7 @@ class ADBClient {
|
||||
/**
|
||||
* Starts the Logcat monitor.
|
||||
* Logcat lines are passed back via onlog callback. If the device disconnects, onclose is called.
|
||||
* @param {{onlog:(e)=>void, onclose:()=>void}} o
|
||||
* @param {{onlog:(e)=>void, onclose:(err)=>void}} o
|
||||
*/
|
||||
async startLogcatMonitor(o) {
|
||||
// onlog:function(e)
|
||||
@@ -157,18 +208,19 @@ class ADBClient {
|
||||
if (!o.onlog) {
|
||||
const logcatbuffer = await this.adbsocket.read_stdout();
|
||||
await this.disconnect_from_adb();
|
||||
return logcatbuffer;
|
||||
return logcatbuffer.toString();
|
||||
}
|
||||
|
||||
// start the logcat monitor
|
||||
let logcatbuffer = Buffer.alloc(0);
|
||||
const next_logcat_lines = async () => {
|
||||
// read the next data from ADB
|
||||
let logcatbuffer = Buffer.alloc(0);
|
||||
let next_data;
|
||||
try{
|
||||
next_data = await this.adbsocket.read_stdout(null);
|
||||
for (;;) {
|
||||
// read the next data from ADB
|
||||
try {
|
||||
next_data = await this.adbsocket.read_stdout();
|
||||
} catch(e) {
|
||||
o.onclose();
|
||||
o.onclose(e);
|
||||
return;
|
||||
}
|
||||
logcatbuffer = Buffer.concat([logcatbuffer, next_data]);
|
||||
@@ -178,16 +230,19 @@ class ADBClient {
|
||||
next_logcat_lines();
|
||||
return;
|
||||
}
|
||||
// split into lines
|
||||
const logs = logcatbuffer.slice(0, last_newline_index).toString().split(/\r\n?|\n/);
|
||||
logcatbuffer = logcatbuffer.slice(last_newline_index);
|
||||
// split into lines, sort and remove duplicates and blanks
|
||||
const logs = logcatbuffer.slice(0, last_newline_index).toString()
|
||||
.split(/\r\n?|\n/)
|
||||
.sort()
|
||||
.filter((line,idx,arr) => line && line !== arr[idx-1]);
|
||||
|
||||
logcatbuffer = logcatbuffer.slice(last_newline_index);
|
||||
const e = {
|
||||
adbclient: this,
|
||||
logs,
|
||||
};
|
||||
o.onlog(e);
|
||||
next_logcat_lines();
|
||||
}
|
||||
}
|
||||
next_logcat_lines();
|
||||
}
|
||||
|
||||
248
src/debugMain.js
248
src/debugMain.js
@@ -4,7 +4,6 @@ const {
|
||||
Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter');
|
||||
|
||||
// node and external modules
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
@@ -17,7 +16,8 @@ const { evaluate } = require('./expression/evaluate');
|
||||
const { PackageInfo } = require('./package-searcher');
|
||||
const ADBSocket = require('./sockets/adbsocket');
|
||||
const { AndroidThread } = require('./threads');
|
||||
const { D, onMessagePrint } = require('./utils/print');
|
||||
const { checkADBStarted, getAndroidSourcesFolder } = require('./utils/android');
|
||||
const { D, initLogToClient, onMessagePrint } = require('./utils/print');
|
||||
const { hasValidSourceFileExtension } = require('./utils/source-file');
|
||||
const { VariableManager } = require('./variable-manager');
|
||||
|
||||
@@ -33,13 +33,6 @@ class AndroidDebugSession extends DebugSession {
|
||||
|
||||
// the base folder of the app (where AndroidManifest.xml and source files should be)
|
||||
this.app_src_root = '<no appSrcRoot>';
|
||||
// the filepathname of the built apk
|
||||
this.apk_fpn = '';
|
||||
/**
|
||||
* the file info, hash and manifest data of the apk
|
||||
* @type {APKFileInfo}
|
||||
*/
|
||||
this.apk_file_info = null;
|
||||
// packages we found in the source tree
|
||||
this.src_packages = {
|
||||
last_src_modified: 0,
|
||||
@@ -50,8 +43,17 @@ class AndroidDebugSession extends DebugSession {
|
||||
this._device = null;
|
||||
// the API level of the device we are debugging
|
||||
this.device_api_level = '';
|
||||
|
||||
|
||||
// the full file path name of the AndroidManifest.xml, taken from the manifestFile launch property
|
||||
this.manifest_fpn = '';
|
||||
// the filepathname of the built apk
|
||||
this.apk_fpn = '';
|
||||
/**
|
||||
* the file info, hash and manifest data of the apk
|
||||
* @type {APKFileInfo}
|
||||
*/
|
||||
this.apk_file_info = null;
|
||||
|
||||
/**
|
||||
* array of custom arguments to pass to `pm install`
|
||||
@@ -94,9 +96,20 @@ class AndroidDebugSession extends DebugSession {
|
||||
// trace flag for printing diagnostic messages to the client Output Window
|
||||
this.trace = false;
|
||||
|
||||
// set to true if we've connected to the device
|
||||
this.debuggerAttached = false;
|
||||
|
||||
/**
|
||||
* @type {'launch'|'attach'}
|
||||
*/
|
||||
this.debug_mode = null;
|
||||
|
||||
// this debugger uses one-based lines and columns
|
||||
this.setDebuggerLinesStartAt1(true);
|
||||
this.setDebuggerColumnsStartAt1(true);
|
||||
|
||||
// override the log function to output to the client Debug Console
|
||||
initLogToClient(this.LOG.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,13 +277,145 @@ class AndroidDebugSession extends DebugSession {
|
||||
})
|
||||
}
|
||||
|
||||
async launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
|
||||
/**
|
||||
* @param {*} obj
|
||||
*/
|
||||
extractPidAndTargetDevice(obj) {
|
||||
let x, pid, serial = '', status;
|
||||
try {
|
||||
x = JSON.parse(`${obj}`);
|
||||
} catch {
|
||||
}
|
||||
if (typeof x === 'number') {
|
||||
pid = x;
|
||||
} else if (typeof x === 'object') {
|
||||
// object passed from PickAndroidProcess in the extension
|
||||
({ pid, serial, status } = x);
|
||||
if (status !== 'ok') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof pid !== "number" || (pid < 0)) {
|
||||
this.LOG(`Attach failed: "processId" property in launch.json is not valid`);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
processId: pid,
|
||||
targetDevice: `${serial}`,
|
||||
}
|
||||
}
|
||||
|
||||
async attachRequest(response, args) {
|
||||
this.debug_mode = 'attach';
|
||||
if (args && args.trace) {
|
||||
this.trace = args.trace;
|
||||
onMessagePrint(this.LOG.bind(this));
|
||||
}
|
||||
D(`Attach: ${JSON.stringify(args)}`);
|
||||
|
||||
if (!args.processId) {
|
||||
this.LOG(`Attach failed: Missing "processId" property in launch.json`);
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
|
||||
// the processId passed in args can be:
|
||||
// - a fixed id defined in launch.json (should be a string, but we allow a number),
|
||||
// - a JSON object returned from the process picker (contains the target device and process ID),
|
||||
let attach_info = this.extractPidAndTargetDevice(args.processId);
|
||||
if (!attach_info) {
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// app_src_root must end in a path-separator for correct validation of sub-paths
|
||||
this.app_src_root = ensure_path_end_slash(args.appSrcRoot);
|
||||
// start by scanning the source folder for stuff we need to know about (packages, manifest, etc)
|
||||
this.src_packages = PackageInfo.scanSourceSync(this.app_src_root);
|
||||
// warn if we couldn't find any packages (-> no source -> cannot debug anything)
|
||||
if (this.src_packages.packages.size === 0)
|
||||
this.WARN('No source files found. Check the "appSrcRoot" setting in launch.json');
|
||||
|
||||
} catch(err) {
|
||||
// wow, we really didn't make it very far...
|
||||
this.LOG(err.message);
|
||||
this.LOG('Check the "appSrcRoot" entries in launch.json');
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let { processId, targetDevice } = attach_info;
|
||||
if (!targetDevice) {
|
||||
targetDevice = args.targetDevice;
|
||||
}
|
||||
// make sure ADB exists and is started and look for a connected device
|
||||
await checkADBStarted(args.autoStartADB !== false);
|
||||
this._device = await this.findSuitableDevice(targetDevice, args.trace);
|
||||
this._device.adbclient = new ADBClient(this._device.serial);
|
||||
|
||||
// try and determine the relevant path for the API sources (based upon the API level of the connected device)
|
||||
await this.configureAPISourcePath();
|
||||
|
||||
const build = new BuildInfo(null, new Map(this.src_packages.packages), null);
|
||||
this.LOG(`Attaching to pid ${processId} on device ${this._device.serial} [API:${this.device_api_level||'?'}]`);
|
||||
|
||||
// try and attach to the specified pid
|
||||
await this.dbgr.attachToProcess(build, processId, this._device.serial);
|
||||
|
||||
this.debuggerAttached = true;
|
||||
|
||||
// if we get this far, the debugger is connected and waiting for the resume command
|
||||
// - set up some events...
|
||||
this.dbgr.on('bpstatechange', e => this.onBreakpointStateChange(e))
|
||||
.on('bphit', e => this.onBreakpointHit(e))
|
||||
.on('step', e => this.onStep(e))
|
||||
.on('exception', e => this.onException(e))
|
||||
.on('threadchange', e => this.onThreadChange(e))
|
||||
.on('disconnect', () => this.onDebuggerDisconnect());
|
||||
|
||||
// - tell the client we're initialised and ready for breakpoint info, etc
|
||||
this.sendEvent(new InitializedEvent());
|
||||
await new Promise(resolve => this.waitForConfigurationDone = resolve);
|
||||
|
||||
// get the debugger to tell us about any thread creations/terminations
|
||||
await this.dbgr.setThreadNotify();
|
||||
|
||||
// config is done - we're all set and ready to go!
|
||||
this.sendResponse(response);
|
||||
|
||||
this.LOG(`Debugger attached`);
|
||||
await this.dbgr.resume();
|
||||
|
||||
} catch(e) {
|
||||
//this.performDisconnect();
|
||||
// exceptions use message, adbclient uses msg
|
||||
this.LOG('Attach failed: '+(e.message||e.msg||'No additional information is available'));
|
||||
// more info for adb connect errors
|
||||
if (/^ADB server is not running/.test(e.msg)) {
|
||||
this.LOG('Make sure the Android SDK Platform Tools are installed and run:');
|
||||
this.LOG(' adb start-server');
|
||||
this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.');
|
||||
}
|
||||
// tell the client we're done
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point to the debugger
|
||||
* @param {*} response
|
||||
* @param {*} args
|
||||
*/
|
||||
async launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
|
||||
this.debug_mode = 'launch';
|
||||
if (args && args.trace) {
|
||||
this.trace = args.trace;
|
||||
onMessagePrint(this.LOG.bind(this));
|
||||
}
|
||||
D(`Launch: ${JSON.stringify(args)}`);
|
||||
|
||||
D(`Launching: ${JSON.stringify(args)}`);
|
||||
// app_src_root must end in a path-separator for correct validation of sub-paths
|
||||
this.app_src_root = ensure_path_end_slash(args.appSrcRoot);
|
||||
this.apk_fpn = args.apkFile;
|
||||
@@ -319,8 +464,8 @@ class AndroidDebugSession extends DebugSession {
|
||||
throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json');
|
||||
|
||||
// make sure ADB exists and is started and look for a device to install on
|
||||
await this.checkADBStarted(args.autoStartADB !== false);
|
||||
this._device = await this.findSuitableDevice(args.targetDevice);
|
||||
await checkADBStarted(args.autoStartADB !== false);
|
||||
this._device = await this.findSuitableDevice(args.targetDevice, true);
|
||||
this._device.adbclient = new ADBClient(this._device.serial);
|
||||
|
||||
// install the APK we are going to debug
|
||||
@@ -336,6 +481,8 @@ class AndroidDebugSession extends DebugSession {
|
||||
// launch the app
|
||||
await this.startLaunchActivity(args.launchActivity);
|
||||
|
||||
this.debuggerAttached = true;
|
||||
|
||||
// if we get this far, the debugger is connected and waiting for the resume command
|
||||
// - set up some events...
|
||||
this.dbgr.on('bpstatechange', e => this.onBreakpointStateChange(e))
|
||||
@@ -372,20 +519,6 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
}
|
||||
|
||||
async checkADBStarted(autoStartADB) {
|
||||
const err = await new ADBClient().test_adb_connection();
|
||||
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
||||
if (err && autoStartADB && process.env.ANDROID_HOME) {
|
||||
const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||
const adbargs = ['-P',`${ADBSocket.ADBPort}`,'start-server'];
|
||||
try {
|
||||
this.LOG([adbpath, ...adbargs].join(' '));
|
||||
const stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
||||
this.LOG(stdout);
|
||||
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||
}
|
||||
}
|
||||
|
||||
checkBuildIsUpToDate(staleBuild) {
|
||||
// check if any source file was modified after the apk
|
||||
if (this.src_packages.last_src_modified >= this.apk_file_info.app_modified) {
|
||||
@@ -422,13 +555,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
const apilevel = await this.getDeviceAPILevel();
|
||||
|
||||
// look for the android sources folder appropriate for this device
|
||||
if (process.env.ANDROID_HOME && apilevel) {
|
||||
const sources_path = path.join(process.env.ANDROID_HOME,'sources',`android-${apilevel}`);
|
||||
fs.stat(sources_path, (err,stat) => {
|
||||
if (!err && stat && stat.isDirectory())
|
||||
this._android_sources_path = sources_path;
|
||||
});
|
||||
}
|
||||
this._android_sources_path = getAndroidSourcesFolder(apilevel, true);
|
||||
}
|
||||
|
||||
async getDeviceAPILevel() {
|
||||
@@ -491,11 +618,13 @@ class AndroidDebugSession extends DebugSession {
|
||||
|
||||
/**
|
||||
* @param {string} target_deviceid
|
||||
* @param {boolean} show_progress
|
||||
*/
|
||||
async findSuitableDevice(target_deviceid) {
|
||||
this.LOG('Searching for devices...');
|
||||
async findSuitableDevice(target_deviceid, show_progress) {
|
||||
show_progress && this.LOG('Searching for devices...');
|
||||
const devices = await this.dbgr.listConnectedDevices()
|
||||
this.LOG(`Found ${devices.length} device${devices.length===1?'':'s'}`);
|
||||
show_progress && this.LOG(`Found ${devices.length} device${devices.length===1?'':'s'}`);
|
||||
|
||||
let reject;
|
||||
if (devices.length === 0) {
|
||||
reject = 'No devices are connected';
|
||||
@@ -543,12 +672,18 @@ class AndroidDebugSession extends DebugSession {
|
||||
async disconnectRequest(response/*, args*/) {
|
||||
D('disconnectRequest');
|
||||
this._isDisconnecting = true;
|
||||
if (this.debuggerAttached) {
|
||||
try {
|
||||
if (this.debug_mode === 'launch') {
|
||||
await this.dbgr.forceStop();
|
||||
await this.dbgr.disconnect();
|
||||
this.LOG(`Debugger stopped`);
|
||||
} else {
|
||||
await this.dbgr.disconnect();
|
||||
this.LOG(`Debugger detached`);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
@@ -635,10 +770,10 @@ class AndroidDebugSession extends DebugSession {
|
||||
const bp_queue_len = this._set_breakpoints_queue.push({args,response,relative_fpn});
|
||||
if (bp_queue_len === 1) {
|
||||
do {
|
||||
const next_bp = this._set_breakpoints_queue[0];
|
||||
const javabp_arr = await this._setup_breakpoints(next_bp);
|
||||
const { args, relative_fpn, response } = this._set_breakpoints_queue[0];
|
||||
const javabp_arr = await this.setupBreakpointsInFile(args.breakpoints, relative_fpn);
|
||||
// send back the VS Breakpoint instances
|
||||
sendBPResponse(next_bp.response, javabp_arr.map(javabp => javabp.vsbp));
|
||||
sendBPResponse(response, javabp_arr.map(javabp => javabp.vsbp));
|
||||
// .. and do the next one
|
||||
this._set_breakpoints_queue.shift();
|
||||
} while (this._set_breakpoints_queue.length);
|
||||
@@ -646,16 +781,14 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} o
|
||||
* @param {number} idx
|
||||
* @param {*[]} javabp_arr
|
||||
*
|
||||
* @param {*[]} breakpoints
|
||||
* @param {string} relative_fpn
|
||||
*/
|
||||
async _setup_breakpoints(o, idx = 0, javabp_arr = []) {
|
||||
const src_bp = o.args.breakpoints[idx];
|
||||
if (!src_bp) {
|
||||
// end of list
|
||||
return javabp_arr;
|
||||
}
|
||||
async setupBreakpointsInFile(breakpoints, relative_fpn) {
|
||||
const java_breakpoints = [];
|
||||
for (let idx = 0; idx < breakpoints.length; idx++) {
|
||||
const src_bp = breakpoints[idx];
|
||||
const dbgline = this.convertClientLineToDebugger(src_bp.line);
|
||||
const options = new BreakpointOptions();
|
||||
if (src_bp.hitCondition) {
|
||||
@@ -669,7 +802,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
const javabp = await this.dbgr.setBreakpoint(o.relative_fpn, dbgline, options);
|
||||
const javabp = await this.dbgr.setBreakpoint(relative_fpn, dbgline, options);
|
||||
if (!javabp.vsbp) {
|
||||
// state is one of: set,notloaded,enabled,removed
|
||||
const verified = !!javabp.state.match(/set|enabled/);
|
||||
@@ -681,8 +814,9 @@ class AndroidDebugSession extends DebugSession {
|
||||
javabp.vsbp = bp;
|
||||
}
|
||||
javabp.vsbp.order = idx;
|
||||
javabp_arr.push(javabp);
|
||||
return this._setup_breakpoints(o, ++idx, javabp_arr);
|
||||
java_breakpoints.push(javabp);
|
||||
}
|
||||
return java_breakpoints;
|
||||
};
|
||||
|
||||
async setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) {
|
||||
@@ -847,13 +981,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
`;
|
||||
// don't actually attempt to load the file here - just recheck to see if the sources
|
||||
// path is valid yet.
|
||||
if (process.env.ANDROID_HOME && this.device_api_level) {
|
||||
const sources_path = path.join(process.env.ANDROID_HOME,'sources','android-'+this.device_api_level);
|
||||
fs.stat(sources_path, (err,stat) => {
|
||||
if (!err && stat && stat.isDirectory())
|
||||
this._android_sources_path = sources_path;
|
||||
});
|
||||
}
|
||||
this._android_sources_path = getAndroidSourcesFolder(this.device_api_level, true);
|
||||
|
||||
response.body = { content };
|
||||
this.sendResponse(response);
|
||||
|
||||
@@ -82,10 +82,10 @@ class DebugSession {
|
||||
this.classPrepareFilters = new Set();
|
||||
|
||||
/**
|
||||
* The set of class signatures already prepared
|
||||
* The set of class signatures loaded by the runtime
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
this.preparedClasses = new Set();
|
||||
this.loadedClasses = new Set();
|
||||
|
||||
/**
|
||||
* Enabled step JDWP IDs for each thread
|
||||
|
||||
125
src/debugger.js
125
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);
|
||||
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
|
||||
|
||||
@@ -1534,7 +1534,7 @@ class JDWP {
|
||||
const res = [];
|
||||
let arrlen = DataCoder.decodeInt(o);
|
||||
while (--arrlen >= 0) {
|
||||
res.push(DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'},{type:'signature'},{genericSignature:'string'},{status:'status'}]));
|
||||
res.push(DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'},{signature:'string'},{genericSignature:'string'},{status:'status'}]));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
111
src/logcat.js
111
src/logcat.js
@@ -7,6 +7,8 @@ const WebSocketServer = require('ws').Server;
|
||||
// our stuff
|
||||
const { ADBClient } = require('./adbclient');
|
||||
const { AndroidContentProvider } = require('./contentprovider');
|
||||
const { checkADBStarted } = require('./utils/android');
|
||||
const { selectTargetDevice } = require('./utils/device');
|
||||
const { D } = require('./utils/print');
|
||||
|
||||
/**
|
||||
@@ -292,83 +294,58 @@ function onWebSocketClientConnection(client, req) {
|
||||
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
|
||||
}
|
||||
|
||||
function getADBPort() {
|
||||
const defaultPort = 5037;
|
||||
const adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
||||
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
||||
return adbPort;
|
||||
return defaultPort;
|
||||
}
|
||||
|
||||
function openLogcatWindow(vscode) {
|
||||
new ADBClient().test_adb_connection()
|
||||
.then(err => {
|
||||
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
||||
const adbport = getADBPort();
|
||||
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
||||
if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
|
||||
const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||
const adbargs = ['-P',''+adbport,'start-server'];
|
||||
try {
|
||||
/*const stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
||||
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||
}
|
||||
})
|
||||
.then(() => new ADBClient().list_devices())
|
||||
.then(devices => {
|
||||
switch(devices.length) {
|
||||
case 0:
|
||||
vscode.window.showInformationMessage('Logcat cannot be displayed. No Android devices are currently connected');
|
||||
return null;
|
||||
case 1:
|
||||
return devices; // only one device - just show it
|
||||
}
|
||||
const prefix = 'Android: View Logcat - ', all = '[ Display All ]';
|
||||
const devicelist = devices.map(d => prefix + d.serial);
|
||||
//devicelist.push(prefix + all);
|
||||
return vscode.window.showQuickPick(devicelist)
|
||||
.then(which => {
|
||||
if (!which) return; // user cancelled
|
||||
which = which.slice(prefix.length);
|
||||
return new ADBClient().list_devices()
|
||||
.then(devices => {
|
||||
if (which === all) {
|
||||
return devices
|
||||
}
|
||||
const found = devices.find(d => d.serial === which);
|
||||
if (found) {
|
||||
return [found];
|
||||
}
|
||||
vscode.window.showInformationMessage('Logcat cannot be displayed. The device is disconnected');
|
||||
return null;
|
||||
});
|
||||
}, () => null);
|
||||
})
|
||||
.then(devices => {
|
||||
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
|
||||
devices.forEach(device => {
|
||||
if (vscode.window.createWebviewPanel) {
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
* @param {*} target_device
|
||||
*/
|
||||
function openWebviewLogcatWindow(vscode, target_device) {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'androidlogcat', // Identifies the type of the webview. Used internally
|
||||
`logcat-${device.serial}`, // Title of the panel displayed to the user
|
||||
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
|
||||
`logcat-${target_device.serial}`, // Title of the panel displayed to the user
|
||||
vscode.ViewColumn.Two, // Editor column to show the new webview panel in.
|
||||
{
|
||||
enableScripts: true,
|
||||
enableScripts: true, // we use embedded scripts to relay logcat info over a websocket
|
||||
}
|
||||
);
|
||||
const logcat = new LogcatContent(device.serial);
|
||||
const logcat = new LogcatContent(target_device.serial);
|
||||
logcat.content().then(html => {
|
||||
panel.webview.html = html;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
* @param {*} target_device
|
||||
*/
|
||||
function openPreviewHtmlLogcatWindow(vscode, target_device) {
|
||||
const uri = AndroidContentProvider.getReadLogcatUri(target_device.serial);
|
||||
vscode.commands.executeCommand("vscode.previewHtml", uri, vscode.ViewColumn.Two);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
*/
|
||||
async function openLogcatWindow(vscode) {
|
||||
try {
|
||||
// if adb is not running, see if we can start it ourselves
|
||||
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
||||
await checkADBStarted(autoStartADB);
|
||||
|
||||
let target_device = await selectTargetDevice(vscode, "Logcat display");
|
||||
if (!target_device) {
|
||||
return;
|
||||
}
|
||||
const uri = AndroidContentProvider.getReadLogcatUri(device.serial);
|
||||
vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
|
||||
});
|
||||
})
|
||||
.catch((/*e*/) => {
|
||||
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
|
||||
});
|
||||
|
||||
if (vscode.window.createWebviewPanel) {
|
||||
// newer versions of vscode use WebviewPanels
|
||||
openWebviewLogcatWindow(vscode, target_device);
|
||||
} else {
|
||||
// older versions of vscode use previewHtml
|
||||
openPreviewHtmlLogcatWindow(vscode, target_device);
|
||||
}
|
||||
} catch (e) {
|
||||
vscode.window.showInformationMessage(`Logcat cannot be displayed. ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
83
src/process-attach.js
Normal file
83
src/process-attach.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const os = require('os');
|
||||
const { ADBClient } = require('./adbclient');
|
||||
const { selectTargetDevice } = require('./utils/device');
|
||||
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
* @param {{pid:number,name:string}[]} pids
|
||||
*/
|
||||
async function showPIDPicker(vscode, pids) {
|
||||
// sort by PID (the user can type the package name to search)
|
||||
const sorted_pids = pids.slice().sort((a,b) => a.pid - b.pid);
|
||||
|
||||
/** @type {import('vscode').QuickPickItem[]} */
|
||||
const device_pick_items = sorted_pids
|
||||
.map(x => ({
|
||||
label: `${x.pid}`,
|
||||
description: x.name,
|
||||
}));
|
||||
|
||||
/** @type {import('vscode').QuickPickOptions} */
|
||||
const device_pick_options = {
|
||||
matchOnDescription: true,
|
||||
canPickMany: false,
|
||||
placeHolder: 'Choose the Android process to attach to',
|
||||
};
|
||||
|
||||
const chosen_option = await vscode.window.showQuickPick(device_pick_items, device_pick_options);
|
||||
return sorted_pids[device_pick_items.indexOf(chosen_option)] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
*/
|
||||
async function selectAndroidProcessID(vscode) {
|
||||
const res = {
|
||||
/** @type {string|'ok'|'cancelled'|'failed'} */
|
||||
status: 'failed',
|
||||
pid: 0,
|
||||
serial: '',
|
||||
}
|
||||
const err = await new ADBClient().test_adb_connection()
|
||||
if (err) {
|
||||
vscode.window.showWarningMessage('Attach failed. ADB is not running.');
|
||||
return res;
|
||||
}
|
||||
|
||||
const device = await selectTargetDevice(vscode, 'Attach');
|
||||
if (!device) {
|
||||
// user cancelled picker
|
||||
res.status = 'cancelled';
|
||||
return res;
|
||||
}
|
||||
|
||||
let named_pids = await new ADBClient(device.serial).named_jdwp_list(5000);
|
||||
if (named_pids.length === 0) {
|
||||
vscode.window.showWarningMessage(
|
||||
'Attach failed. No debuggable processes are running on the device.'
|
||||
+ `${os.EOL}${os.EOL}`
|
||||
+ `To allow a debugger to attach, the app must have the "android:debuggable=true" attribute present in AndroidManifest.xml and be running on the device.`
|
||||
+ `${os.EOL}`
|
||||
+ `See https://developer.android.com/guide/topics/manifest/application-element#debug`
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
// always show the pid picker - even if there's only one
|
||||
const named_pid = await showPIDPicker(vscode, named_pids);
|
||||
if (named_pid === null) {
|
||||
// user cancelled picker
|
||||
res.status = 'cancelled';
|
||||
return res;
|
||||
}
|
||||
|
||||
res.pid = named_pid.pid;
|
||||
res.serial = device.serial;
|
||||
res.status = 'ok';
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
selectAndroidProcessID,
|
||||
}
|
||||
@@ -73,10 +73,13 @@ class ADBSocket extends AndroidSocket {
|
||||
/**
|
||||
* Sends an ADB command, checks the returned status and then reads raw data from the socket
|
||||
* @param {string} command
|
||||
* @param {number} timeout_ms
|
||||
* @param {boolean} [until_closed]
|
||||
*/
|
||||
async cmd_and_read_stdout(command) {
|
||||
async cmd_and_read_stdout(command, timeout_ms, until_closed) {
|
||||
await this.cmd_and_status(command);
|
||||
return this.read_stdout();
|
||||
const buf = await this.read_stdout(timeout_ms, until_closed);
|
||||
return buf.toString('latin1');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,8 +64,9 @@ class AndroidSocket extends EventEmitter {
|
||||
*
|
||||
* @param {number|'length+data'|undefined} length
|
||||
* @param {string} [format]
|
||||
* @param {number} [timeout_ms]
|
||||
*/
|
||||
async read_bytes(length, format) {
|
||||
async read_bytes(length, format, timeout_ms) {
|
||||
//D(`reading ${length} bytes`);
|
||||
let actual_length = length;
|
||||
if (typeof actual_length === 'undefined') {
|
||||
@@ -95,25 +96,40 @@ class AndroidSocket extends EventEmitter {
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
// wait for the socket to update and then retry the read
|
||||
await this.wait_for_socket_data();
|
||||
await this.wait_for_socket_data(timeout_ms);
|
||||
return this.read_bytes(length, format);
|
||||
}
|
||||
|
||||
wait_for_socket_data() {
|
||||
/**
|
||||
*
|
||||
* @param {number} [timeout_ms]
|
||||
*/
|
||||
wait_for_socket_data(timeout_ms) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let done = 0;
|
||||
let done = 0, timer = null;
|
||||
let onDataChanged = () => {
|
||||
if ((done += 1) !== 1) return;
|
||||
this.off('socket-ended', onSocketEnded);
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
let onSocketEnded = () => {
|
||||
if ((done += 1) !== 1) return;
|
||||
this.off('data-changed', onDataChanged);
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`${this.which} socket read failed. Socket closed.`));
|
||||
}
|
||||
let onTimerExpired = () => {
|
||||
if ((done += 1) !== 1) return;
|
||||
this.off('socket-ended', onSocketEnded);
|
||||
this.off('data-changed', onDataChanged);
|
||||
reject(new Error(`${this.which} socket read failed. Read timeout.`));
|
||||
}
|
||||
this.once('data-changed', onDataChanged);
|
||||
this.once('socket-ended', onSocketEnded);
|
||||
if (typeof timeout_ms === 'number' && timeout_ms >= 0) {
|
||||
timer = setTimeout(onTimerExpired, timeout_ms);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,8 +138,26 @@ class AndroidSocket extends EventEmitter {
|
||||
return this.read_bytes(len.readUInt32LE(0), format);
|
||||
}
|
||||
|
||||
read_stdout(format = 'latin1') {
|
||||
return this.read_bytes(undefined, format);
|
||||
/**
|
||||
*
|
||||
* @param {number} [timeout_ms]
|
||||
* @param {boolean} [until_closed]
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async read_stdout(timeout_ms, until_closed) {
|
||||
let buf = await this.read_bytes(undefined, null, timeout_ms);
|
||||
if (!until_closed) {
|
||||
return buf;
|
||||
}
|
||||
const parts = [buf];
|
||||
try {
|
||||
for (;;) {
|
||||
buf = await this.read_bytes(undefined, null);
|
||||
parts.push(buf);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return Buffer.concat(parts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +168,7 @@ class AndroidSocket extends EventEmitter {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.check_socket_active('write');
|
||||
try {
|
||||
// @ts-ignore
|
||||
const flushed = this.socket.write(bytes, () => {
|
||||
flushed ? resolve() : this.socket.once('drain', resolve);
|
||||
});
|
||||
|
||||
87
src/utils/android.js
Normal file
87
src/utils/android.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { ADBClient } = require('../adbclient');
|
||||
const ADBSocket = require('../sockets/adbsocket');
|
||||
const { LOG } = require('../utils/print');
|
||||
|
||||
function getAndroidSDKFolder() {
|
||||
// ANDROID_HOME is deprecated
|
||||
return process.env.ANDROID_HOME || process.env.ANDROID_SDK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} api_level
|
||||
* @param {boolean} check_is_dir
|
||||
*/
|
||||
function getAndroidSourcesFolder(api_level, check_is_dir) {
|
||||
const android_sdk = getAndroidSDKFolder();
|
||||
if (!android_sdk) {
|
||||
return null;
|
||||
}
|
||||
const sources_path = path.join(android_sdk,'sources',`android-${api_level}`);
|
||||
if (check_is_dir) {
|
||||
try {
|
||||
const stat = fs.statSync(sources_path);
|
||||
if (!stat || !stat.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return sources_path;
|
||||
}
|
||||
|
||||
function getADBPathName() {
|
||||
const android_sdk = getAndroidSDKFolder();
|
||||
if (!android_sdk) {
|
||||
return '';
|
||||
}
|
||||
return path.join(android_sdk, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} port
|
||||
*/
|
||||
function startADBServer(port) {
|
||||
if (typeof port !== 'number' || port <= 0 || port >= 65536) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const adb_exe_path = getADBPathName();
|
||||
if (!adb_exe_path) {
|
||||
return false;
|
||||
}
|
||||
const adb_start_server_args = ['-P',`${port}`,'start-server'];
|
||||
try {
|
||||
LOG([adb_exe_path, ...adb_start_server_args].join(' '));
|
||||
const stdout = require('child_process').execFileSync(adb_exe_path, adb_start_server_args, {
|
||||
cwd: getAndroidSDKFolder(),
|
||||
encoding:'utf8',
|
||||
});
|
||||
LOG(stdout);
|
||||
return true;
|
||||
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} auto_start
|
||||
*/
|
||||
async function checkADBStarted(auto_start) {
|
||||
const err = await new ADBClient().test_adb_connection();
|
||||
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
||||
if (err && auto_start) {
|
||||
return startADBServer(ADBSocket.ADBPort);
|
||||
}
|
||||
return !err;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkADBStarted,
|
||||
getADBPathName,
|
||||
getAndroidSDKFolder,
|
||||
getAndroidSourcesFolder,
|
||||
startADBServer,
|
||||
}
|
||||
55
src/utils/device.js
Normal file
55
src/utils/device.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const { ADBClient } = require('../adbclient');
|
||||
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
* @param {{serial:string}[]} devices
|
||||
*/
|
||||
async function showDevicePicker(vscode, devices) {
|
||||
const sorted_devices = devices.slice().sort((a,b) => a.serial.localeCompare(b.serial, undefined, {sensitivity: 'base'}));
|
||||
|
||||
/** @type {import('vscode').QuickPickItem[]} */
|
||||
const quick_pick_items = sorted_devices
|
||||
.map(device => ({
|
||||
label: `${device.serial}`,
|
||||
}));
|
||||
|
||||
/** @type {import('vscode').QuickPickOptions} */
|
||||
const quick_pick_options = {
|
||||
canPickMany: false,
|
||||
placeHolder: 'Choose an Android device',
|
||||
};
|
||||
|
||||
const chosen_option = await vscode.window.showQuickPick(quick_pick_items, quick_pick_options);
|
||||
return sorted_devices[quick_pick_items.indexOf(chosen_option)] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('vscode')} vscode
|
||||
* @param {'Attach'|'Logcat display'} action
|
||||
*/
|
||||
async function selectTargetDevice(vscode, action) {
|
||||
const devices = await new ADBClient().list_devices();
|
||||
let device;
|
||||
switch(devices.length) {
|
||||
case 0:
|
||||
vscode.window.showWarningMessage(`${action} failed. No Android devices are connected.`);
|
||||
return null;
|
||||
case 1:
|
||||
return devices[0]; // only one device - just use it
|
||||
}
|
||||
device = await showDevicePicker(vscode, devices);
|
||||
// the user might take a while to choose the device, so once
|
||||
// chosen, recheck it exists
|
||||
const current_devices = await new ADBClient().list_devices();
|
||||
if (!current_devices.find(d => d.serial === device.serial)) {
|
||||
vscode.window.showInformationMessage(`${action} failed. The target device is disconnected`);
|
||||
return null;
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
selectTargetDevice,
|
||||
showDevicePicker,
|
||||
}
|
||||
@@ -35,6 +35,23 @@ function W(...args) {
|
||||
callMessagePrintCallbacks(args);
|
||||
}
|
||||
|
||||
let printLogToClient;
|
||||
function initLogToClient(fn) {
|
||||
printLogToClient = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a log message
|
||||
* @param {*} msg
|
||||
*/
|
||||
function LOG(msg) {
|
||||
if (printLogToClient) {
|
||||
printLogToClient(msg);
|
||||
} else {
|
||||
D(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a callback to be called when any message is output
|
||||
* @param {Function} cb
|
||||
@@ -46,6 +63,8 @@ function onMessagePrint(cb) {
|
||||
module.exports = {
|
||||
D,
|
||||
E,
|
||||
initLogToClient,
|
||||
LOG,
|
||||
W,
|
||||
onMessagePrint,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user