mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 09:59:25 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d064b9a3f4 | ||
|
|
6439e1b8b7 | ||
|
|
a4ce09d309 | ||
|
|
1535f133d9 | ||
|
|
44d887dd6c | ||
|
|
9aeca6b96b |
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -14,7 +14,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Server",
|
||||
"name": "Debugger Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceRoot}",
|
||||
@@ -35,8 +35,8 @@
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Extension + Server",
|
||||
"configurations": [ "Launch Extension", "Server" ]
|
||||
"name": "Extension + Debugger",
|
||||
"configurations": [ "Launch Extension", "Debugger Server" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
103
README.md
@@ -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
|
||||
|
||||
33
extension.js
33
extension.js
@@ -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
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
84
package.json
84
package.json
@@ -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": {}
|
||||
|
||||
105
src/adbclient.js
105
src/adbclient.js
@@ -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,18 +222,19 @@ class ADBClient {
|
||||
if (!o.onlog) {
|
||||
const logcatbuffer = await this.adbsocket.read_stdout();
|
||||
await this.disconnect_from_adb();
|
||||
return logcatbuffer;
|
||||
return logcatbuffer.toString();
|
||||
}
|
||||
|
||||
// start the logcat monitor
|
||||
let logcatbuffer = Buffer.alloc(0);
|
||||
const next_logcat_lines = async () => {
|
||||
// read the next data from ADB
|
||||
let logcatbuffer = Buffer.alloc(0);
|
||||
let next_data;
|
||||
try{
|
||||
next_data = await this.adbsocket.read_stdout(null);
|
||||
for (;;) {
|
||||
// read the next data from ADB
|
||||
try {
|
||||
next_data = await this.adbsocket.read_stdout();
|
||||
} catch(e) {
|
||||
o.onclose();
|
||||
o.onclose(e);
|
||||
return;
|
||||
}
|
||||
logcatbuffer = Buffer.concat([logcatbuffer, next_data]);
|
||||
@@ -178,16 +244,19 @@ class ADBClient {
|
||||
next_logcat_lines();
|
||||
return;
|
||||
}
|
||||
// split into lines
|
||||
const logs = logcatbuffer.slice(0, last_newline_index).toString().split(/\r\n?|\n/);
|
||||
logcatbuffer = logcatbuffer.slice(last_newline_index);
|
||||
// split into lines, sort and remove duplicates and blanks
|
||||
const logs = logcatbuffer.slice(0, last_newline_index).toString()
|
||||
.split(/\r\n?|\n/)
|
||||
.sort()
|
||||
.filter((line,idx,arr) => line && line !== arr[idx-1]);
|
||||
|
||||
logcatbuffer = logcatbuffer.slice(last_newline_index);
|
||||
const e = {
|
||||
adbclient: this,
|
||||
logs,
|
||||
};
|
||||
o.onlog(e);
|
||||
next_logcat_lines();
|
||||
}
|
||||
}
|
||||
next_logcat_lines();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
549
src/debugMain.js
549
src/debugMain.js
@@ -4,7 +4,6 @@ const {
|
||||
Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter');
|
||||
|
||||
// node and external modules
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
@@ -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);
|
||||
|
||||
// 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) {
|
||||
// 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,15 +741,25 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectRequest(response/*, args*/) {
|
||||
/**
|
||||
*
|
||||
* @param {import('vscode-debugprotocol').DebugProtocol.DisconnectResponse} response
|
||||
*/
|
||||
async disconnectRequest(response) {
|
||||
D('disconnectRequest');
|
||||
this._isDisconnecting = true;
|
||||
if (this.debuggerAttached) {
|
||||
try {
|
||||
if (this.debug_mode === 'launch') {
|
||||
await this.dbgr.forceStop();
|
||||
await this.dbgr.disconnect();
|
||||
this.LOG(`Debugger stopped`);
|
||||
} else {
|
||||
await this.dbgr.disconnect();
|
||||
this.LOG(`Debugger detached`);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
@@ -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,16 +859,13 @@ 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;
|
||||
}
|
||||
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) {
|
||||
@@ -640,7 +879,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
const javabp = await this.dbgr.setBreakpoint(o.relative_fpn, dbgline, options);
|
||||
const javabp = await this.dbgr.setBreakpoint(relative_fpn, dbgline, options);
|
||||
if (!javabp.vsbp) {
|
||||
// state is one of: set,notloaded,enabled,removed
|
||||
const verified = !!javabp.state.match(/set|enabled/);
|
||||
@@ -652,11 +891,16 @@ class AndroidDebugSession extends DebugSession {
|
||||
javabp.vsbp = bp;
|
||||
}
|
||||
javabp.vsbp.order = idx;
|
||||
javabp_arr.push(javabp);
|
||||
return this._setup_breakpoints(o, ++idx, javabp_arr);
|
||||
java_breakpoints.push(javabp);
|
||||
}
|
||||
return java_breakpoints;
|
||||
};
|
||||
|
||||
async setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) {
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
216
src/debugger.js
216
src/debugger.js
@@ -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,6 +24,7 @@ const {
|
||||
JavaTaggedValue,
|
||||
JavaThreadInfo,
|
||||
JavaType,
|
||||
LaunchBuildInfo,
|
||||
MethodInvokeArgs,
|
||||
SourceLocation,
|
||||
TypeNotAvailable,
|
||||
@@ -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);
|
||||
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,8 +319,10 @@ class Debugger extends EventEmitter {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
if (this.session.build instanceof LaunchBuildInfo) {
|
||||
return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 'am force-stop' command to the given device
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
* @typedef {number} JDWPRequestID
|
||||
* @typedef {JDWPRequestID} StepID
|
||||
* @typedef {'caught'|'uncaught'|'both'} ExceptionBreakMode
|
||||
* @typedef {'ignore'|'warn'|'stop'} StaleBuildSetting
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
@@ -1534,7 +1534,7 @@ class JDWP {
|
||||
const res = [];
|
||||
let arrlen = DataCoder.decodeInt(o);
|
||||
while (--arrlen >= 0) {
|
||||
res.push(DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'},{type:'signature'},{genericSignature:'string'},{status:'status'}]));
|
||||
res.push(DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'},{signature:'string'},{genericSignature:'string'},{status:'status'}]));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
111
src/logcat.js
111
src/logcat.js
@@ -7,6 +7,8 @@ const WebSocketServer = require('ws').Server;
|
||||
// our stuff
|
||||
const { ADBClient } = require('./adbclient');
|
||||
const { AndroidContentProvider } = require('./contentprovider');
|
||||
const { checkADBStarted } = require('./utils/android');
|
||||
const { selectTargetDevice } = require('./utils/device');
|
||||
const { D } = require('./utils/print');
|
||||
|
||||
/**
|
||||
@@ -292,83 +294,58 @@ function onWebSocketClientConnection(client, req) {
|
||||
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
|
||||
}
|
||||
|
||||
function getADBPort() {
|
||||
const defaultPort = 5037;
|
||||
const adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
||||
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
||||
return adbPort;
|
||||
return defaultPort;
|
||||
}
|
||||
|
||||
function openLogcatWindow(vscode) {
|
||||
new ADBClient().test_adb_connection()
|
||||
.then(err => {
|
||||
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
||||
const adbport = getADBPort();
|
||||
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
||||
if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
|
||||
const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||
const adbargs = ['-P',''+adbport,'start-server'];
|
||||
try {
|
||||
/*const stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
||||
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||
}
|
||||
})
|
||||
.then(() => new ADBClient().list_devices())
|
||||
.then(devices => {
|
||||
switch(devices.length) {
|
||||
case 0:
|
||||
vscode.window.showInformationMessage('Logcat cannot be displayed. No Android devices are currently connected');
|
||||
return null;
|
||||
case 1:
|
||||
return devices; // only one device - just show it
|
||||
}
|
||||
const prefix = 'Android: View Logcat - ', all = '[ Display All ]';
|
||||
const devicelist = devices.map(d => prefix + d.serial);
|
||||
//devicelist.push(prefix + all);
|
||||
return vscode.window.showQuickPick(devicelist)
|
||||
.then(which => {
|
||||
if (!which) return; // user cancelled
|
||||
which = which.slice(prefix.length);
|
||||
return new ADBClient().list_devices()
|
||||
.then(devices => {
|
||||
if (which === all) {
|
||||
return devices
|
||||
}
|
||||
const found = devices.find(d => d.serial === which);
|
||||
if (found) {
|
||||
return [found];
|
||||
}
|
||||
vscode.window.showInformationMessage('Logcat cannot be displayed. The device is disconnected');
|
||||
return null;
|
||||
});
|
||||
}, () => null);
|
||||
})
|
||||
.then(devices => {
|
||||
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
|
||||
devices.forEach(device => {
|
||||
if (vscode.window.createWebviewPanel) {
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
* @param {*} target_device
|
||||
*/
|
||||
function openWebviewLogcatWindow(vscode, target_device) {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'androidlogcat', // Identifies the type of the webview. Used internally
|
||||
`logcat-${device.serial}`, // Title of the panel displayed to the user
|
||||
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
|
||||
`logcat-${target_device.serial}`, // Title of the panel displayed to the user
|
||||
vscode.ViewColumn.Two, // Editor column to show the new webview panel in.
|
||||
{
|
||||
enableScripts: true,
|
||||
enableScripts: true, // we use embedded scripts to relay logcat info over a websocket
|
||||
}
|
||||
);
|
||||
const logcat = new LogcatContent(device.serial);
|
||||
const logcat = new LogcatContent(target_device.serial);
|
||||
logcat.content().then(html => {
|
||||
panel.webview.html = html;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
* @param {*} target_device
|
||||
*/
|
||||
function openPreviewHtmlLogcatWindow(vscode, target_device) {
|
||||
const uri = AndroidContentProvider.getReadLogcatUri(target_device.serial);
|
||||
vscode.commands.executeCommand("vscode.previewHtml", uri, vscode.ViewColumn.Two);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vscode')} vscode
|
||||
*/
|
||||
async function openLogcatWindow(vscode) {
|
||||
try {
|
||||
// if adb is not running, see if we can start it ourselves
|
||||
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
|
||||
await checkADBStarted(autoStartADB);
|
||||
|
||||
let target_device = await selectTargetDevice(vscode, "Logcat display");
|
||||
if (!target_device) {
|
||||
return;
|
||||
}
|
||||
const uri = AndroidContentProvider.getReadLogcatUri(device.serial);
|
||||
vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
|
||||
});
|
||||
})
|
||||
.catch((/*e*/) => {
|
||||
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
|
||||
});
|
||||
|
||||
if (vscode.window.createWebviewPanel) {
|
||||
// newer versions of vscode use WebviewPanels
|
||||
openWebviewLogcatWindow(vscode, target_device);
|
||||
} else {
|
||||
// older versions of vscode use previewHtml
|
||||
openPreviewHtmlLogcatWindow(vscode, target_device);
|
||||
}
|
||||
} catch (e) {
|
||||
vscode.window.showInformationMessage(`Logcat cannot be displayed. ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
77
src/process-attach.js
Normal file
77
src/process-attach.js
Normal 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,
|
||||
}
|
||||
@@ -73,10 +73,13 @@ class ADBSocket extends AndroidSocket {
|
||||
/**
|
||||
* Sends an ADB command, checks the returned status and then reads raw data from the socket
|
||||
* @param {string} command
|
||||
* @param {number} timeout_ms
|
||||
* @param {boolean} [until_closed]
|
||||
*/
|
||||
async cmd_and_read_stdout(command) {
|
||||
async cmd_and_read_stdout(command, timeout_ms, until_closed) {
|
||||
await this.cmd_and_status(command);
|
||||
return this.read_stdout();
|
||||
const buf = await this.read_stdout(timeout_ms, until_closed);
|
||||
return buf.toString('latin1');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,8 +64,9 @@ class AndroidSocket extends EventEmitter {
|
||||
*
|
||||
* @param {number|'length+data'|undefined} length
|
||||
* @param {string} [format]
|
||||
* @param {number} [timeout_ms]
|
||||
*/
|
||||
async read_bytes(length, format) {
|
||||
async read_bytes(length, format, timeout_ms) {
|
||||
//D(`reading ${length} bytes`);
|
||||
let actual_length = length;
|
||||
if (typeof actual_length === 'undefined') {
|
||||
@@ -95,25 +96,40 @@ class AndroidSocket extends EventEmitter {
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
// wait for the socket to update and then retry the read
|
||||
await this.wait_for_socket_data();
|
||||
await this.wait_for_socket_data(timeout_ms);
|
||||
return this.read_bytes(length, format);
|
||||
}
|
||||
|
||||
wait_for_socket_data() {
|
||||
/**
|
||||
*
|
||||
* @param {number} [timeout_ms]
|
||||
*/
|
||||
wait_for_socket_data(timeout_ms) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let done = 0;
|
||||
let done = 0, timer = null;
|
||||
let onDataChanged = () => {
|
||||
if ((done += 1) !== 1) return;
|
||||
this.off('socket-ended', onSocketEnded);
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
let onSocketEnded = () => {
|
||||
if ((done += 1) !== 1) return;
|
||||
this.off('data-changed', onDataChanged);
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`${this.which} socket read failed. Socket closed.`));
|
||||
}
|
||||
let onTimerExpired = () => {
|
||||
if ((done += 1) !== 1) return;
|
||||
this.off('socket-ended', onSocketEnded);
|
||||
this.off('data-changed', onDataChanged);
|
||||
reject(new Error(`${this.which} socket read failed. Read timeout.`));
|
||||
}
|
||||
this.once('data-changed', onDataChanged);
|
||||
this.once('socket-ended', onSocketEnded);
|
||||
if (typeof timeout_ms === 'number' && timeout_ms >= 0) {
|
||||
timer = setTimeout(onTimerExpired, timeout_ms);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,8 +138,26 @@ class AndroidSocket extends EventEmitter {
|
||||
return this.read_bytes(len.readUInt32LE(0), format);
|
||||
}
|
||||
|
||||
read_stdout(format = 'latin1') {
|
||||
return this.read_bytes(undefined, format);
|
||||
/**
|
||||
*
|
||||
* @param {number} [timeout_ms]
|
||||
* @param {boolean} [until_closed]
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async read_stdout(timeout_ms, until_closed) {
|
||||
let buf = await this.read_bytes(undefined, null, timeout_ms);
|
||||
if (!until_closed) {
|
||||
return buf;
|
||||
}
|
||||
const parts = [buf];
|
||||
try {
|
||||
for (;;) {
|
||||
buf = await this.read_bytes(undefined, null);
|
||||
parts.push(buf);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return Buffer.concat(parts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +168,7 @@ class AndroidSocket extends EventEmitter {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.check_socket_active('write');
|
||||
try {
|
||||
// @ts-ignore
|
||||
const flushed = this.socket.write(bytes, () => {
|
||||
flushed ? resolve() : this.socket.once('drain', resolve);
|
||||
});
|
||||
|
||||
@@ -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
87
src/utils/android.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { ADBClient } = require('../adbclient');
|
||||
const ADBSocket = require('../sockets/adbsocket');
|
||||
const { LOG } = require('../utils/print');
|
||||
|
||||
function getAndroidSDKFolder() {
|
||||
// ANDROID_HOME is deprecated
|
||||
return process.env.ANDROID_HOME || process.env.ANDROID_SDK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} api_level
|
||||
* @param {boolean} check_is_dir
|
||||
*/
|
||||
function getAndroidSourcesFolder(api_level, check_is_dir) {
|
||||
const android_sdk = getAndroidSDKFolder();
|
||||
if (!android_sdk) {
|
||||
return null;
|
||||
}
|
||||
const sources_path = path.join(android_sdk,'sources',`android-${api_level}`);
|
||||
if (check_is_dir) {
|
||||
try {
|
||||
const stat = fs.statSync(sources_path);
|
||||
if (!stat || !stat.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return sources_path;
|
||||
}
|
||||
|
||||
function getADBPathName() {
|
||||
const android_sdk = getAndroidSDKFolder();
|
||||
if (!android_sdk) {
|
||||
return '';
|
||||
}
|
||||
return path.join(android_sdk, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} port
|
||||
*/
|
||||
function startADBServer(port) {
|
||||
if (typeof port !== 'number' || port <= 0 || port >= 65536) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const adb_exe_path = getADBPathName();
|
||||
if (!adb_exe_path) {
|
||||
return false;
|
||||
}
|
||||
const adb_start_server_args = ['-P',`${port}`,'start-server'];
|
||||
try {
|
||||
LOG([adb_exe_path, ...adb_start_server_args].join(' '));
|
||||
const stdout = require('child_process').execFileSync(adb_exe_path, adb_start_server_args, {
|
||||
cwd: getAndroidSDKFolder(),
|
||||
encoding:'utf8',
|
||||
});
|
||||
LOG(stdout);
|
||||
return true;
|
||||
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} auto_start
|
||||
*/
|
||||
async function checkADBStarted(auto_start) {
|
||||
const err = await new ADBClient().test_adb_connection();
|
||||
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
|
||||
if (err && auto_start) {
|
||||
return startADBServer(ADBSocket.ADBPort);
|
||||
}
|
||||
return !err;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkADBStarted,
|
||||
getADBPathName,
|
||||
getAndroidSDKFolder,
|
||||
getAndroidSourcesFolder,
|
||||
startADBServer,
|
||||
}
|
||||
62
src/utils/device.js
Normal file
62
src/utils/device.js
Normal 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,
|
||||
}
|
||||
@@ -35,6 +35,23 @@ function W(...args) {
|
||||
callMessagePrintCallbacks(args);
|
||||
}
|
||||
|
||||
let printLogToClient;
|
||||
function initLogToClient(fn) {
|
||||
printLogToClient = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a log message
|
||||
* @param {*} msg
|
||||
*/
|
||||
function LOG(msg) {
|
||||
if (printLogToClient) {
|
||||
printLogToClient(msg);
|
||||
} else {
|
||||
D(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a callback to be called when any message is output
|
||||
* @param {Function} cb
|
||||
@@ -46,6 +63,8 @@ function onMessagePrint(cb) {
|
||||
module.exports = {
|
||||
D,
|
||||
E,
|
||||
initLogToClient,
|
||||
LOG,
|
||||
W,
|
||||
onMessagePrint,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user