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:
Dave Holoway
2020-04-24 19:03:39 +01:00
committed by GitHub
parent a4ce09d309
commit 6439e1b8b7
14 changed files with 381 additions and 138 deletions

View File

@@ -87,6 +87,10 @@ The following settings are used to configure the debugger:
// 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
}
@@ -109,6 +113,7 @@ Add a `preLaunchTask` item to the launch configuration:
"request": "launch",
"name": "App Build & Launch",
"preLaunchTask": "run gradle",
...
}
]
}
@@ -123,7 +128,22 @@ 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
}
}
]
}

View File

@@ -20,14 +20,33 @@ function activate(context) {
openLogcatWindow(vscode);
}),
// 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 });
// 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 () => {
const o = await selectAndroidProcessID(vscode);
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);
}),

View File

@@ -114,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\"",

View File

@@ -92,17 +92,31 @@ class ADBClient {
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 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 => ({
pid,
name: '',
}))
if (!named_pids.length)
return [];
// retrieve the list of process names from the device
const command = `for pid in ${named_pids.map(np => np.pid).join(' ')}; do cat /proc/$pid/cmdline;echo " $pid"; done`;
const command = `for pid in ${pids.join(' ')}; do cat /proc/$pid/cmdline;echo " $pid"; done`;
const stdout = await this.shell_cmd({
command,
untilclosed: true,

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ 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');
@@ -77,7 +77,7 @@ 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)
@@ -118,26 +118,18 @@ class AndroidDebugSession extends DebugSession {
/**
* 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);
}
@@ -167,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) {
@@ -185,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);
@@ -194,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);
@@ -203,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
@@ -258,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;
@@ -321,6 +315,17 @@ class AndroidDebugSession extends DebugSession {
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) {
@@ -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)
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||'?'}]`);
// try and attach to the specified pid
@@ -427,12 +432,28 @@ 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
* @param {*} response
* @param {*} args
* @param {import('vscode-debugprotocol').DebugProtocol.LaunchResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.LaunchRequestArguments & AndroidLaunchArguments} args
*/
async launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
async launchRequest(response, args) {
this.debug_mode = 'launch';
if (args && args.trace) {
this.trace = args.trace;
@@ -512,7 +533,7 @@ 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;
@@ -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) {
// check if any source file was modified after the apk
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) {
// we're allowed no launchActivity if we have a custom am start command
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||'?'}]`);
if (this.am_start_args) {
@@ -686,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);
@@ -702,7 +741,11 @@ 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) {
@@ -733,7 +776,7 @@ class AndroidDebugSession extends DebugSession {
}
/**
*
* Called by the debugger in response to a JDWP breakpoint hit event
* @param {JavaBreakpointEvent} e
*/
onBreakpointHit(e) {
@@ -745,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;
@@ -814,8 +859,7 @@ class AndroidDebugSession extends DebugSession {
}
/**
*
* @param {*[]} breakpoints
* @param {import('vscode-debugprotocol').DebugProtocol.SourceBreakpoint[]} breakpoints
* @param {string} relative_fpn
*/
async setupBreakpointsInFile(breakpoints, relative_fpn) {
@@ -852,7 +896,11 @@ class AndroidDebugSession extends DebugSession {
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'):
@@ -865,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();
@@ -889,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);
@@ -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}`);
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
const thread = this.getThread(threadId);
@@ -998,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.
@@ -1021,11 +1083,10 @@ class AndroidDebugSession extends DebugSession {
}
/**
*
* @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);
@@ -1109,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);
@@ -1132,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}`);
@@ -1156,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);
}
@@ -1182,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);
@@ -1252,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);
@@ -1289,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.

View File

@@ -4,16 +4,25 @@ const { PackageInfo } = require('./package-searcher');
const { splitSourcePath } = require('./utils/source-file');
class BuildInfo {
/**
* @param {string} pkgname
* @param {Map<string,PackageInfo>} packages
* @param {string} launchActivity
* @param {string[]} [amCommandArgs] custom arguments passed to `am start`
*/
constructor(pkgname, packages, launchActivity, amCommandArgs) {
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 = amCommandArgs || [
@@ -24,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);
}
}
@@ -670,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;
}
}
@@ -747,9 +765,9 @@ class VariableValue {
}
module.exports = {
AttachBuildInfo,
BreakpointLocation,
BreakpointOptions,
BuildInfo,
DebuggerBreakpoint,
DebuggerException,
DebuggerFrameInfo,
@@ -757,6 +775,7 @@ module.exports = {
DebuggerTypeInfo,
DebugSession,
DebuggerValue,
LaunchBuildInfo,
LiteralValue,
JavaBreakpointEvent,
JavaExceptionEvent,

View File

@@ -8,9 +8,9 @@ const { D } = require('./utils/print');
const { sleep } = require('./utils/thread');
const { decodeJavaStringLiteral } = require('./utils/char-decode');
const {
AttachBuildInfo,
BreakpointLocation,
BreakpointOptions,
BuildInfo,
DebuggerBreakpoint,
DebuggerFrameInfo,
DebuggerMethodInfo,
@@ -24,6 +24,7 @@ const {
JavaTaggedValue,
JavaThreadInfo,
JavaType,
LaunchBuildInfo,
MethodInvokeArgs,
SourceLocation,
TypeNotAvailable,
@@ -71,7 +72,7 @@ class Debugger extends EventEmitter {
};
/**
* @param {BuildInfo} build
* @param {LaunchBuildInfo} build
* @param {string} deviceid
*/
async startDebugSession(build, deviceid) {
@@ -82,19 +83,36 @@ class Debugger extends EventEmitter {
const stdout = await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause);
// retrieve the list of debuggable processes
const pids = await Debugger.getDebuggablePIDs(this.session.deviceid, 10e3);
if (pids.length === 0) {
const named_pids = await Debugger.getDebuggableProcesses(deviceid, 10e3);
if (named_pids.length === 0) {
throw new Error(`startDebugSession: No debuggable processes after app launch.`);
}
// choose the last pid in the list
const pid = pids[pids.length - 1];
// 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 {BuildInfo} build
* @param {AttachBuildInfo} build
* @param {number} pid process 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[]} 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(' ')}`,
@@ -301,7 +319,9 @@ class Debugger extends EventEmitter {
if (!this.session) {
return;
}
return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname);
if (this.session.build instanceof LaunchBuildInfo) {
return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname);
}
}
/**
@@ -682,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;
}
@@ -697,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);
}
@@ -743,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 {
@@ -776,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...
@@ -786,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;
}
@@ -799,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];
}
/**

View File

@@ -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) {
@@ -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');
}
@@ -952,7 +962,7 @@ async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) {
* @param {Debugger} dbgr
* @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);
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 = {
evaluate,
}

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
const os = require('os');
const { ADBClient } = require('./adbclient');
const { selectTargetDevice } = require('./utils/device');
/**
* @param {import('vscode')} vscode
@@ -30,28 +29,23 @@ async function showPIDPicker(vscode, pids) {
/**
* @param {import('vscode')} vscode
* @param {string} device_serial
*/
async function selectAndroidProcessID(vscode) {
async function selectAndroidProcessID(vscode, device_serial) {
const res = {
/** @type {string|'ok'|'cancelled'|'failed'} */
status: 'failed',
pid: 0,
serial: '',
}
const err = await new ADBClient().test_adb_connection()
if (err) {
vscode.window.showWarningMessage('Attach failed. ADB is not running.');
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;
}
const device = await selectTargetDevice(vscode, 'Attach');
if (!device) {
// user cancelled picker
res.status = 'cancelled';
return res;
}
let named_pids = await new ADBClient(device.serial).named_jdwp_list(5000);
if (named_pids.length === 0) {
vscode.window.showWarningMessage(
'Attach failed. No debuggable processes are running on the device.'
@@ -72,7 +66,7 @@ async function selectAndroidProcessID(vscode) {
}
res.pid = named_pid.pid;
res.serial = device.serial;
res.serial = device_serial;
res.status = 'ok';
return res;

View File

@@ -137,7 +137,7 @@ class DebuggerStackFrame extends VariableManager {
}
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) {