mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
Version 1.1 improvements (#88)
* fix 0 alignment in binary xml decoding * output reason for APK manifest read failure * try and match package name against process name when determining which pid to attach * make post launch pause user-configurable * code tidy, jsdocs and types * more types in expression parse classes * fix issue with expandable objects not evaluating * update build task example * fix package/type evaluation * improve handling of targetDevice and processID combinations * show full call stack by default * implement a queue for evaluations * improve performance of retrieving single fields * check root term identifiers against this fields
This commit is contained in:
22
README.md
22
README.md
@@ -87,6 +87,10 @@ The following settings are used to configure the debugger:
|
|||||||
// mutually exclusive with "amStartArgs".
|
// mutually exclusive with "amStartArgs".
|
||||||
"launchActivity": ".MainActivity",
|
"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.
|
// Set to true to output debugging logs for diagnostics.
|
||||||
"trace": false
|
"trace": false
|
||||||
}
|
}
|
||||||
@@ -109,6 +113,7 @@ Add a `preLaunchTask` item to the launch configuration:
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "App Build & Launch",
|
"name": "App Build & Launch",
|
||||||
"preLaunchTask": "run gradle",
|
"preLaunchTask": "run gradle",
|
||||||
|
...
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -123,7 +128,22 @@ Add a new task to run the build command:
|
|||||||
"label": "run gradle",
|
"label": "run gradle",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "${workspaceFolder}/gradlew",
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
25
extension.js
25
extension.js
@@ -20,14 +20,33 @@ function activate(context) {
|
|||||||
openLogcatWindow(vscode);
|
openLogcatWindow(vscode);
|
||||||
}),
|
}),
|
||||||
// add the device picker handler - used to choose a target device
|
// add the device picker handler - used to choose a target device
|
||||||
vscode.commands.registerCommand('PickAndroidDevice', async () => {
|
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 });
|
const device = await selectTargetDevice(vscode, "Launch", { alwaysShow:true });
|
||||||
// the debugger requires a string value to be returned
|
// the debugger requires a string value to be returned
|
||||||
return JSON.stringify(device);
|
return JSON.stringify(device);
|
||||||
}),
|
}),
|
||||||
// add the process picker handler - used to choose a PID to attach to
|
// add the process picker handler - used to choose a PID to attach to
|
||||||
vscode.commands.registerCommand('PickAndroidProcess', async () => {
|
vscode.commands.registerCommand('PickAndroidProcess', async (launchConfig) => {
|
||||||
const o = await selectAndroidProcessID(vscode);
|
// 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
|
// the debugger requires a string value to be returned
|
||||||
return JSON.stringify(o);
|
return JSON.stringify(o);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -114,6 +114,11 @@
|
|||||||
"-r"
|
"-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": {
|
"staleBuild": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
|
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
|
||||||
|
|||||||
@@ -92,17 +92,31 @@ class ADBClient {
|
|||||||
return stdout.trim().split(/\s+/).filter(x => x).map(s => parseInt(s, 10));
|
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) {
|
async named_jdwp_list(timeout_ms) {
|
||||||
const named_pids = (await this.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 => ({
|
.map(pid => ({
|
||||||
pid,
|
pid,
|
||||||
name: '',
|
name: '',
|
||||||
}))
|
}))
|
||||||
if (!named_pids.length)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
// retrieve the list of process names from the device
|
// retrieve the list of process names from the device
|
||||||
const command = `for pid in ${named_pids.map(np => np.pid).join(' ')}; do cat /proc/$pid/cmdline;echo " $pid"; done`;
|
const command = `for pid in ${pids.join(' ')}; do cat /proc/$pid/cmdline;echo " $pid"; done`;
|
||||||
const stdout = await this.shell_cmd({
|
const stdout = await this.shell_cmd({
|
||||||
command,
|
command,
|
||||||
untilclosed: true,
|
untilclosed: true,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function decode_spec_value(o, key, value, buf, idx, main) {
|
|||||||
case /^align:\d+$/.test(value): {
|
case /^align:\d+$/.test(value): {
|
||||||
// used for arbitrary padding to a specified alignment
|
// used for arbitrary padding to a specified alignment
|
||||||
const align = parseInt(value.split(':')[1], 10);
|
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);
|
o[key] = buf.slice(idx, idx + byteLength);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,8 +104,7 @@ class APKFileInfo {
|
|||||||
* 2. The decoded manifest from the APK
|
* 2. The decoded manifest from the APK
|
||||||
* 3. The AndroidManifest.xml file from the root of the source tree.
|
* 3. The AndroidManifest.xml file from the root of the source tree.
|
||||||
*/
|
*/
|
||||||
async function getAndroidManifestXml(args) {
|
async function getAndroidManifestXml({manifestFile, apkFile, appSrcRoot}) {
|
||||||
const {manifestFile, apkFile, appSrcRoot} = args;
|
|
||||||
let manifest;
|
let manifest;
|
||||||
|
|
||||||
// a value from the manifestFile overrides the default manifest extraction
|
// a value from the manifestFile overrides the default manifest extraction
|
||||||
@@ -121,7 +120,7 @@ async function getAndroidManifestXml(args) {
|
|||||||
manifest = await extractManifestFromAPK(apkFile);
|
manifest = await extractManifestFromAPK(apkFile);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
// if we fail to get manifest from the APK, revert to the source file version
|
// 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');
|
manifest = await readFile(path.join(appSrcRoot, 'AndroidManifest.xml'), 'utf8');
|
||||||
}
|
}
|
||||||
return manifest;
|
return manifest;
|
||||||
|
|||||||
194
src/debugMain.js
194
src/debugMain.js
@@ -11,7 +11,7 @@ const path = require('path');
|
|||||||
const { ADBClient } = require('./adbclient');
|
const { ADBClient } = require('./adbclient');
|
||||||
const { APKFileInfo } = require('./apk-file-info');
|
const { APKFileInfo } = require('./apk-file-info');
|
||||||
const { Debugger } = require('./debugger');
|
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 { evaluate } = require('./expression/evaluate');
|
||||||
const { PackageInfo } = require('./package-searcher');
|
const { PackageInfo } = require('./package-searcher');
|
||||||
const ADBSocket = require('./sockets/adbsocket');
|
const ADBSocket = require('./sockets/adbsocket');
|
||||||
@@ -77,7 +77,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
this._android_sources_path = '';
|
this._android_sources_path = '';
|
||||||
|
|
||||||
// number of call stack entries to display above the project source
|
// 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)
|
||||||
@@ -118,26 +118,18 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
/**
|
/**
|
||||||
* The 'initialize' request is the first request called by the frontend
|
* The 'initialize' request is the first request called by the frontend
|
||||||
* to interrogate the features the debug adapter provides.
|
* to interrogate the features the debug adapter provides.
|
||||||
|
* @param {import('vscode-debugprotocol').DebugProtocol.InitializeResponse} response
|
||||||
*/
|
*/
|
||||||
initializeRequest(response/*: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments*/) {
|
initializeRequest(response) {
|
||||||
|
|
||||||
// This debug adapter implements the configurationDoneRequest.
|
|
||||||
response.body.supportsConfigurationDoneRequest = true;
|
|
||||||
|
|
||||||
// we support some exception options
|
|
||||||
response.body.exceptionBreakpointFilters = [
|
response.body.exceptionBreakpointFilters = [
|
||||||
{ label:'All Exceptions', filter:'all', default:false },
|
{ label:'All Exceptions', filter:'all', default:false },
|
||||||
{ label:'Uncaught Exceptions', filter:'uncaught', default:true },
|
{ label:'Uncaught Exceptions', filter:'uncaught', default:true },
|
||||||
];
|
];
|
||||||
|
response.body.supportsConfigurationDoneRequest = true;
|
||||||
// we support modifying variable values
|
|
||||||
response.body.supportsSetVariable = true;
|
response.body.supportsSetVariable = true;
|
||||||
|
response.body.supportsEvaluateForHovers = true;
|
||||||
// we support hit-count conditional breakpoints
|
|
||||||
response.body.supportsHitConditionalBreakpoints = true;
|
|
||||||
|
|
||||||
// we support the new ExceptionInfoRequest
|
|
||||||
response.body.supportsExceptionInfoRequest = true;
|
response.body.supportsExceptionInfoRequest = true;
|
||||||
|
response.body.supportsHitConditionalBreakpoints = true;
|
||||||
|
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
}
|
}
|
||||||
@@ -167,6 +159,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} msg
|
* @param {string} msg
|
||||||
|
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
|
||||||
* @param {boolean} silent
|
* @param {boolean} silent
|
||||||
*/
|
*/
|
||||||
failRequest(msg, response, silent = false) {
|
failRequest(msg, response, silent = false) {
|
||||||
@@ -185,7 +178,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
/**
|
/**
|
||||||
* @param {string} requestName
|
* @param {string} requestName
|
||||||
* @param {number} threadId
|
* @param {number} threadId
|
||||||
* @param {*} response
|
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
|
||||||
*/
|
*/
|
||||||
failRequestNoThread(requestName, threadId, response) {
|
failRequestNoThread(requestName, threadId, response) {
|
||||||
this.failRequest(`${requestName} failed. Thread ${threadId} not found`, response);
|
this.failRequest(`${requestName} failed. Thread ${threadId} not found`, response);
|
||||||
@@ -194,7 +187,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
/**
|
/**
|
||||||
* @param {string} requestName
|
* @param {string} requestName
|
||||||
* @param {number} threadId
|
* @param {number} threadId
|
||||||
* @param {*} response
|
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
|
||||||
*/
|
*/
|
||||||
failRequestThreadNotSuspended(requestName, threadId, response) {
|
failRequestThreadNotSuspended(requestName, threadId, response) {
|
||||||
this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response);
|
this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response);
|
||||||
@@ -203,7 +196,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
/**
|
/**
|
||||||
* @param {string} requestName
|
* @param {string} requestName
|
||||||
* @param {number} threadId
|
* @param {number} threadId
|
||||||
* @param {*} response
|
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
|
||||||
*/
|
*/
|
||||||
cancelRequestThreadNotSuspended(requestName, threadId, 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
|
// now that vscode can resume threads before the locals,callstack,etc are retrieved, we only need to cancel the request
|
||||||
@@ -258,10 +251,11 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
// configure the thread names
|
// configure the thread names
|
||||||
threadinfos.forEach(threadinfo => {
|
threadinfos.forEach(threadinfo => {
|
||||||
const thread = this.getThread(threadinfo.threadid);
|
const thread = this.getThread(threadinfo.threadid);
|
||||||
if (thread.name === null) {
|
if (typeof thread.name !== 'string') {
|
||||||
thread.name = threadinfo.name;
|
thread.name = threadinfo.name;
|
||||||
} else if (thread.name !== threadinfo.name) {
|
} else if (thread.name !== threadinfo.name) {
|
||||||
// give the thread a new id for VS code
|
// 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];
|
delete this._threads[thread.vscode_threadid];
|
||||||
thread.allocateNewThreadID();
|
thread.allocateNewThreadID();
|
||||||
this._threads[thread.vscode_threadid] = thread;
|
this._threads[thread.vscode_threadid] = thread;
|
||||||
@@ -321,6 +315,17 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
return s;
|
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) {
|
async attachRequest(response, args) {
|
||||||
this.debug_mode = 'attach';
|
this.debug_mode = 'attach';
|
||||||
if (args && args.trace) {
|
if (args && args.trace) {
|
||||||
@@ -382,7 +387,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
// try and determine the relevant path for the API sources (based upon the API level of the connected device)
|
// try and determine the relevant path for the API sources (based upon the API level of the connected device)
|
||||||
await this.configureAPISourcePath();
|
await this.configureAPISourcePath();
|
||||||
|
|
||||||
const build = new BuildInfo(null, new Map(this.src_packages.packages), null);
|
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||'?'}]`);
|
this.LOG(`Attaching to pid ${processId} on device ${this._device.serial} [API:${this.device_api_level||'?'}]`);
|
||||||
|
|
||||||
// try and attach to the specified pid
|
// try and attach to the specified pid
|
||||||
@@ -428,11 +433,27 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @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
|
* The entry point to the debugger
|
||||||
* @param {*} response
|
* @param {import('vscode-debugprotocol').DebugProtocol.LaunchResponse} response
|
||||||
* @param {*} args
|
* @param {import('vscode-debugprotocol').DebugProtocol.LaunchRequestArguments & AndroidLaunchArguments} args
|
||||||
*/
|
*/
|
||||||
async launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
|
async launchRequest(response, args) {
|
||||||
this.debug_mode = 'launch';
|
this.debug_mode = 'launch';
|
||||||
if (args && args.trace) {
|
if (args && args.trace) {
|
||||||
this.trace = args.trace;
|
this.trace = args.trace;
|
||||||
@@ -512,7 +533,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
await this.configureAPISourcePath();
|
await this.configureAPISourcePath();
|
||||||
|
|
||||||
// launch the app
|
// launch the app
|
||||||
await this.startLaunchActivity(args.launchActivity);
|
await this.startLaunchActivity(args.launchActivity, args.postLaunchPause);
|
||||||
|
|
||||||
this.debuggerAttached = true;
|
this.debuggerAttached = true;
|
||||||
|
|
||||||
@@ -552,6 +573,10 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
checkBuildIsUpToDate(staleBuild) {
|
||||||
// check if any source file was modified after the apk
|
// check if any source file was modified after the apk
|
||||||
if (this.src_packages.last_src_modified >= this.apk_file_info.app_modified) {
|
if (this.src_packages.last_src_modified >= this.apk_file_info.app_modified) {
|
||||||
@@ -564,7 +589,12 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startLaunchActivity(launchActivity) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} launchActivity
|
||||||
|
* @param {number} postLaunchPause
|
||||||
|
*/
|
||||||
|
async startLaunchActivity(launchActivity, postLaunchPause) {
|
||||||
if (!launchActivity) {
|
if (!launchActivity) {
|
||||||
// we're allowed no launchActivity if we have a custom am start command
|
// we're allowed no launchActivity if we have a custom am start command
|
||||||
if (!this.am_start_args) {
|
if (!this.am_start_args) {
|
||||||
@@ -574,7 +604,12 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const build = new BuildInfo(this.apk_file_info.manifest.package, new Map(this.src_packages.packages), launchActivity, this.am_start_args);
|
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||'?'}]`);
|
this.LOG(`Launching on device ${this._device.serial} [API:${this.device_api_level||'?'}]`);
|
||||||
if (this.am_start_args) {
|
if (this.am_start_args) {
|
||||||
@@ -686,7 +721,11 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
throw new Error(reject);
|
throw new Error(reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
configurationDoneRequest(response/*, args*/) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('vscode-debugprotocol').DebugProtocol.ConfigurationDoneResponse} response
|
||||||
|
*/
|
||||||
|
configurationDoneRequest(response) {
|
||||||
D('configurationDoneRequest');
|
D('configurationDoneRequest');
|
||||||
this.waitForConfigurationDone();
|
this.waitForConfigurationDone();
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
@@ -702,7 +741,11 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnectRequest(response/*, args*/) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('vscode-debugprotocol').DebugProtocol.DisconnectResponse} response
|
||||||
|
*/
|
||||||
|
async disconnectRequest(response) {
|
||||||
D('disconnectRequest');
|
D('disconnectRequest');
|
||||||
this._isDisconnecting = true;
|
this._isDisconnecting = true;
|
||||||
if (this.debuggerAttached) {
|
if (this.debuggerAttached) {
|
||||||
@@ -733,7 +776,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Called by the debugger in response to a JDWP breakpoint hit event
|
||||||
* @param {JavaBreakpointEvent} e
|
* @param {JavaBreakpointEvent} e
|
||||||
*/
|
*/
|
||||||
onBreakpointHit(e) {
|
onBreakpointHit(e) {
|
||||||
@@ -745,8 +788,10 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
/**
|
/**
|
||||||
* Called when the user requests a change to breakpoints in a source file
|
* 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
|
* 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;
|
const source_filename = args.source && args.source.path;
|
||||||
D('setBreakPointsRequest: ' + source_filename);
|
D('setBreakPointsRequest: ' + source_filename);
|
||||||
|
|
||||||
@@ -814,8 +859,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @param {import('vscode-debugprotocol').DebugProtocol.SourceBreakpoint[]} breakpoints
|
||||||
* @param {*[]} breakpoints
|
|
||||||
* @param {string} relative_fpn
|
* @param {string} relative_fpn
|
||||||
*/
|
*/
|
||||||
async setupBreakpointsInFile(breakpoints, relative_fpn) {
|
async setupBreakpointsInFile(breakpoints, relative_fpn) {
|
||||||
@@ -852,7 +896,11 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
return java_breakpoints;
|
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();
|
await this.dbgr.clearBreakOnExceptions();
|
||||||
switch(true) {
|
switch(true) {
|
||||||
case args.filters.includes('all'):
|
case args.filters.includes('all'):
|
||||||
@@ -865,7 +913,11 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('vscode-debugprotocol').DebugProtocol.ThreadsResponse} response
|
||||||
|
*/
|
||||||
|
async threadsRequest(response) {
|
||||||
if (!this._threads.length) {
|
if (!this._threads.length) {
|
||||||
try {
|
try {
|
||||||
await this.refreshThreads();
|
await this.refreshThreads();
|
||||||
@@ -889,8 +941,10 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a stack trace for the given threadId
|
* 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}`);
|
D(`stackTraceRequest thread:${args.threadId}`);
|
||||||
// only retrieve the stack if the thread is paused
|
// only retrieve the stack if the thread is paused
|
||||||
const thread = this.getThread(args.threadId);
|
const thread = this.getThread(args.threadId);
|
||||||
@@ -972,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}`);
|
D(`scopesRequest frame:${args.frameId}`);
|
||||||
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
|
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
|
||||||
const thread = this.getThread(threadId);
|
const thread = this.getThread(threadId);
|
||||||
@@ -1000,8 +1058,12 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
this.sendResponse(response);
|
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 =
|
const content =
|
||||||
`/*
|
`/*
|
||||||
The source for this class is unavailable.
|
The source for this class is unavailable.
|
||||||
@@ -1021,11 +1083,10 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @param {import('vscode-debugprotocol').DebugProtocol.VariablesResponse} response
|
||||||
* @param {*} response
|
* @param {import('vscode-debugprotocol').DebugProtocol.VariablesArguments} args
|
||||||
* @param {{variablesReference:VSCVariableReference}} args
|
|
||||||
*/
|
*/
|
||||||
async variablesRequest(response/*: DebugProtocol.VariablesResponse*/, args/*: DebugProtocol.VariablesArguments*/) {
|
async variablesRequest(response, args) {
|
||||||
D(`variablesRequest variablesReference:${args.variablesReference}`);
|
D(`variablesRequest variablesReference:${args.variablesReference}`);
|
||||||
const threadId = AndroidThread.variableRefToThreadId(args.variablesReference);
|
const threadId = AndroidThread.variableRefToThreadId(args.variablesReference);
|
||||||
const thread = this.getThread(threadId);
|
const thread = this.getThread(threadId);
|
||||||
@@ -1109,7 +1170,11 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
await this.dbgr.resumeThread(thread.threadid);
|
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}`);
|
D(`Continue thread:${args.threadId}`);
|
||||||
|
|
||||||
const thread = this.getThread(args.threadId);
|
const thread = this.getThread(args.threadId);
|
||||||
@@ -1132,8 +1197,8 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
/**
|
/**
|
||||||
* Called by the user to start a step operation
|
* Called by the user to start a step operation
|
||||||
* @param {DebuggerStepType} which
|
* @param {DebuggerStepType} which
|
||||||
* @param {*} response
|
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
|
||||||
* @param {*} args
|
* @param {import('vscode-debugprotocol').DebugProtocol.NextArguments} args
|
||||||
*/
|
*/
|
||||||
doStep(which, response, args) {
|
doStep(which, response, args) {
|
||||||
D(`step ${which}`);
|
D(`step ${which}`);
|
||||||
@@ -1156,15 +1221,27 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
this.dbgr.step(which, thread.threadid);
|
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);
|
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);
|
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);
|
this.doStep('out', response, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1182,7 +1259,11 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
this.reportStoppedEvent("exception", e.throwlocation, last_exception);
|
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}`);
|
D(`exceptionInfoRequest: ${args.threadId}`);
|
||||||
const thread = this.getThread(args.threadId);
|
const thread = this.getThread(args.threadId);
|
||||||
if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response);
|
if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response);
|
||||||
@@ -1252,15 +1333,10 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef SetVariableArgs
|
* @param {import('vscode-debugprotocol').DebugProtocol.SetVariableResponse} response
|
||||||
* @property {string} name
|
* @param {import('vscode-debugprotocol').DebugProtocol.SetVariableArguments} args
|
||||||
* @property {string} value
|
|
||||||
* @property {number} variablesReference
|
|
||||||
*
|
|
||||||
* @param {*} response
|
|
||||||
* @param {SetVariableArgs} args
|
|
||||||
*/
|
*/
|
||||||
async setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) {
|
async setVariableRequest(response, args) {
|
||||||
|
|
||||||
const threadId = AndroidThread.variableRefToThreadId(args.variablesReference);
|
const threadId = AndroidThread.variableRefToThreadId(args.variablesReference);
|
||||||
const thread = this.getThread(threadId);
|
const thread = this.getThread(threadId);
|
||||||
@@ -1289,8 +1365,10 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by VSCode to perform watch, console and hover evaluations
|
* 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:
|
// Some notes to remember:
|
||||||
// annoyingly, during stepping, the step can complete before the resume has called evaluateRequest on watches.
|
// annoyingly, during stepping, the step can complete before the resume has called evaluateRequest on watches.
|
||||||
|
|||||||
@@ -4,16 +4,25 @@ const { PackageInfo } = require('./package-searcher');
|
|||||||
const { splitSourcePath } = require('./utils/source-file');
|
const { splitSourcePath } = require('./utils/source-file');
|
||||||
|
|
||||||
class BuildInfo {
|
class BuildInfo {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} pkgname
|
|
||||||
* @param {Map<string,PackageInfo>} packages
|
* @param {Map<string,PackageInfo>} packages
|
||||||
* @param {string} launchActivity
|
|
||||||
* @param {string[]} [amCommandArgs] custom arguments passed to `am start`
|
|
||||||
*/
|
*/
|
||||||
constructor(pkgname, packages, launchActivity, amCommandArgs) {
|
constructor(packages) {
|
||||||
this.pkgname = pkgname;
|
|
||||||
this.packages = 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;
|
this.launchActivity = launchActivity;
|
||||||
/** the arguments passed to `am start` */
|
/** the arguments passed to `am start` */
|
||||||
this.startCommandArgs = amCommandArgs || [
|
this.startCommandArgs = amCommandArgs || [
|
||||||
@@ -24,10 +33,19 @@ class BuildInfo {
|
|||||||
`-n ${pkgname}/${launchActivity}`,
|
`-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.
|
* 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,7 +688,7 @@ class DebuggerTypeInfo {
|
|||||||
// otherwise, leave super undefined to be updated later
|
// 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 !== 'class' || type.signature[0] !== 'L' || type.signature === JavaType.Object.signature) {
|
||||||
if (info.reftype.string !== 'array') {
|
if (info.reftype.string !== 'array') {
|
||||||
/** @type {JavaType} */
|
/** @type {JavaClassType} */
|
||||||
this.super = null;
|
this.super = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -747,9 +765,9 @@ class VariableValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
AttachBuildInfo,
|
||||||
BreakpointLocation,
|
BreakpointLocation,
|
||||||
BreakpointOptions,
|
BreakpointOptions,
|
||||||
BuildInfo,
|
|
||||||
DebuggerBreakpoint,
|
DebuggerBreakpoint,
|
||||||
DebuggerException,
|
DebuggerException,
|
||||||
DebuggerFrameInfo,
|
DebuggerFrameInfo,
|
||||||
@@ -757,6 +775,7 @@ module.exports = {
|
|||||||
DebuggerTypeInfo,
|
DebuggerTypeInfo,
|
||||||
DebugSession,
|
DebugSession,
|
||||||
DebuggerValue,
|
DebuggerValue,
|
||||||
|
LaunchBuildInfo,
|
||||||
LiteralValue,
|
LiteralValue,
|
||||||
JavaBreakpointEvent,
|
JavaBreakpointEvent,
|
||||||
JavaExceptionEvent,
|
JavaExceptionEvent,
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ const { D } = require('./utils/print');
|
|||||||
const { sleep } = require('./utils/thread');
|
const { sleep } = require('./utils/thread');
|
||||||
const { decodeJavaStringLiteral } = require('./utils/char-decode');
|
const { decodeJavaStringLiteral } = require('./utils/char-decode');
|
||||||
const {
|
const {
|
||||||
|
AttachBuildInfo,
|
||||||
BreakpointLocation,
|
BreakpointLocation,
|
||||||
BreakpointOptions,
|
BreakpointOptions,
|
||||||
BuildInfo,
|
|
||||||
DebuggerBreakpoint,
|
DebuggerBreakpoint,
|
||||||
DebuggerFrameInfo,
|
DebuggerFrameInfo,
|
||||||
DebuggerMethodInfo,
|
DebuggerMethodInfo,
|
||||||
@@ -24,6 +24,7 @@ const {
|
|||||||
JavaTaggedValue,
|
JavaTaggedValue,
|
||||||
JavaThreadInfo,
|
JavaThreadInfo,
|
||||||
JavaType,
|
JavaType,
|
||||||
|
LaunchBuildInfo,
|
||||||
MethodInvokeArgs,
|
MethodInvokeArgs,
|
||||||
SourceLocation,
|
SourceLocation,
|
||||||
TypeNotAvailable,
|
TypeNotAvailable,
|
||||||
@@ -71,7 +72,7 @@ class Debugger extends EventEmitter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {BuildInfo} build
|
* @param {LaunchBuildInfo} build
|
||||||
* @param {string} deviceid
|
* @param {string} deviceid
|
||||||
*/
|
*/
|
||||||
async startDebugSession(build, deviceid) {
|
async startDebugSession(build, deviceid) {
|
||||||
@@ -82,19 +83,36 @@ class Debugger extends EventEmitter {
|
|||||||
const stdout = await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause);
|
const stdout = await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause);
|
||||||
|
|
||||||
// retrieve the list of debuggable processes
|
// retrieve the list of debuggable processes
|
||||||
const pids = await Debugger.getDebuggablePIDs(this.session.deviceid, 10e3);
|
const named_pids = await Debugger.getDebuggableProcesses(deviceid, 10e3);
|
||||||
if (pids.length === 0) {
|
if (named_pids.length === 0) {
|
||||||
throw new Error(`startDebugSession: No debuggable processes after app launch.`);
|
throw new Error(`startDebugSession: No debuggable processes after app launch.`);
|
||||||
}
|
}
|
||||||
// choose the last pid in the list
|
// we assume the newly launched app is the last pid in the list, but try and
|
||||||
const pid = pids[pids.length - 1];
|
// 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
|
// after connect(), the caller must call resume() to begin
|
||||||
await this.connect(pid);
|
await this.connect(pid);
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {BuildInfo} build
|
* @param {AttachBuildInfo} build
|
||||||
* @param {number} pid process ID to connect to
|
* @param {number} pid process ID to connect to
|
||||||
* @param {string} deviceid device ID to connect to
|
* @param {string} deviceid device ID to connect to
|
||||||
*/
|
*/
|
||||||
@@ -110,9 +128,9 @@ class Debugger extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @param {string} deviceid Device ID to connect to
|
* @param {string} deviceid Device ID to connect to
|
||||||
* @param {string[]} launch_cmd_args Array of arguments to pass to 'am start'
|
* @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
|
// older (<3) versions of Android only allow target components to be specified with -n
|
||||||
const shell_cmd = {
|
const shell_cmd = {
|
||||||
command: `am start ${launch_cmd_args.join(' ')}`,
|
command: `am start ${launch_cmd_args.join(' ')}`,
|
||||||
@@ -301,8 +319,10 @@ class Debugger extends EventEmitter {
|
|||||||
if (!this.session) {
|
if (!this.session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.session.build instanceof LaunchBuildInfo) {
|
||||||
return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname);
|
return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a 'am force-stop' command to the given device
|
* Sends a 'am force-stop' command to the given device
|
||||||
@@ -682,13 +702,13 @@ class Debugger extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DebuggerValue} value
|
* @param {string} signature
|
||||||
*/
|
*/
|
||||||
async getSuperType(value) {
|
async getSuperType(signature) {
|
||||||
if (value.type.signature === JavaType.Object.signature)
|
if (signature === JavaType.Object.signature)
|
||||||
throw new Error('java.lang.Object has no super type');
|
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);
|
await this._ensureSuperType(typeinfo);
|
||||||
return typeinfo.super;
|
return typeinfo.super;
|
||||||
}
|
}
|
||||||
@@ -697,7 +717,7 @@ class Debugger extends EventEmitter {
|
|||||||
* @param {DebuggerValue} value
|
* @param {DebuggerValue} value
|
||||||
*/
|
*/
|
||||||
async getSuperInstance(value) {
|
async getSuperInstance(value) {
|
||||||
const supertype = await this.getSuperType(value);
|
const supertype = await this.getSuperType(value.type.signature);
|
||||||
if (value.vtype === 'class') {
|
if (value.vtype === 'class') {
|
||||||
return this.getTypeValue(supertype.signature);
|
return this.getTypeValue(supertype.signature);
|
||||||
}
|
}
|
||||||
@@ -743,15 +763,23 @@ class Debugger extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {DebuggerValue} object_value
|
* @param {DebuggerValue} object_value
|
||||||
*/
|
*/
|
||||||
async getFieldValues(object_value) {
|
async getFieldValues(object_value) {
|
||||||
const type = await this.getTypeInfo(object_value.type.signature);
|
const type = await this.getTypeInfo(object_value.type.signature);
|
||||||
await this._ensureFields(type);
|
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
|
// 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...
|
// 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) {
|
if (f.modbits & 8) {
|
||||||
z.static.push(f);
|
z.static.push(f);
|
||||||
} else {
|
} else {
|
||||||
@@ -776,7 +804,7 @@ class Debugger extends EventEmitter {
|
|||||||
let static_fieldvalues = [];
|
let static_fieldvalues = [];
|
||||||
if (splitfields.static.length) {
|
if (splitfields.static.length) {
|
||||||
static_fieldvalues = await this.session.adbclient.jdwp_command({
|
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...
|
// make sure the fields and values match up...
|
||||||
@@ -786,7 +814,8 @@ class Debugger extends EventEmitter {
|
|||||||
res.forEach((value,i) => {
|
res.forEach((value,i) => {
|
||||||
value.data.field = fields[i];
|
value.data.field = fields[i];
|
||||||
value.fqname = `${object_value.fqname || object_value.name}.${value.name}`;
|
value.fqname = `${object_value.fqname || object_value.name}.${value.name}`;
|
||||||
})
|
});
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,21 +828,24 @@ class Debugger extends EventEmitter {
|
|||||||
if (!(object_value.type instanceof JavaClassType)) {
|
if (!(object_value.type instanceof JavaClassType)) {
|
||||||
return null;
|
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 (;;) {
|
for (;;) {
|
||||||
// retrieve all the fields for this instance
|
typeinfo = await this.getTypeInfo(object_type.signature);
|
||||||
const fields = await this.getFieldValues(instance);
|
const fields = await this._ensureFields(typeinfo);
|
||||||
const field = fields.find(f => f.name === fieldname);
|
field = fields.find(f => f.name === fieldname);
|
||||||
if (field) {
|
if (field) {
|
||||||
return field;
|
break;
|
||||||
}
|
}
|
||||||
// if there's no matching field in this instance, check the super
|
if (!includeInherited || object_type.signature === JavaType.Object.signature) {
|
||||||
if (!includeInherited || instance.type.signature === JavaType.Object.signature) {
|
|
||||||
const fully_qualified_typename = `${object_value.type.package}.${object_value.type.typename}`;
|
const fully_qualified_typename = `${object_value.type.package}.${object_value.type.typename}`;
|
||||||
throw new Error(`No such field '${fieldname}' in type ${fully_qualified_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];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -511,8 +511,20 @@ async function evaluate_identifier(dbgr, locals, identifier) {
|
|||||||
if (local) {
|
if (local) {
|
||||||
return 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
|
// 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);
|
return evaluate_qualified_type_name(dbgr, identifier, classes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,17 +532,17 @@ async function evaluate_identifier(dbgr, locals, identifier) {
|
|||||||
*
|
*
|
||||||
* @param {Debugger} dbgr
|
* @param {Debugger} dbgr
|
||||||
* @param {string} dotted_name
|
* @param {string} dotted_name
|
||||||
* @param {*[]} classes
|
* @param {string[]} classes
|
||||||
*/
|
*/
|
||||||
async function evaluate_qualified_type_name(dbgr, dotted_name, 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_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) {
|
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 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) {
|
if (matching_classes.length === 0) {
|
||||||
// the dotted name doesn't match any packages
|
// the dotted name doesn't match any packages
|
||||||
throw new Error(`'${dotted_name}' is not a package, type or variable name`);
|
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++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
value = await evaluate_member(dbgr, locals, thread, term, value);
|
value = await evaluate_member(dbgr, term, value);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (term instanceof ArrayIndexExpression) {
|
if (term instanceof ArrayIndexExpression) {
|
||||||
@@ -822,12 +834,10 @@ async function evaluate_methodcall(dbgr, locals, thread, method_name, m, obj_loc
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Debugger} dbgr
|
* @param {Debugger} dbgr
|
||||||
* @param {DebuggerValue[]} locals
|
|
||||||
* @param {AndroidThread} thread
|
|
||||||
* @param {MemberExpression} member
|
* @param {MemberExpression} member
|
||||||
* @param {DebuggerValue} value
|
* @param {DebuggerValue} value
|
||||||
*/
|
*/
|
||||||
async function evaluate_member(dbgr, locals, thread, member, value) {
|
async function evaluate_member(dbgr, member, value) {
|
||||||
if (!JavaType.isReference(value.type)) {
|
if (!JavaType.isReference(value.type)) {
|
||||||
throw new Error('TypeError: value is not a reference type');
|
throw new Error('TypeError: value is not a reference type');
|
||||||
}
|
}
|
||||||
@@ -952,7 +962,7 @@ async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) {
|
|||||||
* @param {Debugger} dbgr
|
* @param {Debugger} dbgr
|
||||||
* @param {{allowFormatSpecifier:boolean}} [options]
|
* @param {{allowFormatSpecifier:boolean}} [options]
|
||||||
*/
|
*/
|
||||||
async function evaluate(expression, thread, locals, dbgr, options) {
|
async function evaluate_one_expression(expression, thread, locals, dbgr, options) {
|
||||||
D('evaluate: ' + expression);
|
D('evaluate: ' + expression);
|
||||||
await dbgr.ensureConnected();
|
await dbgr.ensureConnected();
|
||||||
|
|
||||||
@@ -994,6 +1004,45 @@ async function evaluate(expression, thread, locals, dbgr, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
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 = {
|
module.exports = {
|
||||||
evaluate,
|
evaluate,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,10 +88,16 @@ class UnaryOpExpression extends ParsedExpression {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TernaryExpression extends ParsedExpression {
|
class TernaryExpression extends ParsedExpression {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ParsedExpression} condition
|
||||||
|
*/
|
||||||
constructor(condition) {
|
constructor(condition) {
|
||||||
super();
|
super();
|
||||||
this.condition = condition;
|
this.condition = condition;
|
||||||
|
/** @type {ParsedExpression} */
|
||||||
this.ternary_true = null;
|
this.ternary_true = null;
|
||||||
|
/** @type {ParsedExpression} */
|
||||||
this.ternary_false = null;
|
this.ternary_false = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,17 +107,24 @@ class QualifierExpression extends ParsedExpression {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ArrayIndexExpression extends QualifierExpression {
|
class ArrayIndexExpression extends QualifierExpression {
|
||||||
constructor(e) {
|
/**
|
||||||
|
* @param {ParsedExpression} index_expression
|
||||||
|
*/
|
||||||
|
constructor(index_expression) {
|
||||||
super();
|
super();
|
||||||
this.indexExpression = e;
|
this.indexExpression = index_expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MethodCallExpression extends QualifierExpression {
|
class MethodCallExpression extends QualifierExpression {
|
||||||
|
/** @type {ParsedExpression[]} */
|
||||||
arguments = [];
|
arguments = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
class MemberExpression extends QualifierExpression {
|
class MemberExpression extends QualifierExpression {
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
constructor(name) {
|
constructor(name) {
|
||||||
super();
|
super();
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|||||||
@@ -108,6 +108,7 @@
|
|||||||
* @typedef {number} JDWPRequestID
|
* @typedef {number} JDWPRequestID
|
||||||
* @typedef {JDWPRequestID} StepID
|
* @typedef {JDWPRequestID} StepID
|
||||||
* @typedef {'caught'|'uncaught'|'both'} ExceptionBreakMode
|
* @typedef {'caught'|'uncaught'|'both'} ExceptionBreakMode
|
||||||
|
* @typedef {'ignore'|'warn'|'stop'} StaleBuildSetting
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { ADBClient } = require('./adbclient');
|
const { ADBClient } = require('./adbclient');
|
||||||
const { selectTargetDevice } = require('./utils/device');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('vscode')} vscode
|
* @param {import('vscode')} vscode
|
||||||
@@ -30,28 +29,23 @@ async function showPIDPicker(vscode, pids) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('vscode')} vscode
|
* @param {import('vscode')} vscode
|
||||||
|
* @param {string} device_serial
|
||||||
*/
|
*/
|
||||||
async function selectAndroidProcessID(vscode) {
|
async function selectAndroidProcessID(vscode, device_serial) {
|
||||||
const res = {
|
const res = {
|
||||||
/** @type {string|'ok'|'cancelled'|'failed'} */
|
/** @type {string|'ok'|'cancelled'|'failed'} */
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
pid: 0,
|
pid: 0,
|
||||||
serial: '',
|
serial: '',
|
||||||
}
|
}
|
||||||
const err = await new ADBClient().test_adb_connection()
|
|
||||||
if (err) {
|
let named_pids;
|
||||||
vscode.window.showWarningMessage('Attach failed. ADB is not running.');
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
const device = await selectTargetDevice(vscode, 'Attach');
|
|
||||||
if (!device) {
|
|
||||||
// user cancelled picker
|
|
||||||
res.status = 'cancelled';
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
let named_pids = await new ADBClient(device.serial).named_jdwp_list(5000);
|
|
||||||
if (named_pids.length === 0) {
|
if (named_pids.length === 0) {
|
||||||
vscode.window.showWarningMessage(
|
vscode.window.showWarningMessage(
|
||||||
'Attach failed. No debuggable processes are running on the device.'
|
'Attach failed. No debuggable processes are running on the device.'
|
||||||
@@ -72,7 +66,7 @@ async function selectAndroidProcessID(vscode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.pid = named_pid.pid;
|
res.pid = named_pid.pid;
|
||||||
res.serial = device.serial;
|
res.serial = device_serial;
|
||||||
res.status = 'ok';
|
res.status = 'ok';
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class DebuggerStackFrame extends VariableManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getObjectFields(varinfo) {
|
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);
|
const fields = await this.dbgr.getFieldValues(varinfo.objvar);
|
||||||
// add an extra msg field for exceptions
|
// add an extra msg field for exceptions
|
||||||
if (varinfo.exception) {
|
if (varinfo.exception) {
|
||||||
|
|||||||
Reference in New Issue
Block a user