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:
adelphes
2017-02-01 10:18:04 +00:00
parent 7b670b36ad
commit d324990e28

View File

@@ -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'));
this.sendResponse(response); // 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);
});
} }
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*/) {