Support multi-threading

This commit is contained in:
Leszek Swirski 2018-02-01 10:53:26 +00:00
commit ab0a3bb4be
7 changed files with 173 additions and 90 deletions

View file

@ -11,6 +11,11 @@ export interface Breakpoint {
countCondition?: string; countCondition?: string;
} }
export interface Thread {
id: number;
name: string;
}
export interface Stack { export interface Stack {
level: number; level: number;
address: string; address: string;
@ -58,9 +63,10 @@ export interface IBackend {
addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]>; addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]>;
removeBreakPoint(breakpoint: Breakpoint): Thenable<boolean>; removeBreakPoint(breakpoint: Breakpoint): Thenable<boolean>;
clearBreakPoints(): Thenable<any>; clearBreakPoints(): Thenable<any>;
getStack(maxLevels: number): Thenable<Stack[]>; getThreads(): Thenable<Thread[]>;
getStack(maxLevels: number, thread: number): Thenable<Stack[]>;
getStackVariables(thread: number, frame: number): Thenable<Variable[]>; getStackVariables(thread: number, frame: number): Thenable<Variable[]>;
evalExpression(name: string): Thenable<any>; evalExpression(name: string, thread: number, frame: number): Thenable<any>;
isReady(): boolean; isReady(): boolean;
changeVariable(name: string, rawValue: string): Thenable<any>; changeVariable(name: string, rawValue: string): Thenable<any>;
examineMemory(from: number, to: number): Thenable<any>; examineMemory(from: number, to: number): Thenable<any>;
@ -114,12 +120,8 @@ export class VariableObject {
evaluateName: this.name, evaluateName: this.name,
value: (this.value === void 0) ? "<unknown>" : this.value, value: (this.value === void 0) ? "<unknown>" : this.value,
type: this.type, type: this.type,
// kind: this.displayhint,
variablesReference: this.id variablesReference: this.id
}; };
if (this.displayhint) {
res.kind = this.displayhint;
}
return res; return res;
} }
} }

View file

@ -5,6 +5,7 @@ const variableRegex = /^[a-zA-Z_\-][a-zA-Z0-9_\-]*/;
const errorRegex = /^\<.+?\>/; const errorRegex = /^\<.+?\>/;
const referenceStringRegex = /^(0x[0-9a-fA-F]+\s*)"/; const referenceStringRegex = /^(0x[0-9a-fA-F]+\s*)"/;
const referenceRegex = /^0x[0-9a-fA-F]+/; const referenceRegex = /^0x[0-9a-fA-F]+/;
const cppReferenceRegex = /^@0x[0-9a-fA-F]+/;
const nullpointerRegex = /^0x0+\b/; const nullpointerRegex = /^0x0+\b/;
const charRegex = /^(\d+) ['"]/; const charRegex = /^(\d+) ['"]/;
const numberRegex = /^\d+(\.\d+)?/; const numberRegex = /^\d+(\.\d+)?/;
@ -168,6 +169,10 @@ export function expandValue(variableCreate: Function, value: string, root: strin
primitive = "*" + match[0]; primitive = "*" + match[0];
value = value.substr(match[0].length).trim(); value = value.substr(match[0].length).trim();
} }
else if (match = cppReferenceRegex.exec(value)) {
primitive = match[0];
value = value.substr(match[0].length).trim();
}
else if (match = charRegex.exec(value)) { else if (match = charRegex.exec(value)) {
primitive = match[1]; primitive = match[1];
value = value.substr(match[0].length - 1); value = value.substr(match[0].length - 1);
@ -222,19 +227,21 @@ export function expandValue(variableCreate: Function, value: string, root: strin
ref = variableCreate(val); ref = variableCreate(val);
val = "Object"; val = "Object";
} }
if (typeof val == "string" && val.startsWith("*0x")) { else if (typeof val == "string" && val.startsWith("*0x")) {
if (extra && MINode.valueOf(extra, "arg") == "1") if (extra && MINode.valueOf(extra, "arg") == "1") {
{
ref = variableCreate(getNamespace("*(" + name), { arg: true }); ref = variableCreate(getNamespace("*(" + name), { arg: true });
val = "<args>"; val = "<args>";
} }
else else {
{
ref = variableCreate(getNamespace("*" + name)); ref = variableCreate(getNamespace("*" + name));
val = "Object@" + val; val = "Object@" + val;
} }
} }
if (typeof val == "string" && val.startsWith("<...>")) { else if (typeof val == "string" && val.startsWith("@0x")) {
ref = variableCreate(getNamespace("*&" + name.substr));
val = "Ref" + val;
}
else if (typeof val == "string" && val.startsWith("<...>")) {
ref = variableCreate(getNamespace(name)); ref = variableCreate(getNamespace(name));
val = "..."; val = "...";
} }

View file

@ -1,4 +1,4 @@
import { Breakpoint, IBackend, Stack, SSHArguments, Variable, VariableObject, MIError } from "../backend" import { Breakpoint, IBackend, Thread, Stack, SSHArguments, Variable, VariableObject, MIError } from "../backend"
import * as ChildProcess from "child_process" import * as ChildProcess from "child_process"
import { EventEmitter } from "events" import { EventEmitter } from "events"
import { parseMI, MINode } from '../mi_parse'; import { parseMI, MINode } from '../mi_parse';
@ -365,6 +365,12 @@ export class MI2 extends EventEmitter implements IBackend {
} }
} else } else
this.log("log", JSON.stringify(parsed)); this.log("log", JSON.stringify(parsed));
} else if (record.type == "notify") {
if (record.asyncClass == "thread-created") {
this.emit("thread-created", parsed);
} else if (record.asyncClass == "thread-exited") {
this.emit("thread-exited", parsed);
}
} }
} }
}); });
@ -584,39 +590,55 @@ export class MI2 extends EventEmitter implements IBackend {
}); });
} }
getStack(maxLevels: number): Thenable<Stack[]> { async getThreads(): Promise<Thread[]> {
if (trace) if (trace) this.log("stderr", "getThreads");
this.log("stderr", "getStack");
return new Promise((resolve, reject) => { let command = "thread-info";
let command = "stack-list-frames"; let result = await this.sendCommand(command);
if (maxLevels) { let threads = result.result("threads");
command += " 0 " + maxLevels; let ret: Thread[] = [];
} return threads.map(element => {
this.sendCommand(command).then((result) => { let id = parseInt(MINode.valueOf(element, "id"));
let stack = result.result("stack"); let name = MINode.valueOf(element, "name") + "";
let ret: Stack[] = []; return {
stack.forEach(element => { id,
let level = MINode.valueOf(element, "@frame.level"); name
let addr = MINode.valueOf(element, "@frame.addr"); };
let func = MINode.valueOf(element, "@frame.func"); });
let filename = MINode.valueOf(element, "@frame.file"); }
let file = MINode.valueOf(element, "@frame.fullname");
let line = 0; async getStack(maxLevels: number, thread: number): Promise<Stack[]> {
let lnstr = MINode.valueOf(element, "@frame.line"); if (trace) this.log("stderr", "getStack");
if (lnstr)
line = parseInt(lnstr); let command = "stack-list-frames";
let from = parseInt(MINode.valueOf(element, "@frame.from")); if (thread != 0) {
ret.push({ command += ` --thread ${thread}`;
address: addr, }
fileName: filename, if (maxLevels) {
file: file, command += " 0 " + maxLevels;
function: func || from, }
level: level, let result = await this.sendCommand(command);
line: line let stack = result.result("stack");
}); let ret: Stack[] = [];
}); return stack.map(element => {
resolve(ret); let level = MINode.valueOf(element, "@frame.level");
}, reject); let addr = MINode.valueOf(element, "@frame.addr");
let func = MINode.valueOf(element, "@frame.func");
let filename = MINode.valueOf(element, "@frame.file");
let file = MINode.valueOf(element, "@frame.fullname");
let line = 0;
let lnstr = MINode.valueOf(element, "@frame.line");
if (lnstr)
line = parseInt(lnstr);
let from = parseInt(MINode.valueOf(element, "@frame.from"));
return {
address: addr,
fileName: filename,
file: file,
function: func || from,
level: level,
line: line
};
}); });
} }
@ -651,14 +673,17 @@ export class MI2 extends EventEmitter implements IBackend {
}); });
} }
evalExpression(name: string): Thenable<any> { async evalExpression(name: string, thread: number, frame: number): Promise<MINode> {
if (trace) if (trace)
this.log("stderr", "evalExpression"); this.log("stderr", "evalExpression");
return new Promise((resolve, reject) => {
this.sendCommand("data-evaluate-expression " + name).then((result) => { let command = "data-evaluate-expression ";
resolve(result); if (thread != 0) {
}, reject); command += `--thread ${thread} --frame ${frame} `;
}); }
command += name;
return await this.sendCommand(command);
} }
async varCreate(expression: string, name: string = "-"): Promise<VariableObject> { async varCreate(expression: string, name: string = "-"): Promise<VariableObject> {
@ -704,13 +729,12 @@ export class MI2 extends EventEmitter implements IBackend {
this.emit("msg", type, msg[msg.length - 1] == '\n' ? msg : (msg + "\n")); this.emit("msg", type, msg[msg.length - 1] == '\n' ? msg : (msg + "\n"));
} }
sendUserInput(command: string): Thenable<any> { sendUserInput(command: string, threadId: number = 0, frameLevel: number = 0): Thenable<any> {
if (command.startsWith("-")) { if (command.startsWith("-")) {
return this.sendCommand(command.substr(1)); return this.sendCommand(command.substr(1));
} }
else { else {
this.sendRaw(command); return this.sendCliCommand(command, threadId, frameLevel);
return Promise.resolve(undefined);
} }
} }
@ -723,6 +747,15 @@ export class MI2 extends EventEmitter implements IBackend {
this.process.stdin.write(raw + "\n"); this.process.stdin.write(raw + "\n");
} }
async sendCliCommand(command: string, threadId: number = 0, frameLevel: number = 0) {
let mi_command = "interpreter-exec ";
if (threadId != 0) {
mi_command += `--thread ${threadId} --frame ${frameLevel} `;
}
mi_command += `console "${command.replace(/[\\"']/g, '\\$&')}"`;
await this.sendCommand(mi_command);
}
sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> { sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
let sel = this.currentToken++; let sel = this.currentToken++;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -3,7 +3,7 @@ import { Stack } from "../backend"
import { MINode } from "../mi_parse" import { MINode } from "../mi_parse"
export class MI2_Mago extends MI2_LLDB { export class MI2_Mago extends MI2_LLDB {
getStack(maxLevels: number): Thenable<Stack[]> { getStack(maxLevels: number, thread: number): Promise<Stack[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let command = "stack-list-frames"; let command = "stack-list-frames";
this.sendCommand(command).then((result) => { this.sendCommand(command).then((result) => {

View file

@ -205,8 +205,10 @@ export function parseMI(output: string): MINode {
let oldContent = output; let oldContent = output;
let canBeValueList = output[0] == '['; let canBeValueList = output[0] == '[';
output = output.substr(1); output = output.substr(1);
if (output[0] == '}' || output[0] == ']') if (output[0] == '}' || output[0] == ']') {
output = output.substr(1); // ] or }
return []; return [];
}
if (canBeValueList) { if (canBeValueList) {
let value = parseValue(); let value = parseValue();
if (value) { // is value list if (value) { // is value list

View file

@ -32,7 +32,7 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum
class MagoDebugSession extends MI2DebugSession { class MagoDebugSession extends MI2DebugSession {
public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) {
super(debuggerLinesStartAt1, isServer, 0); super(debuggerLinesStartAt1, isServer);
} }
protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {

View file

@ -1,4 +1,5 @@
import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; import * as DebugAdapter from 'vscode-debugadapter';
import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, ThreadEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter';
import { DebugProtocol } from 'vscode-debugprotocol'; import { DebugProtocol } from 'vscode-debugprotocol';
import { Breakpoint, IBackend, Variable, VariableObject, ValuesFormattingMode, MIError } from './backend/backend'; import { Breakpoint, IBackend, Variable, VariableObject, ValuesFormattingMode, MIError } from './backend/backend';
import { MINode } from './backend/mi_parse'; import { MINode } from './backend/mi_parse';
@ -19,7 +20,7 @@ class ExtendedVariable {
} }
const STACK_HANDLES_START = 1000; const STACK_HANDLES_START = 1000;
const VAR_HANDLES_START = 2000; const VAR_HANDLES_START = 512 * 256 + 1000;
export class MI2DebugSession extends DebugSession { export class MI2DebugSession extends DebugSession {
protected variableHandles = new Handles<string | VariableObject | ExtendedVariable>(VAR_HANDLES_START); protected variableHandles = new Handles<string | VariableObject | ExtendedVariable>(VAR_HANDLES_START);
@ -35,12 +36,10 @@ export class MI2DebugSession extends DebugSession {
protected crashed: boolean; protected crashed: boolean;
protected debugReady: boolean; protected debugReady: boolean;
protected miDebugger: MI2; protected miDebugger: MI2;
protected threadID: number = 1;
protected commandServer: net.Server; protected commandServer: net.Server;
public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false, threadID: number = 1) { public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) {
super(debuggerLinesStartAt1, isServer); super(debuggerLinesStartAt1, isServer);
this.threadID = threadID;
} }
protected initDebugger() { protected initDebugger() {
@ -53,6 +52,8 @@ export class MI2DebugSession extends DebugSession {
this.miDebugger.on("step-end", this.handleBreak.bind(this)); this.miDebugger.on("step-end", this.handleBreak.bind(this));
this.miDebugger.on("step-out-end", this.handleBreak.bind(this)); this.miDebugger.on("step-out-end", this.handleBreak.bind(this));
this.miDebugger.on("signal-stop", this.handlePause.bind(this)); this.miDebugger.on("signal-stop", this.handlePause.bind(this));
this.miDebugger.on("thread-created", this.threadCreatedEvent.bind(this));
this.miDebugger.on("thread-exited", this.threadExitedEvent.bind(this));
this.sendEvent(new InitializedEvent()); this.sendEvent(new InitializedEvent());
try { try {
this.commandServer = net.createServer(c => { this.commandServer = net.createServer(c => {
@ -109,22 +110,39 @@ export class MI2DebugSession extends DebugSession {
} }
protected handleBreakpoint(info: MINode) { protected handleBreakpoint(info: MINode) {
this.sendEvent(new StoppedEvent("breakpoint", this.threadID)); let event = new StoppedEvent("breakpoint", parseInt(info.record("thread-id")));
(event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all";
this.sendEvent(event);
} }
protected handleBreak(info: MINode) { protected handleBreak(info: MINode) {
this.sendEvent(new StoppedEvent("step", this.threadID)); let event = new StoppedEvent("step", parseInt(info.record("thread-id")));
(event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all";
this.sendEvent(event);
} }
protected handlePause(info: MINode) { protected handlePause(info: MINode) {
this.sendEvent(new StoppedEvent("user request", this.threadID)); let event = new StoppedEvent("user request", parseInt(info.record("thread-id")));
(event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all";
this.sendEvent(event);
} }
protected stopEvent(info: MINode) { protected stopEvent(info: MINode) {
if (!this.started) if (!this.started)
this.crashed = true; this.crashed = true;
if (!this.quit) if (!this.quit) {
this.sendEvent(new StoppedEvent("exception", this.threadID)); let event = new StoppedEvent("exception", parseInt(info.record("thread-id")));
(event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all";
this.sendEvent(event);
}
}
protected threadCreatedEvent(info: MINode) {
this.sendEvent(new ThreadEvent("started", info.record("id")));
}
protected threadExitedEvent(info: MINode) {
this.sendEvent(new ThreadEvent("exited", info.record("id")));
} }
protected quitEvent() { protected quitEvent() {
@ -211,15 +229,14 @@ export class MI2DebugSession extends DebugSession {
path = relative(this.trimCWD.replace(/\\/g, "/"), path.replace(/\\/g, "/")); path = relative(this.trimCWD.replace(/\\/g, "/"), path.replace(/\\/g, "/"));
path = resolve(this.switchCWD.replace(/\\/g, "/"), path.replace(/\\/g, "/")); path = resolve(this.switchCWD.replace(/\\/g, "/"), path.replace(/\\/g, "/"));
} }
let all = []; let all = args.breakpoints.map(brk => {
args.breakpoints.forEach(brk => { return this.miDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition, countCondition: brk.hitCondition });
all.push(this.miDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition, countCondition: brk.hitCondition }));
}); });
Promise.all(all).then(brkpoints => { Promise.all(all).then(brkpoints => {
let finalBrks = []; let finalBrks = [];
brkpoints.forEach(brkp => { brkpoints.forEach(brkp => {
if (brkp[0]) if (brkp[0])
finalBrks.push({ line: brkp[1].line }); finalBrks.push(new DebugAdapter.Breakpoint(true, brkp[1].line));
}); });
response.body = { response.body = {
breakpoints: finalBrks breakpoints: finalBrks
@ -239,18 +256,31 @@ export class MI2DebugSession extends DebugSession {
} }
protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { protected threadsRequest(response: DebugProtocol.ThreadsResponse): void {
response.body = { this.miDebugger.getThreads().then(
threads: [ threads => {
new Thread(this.threadID, "Thread 1") response.body = {
] threads: []
}; };
this.sendResponse(response); for (const thread of threads) {
response.body.threads.push(new Thread(thread.id, thread.id + ":" + thread.name));
}
this.sendResponse(response);
});
}
// Supports 256 threads.
protected threadAndLevelToFrameId(threadId: number, level: number) {
return level << 8 | threadId;
}
protected frameIdToThreadAndLevel(frameId: number) {
return [frameId & 0xff, frameId >> 8];
} }
protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
this.miDebugger.getStack(args.levels).then(stack => { this.miDebugger.getStack(args.levels, args.threadId).then(stack => {
let ret: StackFrame[] = []; let ret: StackFrame[] = [];
stack.forEach(element => { stack.forEach(element => {
let source = null;
let file = element.file; let file = element.file;
if (file) { if (file) {
if (this.isSSH) { if (this.isSSH) {
@ -262,10 +292,15 @@ export class MI2DebugSession extends DebugSession {
file = file[10] + ":" + file.substr(11); // replaces /cygdrive/c/foo/bar.txt with c:/foo/bar.txt file = file[10] + ":" + file.substr(11); // replaces /cygdrive/c/foo/bar.txt with c:/foo/bar.txt
} }
} }
ret.push(new StackFrame(element.level, element.function + "@" + element.address, new Source(element.fileName, file), element.line, 0)); source = new Source(element.fileName, file);
} }
else
ret.push(new StackFrame(element.level, element.function + "@" + element.address, null, element.line, 0)); ret.push(new StackFrame(
this.threadAndLevelToFrameId(args.threadId, element.level),
element.function + "@" + element.address,
source,
element.line,
0));
}); });
response.body = { response.body = {
stackFrames: ret stackFrames: ret
@ -331,11 +366,12 @@ export class MI2DebugSession extends DebugSession {
if (typeof id == "number") { if (typeof id == "number") {
let stack: Variable[]; let stack: Variable[];
try { try {
stack = await this.miDebugger.getStackVariables(this.threadID, id); let [threadId, level] = this.frameIdToThreadAndLevel(id);
stack = await this.miDebugger.getStackVariables(threadId, level);
for (const variable of stack) { for (const variable of stack) {
if (this.useVarObjects) { if (this.useVarObjects) {
try { try {
let varObjName = `var_${variable.name}`; let varObjName = `var_${id}_${variable.name}`;
let varObj: VariableObject; let varObj: VariableObject;
try { try {
const changes = await this.miDebugger.varUpdate(varObjName); const changes = await this.miDebugger.varUpdate(varObjName);
@ -406,7 +442,8 @@ export class MI2DebugSession extends DebugSession {
// Variable members // Variable members
let variable; let variable;
try { try {
variable = await this.miDebugger.evalExpression(JSON.stringify(id)); // TODO: this evals on an (effectively) unknown thread for multithreaded programs.
variable = await this.miDebugger.evalExpression(JSON.stringify(id), 0, 0);
try { try {
let expanded = expandValue(createVariable, variable.result("value"), id, variable); let expanded = expandValue(createVariable, variable.result("value"), id, variable);
if (!expanded) { if (!expanded) {
@ -469,7 +506,8 @@ export class MI2DebugSession extends DebugSession {
this.sendResponse(response); this.sendResponse(response);
}; };
let addOne = async () => { let addOne = async () => {
const variable = await this.miDebugger.evalExpression(JSON.stringify(`${varReq.name}+${arrIndex})`)); // TODO: this evals on an (effectively) unknown thread for multithreaded programs.
const variable = await this.miDebugger.evalExpression(JSON.stringify(`${varReq.name}+${arrIndex})`), 0, 0);
try { try {
let expanded = expandValue(createVariable, variable.result("value"), varReq.name, variable); let expanded = expandValue(createVariable, variable.result("value"), varReq.name, variable);
if (!expanded) { if (!expanded) {
@ -589,8 +627,9 @@ export class MI2DebugSession extends DebugSession {
} }
protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void {
if (args.context == "watch" || args.context == "hover") let [threadId, level] = this.frameIdToThreadAndLevel(args.frameId);
this.miDebugger.evalExpression(args.expression).then((res) => { if (args.context == "watch" || args.context == "hover") {
this.miDebugger.evalExpression(args.expression, threadId, level).then((res) => {
response.body = { response.body = {
variablesReference: 0, variablesReference: 0,
result: res.result("value") result: res.result("value")
@ -599,8 +638,8 @@ export class MI2DebugSession extends DebugSession {
}, msg => { }, msg => {
this.sendErrorResponse(response, 7, msg.toString()); this.sendErrorResponse(response, 7, msg.toString());
}); });
else { } else {
this.miDebugger.sendUserInput(args.expression).then(output => { this.miDebugger.sendUserInput(args.expression, threadId, level).then(output => {
if (typeof output == "undefined") if (typeof output == "undefined")
response.body = { response.body = {
result: "", result: "",