6 Commits

Author SHA1 Message Date
Dave Holoway
d064b9a3f4 version 1.1 2020-04-24 19:19:21 +01:00
Dave Holoway
6439e1b8b7 Version 1.1 improvements (#88)
* fix 0 alignment in binary xml decoding

* output reason for APK manifest read failure

* try and match package name against process name
when determining which pid to attach

* make post launch pause user-configurable

* code tidy, jsdocs and types

* more types in expression parse classes

* fix issue with expandable objects not evaluating

* update build task example

* fix package/type evaluation

* improve handling of targetDevice and processID combinations

* show full call stack by default

* implement a queue for evaluations

* improve performance of retrieving single fields

* check root term identifiers against this fields
2020-04-24 19:03:39 +01:00
Dave Holoway
a4ce09d309 Expression format specifier support (#87)
* initial support for format specifier

* add readme notes for format specifiers

* add support for showing arrays and objects  with format specifiers

* create unique object variable references for different display formats

* add notes on applying formatting to objects and arrays
2020-04-24 14:37:51 +01:00
Dave Holoway
1535f133d9 Support for device picker during launch (#86) 2020-04-23 16:44:19 +01:00
Dave Holoway
44d887dd6c 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
2020-04-23 13:28:03 +01:00
Dave Holoway
9aeca6b96b support custom launch arguments (#84)
* add amStartArgs launch property to allow launch command arguments to be customised.

* update readme launch options docs
2020-04-23 13:27:17 +01:00
25 changed files with 1504 additions and 499 deletions

6
.vscode/launch.json vendored
View File

@@ -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" ]
}
]
}

View File

@@ -1,5 +1,12 @@
# Change Log
### version 1.1.0
* App launch arguments overriden in a new `amStartArgs` launch configuration property.
* A new "attach" launch configuration allows the debugger to attach to running processes.
* A "${command:PickAndroidDevice}" value allows a deployment device to be chosen during each launch
* Watch and repl expressions now support format specifiers
* Small bug fixes and performance improvements
### version 1.0.0
* Update extension to support minimum version of node v10
* refactoring and improvement of type-checking using jsdocs

103
README.md
View File

@@ -34,31 +34,65 @@ The following settings are used to configure the debugger:
"version": "0.2.0",
"configurations": [
{
// configuration type, request and name. "launch" is used to deploy the app to your device and start a debugging session
// configuration type, request and name. "launch" is used to deploy the app
// to your device and start a debugging session.
"type": "android",
"request": "launch",
"name": "Launch App",
// Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)
// Location of the App source files. This value must point to the root of
// your App source tree (containing AndroidManifest.xml).
"appSrcRoot": "${workspaceRoot}/app/src/main",
// Fully qualified path to the built APK (Android Application Package)
// Fully qualified path to the built APK (Android Application Package).
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
// Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037
// Port number to connect to the local ADB (Android Debug Bridge) instance.
// Default: 5037
"adbPort": 5037,
// Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn
// Automatically launch 'adb start-server' if not already started.
// Default: true
"autoStartADB": true,
// Launch behaviour if source files have been saved after the APK was built.
// One of: [ ignore warn stop ]. Default: warn
"staleBuild": "warn",
// Fully qualified path to the AndroidManifest.xml file compiled in the APK. Default: appSrcRoot/AndroidManifest.xml
// Target Device ID (as indicated by 'adb devices').
// Use this to specify which device is used for deployment
// when multiple devices are connected.
"targetDevice": "",
// Fully qualified path to the AndroidManifest.xml file compiled into the APK.
// Default: "${appSrcRoot}/AndroidManifest.xml"
"manifestFile": "${workspaceRoot}/app/src/main/AndroidManifest.xml",
// APK install arguments passed to the Android package manager. Run 'adb shell pm' to show valid arguments. Default: ["-r"]
// Custom arguments passed to the Android package manager to install the app.
// Run 'adb shell pm' to show valid arguments. Default: ["-r"]
"pmInstallArgs": ["-r"],
// Manually specify the activity to run when the app is started.
"launchActivity": ".MainActivity"
// Custom arguments passed to the Android application manager to start the app.
// Run `adb shell am` to show valid arguments.
// Note that `-D` is required to enable debugging.
"amStartArgs": [
"-D",
"--activity-brought-to-front",
"-a android.intent.action.MAIN",
"-c android.intent.category.LAUNCHER",
"-n package.name/launch.activity"
],
// Manually specify the activity to run when the app is started. This option is
// mutually exclusive with "amStartArgs".
"launchActivity": ".MainActivity",
// Time in milliseconds to wait after launching an app before attempting to attach
// the debugger. Default: 1000ms
"postLaunchPause": 1000,
// Set to true to output debugging logs for diagnostics.
"trace": false
}
]
}
@@ -79,6 +113,7 @@ Add a `preLaunchTask` item to the launch configuration:
"request": "launch",
"name": "App Build & Launch",
"preLaunchTask": "run gradle",
...
}
]
}
@@ -93,12 +128,60 @@ Add a new task to run the build command:
"label": "run gradle",
"type": "shell",
"command": "${workspaceFolder}/gradlew",
"args": ["assembleDebug"]
"args": [
"assembleDebug"
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
```
## Expression evaluation
Format specifiers can be appended to watch and repl expressions to change how the evaluated result is displayed.
The specifiers work with the same syntax used in Visual Studio.
See https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp for examples.
```
123 123
123,x 0x0000007b
123,xb 0000007b
123,X 0x0000007B
123,o 000000000173
123,b 0b00000000000000000000000001111011
123,bb 00000000000000000000000001111011
123,c '{'
"one\ntwo" "one\ntwo"
"one\ntwo",sb one\ntwo
"one\ntwo",! one
two
```
You can also apply the specifiers to object and array instances to format fields and elements:
```
arr,x int[3]
[0] 0x00000001
[1] 0x00000002
[1] 0x00000003
```
Note: Format specifiers for floating point values (`e`/`g`) and string encoding conversions (`s8`/`su`/`s32`) are not supported.
## Powered by coffee
The Android Developer Extension is a completely free, fully open-source project. If you've found the extension useful, you

View File

@@ -3,6 +3,8 @@
const vscode = require('vscode');
const { AndroidContentProvider } = require('./src/contentprovider');
const { openLogcatWindow } = require('./src/logcat');
const { selectAndroidProcessID } = require('./src/process-attach');
const { selectTargetDevice } = require('./src/utils/device');
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
@@ -17,6 +19,37 @@ function activate(context) {
vscode.commands.registerCommand('android-dev-ext.view_logcat', () => {
openLogcatWindow(vscode);
}),
// add the device picker handler - used to choose a target device
vscode.commands.registerCommand('PickAndroidDevice', async (launchConfig) => {
// if the config has both PickAndroidDevice and PickAndroidProcess, ignore this
// request as PickAndroidProcess already includes chooosing a device...
if (launchConfig && launchConfig.processId === '${command:PickAndroidProcess}') {
return '';
}
const device = await selectTargetDevice(vscode, "Launch", { alwaysShow:true });
// the debugger requires a string value to be returned
return JSON.stringify(device);
}),
// add the process picker handler - used to choose a PID to attach to
vscode.commands.registerCommand('PickAndroidProcess', async (launchConfig) => {
// if the config has a targetDevice specified, use it instead of choosing a device...
let target_device = '';
if (launchConfig && typeof launchConfig.targetDevice === 'string') {
target_device = launchConfig.targetDevice;
}
const explicit_pick_device = target_device === '${command:PickAndroidDevice}';
if (!target_device || explicit_pick_device) {
// no targetDevice (or it's set to ${command:PickAndroidDevice})
const device = await selectTargetDevice(vscode, 'Attach', { alwaysShow: explicit_pick_device });
if (!device) {
return JSON.stringify({status: 'cancelled'});
}
target_device = device.serial;
}
const o = await selectAndroidProcessID(vscode, target_device);
// the debugger requires a string value to be returned
return JSON.stringify(o);
}),
];
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);

14
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "android-dev-ext",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -37,9 +37,9 @@
"dev": true
},
"@types/node": {
"version": "10.17.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.20.tgz",
"integrity": "sha512-XgDgo6W10SeGEAM0k7FosJpvLCynOTYns4Xk3J5HGrA+UI/bKZ30PGMzOP5Lh2zs4259I71FSYLAtjnx3qhObw==",
"version": "10.17.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.21.tgz",
"integrity": "sha512-PQKsydPxYxF1DsAFWmunaxd3sOi3iMt6Zmx/tgaagHYmwJ/9cRH91hQkeJZaUGWbvn0K5HlSVEXkn5U/llWPpQ==",
"dev": true
},
"@types/vscode": {
@@ -1183,9 +1183,9 @@
"dev": true
},
"unzipper": {
"version": "0.10.10",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz",
"integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==",
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
"requires": {
"big-integer": "^1.6.17",
"binary": "~0.3.0",

View File

@@ -2,7 +2,7 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "1.0.0",
"version": "1.1.0",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
@@ -18,7 +18,9 @@
"theme": "dark"
},
"activationEvents": [
"onCommand:android-dev-ext.view_logcat"
"onCommand:android-dev-ext.view_logcat",
"onCommand:PickAndroidDevice",
"onCommand:PickAndroidProcess"
],
"repository": {
"type": "git",
@@ -54,6 +56,17 @@
"adbPort"
],
"properties": {
"amStartArgs": {
"type": "array",
"description": "Custom arguments to pass to the Android application manager to start the app. Run `adb shell am` to show valid arguments. Note that `-D` is required to enable debugging.\r\nBe careful using this option - you must specify the correct parameters or the app will not start.\r\n\r\nThis option is incompatible with the `launchActivity` option.",
"default": [
"-D",
"--activity-brought-to-front",
"-a android.intent.action.MAIN",
"-c android.intent.category.LAUNCHER",
"-n package.name/launch.activity"
]
},
"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)",
@@ -101,6 +114,11 @@
"-r"
]
},
"postLaunchPause": {
"type": "number",
"description": "Time in milliseconds to wait after launching an app before attempting to attach the debugger. Default: 1000",
"default": 1000
},
"staleBuild": {
"type": "string",
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
@@ -109,7 +127,41 @@
"targetDevice": {
"type": "string",
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
"default": ""
"default": "${command:PickAndroidDevice}"
},
"trace": {
"type": "boolean",
"description": "Set to true to output debugging logs for diagnostics",
"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": "${command:PickAndroidDevice}"
},
"trace": {
"type": "boolean",
@@ -122,25 +174,45 @@
"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": {}

View File

@@ -68,14 +68,77 @@ 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));
}
/**
* Retrieve a list of named debuggable pids
* @param {number} timeout_ms
*/
async named_jdwp_list(timeout_ms) {
const pids = await this.jdwp_list(timeout_ms);
return this.get_named_processes(pids);
}
/**
* Convert a list of pids to named-process objects
* @param {number[]} pids
*/
async get_named_processes(pids) {
if (!pids.length) {
return [];
}
const named_pids = pids
.map(pid => ({
pid,
name: '',
}))
// retrieve the list of process names from the device
const command = `for pid in ${pids.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 +195,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 +210,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,37 +222,41 @@ 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);
} catch(e) {
o.onclose();
return;
for (;;) {
// read the next data from ADB
try {
next_data = await this.adbsocket.read_stdout();
} catch(e) {
o.onclose(e);
return;
}
logcatbuffer = Buffer.concat([logcatbuffer, next_data]);
const last_newline_index = logcatbuffer.lastIndexOf(10) + 1;
if (last_newline_index === 0) {
// wait for a whole line
next_logcat_lines();
return;
}
// 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);
}
logcatbuffer = Buffer.concat([logcatbuffer, next_data]);
const last_newline_index = logcatbuffer.lastIndexOf(10) + 1;
if (last_newline_index === 0) {
// wait for a whole line
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);
const e = {
adbclient: this,
logs,
};
o.onlog(e);
next_logcat_lines();
}
next_logcat_lines();
}

View File

@@ -107,7 +107,7 @@ function decode_spec_value(o, key, value, buf, idx, main) {
case /^align:\d+$/.test(value): {
// used for arbitrary padding to a specified alignment
const align = parseInt(value.split(':')[1], 10);
byteLength = align - (idx % align);
byteLength = idx - (Math.trunc(idx / align) * align);
o[key] = buf.slice(idx, idx + byteLength);
break;
}

View File

@@ -104,8 +104,7 @@ class APKFileInfo {
* 2. The decoded manifest from the APK
* 3. The AndroidManifest.xml file from the root of the source tree.
*/
async function getAndroidManifestXml(args) {
const {manifestFile, apkFile, appSrcRoot} = args;
async function getAndroidManifestXml({manifestFile, apkFile, appSrcRoot}) {
let manifest;
// a value from the manifestFile overrides the default manifest extraction
@@ -121,7 +120,7 @@ async function getAndroidManifestXml(args) {
manifest = await extractManifestFromAPK(apkFile);
} catch(err) {
// if we fail to get manifest from the APK, revert to the source file version
D(`Reading source manifest from ${appSrcRoot}`);
D(`Reading source manifest from ${appSrcRoot} (${err.message})`);
manifest = await readFile(path.join(appSrcRoot, 'AndroidManifest.xml'), 'utf8');
}
return manifest;

View File

@@ -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');
@@ -12,12 +11,13 @@ const path = require('path');
const { ADBClient } = require('./adbclient');
const { APKFileInfo } = require('./apk-file-info');
const { Debugger } = require('./debugger');
const { BreakpointOptions, BuildInfo, DebuggerException, DebuggerValue, JavaBreakpointEvent, JavaClassType, JavaExceptionEvent, SourceLocation } = require('./debugger-types');
const { AttachBuildInfo, BreakpointOptions, DebuggerException, DebuggerValue, JavaBreakpointEvent, JavaClassType, JavaExceptionEvent, LaunchBuildInfo, SourceLocation } = require('./debugger-types');
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,29 @@ 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`
* @type {string[]}
*/
this.pm_install_args = null;
/**
* array of custom arguments to pass to `am start`
* @type {string[]}
*/
this.am_start_args = null;
/**
* the threads (from the last refreshThreads() call)
@@ -63,9 +77,12 @@ class AndroidDebugSession extends DebugSession {
this._android_sources_path = '';
// number of call stack entries to display above the project source
this.callStackDisplaySize = 1;
this.callStackDisplaySize = 0;
// the fifo queue of evaluations (watches, hover, etc)
/**
* the fifo queue of evaluations (watches, hover, etc)
* @type {EvalQueueEntry[]}
*/
this._evals_queue = [];
// since we want to send breakpoint events, we will assign an id to every event
@@ -82,34 +99,37 @@ 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);
this.setDebuggerColumnsStartAt1(true);
// override the log function to output to the client Debug Console
initLogToClient(this.LOG.bind(this));
}
/**
* The 'initialize' request is the first request called by the frontend
* to interrogate the features the debug adapter provides.
* @param {import('vscode-debugprotocol').DebugProtocol.InitializeResponse} response
*/
initializeRequest(response/*: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments*/) {
// This debug adapter implements the configurationDoneRequest.
response.body.supportsConfigurationDoneRequest = true;
// we support some exception options
initializeRequest(response) {
response.body.exceptionBreakpointFilters = [
{ label:'All Exceptions', filter:'all', default:false },
{ label:'Uncaught Exceptions', filter:'uncaught', default:true },
];
// we support modifying variable values
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsSetVariable = true;
// we support hit-count conditional breakpoints
response.body.supportsHitConditionalBreakpoints = true;
// we support the new ExceptionInfoRequest
response.body.supportsEvaluateForHovers = true;
response.body.supportsExceptionInfoRequest = true;
response.body.supportsHitConditionalBreakpoints = true;
this.sendResponse(response);
}
@@ -139,6 +159,7 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} msg
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
* @param {boolean} silent
*/
failRequest(msg, response, silent = false) {
@@ -157,7 +178,7 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} requestName
* @param {number} threadId
* @param {*} response
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
*/
failRequestNoThread(requestName, threadId, response) {
this.failRequest(`${requestName} failed. Thread ${threadId} not found`, response);
@@ -166,7 +187,7 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} requestName
* @param {number} threadId
* @param {*} response
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
*/
failRequestThreadNotSuspended(requestName, threadId, response) {
this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response);
@@ -175,7 +196,7 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} requestName
* @param {number} threadId
* @param {*} response
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
*/
cancelRequestThreadNotSuspended(requestName, threadId, response) {
// now that vscode can resume threads before the locals,callstack,etc are retrieved, we only need to cancel the request
@@ -230,10 +251,11 @@ class AndroidDebugSession extends DebugSession {
// configure the thread names
threadinfos.forEach(threadinfo => {
const thread = this.getThread(threadinfo.threadid);
if (thread.name === null) {
if (typeof thread.name !== 'string') {
thread.name = threadinfo.name;
} else if (thread.name !== threadinfo.name) {
// give the thread a new id for VS code
// - note: this will invalidate all current variable references for this thread
delete this._threads[thread.vscode_threadid];
thread.allocateNewThreadID();
this._threads[thread.vscode_threadid] = thread;
@@ -252,21 +274,217 @@ 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}`,
}
}
extractTargetDeviceID(s) {
if (!s || typeof s !== 'string') {
return '';
}
// the device picker returns a stringified object
try {
const o = JSON.parse(s);
return o.serial || s;
} catch {
}
return s;
}
/**
* @typedef AndroidAttachArguments
* @property {string} appSrcRoot
* @property {boolean} autoStartADB
* @property {number} processId
* @property {string} targetDevice
* @property {boolean} trace
*
* @param {import('vscode-debugprotocol').DebugProtocol.AttachResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.AttachRequestArguments & AndroidAttachArguments} args
*/
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.targetDevice === 'null') {
// "null" is returned from the device picker if there's an error or if the
// user cancels.
D('targetDevice === "null"');
this.sendEvent(new TerminatedEvent(false));
return;
}
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 = this.extractTargetDeviceID(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 AttachBuildInfo(new Map(this.src_packages.packages));
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));
}
}
/**
* @typedef AndroidLaunchArguments
* @property {number} adbPort
* @property {string[]} amStartArgs
* @property {string} apkFile
* @property {string} appSrcRoot
* @property {boolean} autoStartADB
* @property {number} callStackDisplaySize
* @property {string} launchActivity
* @property {string} manifestFile
* @property {string[]} pmInstallArgs
* @property {number} postLaunchPause
* @property {number} processId
* @property {StaleBuildSetting} staleBuild
* @property {string} targetDevice
* @property {boolean} trace
* The entry point to the debugger
* @param {import('vscode-debugprotocol').DebugProtocol.LaunchResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.LaunchRequestArguments & AndroidLaunchArguments} args
*/
async launchRequest(response, args) {
this.debug_mode = 'launch';
if (args && args.trace) {
this.trace = args.trace;
onMessagePrint(this.LOG.bind(this));
}
D(`Launch: ${JSON.stringify(args)}`);
if (args.targetDevice === 'null') {
// "null" is returned from the device picker if there's an error or if the
// user cancels.
D('targetDevice === "null"');
this.sendEvent(new TerminatedEvent(false));
return;
}
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;
this.manifest_fpn = args.manifestFile;
this.pmInstallArgs = args.pmInstallArgs;
this.pm_install_args = args.pmInstallArgs;
this.am_start_args = args.amStartArgs;
if (typeof args.callStackDisplaySize === 'number' && args.callStackDisplaySize >= 0)
this.callStackDisplaySize = args.callStackDisplaySize|0;
// we don't allow both amStartArgs and launchActivity to be specified (the launch activity must be included in amStartArgs)
if (args.amStartArgs && args.launchActivity) {
this.LOG('amStartArgs and launchActivity options cannot both be specified in the launch configuration.');
this.sendEvent(new TerminatedEvent(false));
return;
}
// set the custom ADB port - this should be changed to pass it to each ADBClient instance
if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) {
ADBSocket.ADBPort = args.adbPort;
@@ -299,8 +517,9 @@ 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);
const targetDevice = this.extractTargetDeviceID(args.targetDevice);
this._device = await this.findSuitableDevice(targetDevice, true);
this._device.adbclient = new ADBClient(this._device.serial);
// install the APK we are going to debug
@@ -314,7 +533,9 @@ class AndroidDebugSession extends DebugSession {
await this.configureAPISourcePath();
// launch the app
await this.startLaunchActivity(args.launchActivity);
await this.startLaunchActivity(args.launchActivity, args.postLaunchPause);
this.debuggerAttached = true;
// if we get this far, the debugger is connected and waiting for the resume command
// - set up some events...
@@ -352,20 +573,10 @@ 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
}
}
/**
* Check if the build is out of date (i.e a source file has been modified since the last build)
* @param {StaleBuildSetting} staleBuild
*/
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) {
@@ -378,29 +589,41 @@ class AndroidDebugSession extends DebugSession {
}
}
startLaunchActivity(launchActivity) {
/**
*
* @param {string} launchActivity
* @param {number} postLaunchPause
*/
async startLaunchActivity(launchActivity, postLaunchPause) {
if (!launchActivity) {
if (!(launchActivity = this.apk_file_info.manifest.launcher)) {
throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json');
// we're allowed no launchActivity if we have a custom am start command
if (!this.am_start_args) {
if (!(launchActivity = this.apk_file_info.manifest.launcher)) {
throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json');
}
}
}
const build = new BuildInfo(this.apk_file_info.manifest.package, new Map(this.src_packages.packages), launchActivity);
this.LOG(`Launching ${build.pkgname}/${launchActivity} on device ${this._device.serial} [API:${this.device_api_level||'?'}]`);
return this.dbgr.startDebugSession(build, this._device.serial);
const build = new LaunchBuildInfo(
new Map(this.src_packages.packages),
this.apk_file_info.manifest.package,
launchActivity,
this.am_start_args,
postLaunchPause);
this.LOG(`Launching on device ${this._device.serial} [API:${this.device_api_level||'?'}]`);
if (this.am_start_args) {
this.LOG(`Using custom launch arguments '${this.am_start_args.join(' ')}'`);
}
const am_stdout = await this.dbgr.startDebugSession(build, this._device.serial);
this.LOG(am_stdout);
}
async configureAPISourcePath() {
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() {
@@ -440,7 +663,8 @@ class AndroidDebugSession extends DebugSession {
})
// send the install command
this.LOG('Installing...');
const command = `pm install ${Array.isArray(this.pmInstallArgs) ? this.pmInstallArgs.join(' ') : '-r'} ${device_apk_fpn}`;
const pm_install_args = Array.isArray(this.pm_install_args) ? this.pm_install_args.join(' ') : '-r';
const command = `pm install ${pm_install_args} ${device_apk_fpn}`;
D(command);
const stdout = await this._device.adbclient.shell_cmd({
command,
@@ -462,11 +686,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';
@@ -495,7 +721,11 @@ class AndroidDebugSession extends DebugSession {
throw new Error(reject);
}
configurationDoneRequest(response/*, args*/) {
/**
*
* @param {import('vscode-debugprotocol').DebugProtocol.ConfigurationDoneResponse} response
*/
configurationDoneRequest(response) {
D('configurationDoneRequest');
this.waitForConfigurationDone();
this.sendResponse(response);
@@ -511,14 +741,24 @@ class AndroidDebugSession extends DebugSession {
}
}
async disconnectRequest(response/*, args*/) {
/**
*
* @param {import('vscode-debugprotocol').DebugProtocol.DisconnectResponse} response
*/
async disconnectRequest(response) {
D('disconnectRequest');
this._isDisconnecting = true;
try {
await this.dbgr.forceStop();
await this.dbgr.disconnect();
this.LOG(`Debugger stopped`);
} catch (e) {
if (this.debuggerAttached) {
try {
if (this.debug_mode === 'launch') {
await this.dbgr.forceStop();
this.LOG(`Debugger stopped`);
} else {
await this.dbgr.disconnect();
this.LOG(`Debugger detached`);
}
} catch (e) {
}
}
this.sendResponse(response);
}
@@ -536,7 +776,7 @@ class AndroidDebugSession extends DebugSession {
}
/**
*
* Called by the debugger in response to a JDWP breakpoint hit event
* @param {JavaBreakpointEvent} e
*/
onBreakpointHit(e) {
@@ -548,13 +788,15 @@ class AndroidDebugSession extends DebugSession {
/**
* Called when the user requests a change to breakpoints in a source file
* Note: all breakpoints in a file are always sent in args, even if they are not changing
* @param {import('vscode-debugprotocol').DebugProtocol.SetBreakpointsResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.SetBreakpointsArguments} args
*/
async setBreakPointsRequest(response/*: DebugProtocol.SetBreakpointsResponse*/, args/*: DebugProtocol.SetBreakpointsArguments*/) {
async setBreakPointsRequest(response, args) {
const source_filename = args.source && args.source.path;
D('setBreakPointsRequest: ' + source_filename);
const unverified_breakpoint = (src_bp,reason) => {
const bp = new Breakpoint(false,src_bp.line);
const bp = new Breakpoint(false, src_bp.line);
bp['id'] = ++this._breakpointId;
bp['message'] = reason;
return bp;
@@ -606,10 +848,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);
@@ -617,46 +859,48 @@ class AndroidDebugSession extends DebugSession {
}
/**
* @param {*} o
* @param {number} idx
* @param {*[]} javabp_arr
* @param {import('vscode-debugprotocol').DebugProtocol.SourceBreakpoint[]} 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;
}
const dbgline = this.convertClientLineToDebugger(src_bp.line);
const options = new BreakpointOptions();
if (src_bp.hitCondition) {
// the hit condition is an expression that requires evaluation
// until we get more comprehensive evaluation support, just allow integer literals
const m = src_bp.hitCondition.match(/^\s*(?:0x([0-9a-f]+)|0b([01]+)|0*(\d+([e]\+?\d+)?))\s*$/i);
if (m) {
const hitcount = m[3] ? parseFloat(m[3]) : m[2] ? parseInt(m[2],2) : parseInt(m[1],16);
if ((hitcount > 0) && (hitcount <= 0x7fffffff)) {
options.hitcount = hitcount;
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) {
// the hit condition is an expression that requires evaluation
// until we get more comprehensive evaluation support, just allow integer literals
const m = src_bp.hitCondition.match(/^\s*(?:0x([0-9a-f]+)|0b([01]+)|0*(\d+([e]\+?\d+)?))\s*$/i);
if (m) {
const hitcount = m[3] ? parseFloat(m[3]) : m[2] ? parseInt(m[2],2) : parseInt(m[1],16);
if ((hitcount > 0) && (hitcount <= 0x7fffffff)) {
options.hitcount = hitcount;
}
}
}
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/);
const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(dbgline));
// the breakpoint *must* have an id field or it won't update properly
bp['id'] = ++this._breakpointId;
if (javabp.state === 'notloaded')
bp['message'] = 'The runtime hasn\'t loaded this code location';
javabp.vsbp = bp;
}
javabp.vsbp.order = idx;
java_breakpoints.push(javabp);
}
const javabp = await this.dbgr.setBreakpoint(o.relative_fpn, dbgline, options);
if (!javabp.vsbp) {
// state is one of: set,notloaded,enabled,removed
const verified = !!javabp.state.match(/set|enabled/);
const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(dbgline));
// the breakpoint *must* have an id field or it won't update properly
bp['id'] = ++this._breakpointId;
if (javabp.state === 'notloaded')
bp['message'] = 'The runtime hasn\'t loaded this code location';
javabp.vsbp = bp;
}
javabp.vsbp.order = idx;
javabp_arr.push(javabp);
return this._setup_breakpoints(o, ++idx, javabp_arr);
return java_breakpoints;
};
async setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.SetExceptionBreakpointsResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.SetExceptionBreakpointsArguments} args
*/
async setExceptionBreakPointsRequest(response, args) {
await this.dbgr.clearBreakOnExceptions();
switch(true) {
case args.filters.includes('all'):
@@ -669,7 +913,11 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response);
}
async threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
/**
*
* @param {import('vscode-debugprotocol').DebugProtocol.ThreadsResponse} response
*/
async threadsRequest(response) {
if (!this._threads.length) {
try {
await this.refreshThreads();
@@ -693,8 +941,10 @@ class AndroidDebugSession extends DebugSession {
/**
* Returns a stack trace for the given threadId
* @param {import('vscode-debugprotocol').DebugProtocol.StackTraceResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.StackTraceArguments} args
*/
async stackTraceRequest(response/*: DebugProtocol.StackTraceResponse*/, args/*: DebugProtocol.StackTraceArguments*/) {
async stackTraceRequest(response, args) {
D(`stackTraceRequest thread:${args.threadId}`);
// only retrieve the stack if the thread is paused
const thread = this.getThread(args.threadId);
@@ -776,7 +1026,11 @@ class AndroidDebugSession extends DebugSession {
}
}
async scopesRequest(response/*: DebugProtocol.ScopesResponse*/, args/*: DebugProtocol.ScopesArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.ScopesResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.ScopesArguments} args
*/
async scopesRequest(response, args) {
D(`scopesRequest frame:${args.frameId}`);
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
const thread = this.getThread(threadId);
@@ -802,10 +1056,14 @@ class AndroidDebugSession extends DebugSession {
} catch(e) {
}
this.sendResponse(response);
}
}
sourceRequest(response/*: DebugProtocol.SourceResponse*/, args/*: DebugProtocol.SourceArguments*/) {
D(`sourceRequest: ${args.sourceId}`);
/**
* @param {import('vscode-debugprotocol').DebugProtocol.SourceResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.SourceArguments} args
*/
sourceRequest(response, args) {
D(`sourceRequest: ${args.sourceReference}`);
const content =
`/*
The source for this class is unavailable.
@@ -818,24 +1076,17 @@ 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);
}
/**
*
* @param {*} response
* @param {{variablesReference:VSCVariableReference}} args
* @param {import('vscode-debugprotocol').DebugProtocol.VariablesResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.VariablesArguments} args
*/
async variablesRequest(response/*: DebugProtocol.VariablesResponse*/, args/*: DebugProtocol.VariablesArguments*/) {
async variablesRequest(response, args) {
D(`variablesRequest variablesReference:${args.variablesReference}`);
const threadId = AndroidThread.variableRefToThreadId(args.variablesReference);
const thread = this.getThread(threadId);
@@ -919,7 +1170,11 @@ class AndroidDebugSession extends DebugSession {
await this.dbgr.resumeThread(thread.threadid);
}
continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.ContinueResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.ContinueArguments} args
*/
continueRequest(response, args) {
D(`Continue thread:${args.threadId}`);
const thread = this.getThread(args.threadId);
@@ -942,8 +1197,8 @@ class AndroidDebugSession extends DebugSession {
/**
* Called by the user to start a step operation
* @param {DebuggerStepType} which
* @param {*} response
* @param {*} args
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.NextArguments} args
*/
doStep(which, response, args) {
D(`step ${which}`);
@@ -966,15 +1221,27 @@ class AndroidDebugSession extends DebugSession {
this.dbgr.step(which, thread.threadid);
}
stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.StepInArguments} args
*/
stepInRequest(response, args) {
this.doStep('in', response, args);
}
nextRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.NextArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.NextArguments} args
*/
nextRequest(response, args) {
this.doStep('over', response, args);
}
stepOutRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepOutArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.StepOutArguments} args
*/
stepOutRequest(response, args) {
this.doStep('out', response, args);
}
@@ -992,7 +1259,11 @@ class AndroidDebugSession extends DebugSession {
this.reportStoppedEvent("exception", e.throwlocation, last_exception);
}
async exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.ExceptionInfoResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.ExceptionInfoArguments} args
*/
async exceptionInfoRequest(response, args) {
D(`exceptionInfoRequest: ${args.threadId}`);
const thread = this.getThread(args.threadId);
if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response);
@@ -1062,15 +1333,10 @@ class AndroidDebugSession extends DebugSession {
}
/**
* @typedef SetVariableArgs
* @property {string} name
* @property {string} value
* @property {number} variablesReference
*
* @param {*} response
* @param {SetVariableArgs} args
* @param {import('vscode-debugprotocol').DebugProtocol.SetVariableResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.SetVariableArguments} args
*/
async setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) {
async setVariableRequest(response, args) {
const threadId = AndroidThread.variableRefToThreadId(args.variablesReference);
const thread = this.getThread(threadId);
@@ -1082,7 +1348,7 @@ class AndroidDebugSession extends DebugSession {
const stack_frame = thread.findStackFrame(args.variablesReference);
// evaluate the expression
const locals = await stack_frame.getLocals();
const value = await evaluate(args.value, thread, locals, this.dbgr);
const { value } = await evaluate(args.value, thread, locals, this.dbgr);
// update the variable
const vsvar = await stack_frame.setVariableValue(args.variablesReference, args.name, value);
response.body = {
@@ -1099,8 +1365,10 @@ class AndroidDebugSession extends DebugSession {
/**
* Called by VSCode to perform watch, console and hover evaluations
* @param {import('vscode-debugprotocol').DebugProtocol.EvaluateResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.EvaluateArguments} args
*/
async evaluateRequest(response/*: DebugProtocol.EvaluateResponse*/, args/*: DebugProtocol.EvaluateArguments*/) {
async evaluateRequest(response, args) {
// Some notes to remember:
// annoyingly, during stepping, the step can complete before the resume has called evaluateRequest on watches.
@@ -1120,25 +1388,15 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(prev.response);
}
const eval_info = {
expression: args.expression,
response,
/** @type {DebuggerValue[]} */
locals: null,
/** @type {VariableManager} */
var_manager: null,
/** @type {AndroidThread} */
thread: null,
}
let eval_info;
if (args.frameId) {
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
const thread = this.getThread(threadId);
if (!thread) return this.failRequestNoThread('Evaluate',threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Evaluate',threadId, response);
eval_info.thread = thread;
const stack_frame = thread.findStackFrame(args.frameId);
eval_info.var_manager = stack_frame;
eval_info.locals = await stack_frame.getLocals();
const locals = await stack_frame.getLocals();
eval_info = new EvalQueueEntry(args.expression, response, locals, stack_frame, thread);
} else {
// if there's no frameId, we are being asked to evaluate the value in the 'global' context.
// This is a problem because there's no associated stack frame, so we include any locals in the evaluation.
@@ -1147,9 +1405,7 @@ class AndroidDebugSession extends DebugSession {
// would require primitive literals)
const thread = this._threads.find(t => t && t.paused);
if (!thread) return this.failRequest(`No threads are paused`, response);
eval_info.thread = thread;
eval_info.var_manager = thread.getGlobalVariableManager();
eval_info.locals = [];
eval_info = new EvalQueueEntry(args.expression, response, [], thread.getGlobalVariableManager(), thread);
}
const queue_len = this._evals_queue.push(eval_info);
@@ -1160,8 +1416,8 @@ class AndroidDebugSession extends DebugSession {
while (this._evals_queue.length > 0) {
const { expression, response, locals, var_manager, thread } = this._evals_queue[0];
try {
const value = await evaluate(expression, thread, locals, this.dbgr);
const v = var_manager.makeVariableValue(value);
const { value, display_format } = await evaluate(expression, thread, locals, this.dbgr, { allowFormatSpecifier:true });
const v = var_manager.makeVariableValue(value, display_format);
response.body = {
result: v.value,
variablesReference: v.variablesReference|0
@@ -1176,6 +1432,23 @@ class AndroidDebugSession extends DebugSession {
}
}
class EvalQueueEntry {
/**
* @param {string} expression
* @param {import('vscode-debugprotocol').DebugProtocol.EvaluateResponse} response
* @param {DebuggerValue[]} locals
* @param {VariableManager} var_manager
* @param {AndroidThread} thread
*/
constructor(expression, response, locals, var_manager, thread) {
this.expression = expression;
this.response = response;
this.locals = locals;
this.var_manager = var_manager;
this.thread = thread;
}
}
/**
* @param {string} p
*/

View File

@@ -4,18 +4,28 @@ const { PackageInfo } = require('./package-searcher');
const { splitSourcePath } = require('./utils/source-file');
class BuildInfo {
/**
* @param {string} pkgname
* @param {Map<string,PackageInfo>} packages
* @param {string} launchActivity
*/
constructor(pkgname, packages, launchActivity) {
this.pkgname = pkgname;
constructor(packages) {
this.packages = packages;
}
}
class LaunchBuildInfo extends BuildInfo {
/**
* @param {Map<string,PackageInfo>} packages
* @param {string} pkgname
* @param {string} launchActivity
* @param {string[]} amCommandArgs custom arguments passed to `am start`
* @param {number} postLaunchPause amount of time (in ms) to wait after launch before we attempt a debugger connection
*/
constructor(packages, pkgname, launchActivity, amCommandArgs, postLaunchPause) {
super(packages);
this.pkgname = pkgname;
this.launchActivity = launchActivity;
/** the arguments passed to `am start` */
this.startCommandArgs = [
this.startCommandArgs = amCommandArgs || [
'-D', // enable debugging
'--activity-brought-to-front',
'-a android.intent.action.MAIN',
@@ -23,10 +33,19 @@ class BuildInfo {
`-n ${pkgname}/${launchActivity}`,
];
/**
* the amount of time to wait after 'am start ...' is invoked.
* the amount of time (in millis) to wait after 'am start ...' is invoked.
* We need this because invoking JDWP too soon causes a hang.
*/
this.postLaunchPause = 1000;
this.postLaunchPause = ((typeof postLaunchPause === 'number') && (postLaunchPause >= 0)) ? postLaunchPause : 1000;
}
}
class AttachBuildInfo extends BuildInfo {
/**
* @param {Map<string,PackageInfo>} packages
*/
constructor(packages) {
super(packages);
}
}
@@ -81,10 +100,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
@@ -669,7 +688,7 @@ class DebuggerTypeInfo {
// otherwise, leave super undefined to be updated later
if (info.reftype.string !== 'class' || type.signature[0] !== 'L' || type.signature === JavaType.Object.signature) {
if (info.reftype.string !== 'array') {
/** @type {JavaType} */
/** @type {JavaClassType} */
this.super = null;
}
}
@@ -746,9 +765,9 @@ class VariableValue {
}
module.exports = {
AttachBuildInfo,
BreakpointLocation,
BreakpointOptions,
BuildInfo,
DebuggerBreakpoint,
DebuggerException,
DebuggerFrameInfo,
@@ -756,6 +775,7 @@ module.exports = {
DebuggerTypeInfo,
DebugSession,
DebuggerValue,
LaunchBuildInfo,
LiteralValue,
JavaBreakpointEvent,
JavaExceptionEvent,

View File

@@ -8,9 +8,9 @@ const { D } = require('./utils/print');
const { sleep } = require('./utils/thread');
const { decodeJavaStringLiteral } = require('./utils/char-decode');
const {
AttachBuildInfo,
BreakpointLocation,
BreakpointOptions,
BuildInfo,
DebuggerBreakpoint,
DebuggerFrameInfo,
DebuggerMethodInfo,
@@ -24,12 +24,13 @@ const {
JavaTaggedValue,
JavaThreadInfo,
JavaType,
LaunchBuildInfo,
MethodInvokeArgs,
SourceLocation,
TypeNotAvailable,
} = require('./debugger-types');
class Debugger extends EventEmitter {
class Debugger extends EventEmitter {
constructor () {
super();
@@ -71,30 +72,68 @@ class Debugger extends EventEmitter {
};
/**
* @param {BuildInfo} build
* @param {LaunchBuildInfo} build
* @param {string} deviceid
*/
async startDebugSession(build, deviceid) {
if (this.status() !== 'disconnected') {
throw new Error('startDebugSession: session already active');
}
this.session = new DebugSession(build, deviceid);
await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause);
const stdout = await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause);
// retrieve the list of debuggable processes
const pids = await this.getDebuggablePIDs(this.session.deviceid);
// choose the last pid in the list
const pid = pids[pids.length - 1];
const named_pids = await Debugger.getDebuggableProcesses(deviceid, 10e3);
if (named_pids.length === 0) {
throw new Error(`startDebugSession: No debuggable processes after app launch.`);
}
// we assume the newly launched app is the last pid in the list, but try and
// validate using the process names
const matched_named_pids = build.pkgname ? named_pids.filter(np => np.name === build.pkgname) : [];
let pid;
switch (matched_named_pids.length) {
case 0:
// no name match - warn, but choose the last entry anyway
D('No process name match - choosing last jdwp pid');
pid = named_pids[named_pids.length - 1].pid;
break;
case 1:
pid = matched_named_pids[0].pid;
break;
default:
// more than one choice - warn, but choose we'll use the last one anyway
D('Multiple process names match - choosing last matching entry');
pid = matched_named_pids[matched_named_pids.length - 1].pid;
break;
}
// after connect(), the caller must call resume() to begin
await this.connect(pid);
return stdout;
}
/**
* @param {AttachBuildInfo} 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'
* @param {number} [post_launch_pause] amount to time to wait after each launch attempt
* @param {number} post_launch_pause amount of time (in ms) to wait after each launch attempt
*/
static async runApp(deviceid, launch_cmd_args, post_launch_pause = 1000) {
static async runApp(deviceid, launch_cmd_args, post_launch_pause) {
// older (<3) versions of Android only allow target components to be specified with -n
const shell_cmd = {
command: 'am start ' + launch_cmd_args.join(' '),
command: `am start ${launch_cmd_args.join(' ')}`,
};
let retries = 10
for (;;) {
@@ -104,12 +143,14 @@ class Debugger extends EventEmitter {
await sleep(post_launch_pause);
// failures:
// Error: Activity not started...
const m = stdout.match(/Error:.*/g);
// /system/bin/sh: syntax error: unexpected EOF - this happens with invalid am command arguments
const m = stdout.match(/Error:.*|syntax error:/gi);
if (!m) {
break;
// return the stdout from am (it shows the fully qualified component name)
return stdout.toString().trim();
}
else if (retries <= 0){
throw new Error(m[0]);
throw new Error(stdout.toString().trim());
}
retries -= 1;
}
@@ -124,54 +165,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);
}
/**
@@ -224,6 +232,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,
@@ -233,13 +242,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...
@@ -255,6 +272,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));
}
/**
@@ -296,7 +319,9 @@ class Debugger extends EventEmitter {
if (!this.session) {
return;
}
return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname);
if (this.session.build instanceof LaunchBuildInfo) {
return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname);
}
}
/**
@@ -334,17 +359,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
@@ -352,8 +380,7 @@ class Debugger extends EventEmitter {
Debugger.portManager.freeport(connection.localport);
}
// clear the session
this.session = null;
this.emit('disconnect');
return previous_state;
}
@@ -536,17 +563,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
@@ -680,13 +702,13 @@ class Debugger extends EventEmitter {
}
/**
* @param {DebuggerValue} value
* @param {string} signature
*/
async getSuperType(value) {
if (value.type.signature === JavaType.Object.signature)
async getSuperType(signature) {
if (signature === JavaType.Object.signature)
throw new Error('java.lang.Object has no super type');
const typeinfo = await this.getTypeInfo(value.type.signature);
const typeinfo = await this.getTypeInfo(signature);
await this._ensureSuperType(typeinfo);
return typeinfo.super;
}
@@ -695,7 +717,7 @@ class Debugger extends EventEmitter {
* @param {DebuggerValue} value
*/
async getSuperInstance(value) {
const supertype = await this.getSuperType(value);
const supertype = await this.getSuperType(value.type.signature);
if (value.vtype === 'class') {
return this.getTypeValue(supertype.signature);
}
@@ -741,15 +763,23 @@ class Debugger extends EventEmitter {
}
/**
*
* @param {DebuggerValue} object_value
*/
async getFieldValues(object_value) {
const type = await this.getTypeInfo(object_value.type.signature);
await this._ensureFields(type);
return this.fetchFieldValues(object_value, type.info.typeid, type.fields);
}
/**
* @param {DebuggerValue} object_value
* @param {JavaTypeID} typeid
* @param {JavaField[]} field_list
*/
async fetchFieldValues(object_value, typeid, field_list) {
// the Android runtime now pointlessly barfs into logcat if an instance value is used
// to retrieve a static field. So, we now split into two calls...
const splitfields = type.fields.reduce((z, f) => {
const splitfields = field_list.reduce((z, f) => {
if (f.modbits & 8) {
z.static.push(f);
} else {
@@ -774,7 +804,7 @@ class Debugger extends EventEmitter {
let static_fieldvalues = [];
if (splitfields.static.length) {
static_fieldvalues = await this.session.adbclient.jdwp_command({
cmd: JDWP.Commands.GetStaticFieldValues(type.info.typeid, splitfields.static),
cmd: JDWP.Commands.GetStaticFieldValues(typeid, splitfields.static),
});
}
// make sure the fields and values match up...
@@ -784,7 +814,8 @@ class Debugger extends EventEmitter {
res.forEach((value,i) => {
value.data.field = fields[i];
value.fqname = `${object_value.fqname || object_value.name}.${value.name}`;
})
});
return res;
}
@@ -797,21 +828,24 @@ class Debugger extends EventEmitter {
if (!(object_value.type instanceof JavaClassType)) {
return null;
}
let instance = object_value;
// retrieving field values is expensive, so we search through the class
// fields (which will be cached) until we find a match
let field, object_type = object_value.type, typeinfo;
for (;;) {
// retrieve all the fields for this instance
const fields = await this.getFieldValues(instance);
const field = fields.find(f => f.name === fieldname);
typeinfo = await this.getTypeInfo(object_type.signature);
const fields = await this._ensureFields(typeinfo);
field = fields.find(f => f.name === fieldname);
if (field) {
return field;
break;
}
// if there's no matching field in this instance, check the super
if (!includeInherited || instance.type.signature === JavaType.Object.signature) {
if (!includeInherited || object_type.signature === JavaType.Object.signature) {
const fully_qualified_typename = `${object_value.type.package}.${object_value.type.typename}`;
throw new Error(`No such field '${fieldname}' in type ${fully_qualified_typename}`);
}
instance = await this.getSuperInstance(instance);
object_type = await this.getSuperType(object_type.signature);
}
const values = await this.fetchFieldValues(object_value, typeinfo.info.typeid, [field]);
return values[0];
}
/**
@@ -1489,10 +1523,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

View File

@@ -471,7 +471,7 @@ async function evaluate_binary_expression(dbgr, locals, thread, lhs, rhs, operat
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} operator
* @param {*} expr
* @param {ParsedExpression} expr
*/
async function evaluate_unary_expression(dbgr, locals, thread, operator, expr) {
/** @type {DebuggerValue} */
@@ -511,8 +511,20 @@ async function evaluate_identifier(dbgr, locals, identifier) {
if (local) {
return local;
}
// check if the identifier is an unqualified member of the current 'this' context
const this_context = locals.find(l => l.name === 'this');
if (this_context) {
try {
const member = await evaluate_member(dbgr, new MemberExpression(identifier), this_context);
return member;
} catch {
// not a member of this - just continue
}
}
// if it's not a local, it could be the start of a package name or a type
const classes = await dbgr.getAllClasses();
const classes = Array.from(dbgr.session.loadedClasses);
return evaluate_qualified_type_name(dbgr, identifier, classes);
}
@@ -520,17 +532,17 @@ async function evaluate_identifier(dbgr, locals, identifier) {
*
* @param {Debugger} dbgr
* @param {string} dotted_name
* @param {*[]} classes
* @param {string[]} classes
*/
async function evaluate_qualified_type_name(dbgr, dotted_name, classes) {
const exact_class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace(/\./g,'[$/]')};$`);
const exact_class = classes.find(c => exact_class_matcher.test(c.type.signature));
const exact_class = classes.find(signature => exact_class_matcher.test(signature));
if (exact_class) {
return dbgr.getTypeValue(exact_class.type.signature);
return dbgr.getTypeValue(exact_class);
}
const class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace('.','[$/]')}/`);
const matching_classes = classes.filter(c => class_matcher.test(c.type.signature));
const matching_classes = classes.filter(signature => class_matcher.test(signature));
if (matching_classes.length === 0) {
// the dotted name doesn't match any packages
throw new Error(`'${dotted_name}' is not a package, type or variable name`);
@@ -623,7 +635,7 @@ async function evaluate_qualifiers(dbgr, locals, thread, value, qualified_terms)
i++;
continue;
}
value = await evaluate_member(dbgr, locals, thread, term, value);
value = await evaluate_member(dbgr, term, value);
continue;
}
if (term instanceof ArrayIndexExpression) {
@@ -684,7 +696,7 @@ function evaluate_expression(dbgr, locals, thread, expr) {
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} index_expr
* @param {ParsedExpression} index_expr
* @param {DebuggerValue} arr_local
*/
async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_local) {
@@ -712,36 +724,36 @@ async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_loca
/**
* Build a regular expression which matches the possible parameter types for a value
* @param {Debugger} dbgr
* @param {DebuggerValue} v
* @param {DebuggerValue} argument
*/
async function getParameterSignatureRegex(dbgr, v) {
if (v.type.signature == 'Lnull;') {
async function getParameterSignatureRegex(dbgr, argument) {
if (argument.type.signature == 'Lnull;') {
return /^[LT[]/; // null matches any reference type
}
if (/^L/.test(v.type.signature)) {
if (/^L/.test(argument.type.signature)) {
// for class reference types, retrieve a list of inherited classes
// since subclass instances can be passed as arguments
const sigs = await dbgr.getClassInheritanceList(v.type.signature);
const sigs = await dbgr.getClassInheritanceList(argument.type.signature);
const re_sigs = sigs.map(signature => signature.replace(/[$]/g, '\\$'));
return new RegExp(`(^${re_sigs.join('$)|(^')}$)`);
}
if (/^\[/.test(v.type.signature)) {
if (/^\[/.test(argument.type.signature)) {
// for array types, only an exact array match or Object is allowed
return new RegExp(`^(${v.type.signature})|(${JavaType.Object.signature})$`);
return new RegExp(`^(${argument.type.signature})|(${JavaType.Object.signature})$`);
}
switch(v.type.signature) {
switch(argument.type.signature) {
case 'I':
// match bytes/shorts/ints/longs/floats/doubles literals within range
if (v.value >= -128 && v.value <= 127)
if (argument.value >= -128 && argument.value <= 127)
return /^[BSIJFD]$/
if (v.value >= -32768 && v.value <= 32767)
if (argument.value >= -32768 && argument.value <= 32767)
return /^[SIJFD]$/
return /^[IJFD]$/;
case 'F':
return /^[FD]$/; // floats can be assigned to floats or doubles
default:
// anything else must be an exact match (no implicit cast is valid)
return new RegExp(`^${v.type.signature}$`);
return new RegExp(`^${argument.type.signature}$`);
}
}
@@ -822,12 +834,10 @@ async function evaluate_methodcall(dbgr, locals, thread, method_name, m, obj_loc
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {MemberExpression} member
* @param {DebuggerValue} value
*/
async function evaluate_member(dbgr, locals, thread, member, value) {
async function evaluate_member(dbgr, member, value) {
if (!JavaType.isReference(value.type)) {
throw new Error('TypeError: value is not a reference type');
}
@@ -950,8 +960,9 @@ async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) {
* @param {AndroidThread} thread
* @param {DebuggerValue[]} locals
* @param {Debugger} dbgr
* @param {{allowFormatSpecifier:boolean}} [options]
*/
async function evaluate(expression, thread, locals, dbgr) {
async function evaluate_one_expression(expression, thread, locals, dbgr, options) {
D('evaluate: ' + expression);
await dbgr.ensureConnected();
@@ -967,6 +978,17 @@ async function evaluate(expression, thread, locals, dbgr) {
}
const parsed_expression = parse_expression(e);
let display_format = null;
if (options && options.allowFormatSpecifier) {
// look for formatting specifiers in the form of ',<x>'
// ref: https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp
const df_match = e.expr.match(/^,([doc!]|[xX]b?|bb?|sb?)/);
if (df_match) {
display_format = df_match[1];
e.expr = e.expr.slice(df_match[0].length)
}
}
// if there's anything left, it's an error
if (!parsed_expression || e.expr) {
// the expression is not well-formed
@@ -975,9 +997,52 @@ async function evaluate(expression, thread, locals, dbgr) {
// the expression is well-formed - start the (asynchronous) evaluation
const value = await evaluate_expression(dbgr, locals, thread, parsed_expression);
return value;
return {
value,
display_format,
}
}
/**
*
*/
const queuedExpressions = [];
/**
* @param {string} expression
* @param {AndroidThread} thread
* @param {DebuggerValue[]} locals
* @param {Debugger} dbgr
* @param {{allowFormatSpecifier:boolean}} [options]
*/
async function evaluate(expression, thread, locals, dbgr, options) {
return new Promise(async (resolve, reject) => {
const queue_length = queuedExpressions.push({
expression, thread, locals, dbgr, options,
resolve, reject
});
if (queue_length > 1) {
return;
}
// run the queue
while (queuedExpressions.length) {
const {
expression, thread, locals, dbgr, options,
resolve, reject
} = queuedExpressions[0];
try {
const res = await evaluate_one_expression(expression, thread, locals, dbgr, options);
resolve(res);
} catch (err) {
reject(err);
}
queuedExpressions.shift();
}
});
}
module.exports = {
evaluate,
}

View File

@@ -88,10 +88,16 @@ class UnaryOpExpression extends ParsedExpression {
}
class TernaryExpression extends ParsedExpression {
/**
* @param {ParsedExpression} condition
*/
constructor(condition) {
super();
this.condition = condition;
/** @type {ParsedExpression} */
this.ternary_true = null;
/** @type {ParsedExpression} */
this.ternary_false = null;
}
}
@@ -101,17 +107,24 @@ class QualifierExpression extends ParsedExpression {
}
class ArrayIndexExpression extends QualifierExpression {
constructor(e) {
/**
* @param {ParsedExpression} index_expression
*/
constructor(index_expression) {
super();
this.indexExpression = e;
this.indexExpression = index_expression;
}
}
class MethodCallExpression extends QualifierExpression {
/** @type {ParsedExpression[]} */
arguments = [];
}
class MemberExpression extends QualifierExpression {
/**
* @param {string} name
*/
constructor(name) {
super();
this.name = name;

View File

@@ -108,6 +108,7 @@
* @typedef {number} JDWPRequestID
* @typedef {JDWPRequestID} StepID
* @typedef {'caught'|'uncaught'|'both'} ExceptionBreakMode
* @typedef {'ignore'|'warn'|'stop'} StaleBuildSetting
*
*/

View File

@@ -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;
}

View File

@@ -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;
/**
* @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-${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, // we use embedded scripts to relay logcat info over a websocket
}
);
const logcat = new LogcatContent(target_device.serial);
logcat.content().then(html => {
panel.webview.html = html;
});
}
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();
/**
* @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);
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
await checkADBStarted(autoStartADB);
let target_device = await selectTargetDevice(vscode, "Logcat display");
if (!target_device) {
return;
}
})
.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
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);
}
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) {
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.
{
enableScripts: true,
}
);
const logcat = new LogcatContent(device.serial);
logcat.content().then(html => {
panel.webview.html = html;
});
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?');
});
} catch (e) {
vscode.window.showInformationMessage(`Logcat cannot be displayed. ${e.message}`);
}
}
module.exports = {

77
src/process-attach.js Normal file
View File

@@ -0,0 +1,77 @@
const os = require('os');
const { ADBClient } = require('./adbclient');
/**
* @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
* @param {string} device_serial
*/
async function selectAndroidProcessID(vscode, device_serial) {
const res = {
/** @type {string|'ok'|'cancelled'|'failed'} */
status: 'failed',
pid: 0,
serial: '',
}
let named_pids;
try {
named_pids = await new ADBClient(device_serial).named_jdwp_list(5000);
} catch {
vscode.window.showWarningMessage(`Attach failed. Check the device ${device_serial} is connected.`);
return res;
}
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,
}

View File

@@ -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');
}
/**

View File

@@ -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);
});

View File

@@ -53,7 +53,7 @@ class DebuggerStackFrame extends VariableManager {
}
const fetch_locals = async () => {
const values = await this.dbgr.getLocals(this.frame);
// display the variables in (case-insensitive) alphabetical order, with 'this' first and all-caps last
// display the variables in (case-insensitive) alphabetical order, with 'this' first
return this.locals = sortVariables(values, true, false);
}
// @ts-ignore
@@ -133,11 +133,11 @@ class DebuggerStackFrame extends VariableManager {
values = [await this.getBigString(varinfo)];
}
return (varinfo.cached = values).map(v => this.makeVariableValue(v));
return (varinfo.cached = values).map(v => this.makeVariableValue(v, varinfo.display_format));
}
async getObjectFields(varinfo) {
const supertype = await this.dbgr.getSuperType(varinfo.objvar);
const supertype = await this.dbgr.getSuperType(varinfo.objvar.type.signature);
const fields = await this.dbgr.getFieldValues(varinfo.objvar);
// add an extra msg field for exceptions
if (varinfo.exception) {

87
src/utils/android.js Normal file
View 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,
}

62
src/utils/device.js Normal file
View File

@@ -0,0 +1,62 @@
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 {'Launch'|'Attach'|'Logcat display'} action
* @param {{alwaysShow:boolean}} [options]
*/
async function selectTargetDevice(vscode, action, options) {
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:
if (!options || !options.alwaysShow) {
return devices[0]; // only one device - just use it
}
break;
}
device = await showDevicePicker(vscode, devices);
if (!device) {
return null; // user cancelled
}
// 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,
}

View File

@@ -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
@@ -45,7 +62,9 @@ function onMessagePrint(cb) {
module.exports = {
D,
E,
E,
initLogToClient,
LOG,
W,
onMessagePrint,
}

View File

@@ -12,8 +12,6 @@ class VariableManager {
* @param {VSCVariableReference} base_variable_reference The reference value for values stored by this manager
*/
constructor(base_variable_reference) {
// expandable variables get allocated new variable references.
this._expandable_prims = false;
/** @type {VSCVariableReference} */
this.nextVariableRef = base_variable_reference + 10;
@@ -40,10 +38,20 @@ class VariableManager {
this.variableValues.set(variablesReference, value);
}
_getObjectIdReference(type, objvalue) {
/**
* Retrieve or create a variable reference for a given object instance
* @param {JavaType} type
* @param {JavaObjectID} instance_id
* @param {string} display_format
*/
_getObjectIdReference(type, instance_id, display_format) {
// we need the type signature because we must have different id's for
// an instance and it's supertype instance (which obviously have the same objvalue)
const key = type.signature + objvalue;
// an instance and it's supertype instance (which obviously have the same instance_id)
//
// display_format is also included to give unique variable references for each display type.
// This is because VSCode caches expanded values, so once evaluated in one format, they can
// never be changed.
const key = `${type.signature}:${instance_id}:${display_format || ''}`;
let value = this.objIdCache.get(key);
if (!value) {
this.objIdCache.set(key, value = this.nextVariableRef += 1);
@@ -54,12 +62,12 @@ class VariableManager {
/**
* Convert to a VariableValue object used by VSCode
* @param {DebuggerValue} v
* @param {string} [display_format]
*/
makeVariableValue(v) {
makeVariableValue(v, display_format) {
let varref = 0;
let value = '';
const evaluateName = v.fqname || v.name;
const formats = {};
const full_typename = v.type.fullyQualifiedName();
switch(true) {
case v.hasnullvalue && JavaType.isReference(v.type):
@@ -74,93 +82,161 @@ class VariableManager {
value = v.type.typename;
break;
case v.type.signature === JavaType.String.signature:
value = JSON.stringify(v.string);
if (v.biglen) {
// since this is a big string - make it viewable on expand
varref = this._addVariable({
bigstring: v,
});
value = `String (length:${v.biglen})`;
}
else if (this._expandable_prims) {
// as a courtesy, allow strings to be expanded to see their length
varref = this._addVariable({
signature: v.type.signature,
primitive: true,
value: v.string.length
});
} else {
value = formatString(v.string, display_format);
}
break;
case JavaType.isArray(v.type):
// non-null array type - if it's not zero-length add another variable reference so the user can expand
if (v.arraylen) {
varref = this._getObjectIdReference(v.type, v.value);
varref = this._getObjectIdReference(v.type, v.value, display_format);
this._setVariable(varref, {
varref,
arrvar: v,
range:[0, v.arraylen],
display_format,
});
}
value = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
break;
case JavaType.isClass(v.type):
// non-null object instance - add another variable reference so the user can expand
varref = this._getObjectIdReference(v.type, v.value);
varref = this._getObjectIdReference(v.type, v.value, display_format);
this._setVariable(varref, {
varref,
objvar: v,
display_format,
});
value = v.type.typename;
break;
case v.type.signature === JavaType.char.signature:
// character types have a integer value
const char = String.fromCodePoint(v.value);
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\','\0':'0'};
if (cmap[char]) {
value = `'\\${cmap[char]}'`;
} else if (v.value < 32) {
value = `'\\u${v.value.toString(16).padStart(4,'0')}'`;
} else value = `'${char}'`;
value = formatChar(v.value, display_format);
break;
case v.type.signature === JavaType.long.signature:
// because JS cannot handle 64bit ints, we need a bit of extra work
const v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
value = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
formats.oct = formats.bin = '';
// 24 bit chunks...
for (let s = v64hex; s; s = s.slice(0,-6)) {
const uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
formats.oct = uint.toString(8) + formats.oct;
formats.bin = uint.toString(2) + formats.bin;
}
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
value = formatLong(v64hex, display_format);
break;
case JavaType.isInteger(v.type):
value = formats.dec = v.value.toString();
const uint = (v.value >>> 0);
formats.hex = '0x' + uint.toString(16);
formats.oct = '0c' + uint.toString(8);
formats.bin = '0b' + uint.toString(2);
value = formatInteger(v.value, v.type.signature, display_format);
break;
default:
// other primitives: boolean, etc
value = v.value.toString();
break;
}
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
varref = this._addVariable({
signature: v.type.signature,
primitive: true,
value: v.value,
});
}
return new VariableValue(v.name, value, full_typename, varref, evaluateName);
}
}
const cmap = {
'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t',
'\v':'v','\'':'\'','\\':'\\','\0':'0'
};
function makeJavaChar(i) {
let value;
const char = String.fromCodePoint(i);
if (cmap[char]) {
value = `'\\${cmap[char]}'`;
} else if (i < 32) {
value = `'\\u${i.toString(16).padStart(4,'0')}'`;
} else value = `'${char}'`;
return value;
}
/**
* @param {number} c
* @param {string} df
*/
function formatChar(c, df) {
if (/[xX]b|o|bb|d/.test(df)) {
return formatInteger(c, 'C', df);
}
return makeJavaChar(c);
}
/**
*
* @param {string} s
* @param {string} display_format
*/
function formatString(s, display_format) {
if (display_format === '!') {
return s;
}
let value = JSON.stringify(s);
if (display_format === 'sb') {
// remove quotes
value = value.slice(1,-1);
}
return value;
}
/**
* @param {hex64} hex64
* @param {string} df
*/
function formatLong(hex64, df) {
let minlength;
if (/[xX]b?/.test(df)) {
minlength = Math.ceil(64 / 4);
let s = `${df[1]?'':'0x'}${hex64.padStart(minlength,'0')}`;
return df[0] === 'x' ? s.toLowerCase() : s.toUpperCase();
}
if (/o/.test(df)) {
minlength = Math.ceil(64 / 3);
return `${df[1]?'':'0'}${NumberBaseConverter.convertBase(hex64,16,8).padStart(minlength, '0')}`;
}
if (/bb?/.test(df)) {
minlength = 64;
return `${df[1]?'':'0b'}${NumberBaseConverter.convertBase(hex64,16,2).padStart(minlength, '0')}`;
}
if (/c/.test(df)) {
return makeJavaChar(parseInt(hex64.slice(-4), 16));
}
return NumberBaseConverter.convertBase(hex64, 16, 10);
}
/**
* @param {number} i
* @param {string} signature
* @param {string} df
*/
function formatInteger(i, signature, df) {
const bits = { B:8,S:16,I:32,C:16 }[signature];
let u = (i & (-1 >>> (32 - bits))) >>> 0;
let minlength;
if (/[xX]b?/.test(df)) {
minlength = Math.ceil(bits / 4);
let s = u.toString(16).padStart(minlength,'0');
s = df[0] === 'x' ? s.toLowerCase() : s.toUpperCase();
return `${df[1]?'':'0x'}${s}`;
}
if (/o/.test(df)) {
minlength = Math.ceil(bits / 3);
return `${df[1]?'':'0'}${u.toString(8).padStart(minlength, '0')}`;
}
if (/bb?/.test(df)) {
minlength = bits;
return `${df[1]?'':'0b'}${u.toString(2).padStart(minlength, '0')}`;
}
if (/c/.test(df)) {
minlength = bits;
return makeJavaChar(u & 0xffff);
}
return i.toString();
}
module.exports = {
VariableManager,
}