mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
Improved multi-threaded debug support
Include thread names in display Use a thread id mapping for vscode to fix problems with Android reusing thread ids Stepping only resumes the paused thread, Continue resumes all.
This commit is contained in:
180
src/debugMain.js
180
src/debugMain.js
@@ -1,7 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
const {
|
const {
|
||||||
DebugSession,
|
DebugSession,
|
||||||
InitializedEvent, ExitedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, OutputEvent, Event,
|
ContinuedEvent, InitializedEvent, ExitedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, OutputEvent, Event,
|
||||||
Thread, StackFrame, Scope, Source, Handles, Breakpoint } = require('vscode-debugadapter');
|
Thread, StackFrame, Scope, Source, Handles, Breakpoint } = require('vscode-debugadapter');
|
||||||
const DebugProtocol = { //require('vscode-debugprotocol');
|
const DebugProtocol = { //require('vscode-debugprotocol');
|
||||||
/** Arguments for 'launch' request. */
|
/** Arguments for 'launch' request. */
|
||||||
@@ -60,13 +60,6 @@ function is_subpath_of(fpn, subpath) {
|
|||||||
return fpn.slice(0,subpath.length) === subpath;
|
return fpn.slice(0,subpath.length) === subpath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_thread_id(tid, format) {
|
|
||||||
switch(format) {
|
|
||||||
case 'string': return ('000000000000000' + tid.toString(16)).slice(-16);
|
|
||||||
case 'int': return parseInt(tid, 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decode_char(c) {
|
function decode_char(c) {
|
||||||
switch(true) {
|
switch(true) {
|
||||||
case /^\\[^u]$/.test(c):
|
case /^\\[^u]$/.test(c):
|
||||||
@@ -127,6 +120,10 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
this._frameBaseId = 0x00010000; // high, so we don't clash with thread id's
|
this._frameBaseId = 0x00010000; // high, so we don't clash with thread id's
|
||||||
this._nextObjVarRef = 0x10000000; // high, so we don't clash with thread or frame id's
|
this._nextObjVarRef = 0x10000000; // high, so we don't clash with thread or frame id's
|
||||||
this._sourceRefs = { all:[null] }; // hashmap + array of (non-zero) source references
|
this._sourceRefs = { all:[null] }; // hashmap + array of (non-zero) source references
|
||||||
|
this._threadStates = null; // current state of threads at last stopped event
|
||||||
|
this._pausedThreads = {}; // hashmap of threadIds that vscode has been told are paused
|
||||||
|
this._nextThreadId = 0; // vscode doesn't like thread id reuse (the Android runtime is OK with it)
|
||||||
|
this._threadIDmap = []; // list of objects used to map vscode thread id's to java thread infos
|
||||||
|
|
||||||
// flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests
|
// flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests
|
||||||
this._isDisconnecting = false;
|
this._isDisconnecting = false;
|
||||||
@@ -179,6 +176,91 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_vscode_thread_id(threadid) {
|
||||||
|
return this._threadIDmap.find(x => x.threadid === threadid).vscodeid;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_java_thread_id(vscodeid) {
|
||||||
|
return this._threadIDmap.find(x => x.vscodeid === vscodeid).threadid;
|
||||||
|
}
|
||||||
|
|
||||||
|
reportStoppedEvent(reason, threadid) {
|
||||||
|
this.getThreadStates({reason, threadid})
|
||||||
|
.then((threadStates, o) => {
|
||||||
|
|
||||||
|
this._pausedThreads[o.threadid] = { threadid:o.threadid, reported:false };
|
||||||
|
var def = this.dbgr.suspendthread(o.threadid);
|
||||||
|
|
||||||
|
// Even though we request all threads to suspend on bp, step and exception events and JDWP
|
||||||
|
// reports all threads suspended, in reality, we can't get the call stacks because JDWP flunks
|
||||||
|
// an error that the other worker threads are still running.
|
||||||
|
// So... we only choose to report the currently-stopped thread, the thread named 'main' and
|
||||||
|
// any others named 'Thread-nnn' which are in the suspended state
|
||||||
|
// It's bad, but better than nothing.
|
||||||
|
var others = threadStates.filter(t => t.threadid !== o.threadid && /^(main|Thread-\d+)$/.test(t.name) && t.status.thread === 'running' && t.status.suspend==='suspended');
|
||||||
|
others.forEach(t => {
|
||||||
|
if (this._pausedThreads[t.threadid])
|
||||||
|
return; // we've already told vscode that this thread is currently paused
|
||||||
|
|
||||||
|
this._pausedThreads[t.threadid] = { threadid:t.threadid, reported:false };
|
||||||
|
def = def.then(function() {
|
||||||
|
return this.dbgr.suspendthread(this.pausedThread.threadid);
|
||||||
|
}.bind({dbgr:this.dbgr, pausedThread:this._pausedThreads[t.threadid]}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return def.then(function(){ return this }.bind(o));
|
||||||
|
})
|
||||||
|
.then(o => {
|
||||||
|
// notify vscode of any newly suspended threads
|
||||||
|
// - use the stopped thread id as the reason
|
||||||
|
var other_reason = 'Thread:'+parseInt(o.threadid,16);
|
||||||
|
for (var threadid in this._pausedThreads) {
|
||||||
|
if (threadid === o.threadid)
|
||||||
|
continue; // leave the stopped event thread until last
|
||||||
|
if (this._pausedThreads[threadid].reported)
|
||||||
|
continue; // vscode already knows this thread is paused
|
||||||
|
|
||||||
|
this._pausedThreads[threadid].reported = true;
|
||||||
|
this.sendEvent(new StoppedEvent(other_reason, this.get_vscode_thread_id(threadid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastly, tell vscode about the thread that caused the stop
|
||||||
|
this._pausedThreads[o.threadid].reported = true;
|
||||||
|
this.sendEvent(new StoppedEvent(o.reason, this.get_vscode_thread_id(o.threadid)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getThreadStates(extra) {
|
||||||
|
if (this._threadStates)
|
||||||
|
return $.Deferred().resolveWith(this,[this._threadStates, extra]);
|
||||||
|
|
||||||
|
return this.dbgr.allthreads(extra)
|
||||||
|
.then((thread_ids, extra) => this.dbgr.threadinfos(thread_ids, extra))
|
||||||
|
.then((threadinfos, extra) => {
|
||||||
|
// during startup, VSCode can request the threads while we are resuming
|
||||||
|
// - to make sure we don't use stale info, only cache it if we are not running
|
||||||
|
if (!this._running)
|
||||||
|
this._threadStates = threadinfos;
|
||||||
|
|
||||||
|
// because vscode doesn't allow threadid reuse (and the android runtime does), we need to manually
|
||||||
|
// map them.
|
||||||
|
var new_mappings = [];
|
||||||
|
threadinfos.forEach(ti => {
|
||||||
|
var existing_mapping = this._threadIDmap.find(x => x.threadid === ti.threadid && x.name === ti.name);
|
||||||
|
if (existing_mapping) {
|
||||||
|
// we have a mapping that already matches the Java thread id and name - use it
|
||||||
|
ti.vscodeid = existing_mapping.vscodeid;
|
||||||
|
new_mappings.push(existing_mapping);
|
||||||
|
} else {
|
||||||
|
// if there's no current mapping, create one
|
||||||
|
ti.vscodeid = ++this._nextThreadId;
|
||||||
|
new_mappings.push({threadid:ti.threadid, name:ti.name, vscodeid:ti.vscodeid});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._threadIDmap = new_mappings;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
|
launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
|
||||||
|
|
||||||
try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {}
|
try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {}
|
||||||
@@ -500,16 +582,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
if (!this._running) return;
|
if (!this._running) return;
|
||||||
D('Breakpoint hit: ' + JSON.stringify(e.stoppedlocation));
|
D('Breakpoint hit: ' + JSON.stringify(e.stoppedlocation));
|
||||||
this._running = false;
|
this._running = false;
|
||||||
this.sendEvent(new StoppedEvent("breakpoint", get_thread_id(e.stoppedlocation.threadid,'int')));
|
this.reportStoppedEvent("breakpoint", e.stoppedlocation.threadid);
|
||||||
}
|
|
||||||
|
|
||||||
markAllThreadsStopped(reason, exclude) {
|
|
||||||
this.dbgr.allthreads(reason)
|
|
||||||
.then(threads => {
|
|
||||||
if (Array.isArray(exclude))
|
|
||||||
threads = threads.filter(t => !exclude.includes(t));
|
|
||||||
threads.forEach(tid => this.sendEvent(new StoppedEvent(reason, get_thread_id(tid,'int'))));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -643,12 +716,13 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
|
|
||||||
threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
|
threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
|
||||||
|
|
||||||
this.dbgr.allthreads(response)
|
this.getThreadStates(response)
|
||||||
.then((threads, response) => {
|
.then((threadStates,response) => {
|
||||||
// convert the (hex) thread strings into real numbers
|
|
||||||
var tids = threads.map(tid => get_thread_id(tid,'int'));
|
|
||||||
response.body = {
|
response.body = {
|
||||||
threads: tids.map(tid => new Thread(tid, `Thread (id:${tid})`))
|
threads: threadStates.map(t => {
|
||||||
|
var javaid = parseInt(t.threadid, 16);
|
||||||
|
return new Thread(t.vscodeid, `Thread (id:${javaid}) ${t.name}`);
|
||||||
|
})
|
||||||
};
|
};
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
})
|
})
|
||||||
@@ -664,7 +738,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
stackTraceRequest(response/*: DebugProtocol.StackTraceResponse*/, args/*: DebugProtocol.StackTraceArguments*/) {
|
stackTraceRequest(response/*: DebugProtocol.StackTraceResponse*/, args/*: DebugProtocol.StackTraceArguments*/) {
|
||||||
|
|
||||||
// debugger threadid's are a padded 64bit hex string
|
// debugger threadid's are a padded 64bit hex string
|
||||||
var threadid = get_thread_id(args.threadId, 'string');
|
var threadid = this.get_java_thread_id(args.threadId);
|
||||||
// retrieve the (stack) frames from the debugger
|
// retrieve the (stack) frames from the debugger
|
||||||
this.dbgr.getframes(threadid, {response:response, args:args})
|
this.dbgr.getframes(threadid, {response:response, args:args})
|
||||||
.then((frames, x) => {
|
.then((frames, x) => {
|
||||||
@@ -726,6 +800,9 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
totalFrames: totalFrames,
|
totalFrames: totalFrames,
|
||||||
};
|
};
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
|
})
|
||||||
|
.fail((e,x) => {
|
||||||
|
this.failRequest('No call stack is available', response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1000,27 +1077,41 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
|
|
||||||
continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) {
|
continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) {
|
||||||
D('Continue');
|
D('Continue');
|
||||||
|
|
||||||
|
var multiple_threads_stopped = Object.keys(this._pausedThreads).length > 1;
|
||||||
|
// undo the manual suspensions for all the paused threads
|
||||||
|
var unsuspend = $.Deferred().resolve();
|
||||||
|
for (var threadid in this._pausedThreads) {
|
||||||
|
unsuspend = unsuspend.then(function() {
|
||||||
|
return this.dbgr.resumethread(this.pausedThread.threadid);
|
||||||
|
}.bind({dbgr:this.dbgr, pausedThread:this._pausedThreads[threadid]}));
|
||||||
|
|
||||||
|
delete this._pausedThreads[threadid];
|
||||||
|
}
|
||||||
|
|
||||||
this._variableHandles = {};
|
this._variableHandles = {};
|
||||||
this._last_exception = null;
|
this._last_exception = null;
|
||||||
this._locals_done = {};
|
this._locals_done = {};
|
||||||
|
this._threadStates = null;
|
||||||
|
|
||||||
// sometimes, the device is so quick that a breakpoint is hit
|
// sometimes, the device is so quick that a breakpoint is hit
|
||||||
// before we've completed the resume promise chain.
|
// before we've completed the resume promise chain.
|
||||||
// so tell the client that we've resumed now and just send a StoppedEvent
|
// so tell the client that we've resumed now and just send a StoppedEvent
|
||||||
// if it ends up failing
|
// if it ends up failing
|
||||||
this._running = true;
|
this._running = true;
|
||||||
this.dbgr.resume()
|
unsuspend.then(() => {
|
||||||
|
return this.dbgr.resume()
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (args.is_start)
|
if (args.is_start)
|
||||||
this.LOG(`App started`);
|
this.LOG(`App started`);
|
||||||
})
|
})
|
||||||
.fail(() => {
|
|
||||||
if (!response)
|
this.sendResponse(response);
|
||||||
this.sendEvent(new StoppedEvent('Continue failed'));
|
if (multiple_threads_stopped) {
|
||||||
this.failRequest('Resume command failed', response);
|
// send an additional event to indicate that all threads are running again
|
||||||
response = null;
|
this.sendEvent(new ContinuedEvent(args.threadId, true));
|
||||||
});
|
}
|
||||||
response && this.sendResponse(response) && D('Sent continue response');
|
|
||||||
response = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1031,7 +1122,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
if (!this._running) return;
|
if (!this._running) return;
|
||||||
D('step hit: ' + JSON.stringify(e.stoppedlocation));
|
D('step hit: ' + JSON.stringify(e.stoppedlocation));
|
||||||
this._running = false;
|
this._running = false;
|
||||||
this.sendEvent(new StoppedEvent("step", get_thread_id(e.stoppedlocation.threadid,'int')));
|
this.reportStoppedEvent("step", e.stoppedlocation.threadid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1042,9 +1133,26 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
this._variableHandles = {};
|
this._variableHandles = {};
|
||||||
this._last_exception = null;
|
this._last_exception = null;
|
||||||
this._locals_done = {};
|
this._locals_done = {};
|
||||||
|
this._threadStates = null;
|
||||||
this._running = true;
|
this._running = true;
|
||||||
this.dbgr.step(which, get_thread_id(args.threadId,'string'));
|
|
||||||
|
// when we step, manually resume the (single) thread we are stepping and remove it from the list of paused threads
|
||||||
|
// - any other paused threads should remain suspended during the step
|
||||||
|
var unpause = $.Deferred().resolve();
|
||||||
|
var threadid = this.get_java_thread_id(args.threadId);
|
||||||
|
var pausedThread = this._pausedThreads[threadid];
|
||||||
|
if (pausedThread) {
|
||||||
|
unpause = unpause.then(function() {
|
||||||
|
return this.dbgr.resumethread(this.pausedThread.threadid);
|
||||||
|
}.bind({dbgr:this.dbgr, pausedThread:pausedThread}));
|
||||||
|
|
||||||
|
delete this._pausedThreads[threadid];
|
||||||
|
}
|
||||||
|
|
||||||
|
unpause.then(() => {
|
||||||
|
this.dbgr.step(which, threadid);
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) {
|
stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) {
|
||||||
@@ -1074,7 +1182,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
varref: ++this._nextObjVarRef,
|
varref: ++this._nextObjVarRef,
|
||||||
};
|
};
|
||||||
this._variableHandles[this._last_exception.varref] = this._last_exception;
|
this._variableHandles[this._last_exception.varref] = this._last_exception;
|
||||||
this.sendEvent(new StoppedEvent("exception", get_thread_id(e.throwlocation.threadid,'int')));
|
this.reportStoppedEvent("exception", e.throwlocation.threadid);
|
||||||
}
|
}
|
||||||
|
|
||||||
setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) {
|
setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) {
|
||||||
|
|||||||
Reference in New Issue
Block a user