diff --git a/package.json b/package.json index bea5c72..87c8f56 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "activationEvents": [ "onCommand:code-debug.examineMemoryLocation", "onCommand:code-debug.getFileNameNoExt", - "onCommand:code-debug.getFileBasenameNoExt" + "onCommand:code-debug.getFileBasenameNoExt", + "onDebug" ], "categories": [ "Debuggers" @@ -204,6 +205,11 @@ "description": "Prints all GDB calls to the console", "default": false }, + "multiProcess": { + "type": "boolean", + "description": "Allow multiple process debugging", + "default": false + }, "showDevDebugOutput": { "type": "boolean", "description": "Prints all GDB responses to the console", @@ -594,6 +600,41 @@ } ] }, + { + "type": "mi-inferior", + "program": "./out/src/miinferior.js", + "runtime": "node", + "label": "GDB", + "languages": [ + "c", + "cpp", + "d", + "objective-c", + "fortran", + "fortran-modern", + "fortran90", + "fortran_free-form", + "fortran_fixed-form", + "rust", + "pascal", + "objectpascal", + "ada", + "nim", + "arm", + "asm", + "vala", + "crystal", + "kotlin", + "zig", + "riscv" + ], + "variables": { + "FileBasenameNoExt": "code-debug.getFileBasenameNoExt", + "FileNameNoExt": "code-debug.getFileNameNoExt" + }, + "initialConfigurations": [ ], + "configurationSnippets": [ ] + }, { "type": "lldb-mi", "program": "./out/src/lldb.js", diff --git a/src/backend/backend.ts b/src/backend/backend.ts index e26bd36..4aeed3e 100644 --- a/src/backend/backend.ts +++ b/src/backend/backend.ts @@ -64,8 +64,8 @@ export interface IBackend { start(runToStart: boolean): Thenable; stop(): void; detach(): void; - interrupt(): Thenable; - continue(): Thenable; + interrupt(threadId?: number): Thenable; + continue(reverse?: boolean, threadId?: number): Thenable; next(): Thenable; step(): Thenable; stepOut(): Thenable; diff --git a/src/backend/mi2/mi2.ts b/src/backend/mi2/mi2.ts index 1a5c2b7..50333a6 100644 --- a/src/backend/mi2/mi2.ts +++ b/src/backend/mi2/mi2.ts @@ -250,7 +250,7 @@ export class MI2 extends EventEmitter implements IBackend { target = debuggerPath.join(cwd, target); const cmds = [ - this.sendCommand("gdb-set target-async on", true), + this.sendCommand("gdb-set mi-async on", true), new Promise(resolve => { this.sendCommand("list-features").then(done => { this.features = done.result("features"); @@ -269,6 +269,16 @@ export class MI2 extends EventEmitter implements IBackend { cmds.push(this.sendCommand("enable-pretty-printing")); if (this.frameFilters) cmds.push(this.sendCommand("enable-frame-filters")); + if (this.multiProcess) { + cmds.push( + this.sendCommand("gdb-set follow-fork-mode parent"), + this.sendCommand("gdb-set detach-on-fork off"), + this.sendCommand("gdb-set non-stop on"), + this.sendCommand("gdb-set schedule-multiple on"), + + this.sendCommand("interpreter-exec console \"handle SIGSYS nostop noprint\"") + ); + } for (const cmd of this.extraCommands) { cmds.push(this.sendCommand(cmd)); } @@ -483,6 +493,10 @@ export class MI2 extends EventEmitter implements IBackend { this.emit("thread-created", parsed); } else if (record.asyncClass === "thread-exited") { this.emit("thread-exited", parsed); + } else if (record.asyncClass == "thread-group-started") { + this.emit("thread-group-started", parsed); + } else if (record.asyncClass == "thread-group-exited") { + this.emit("thread-group-exited", parsed); } } } @@ -546,51 +560,51 @@ export class MI2 extends EventEmitter implements IBackend { this.sendRaw("-target-detach"); } - interrupt(): Thenable { + interrupt(threadId?: number): Thenable { if (trace) - this.log("stderr", "interrupt"); + this.log("stderr", "interrupt" + (threadId ? " --thread " + threadId : "")); return new Promise((resolve, reject) => { - this.sendCommand("exec-interrupt").then((info) => { + this.sendCommand("exec-interrupt" + (threadId ? " --thread " + threadId : "")).then((info) => { resolve(info.resultRecords.resultClass === "done"); }, reject); }); } - continue(reverse: boolean = false): Thenable { + continue(reverse: boolean = false, threadId?: number): Thenable { if (trace) - this.log("stderr", "continue"); + this.log("stderr", "continue" + (reverse ? " --reverse" : "") + (threadId ? " --thread " + threadId : "")); return new Promise((resolve, reject) => { - this.sendCommand("exec-continue" + (reverse ? " --reverse" : "")).then((info) => { + this.sendCommand("exec-continue" + (reverse ? " --reverse" : "") + (threadId ? " --thread " + threadId : "")).then((info) => { resolve(info.resultRecords.resultClass === "running"); }, reject); }); } - next(reverse: boolean = false): Thenable { + next(reverse: boolean = false, threadId?: number): Thenable { if (trace) this.log("stderr", "next"); return new Promise((resolve, reject) => { - this.sendCommand("exec-next" + (reverse ? " --reverse" : "")).then((info) => { + this.sendCommand("exec-next" + (reverse ? " --reverse" : "") + (threadId ? " --thread " + threadId : "")).then((info) => { resolve(info.resultRecords.resultClass === "running"); }, reject); }); } - step(reverse: boolean = false): Thenable { + step(reverse: boolean = false, threadId?: number): Thenable { if (trace) this.log("stderr", "step"); return new Promise((resolve, reject) => { - this.sendCommand("exec-step" + (reverse ? " --reverse" : "")).then((info) => { + this.sendCommand("exec-step" + (reverse ? " --reverse" : "") + (threadId ? " --thread " + threadId : "")).then((info) => { resolve(info.resultRecords.resultClass === "running"); }, reject); }); } - stepOut(reverse: boolean = false): Thenable { + stepOut(reverse: boolean = false, threadId?: number): Thenable { if (trace) this.log("stderr", "stepOut"); return new Promise((resolve, reject) => { - this.sendCommand("exec-finish" + (reverse ? " --reverse" : "")).then((info) => { + this.sendCommand("exec-finish" + (reverse ? " --reverse" : "") + (threadId ? " --thread " + threadId : "")).then((info) => { resolve(info.resultRecords.resultClass === "running"); }, reject); }); @@ -1023,6 +1037,7 @@ export class MI2 extends EventEmitter implements IBackend { prettyPrint: boolean = true; frameFilters: boolean = true; + multiProcess: boolean = false; printCalls: boolean; debugOutput: boolean; features: string[]; diff --git a/src/gdb.ts b/src/gdb.ts index c1e178c..6188705 100644 --- a/src/gdb.ts +++ b/src/gdb.ts @@ -1,4 +1,4 @@ -import { MI2DebugSession, RunCommand } from './mibase'; +import { MI2DebugSession, RunCommand, SharedState } from './mibase'; import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; import { MI2, escape } from "./backend/mi2/mi2"; @@ -19,6 +19,7 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum valuesFormatting: ValuesFormattingMode; frameFilters: boolean; printCalls: boolean; + multiProcess: boolean; showDevDebugOutput: boolean; registerLimit: string; } @@ -39,11 +40,16 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum valuesFormatting: ValuesFormattingMode; frameFilters: boolean; printCalls: boolean; + multiProcess: boolean; showDevDebugOutput: boolean; registerLimit: string; } -class GDBDebugSession extends MI2DebugSession { +export class GDBDebugSession extends MI2DebugSession { + constructor(debuggerLinesStartAt1?: boolean, isServer?: boolean) { + super(new SharedState(), debuggerLinesStartAt1, isServer) + } + protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { response.body.supportsGotoTargetsRequest = true; response.body.supportsHitConditionalBreakpoints = true; @@ -63,21 +69,22 @@ class GDBDebugSession extends MI2DebugSession { this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); return; } - this.miDebugger = new MI2(dbgCommand, ["-q", "--interpreter=mi2"], args.debugger_args, args.env); + this.shared.miDebugger = new MI2(dbgCommand, ["-q", "--interpreter=mi2"], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; this.attached = false; this.initialRunCommand = RunCommand.RUN; - this.isSSH = false; + this.shared.isSSH = false; this.started = false; this.crashed = false; this.setValuesFormattingMode(args.valuesFormatting); - this.miDebugger.frameFilters = !!args.frameFilters; - this.miDebugger.printCalls = !!args.printCalls; - this.miDebugger.debugOutput = !!args.showDevDebugOutput; - this.stopAtEntry = args.stopAtEntry; - this.miDebugger.registerLimit = args.registerLimit ?? ""; + this.shared.miDebugger.frameFilters = !!args.frameFilters; + this.shared.miDebugger.printCalls = !!args.printCalls; + this.shared.miDebugger.multiProcess = !!args.multiProcess; + this.shared.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.shared.stopAtEntry = args.stopAtEntry; + this.shared.miDebugger.registerLimit = args.registerLimit ?? ""; if (args.ssh !== undefined) { if (args.ssh.forwardX11 === undefined) args.ssh.forwardX11 = true; @@ -89,15 +96,15 @@ class GDBDebugSession extends MI2DebugSession { args.ssh.x11host = "localhost"; if (args.ssh.remotex11screen === undefined) args.ssh.remotex11screen = 0; - this.isSSH = true; + this.shared.isSSH = true; this.setSourceFileMap(args.ssh.sourceFileMap, args.ssh.cwd, args.cwd); - this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, args.terminal, false, args.autorun || []).then(() => { + this.shared.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, args.terminal, false, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 105, `Failed to SSH: ${err.toString()}`); }); } else { - this.miDebugger.load(args.cwd, args.target, args.arguments, args.terminal, args.autorun || []).then(() => { + this.shared.miDebugger.load(args.cwd, args.target, args.arguments, args.terminal, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 103, `Failed to load MI Debugger: ${err.toString()}`); @@ -111,19 +118,20 @@ class GDBDebugSession extends MI2DebugSession { this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); return; } - this.miDebugger = new MI2(dbgCommand, ["-q", "--interpreter=mi2"], args.debugger_args, args.env); + this.shared.miDebugger = new MI2(dbgCommand, ["-q", "--interpreter=mi2"], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; this.attached = !args.remote; this.initialRunCommand = args.stopAtConnect ? RunCommand.NONE : RunCommand.CONTINUE; - this.isSSH = false; + this.shared.isSSH = false; this.setValuesFormattingMode(args.valuesFormatting); - this.miDebugger.frameFilters = !!args.frameFilters; - this.miDebugger.printCalls = !!args.printCalls; - this.miDebugger.debugOutput = !!args.showDevDebugOutput; - this.stopAtEntry = args.stopAtEntry; - this.miDebugger.registerLimit = args.registerLimit ?? ""; + this.shared.miDebugger.frameFilters = !!args.frameFilters; + this.shared.miDebugger.printCalls = !!args.printCalls; + this.shared.miDebugger.multiProcess = !!args.multiProcess; + this.shared.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.shared.stopAtEntry = args.stopAtEntry; + this.shared.miDebugger.registerLimit = args.registerLimit ?? ""; if (args.ssh !== undefined) { if (args.ssh.forwardX11 === undefined) args.ssh.forwardX11 = true; @@ -135,22 +143,22 @@ class GDBDebugSession extends MI2DebugSession { args.ssh.x11host = "localhost"; if (args.ssh.remotex11screen === undefined) args.ssh.remotex11screen = 0; - this.isSSH = true; + this.shared.isSSH = true; this.setSourceFileMap(args.ssh.sourceFileMap, args.ssh.cwd, args.cwd); - this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, "", undefined, true, args.autorun || []).then(() => { + this.shared.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, "", undefined, true, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 104, `Failed to SSH: ${err.toString()}`); }); } else { if (args.remote) { - this.miDebugger.connect(args.cwd, args.executable, args.target, args.autorun || []).then(() => { + this.shared.miDebugger.connect(args.cwd, args.executable, args.target, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 102, `Failed to attach: ${err.toString()}`); }); } else { - this.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { + this.shared.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 101, `Failed to attach: ${err.toString()}`); @@ -163,7 +171,7 @@ class GDBDebugSession extends MI2DebugSession { protected setPathSubstitutions(substitutions: { [index: string]: string }): void { if (substitutions) { Object.keys(substitutions).forEach(source => { - this.miDebugger.extraCommands.push("gdb-set substitute-path \"" + escape(source) + "\" \"" + escape(substitutions[source]) + "\""); + this.shared.miDebugger.extraCommands.push("gdb-set substitute-path \"" + escape(source) + "\" \"" + escape(substitutions[source]) + "\""); }); } } diff --git a/src/lldb.ts b/src/lldb.ts index 0d7bbd4..1db5b76 100644 --- a/src/lldb.ts +++ b/src/lldb.ts @@ -1,4 +1,4 @@ -import { MI2DebugSession, RunCommand } from './mibase'; +import { MI2DebugSession, RunCommand, SharedState } from './mibase'; import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; import { MI2_LLDB } from "./backend/mi2/mi2lldb"; @@ -39,6 +39,10 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum } class LLDBDebugSession extends MI2DebugSession { + constructor(debuggerLinesStartAt1?: boolean, isServer?: boolean) { + super(new SharedState(), debuggerLinesStartAt1, isServer) + } + protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { response.body.supportsGotoTargetsRequest = true; response.body.supportsHitConditionalBreakpoints = true; @@ -55,20 +59,20 @@ class LLDBDebugSession extends MI2DebugSession { this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); return; } - this.miDebugger = new MI2_LLDB(dbgCommand, [], args.debugger_args, args.env); + this.shared.miDebugger = new MI2_LLDB(dbgCommand, [], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; this.attached = false; this.initialRunCommand = RunCommand.RUN; - this.isSSH = false; + this.shared.isSSH = false; this.started = false; this.crashed = false; this.setValuesFormattingMode(args.valuesFormatting); - this.miDebugger.printCalls = !!args.printCalls; - this.miDebugger.debugOutput = !!args.showDevDebugOutput; - this.stopAtEntry = args.stopAtEntry; - this.miDebugger.registerLimit = args.registerLimit ?? ""; + this.shared.miDebugger.printCalls = !!args.printCalls; + this.shared.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.shared.stopAtEntry = args.stopAtEntry; + this.shared.miDebugger.registerLimit = args.registerLimit ?? ""; if (args.ssh !== undefined) { if (args.ssh.forwardX11 === undefined) args.ssh.forwardX11 = true; @@ -80,15 +84,15 @@ class LLDBDebugSession extends MI2DebugSession { args.ssh.x11host = "localhost"; if (args.ssh.remotex11screen === undefined) args.ssh.remotex11screen = 0; - this.isSSH = true; + this.shared.isSSH = true; this.setSourceFileMap(args.ssh.sourceFileMap, args.ssh.cwd, args.cwd); - this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, undefined, false, args.autorun || []).then(() => { + this.shared.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, undefined, false, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 106, `Failed to SSH: ${err.toString()}`); }); } else { - this.miDebugger.load(args.cwd, args.target, args.arguments, undefined, args.autorun || []).then(() => { + this.shared.miDebugger.load(args.cwd, args.target, args.arguments, undefined, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 107, `Failed to load MI Debugger: ${err.toString()}`); @@ -102,19 +106,19 @@ class LLDBDebugSession extends MI2DebugSession { this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); return; } - this.miDebugger = new MI2_LLDB(dbgCommand, [], args.debugger_args, args.env); + this.shared.miDebugger = new MI2_LLDB(dbgCommand, [], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; this.attached = true; this.initialRunCommand = args.stopAtConnect ? RunCommand.NONE : RunCommand.CONTINUE; - this.isSSH = false; + this.shared.isSSH = false; this.setValuesFormattingMode(args.valuesFormatting); - this.miDebugger.printCalls = !!args.printCalls; - this.miDebugger.debugOutput = !!args.showDevDebugOutput; - this.stopAtEntry = args.stopAtEntry; - this.miDebugger.registerLimit = args.registerLimit ?? ""; - this.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { + this.shared.miDebugger.printCalls = !!args.printCalls; + this.shared.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.shared.stopAtEntry = args.stopAtEntry; + this.shared.miDebugger.registerLimit = args.registerLimit ?? ""; + this.shared.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 108, `Failed to attach: ${err.toString()}`); @@ -125,7 +129,7 @@ class LLDBDebugSession extends MI2DebugSession { protected setPathSubstitutions(substitutions: { [index: string]: string }): void { if (substitutions) { Object.keys(substitutions).forEach(source => { - this.miDebugger.extraCommands.push("settings append target.source-map " + source + " " + substitutions[source]); + this.shared.miDebugger.extraCommands.push("settings append target.source-map " + source + " " + substitutions[source]); }); } } diff --git a/src/mago.ts b/src/mago.ts index 20f7f2f..4371030 100644 --- a/src/mago.ts +++ b/src/mago.ts @@ -1,4 +1,4 @@ -import { MI2DebugSession, RunCommand } from './mibase'; +import { MI2DebugSession, RunCommand, SharedState } from './mibase'; import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; import { MI2_Mago } from "./backend/mi2/mi2mago"; @@ -34,8 +34,8 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum } class MagoDebugSession extends MI2DebugSession { - public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { - super(debuggerLinesStartAt1, isServer); + constructor(debuggerLinesStartAt1?: boolean, isServer?: boolean) { + super(new SharedState(), debuggerLinesStartAt1, isServer) } protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { @@ -57,19 +57,19 @@ class MagoDebugSession extends MI2DebugSession { this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); return; } - this.miDebugger = new MI2_Mago(dbgCommand, ["-q"], args.debugger_args, args.env); + this.shared.miDebugger = new MI2_Mago(dbgCommand, ["-q"], args.debugger_args, args.env); this.initDebugger(); this.quit = false; this.attached = false; this.initialRunCommand = RunCommand.RUN; - this.isSSH = false; + this.shared.isSSH = false; this.started = false; this.crashed = false; this.setValuesFormattingMode(args.valuesFormatting); - this.miDebugger.printCalls = !!args.printCalls; - this.miDebugger.debugOutput = !!args.showDevDebugOutput; - this.miDebugger.registerLimit = args.registerLimit ?? ""; - this.miDebugger.load(args.cwd, args.target, args.arguments, undefined, args.autorun || []).then(() => { + this.shared.miDebugger.printCalls = !!args.printCalls; + this.shared.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.shared.miDebugger.registerLimit = args.registerLimit ?? ""; + this.shared.miDebugger.load(args.cwd, args.target, args.arguments, undefined, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 109, `Failed to load MI Debugger: ${err.toString()}`); @@ -82,17 +82,17 @@ class MagoDebugSession extends MI2DebugSession { this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); return; } - this.miDebugger = new MI2_Mago(dbgCommand, ["-q"], args.debugger_args, args.env); + this.shared.miDebugger = new MI2_Mago(dbgCommand, ["-q"], args.debugger_args, args.env); this.initDebugger(); this.quit = false; this.attached = true; this.initialRunCommand = args.stopAtConnect ? RunCommand.NONE : RunCommand.CONTINUE; - this.isSSH = false; + this.shared.isSSH = false; this.setValuesFormattingMode(args.valuesFormatting); - this.miDebugger.printCalls = !!args.printCalls; - this.miDebugger.debugOutput = !!args.showDevDebugOutput; - this.miDebugger.registerLimit = args.registerLimit ?? ""; - this.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { + this.shared.miDebugger.printCalls = !!args.printCalls; + this.shared.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.shared.miDebugger.registerLimit = args.registerLimit ?? ""; + this.shared.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { this.sendResponse(response); }, err => { this.sendErrorResponse(response, 110, `Failed to attach: ${err.toString()}`); diff --git a/src/mibase.ts b/src/mibase.ts index eb22088..fdb49cf 100644 --- a/src/mibase.ts +++ b/src/mibase.ts @@ -1,9 +1,9 @@ import * as DebugAdapter from 'vscode-debugadapter'; -import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, ThreadEvent, OutputEvent, ContinuedEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; +import * as Net from 'net'; +import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, ThreadEvent, OutputEvent, ContinuedEvent, Thread, StackFrame, Scope, Source, Handles, ExitedEvent, Event } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; import { Breakpoint, IBackend, Variable, VariableObject, ValuesFormattingMode, MIError } from './backend/backend'; import { MINode } from './backend/mi_parse'; -import { expandValue, isExpandable } from './backend/gdb_expansion'; import { MI2 } from './backend/mi2/mi2'; import { execSync } from 'child_process'; import * as systemPath from "path"; @@ -11,13 +11,16 @@ import * as net from "net"; import * as os from "os"; import * as fs from "fs"; import { SourceFileMap } from "./source_file_map"; +import { MI2InferiorServer, MI2InferiorSession } from "./miinferior" -class ExtendedVariable { +export enum RunCommand { CONTINUE, RUN, NONE } + +export class ExtendedVariable { constructor(public name: string, public options: { "arg": any }) { } } -class VariableScope { +export class VariableScope { constructor(public readonly name: string, public readonly threadId: number, public readonly level: number) { } @@ -26,44 +29,45 @@ class VariableScope { } } -export enum RunCommand { CONTINUE, RUN, NONE } +export class SharedState { + miDebugger: MI2; + threadGroupPids = new Map(); + threadToPid = new Map(); + mi2Inferiors = new Array(); + inferiorServers = new Array(); + variableHandles = new Handles(); + variableHandlesReverse: { [id: string]: number } = {}; + scopeHandlesReverse: { [key: string]: number } = {}; + stopAtEntry: boolean | string; + isSSH: boolean; + sourceFileMap: SourceFileMap; +} + +export class MI2DebugSession extends MI2InferiorSession { + public constructor(shared: SharedState, debuggerLinesStartAt1?: boolean, isServer?: boolean) { + super(shared, debuggerLinesStartAt1, isServer); + } -export class MI2DebugSession extends DebugSession { - protected variableHandles = new Handles(); - protected variableHandlesReverse: { [id: string]: number } = {}; - protected scopeHandlesReverse: { [key: string]: number } = {}; - protected useVarObjects: boolean; - protected quit: boolean; - protected attached: boolean; protected initialRunCommand: RunCommand; - protected stopAtEntry: boolean | string; - protected isSSH: boolean; - protected sourceFileMap: SourceFileMap; - protected started: boolean; - protected crashed: boolean; - protected miDebugger: MI2; protected commandServer: net.Server; protected serverPath: string; - public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { - super(debuggerLinesStartAt1, isServer); - } - protected initDebugger() { - this.miDebugger.on("launcherror", this.launchError.bind(this)); - this.miDebugger.on("quit", this.quitEvent.bind(this)); - this.miDebugger.on("exited-normally", this.quitEvent.bind(this)); - this.miDebugger.on("stopped", this.stopEvent.bind(this)); - this.miDebugger.on("msg", this.handleMsg.bind(this)); - this.miDebugger.on("breakpoint", this.handleBreakpoint.bind(this)); - this.miDebugger.on("watchpoint", this.handleBreak.bind(this)); // consider to parse old/new, too (otherwise it is in the console only) - this.miDebugger.on("step-end", this.handleBreak.bind(this)); - //this.miDebugger.on("step-out-end", this.handleBreak.bind(this)); // was combined into step-end - this.miDebugger.on("step-other", this.handleBreak.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.miDebugger.once("debug-ready", (() => this.sendEvent(new InitializedEvent()))); + this.shared.miDebugger.on("launcherror", this.launchError.bind(this)); + this.shared.miDebugger.on("stopped", this.stopEvent.bind(this)); + this.shared.miDebugger.on("msg", this.handleMsg.bind(this)); + this.shared.miDebugger.on("breakpoint", this.handleBreakpoint.bind(this)); + this.shared.miDebugger.on("watchpoint", this.handleBreak.bind(this)); // consider to parse old/new, too (otherwise it is in the console only) + this.shared.miDebugger.on("step-end", this.handleBreak.bind(this)); + //this.shared.miDebugger.on("step-out-end", this.handleBreak.bind(this)); // was combined into step-end + this.shared.miDebugger.on("step-other", this.handleBreak.bind(this)); + this.shared.miDebugger.on("signal-stop", this.handlePause.bind(this)); + this.shared.miDebugger.on("thread-created", this.threadCreatedEvent.bind(this)); + this.shared.miDebugger.on("thread-exited", this.threadExitedEvent.bind(this)); + this.shared.miDebugger.once("debug-ready", (() => this.sendEvent(new InitializedEvent()))); + this.shared.miDebugger.on("thread-group-started", this.threadGroupStartedEvent.bind(this)); + this.shared.miDebugger.on("thread-group-exited", this.threadGroupExitedEvent.bind(this)); + try { this.commandServer = net.createServer(c => { c.on("data", data => { @@ -75,7 +79,7 @@ export class MI2DebugSession extends DebugSession { func = rawCmd.substring(0, spaceIndex); args = JSON.parse(rawCmd.substring(spaceIndex + 1)); } - Promise.resolve((this.miDebugger as any)[func].apply(this.miDebugger, args)).then(data => { + Promise.resolve((this.shared.miDebugger as any)[func].apply(this.shared.miDebugger, args)).then(data => { c.write(data.toString()); }); }); @@ -114,16 +118,16 @@ export class MI2DebugSession extends DebugSession { switch (mode) { case "disabled": this.useVarObjects = true; - this.miDebugger.prettyPrint = false; + this.shared.miDebugger.prettyPrint = false; break; case "prettyPrinters": this.useVarObjects = true; - this.miDebugger.prettyPrint = true; + this.shared.miDebugger.prettyPrint = true; break; case "parseText": default: this.useVarObjects = false; - this.miDebugger.prettyPrint = false; + this.shared.miDebugger.prettyPrint = false; } } @@ -135,100 +139,217 @@ export class MI2DebugSession extends DebugSession { this.sendEvent(new OutputEvent(msg, type)); } + protected sendEventToDebugSession(pid: string, event: Event) { + if (pid == this.sessionPid) { + this.sendEvent(event); + } else { + this.shared.mi2Inferiors.forEach(inferior => { + if (pid == inferior.sessionPid) inferior.sendEvent(event) + }); + } + } + protected handleBreakpoint(info: MINode) { + let threadPid = this.shared.threadToPid.get(parseInt(info.record("thread-id"), 10)); + const event = new StoppedEvent("breakpoint", parseInt(info.record("thread-id"))); (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") === "all"; - this.sendEvent(event); + + if (threadPid == this.sessionPid) { + this.sendEvent(event); + } else { + this.shared.mi2Inferiors.forEach(inferior => { + if (threadPid == inferior.sessionPid) inferior.sendEvent(event) + }); + } } protected handleBreak(info?: MINode) { + let threadPid = this.shared.threadToPid.get(parseInt(info.record("thread-id"), 10)); + const event = new StoppedEvent("step", info ? parseInt(info.record("thread-id")) : 1); (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info ? info.record("stopped-threads") === "all" : true; - this.sendEvent(event); + + if (threadPid == this.sessionPid) { + this.sendEvent(event); + } else { + this.shared.mi2Inferiors.forEach(inferior => { + if (threadPid == inferior.sessionPid) inferior.sendEvent(event) + }); + } } protected handlePause(info: MINode) { + let threadPid = this.shared.threadToPid.get(parseInt(info.record("thread-id"), 10)); + const event = new StoppedEvent("user request", parseInt(info.record("thread-id"))); (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") === "all"; - this.sendEvent(event); + + if (threadPid == this.sessionPid) { + this.sendEvent(event); + } else { + this.shared.mi2Inferiors.forEach(inferior => { + if (threadPid == inferior.sessionPid) inferior.sendEvent(event) + }); + } } protected stopEvent(info: MINode) { if (!this.started) this.crashed = true; if (!this.quit) { + let threadPid = this.shared.threadToPid.get(parseInt(info.record("thread-id"), 10)); + const event = new StoppedEvent("exception", parseInt(info.record("thread-id"))); (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") === "all"; - this.sendEvent(event); + + if (threadPid == this.sessionPid) { + this.sendEvent(event); + } else { + this.shared.mi2Inferiors.forEach(inferior => { + if (threadPid == inferior.sessionPid) inferior.sendEvent(event) + }); + } } } protected threadCreatedEvent(info: MINode) { - this.sendEvent(new ThreadEvent("started", info.record("id"))); + let threadId = parseInt(info.record("id"), 10); + + let threadPid = this.shared.threadGroupPids.get(info.record("group-id")); + this.shared.threadToPid.set(threadId, threadPid); + + if (threadPid == this.sessionPid) { + this.sendEvent(new ThreadEvent("started", threadId)); + } else { + this.shared.mi2Inferiors.forEach(inferior => { + if (threadPid == inferior.sessionPid) inferior.sendEvent(new ThreadEvent("started", threadId)); + }); + } } protected threadExitedEvent(info: MINode) { - this.sendEvent(new ThreadEvent("exited", info.record("id"))); + let threadId = parseInt(info.record("id"), 10); + + let threadPid = this.shared.threadGroupPids.get(info.record("group-id")); + this.shared.threadToPid.delete(info.record("group-id")); + + if (threadPid == this.sessionPid) { + this.sendEvent(new ThreadEvent("exited", threadId)); + } else { + this.shared.mi2Inferiors.forEach(inferior => { + if (threadPid == inferior.sessionPid) inferior.sendEvent(new ThreadEvent("exited", threadId)); + }); + } + } + + private openInferiorDebugServer(superiorServer: MI2DebugSession) { + const server = Net.createServer((socket) => { + console.error('>> accepted connection from client'); + socket.on('end', () => { + console.error('>> client connection closed\n'); + }); + const session = new MI2InferiorServer(this.shared, false, true); + session.setRunAsServer(true); + session.start(socket, socket); + + this.shared.mi2Inferiors.push(session); + }).listen(); + + this.shared.inferiorServers.push(server); + + return server; + } + + protected threadGroupStartedEvent(info: MINode) { + if (!this.shared.miDebugger.multiProcess) { + return; + } + + let pid = info.record("pid"); + + if (typeof this.sessionPid === "undefined") { + this.sessionPid = pid; + } + + this.shared.threadGroupPids.set(info.record("id"), info.record("pid")); + + // If there are more than 1 threadgroups active, start a new debugger session in VSCode + // This makes the UI all fancy with subprocesses and threads etc. + if (this.shared.threadGroupPids.size > 1) { + // Open a new port for the new DebugSession to attach to + const server = this.openInferiorDebugServer(this).on("listening", () => { + const serverAddress = (server.address() as Net.AddressInfo).port; + const pid = info.record("pid"); + + // Necessary until vscode-debugadapter-node supports `startDebuggingRequest` + this.sendRequest('startDebugging', { + request: "attach", + configuration: { + type: "mi-inferior", + target: pid, + name: `Child (${pid})`, + cwd: "${workspaceRoot}", + debugServer: serverAddress + } + }, 1000, () => {}); + }) + } } - protected quitEvent() { + protected threadGroupExitedEvent(info: MINode) { + if (!this.shared.miDebugger.multiProcess) { + return; + } + + const threadGroupid = info.record("id"); + const pid = this.shared.threadGroupPids.get(threadGroupid); + const exit_code = info.record("exit-code"); + + // Only if the exit_code is defined, the process has exited. + // If exit_code is undefined it is still running (even if it has no threads). + // This happens, for instance, when ld-linux has finished dynamic loading. + if (typeof exit_code != "undefined") { + this.quitEvent(parseInt(pid, 10), parseInt(exit_code, 10)) + } + + this.shared.threadGroupPids.delete(info.record("id")); + } + + protected quitEvent(pid: number, exit_code: number) { this.quit = true; - this.sendEvent(new TerminatedEvent()); - if (this.serverPath) + this.sendEventToDebugSession(pid.toString(), new ExitedEvent(exit_code)); + this.sendEventToDebugSession(pid.toString(), new TerminatedEvent(false)); + + if (this.serverPath) { fs.unlink(this.serverPath, (err) => { // eslint-disable-next-line no-console console.error("Failed to unlink debug server"); }); + } } protected launchError(err: any) { this.handleMsg("stderr", "Could not start debugger process, does the program exist in filesystem?\n"); this.handleMsg("stderr", err.toString() + "\n"); - this.quitEvent(); + this.sendEvent(new TerminatedEvent(false)); } protected override disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { if (this.attached) - this.miDebugger.detach(); + this.shared.miDebugger.detach(); else - this.miDebugger.stop(); + this.shared.miDebugger.stop(); this.commandServer.close(); this.commandServer = undefined; this.sendResponse(response); } - protected override async setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): Promise { - try { - if (this.useVarObjects) { - let name = args.name; - const parent = this.variableHandles.get(args.variablesReference); - if (parent instanceof VariableScope) { - name = VariableScope.variableName(args.variablesReference, name); - } else if (parent instanceof VariableObject) { - name = `${parent.name}.${name}`; - } - - const res = await this.miDebugger.varAssign(name, args.value); - response.body = { - value: res.result("value") - }; - } else { - await this.miDebugger.changeVariable(args.name, args.value); - response.body = { - value: args.value - }; - } - this.sendResponse(response); - } catch (err) { - this.sendErrorResponse(response, 11, `Could not continue: ${err}`); - } - } - protected override setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, args: DebugProtocol.SetFunctionBreakpointsArguments): void { const all: Thenable<[boolean, Breakpoint]>[] = []; args.breakpoints.forEach(brk => { - all.push(this.miDebugger.addBreakPoint({ raw: brk.name, condition: brk.condition, countCondition: brk.hitCondition })); + all.push(this.shared.miDebugger.addBreakPoint({ raw: brk.name, condition: brk.condition, countCondition: brk.hitCondition })); }); Promise.all(all).then(brkpoints => { const finalBrks: DebugProtocol.Breakpoint[] = []; @@ -247,13 +368,13 @@ export class MI2DebugSession extends DebugSession { protected override setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { let path = args.source.path; - if (this.isSSH) { + if (this.shared.isSSH) { // convert local path to ssh path - path = this.sourceFileMap.toRemotePath(path); + path = this.shared.sourceFileMap.toRemotePath(path); } - this.miDebugger.clearBreakPoints(path).then(() => { + this.shared.miDebugger.clearBreakPoints(path).then(() => { const all = args.breakpoints.map(brk => { - return this.miDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition, countCondition: brk.hitCondition, logMessage: brk.logMessage }); + return this.shared.miDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition, countCondition: brk.hitCondition, logMessage: brk.logMessage }); }); Promise.all(all).then(brkpoints => { const finalBrks: DebugProtocol.Breakpoint[] = []; @@ -275,71 +396,6 @@ export class MI2DebugSession extends DebugSession { }); } - protected override threadsRequest(response: DebugProtocol.ThreadsResponse): void { - if (!this.miDebugger) { - this.sendResponse(response); - return; - } - this.miDebugger.getThreads().then(threads => { - response.body = { - threads: [] - }; - for (const thread of threads) { - const threadName = thread.name || thread.targetId || ""; - response.body.threads.push(new Thread(thread.id, thread.id + ":" + threadName)); - } - this.sendResponse(response); - }).catch((error: MIError) => { - if (error.message === 'Selected thread is running.') { - this.sendResponse(response); - return; - } - this.sendErrorResponse(response, 17, `Could not get threads: ${error}`); - }); - } - - // Supports 65535 threads. - protected threadAndLevelToFrameId(threadId: number, level: number) { - return level << 16 | threadId; - } - protected frameIdToThreadAndLevel(frameId: number) { - return [frameId & 0xffff, frameId >> 16]; - } - - protected override stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { - this.miDebugger.getStack(args.startFrame, args.levels, args.threadId).then(stack => { - const ret: StackFrame[] = []; - stack.forEach(element => { - let source = undefined; - let path = element.file; - if (path) { - if (this.isSSH) { - // convert ssh path to local path - path = this.sourceFileMap.toLocalPath(path); - } else if (process.platform === "win32") { - if (path.startsWith("\\cygdrive\\") || path.startsWith("/cygdrive/")) { - path = path[10] + ":" + path.substring(11); // replaces /cygdrive/c/foo/bar.txt with c:/foo/bar.txt - } - } - source = new Source(element.fileName, path); - } - - ret.push(new StackFrame( - this.threadAndLevelToFrameId(args.threadId, element.level), - element.function + (element.address ? "@" + element.address : ""), - source, - element.line, - 0)); - }); - response.body = { - stackFrames: ret - }; - this.sendResponse(response); - }, err => { - this.sendErrorResponse(response, 12, `Failed to get Stack Trace: ${err.toString()}`); - }); - } - protected override configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { const promises: Thenable[] = []; let entryPoint: string | undefined = undefined; @@ -348,28 +404,28 @@ export class MI2DebugSession extends DebugSession { switch (this.initialRunCommand) { case RunCommand.CONTINUE: case RunCommand.NONE: - if (typeof this.stopAtEntry === 'boolean' && this.stopAtEntry) + if (typeof this.shared.stopAtEntry === 'boolean' && this.shared.stopAtEntry) entryPoint = "main"; // sensible default - else if (typeof this.stopAtEntry === 'string') - entryPoint = this.stopAtEntry; + else if (typeof this.shared.stopAtEntry === 'string') + entryPoint = this.shared.stopAtEntry; break; case RunCommand.RUN: - if (typeof this.stopAtEntry === 'boolean' && this.stopAtEntry) { - if (this.miDebugger.features.includes("exec-run-start-option")) + if (typeof this.shared.stopAtEntry === 'boolean' && this.shared.stopAtEntry) { + if (this.shared.miDebugger.features.includes("exec-run-start-option")) runToStart = true; else entryPoint = "main"; // sensible fallback - } else if (typeof this.stopAtEntry === 'string') - entryPoint = this.stopAtEntry; + } else if (typeof this.shared.stopAtEntry === 'string') + entryPoint = this.shared.stopAtEntry; break; default: throw new Error('Unhandled run command: ' + RunCommand[this.initialRunCommand]); } if (entryPoint) - promises.push(this.miDebugger.setEntryBreakPoint(entryPoint)); + promises.push(this.shared.miDebugger.setEntryBreakPoint(entryPoint)); switch (this.initialRunCommand) { case RunCommand.CONTINUE: - promises.push(this.miDebugger.continue().then(() => { + promises.push(this.shared.miDebugger.continue().then(() => { // Some debuggers will provide an out-of-band status that they are stopped // when attaching (e.g., gdb), so the client assumes we are stopped and gets // confused if we start running again on our own. @@ -382,7 +438,7 @@ export class MI2DebugSession extends DebugSession { })); break; case RunCommand.RUN: - promises.push(this.miDebugger.start(runToStart).then(() => { + promises.push(this.shared.miDebugger.start(runToStart).then(() => { this.started = true; if (this.crashed) this.handlePause(undefined); @@ -409,387 +465,12 @@ export class MI2DebugSession extends DebugSession { }); } - protected override scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { - const scopes = new Array(); - const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId); - - const createScope = (scopeName: string, expensive: boolean): Scope => { - const key: string = scopeName + ":" + threadId + ":" + level; - let handle: number; - - if (this.scopeHandlesReverse.hasOwnProperty(key)) { - handle = this.scopeHandlesReverse[key]; - } else { - handle = this.variableHandles.create(new VariableScope(scopeName, threadId, level)); - this.scopeHandlesReverse[key] = handle; - } - - return new Scope(scopeName, handle, expensive); - }; - - scopes.push(createScope("Locals", false)); - scopes.push(createScope("Registers", false)); - - response.body = { - scopes: scopes - }; - this.sendResponse(response); - } - - protected override async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise { - const variables: DebugProtocol.Variable[] = []; - const id: VariableScope | string | VariableObject | ExtendedVariable = this.variableHandles.get(args.variablesReference); - - const createVariable = (arg: string | VariableObject, options?: any) => { - if (options) - return this.variableHandles.create(new ExtendedVariable(typeof arg === 'string' ? arg : arg.name, options)); - else - return this.variableHandles.create(arg); - }; - - const findOrCreateVariable = (varObj: VariableObject): number => { - let id: number; - if (this.variableHandlesReverse.hasOwnProperty(varObj.name)) { - id = this.variableHandlesReverse[varObj.name]; - } else { - id = createVariable(varObj); - this.variableHandlesReverse[varObj.name] = id; - } - return varObj.isCompound() ? id : 0; - }; - - if (id instanceof VariableScope) { - try { - if (id.name === "Registers") { - const registers = await this.miDebugger.getRegisters(); - for (const reg of registers) { - variables.push({ - name: reg.name, - value: reg.valueStr, - variablesReference: 0 - }); - } - } else { - const stack: Variable[] = await this.miDebugger.getStackVariables(id.threadId, id.level); - for (const variable of stack) { - if (this.useVarObjects) { - try { - const varObjName = VariableScope.variableName(args.variablesReference, variable.name); - let varObj: VariableObject; - try { - const changes = await this.miDebugger.varUpdate(varObjName); - const changelist = changes.result("changelist"); - changelist.forEach((change: any) => { - const name = MINode.valueOf(change, "name"); - const vId = this.variableHandlesReverse[name]; - const v = this.variableHandles.get(vId) as any; - v.applyChanges(change); - }); - const varId = this.variableHandlesReverse[varObjName]; - varObj = this.variableHandles.get(varId) as any; - } catch (err) { - if (err instanceof MIError && (err.message === "Variable object not found" || err.message.endsWith("does not exist"))) { - varObj = await this.miDebugger.varCreate(id.threadId, id.level, variable.name, varObjName); - const varId = findOrCreateVariable(varObj); - varObj.exp = variable.name; - varObj.id = varId; - } else { - throw err; - } - } - variables.push(varObj.toProtocolVariable()); - } catch (err) { - variables.push({ - name: variable.name, - value: `<${err}>`, - variablesReference: 0 - }); - } - } else { - if (variable.valueStr !== undefined) { - let expanded = expandValue(createVariable, `{${variable.name}=${variable.valueStr})`, "", variable.raw); - if (expanded) { - if (typeof expanded[0] === "string") - expanded = [ - { - name: "", - value: prettyStringArray(expanded), - variablesReference: 0 - } - ]; - variables.push(expanded[0]); - } - } else - variables.push({ - name: variable.name, - type: variable.type, - value: variable.type, - variablesReference: createVariable(variable.name) - }); - } - } - } - response.body = { - variables: variables - }; - this.sendResponse(response); - } catch (err) { - this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); - } - } else if (typeof id === "string") { - // Variable members - let variable; - try { - // TODO: this evaluates on an (effectively) unknown thread for multithreaded programs. - variable = await this.miDebugger.evalExpression(JSON.stringify(id), 0, 0); - try { - let variableValue = variable.result("value"); - const pattern = /'([^']*)' /g; - variableValue = variableValue.replace(pattern, (_: any, char: string, count: string) => { - const repeatCount = parseInt(count, 10) + 1; - const repeatedArray = Array(repeatCount).fill(char); - return `{${repeatedArray.map(item => `'${item}'`).join(', ')}}`; - }); - let expanded = expandValue(createVariable, variableValue, id, variable); - if (!expanded) { - this.sendErrorResponse(response, 2, `Could not expand variable`); - } else { - if (typeof expanded[0] === "string") - expanded = [ - { - name: "", - value: prettyStringArray(expanded), - variablesReference: 0 - } - ]; - response.body = { - variables: expanded - }; - this.sendResponse(response); - } - } catch (e) { - this.sendErrorResponse(response, 2, `Could not expand variable: ${e}`); - } - } catch (err) { - this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); - } - } else if (typeof id === "object") { - if (id instanceof VariableObject) { - // Variable members - let children: VariableObject[]; - try { - children = await this.miDebugger.varListChildren(id.name); - const vars = children.map(child => { - const varId = findOrCreateVariable(child); - child.id = varId; - return child.toProtocolVariable(); - }); - - response.body = { - variables: vars - }; - this.sendResponse(response); - } catch (err) { - this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); - } - } else if (id instanceof ExtendedVariable) { - const varReq = id; - if (varReq.options.arg) { - const strArr: DebugProtocol.Variable[] = []; - let argsPart = true; - let arrIndex = 0; - const submit = () => { - response.body = { - variables: strArr - }; - this.sendResponse(response); - }; - const addOne = async () => { - // TODO: this evaluates on an (effectively) unknown thread for multithreaded programs. - const variable = await this.miDebugger.evalExpression(JSON.stringify(`${varReq.name}+${arrIndex})`), 0, 0); - try { - const expanded = expandValue(createVariable, variable.result("value"), varReq.name, variable); - if (!expanded) { - this.sendErrorResponse(response, 15, `Could not expand variable`); - } else { - if (typeof expanded === "string") { - if (expanded === "") { - if (argsPart) - argsPart = false; - else - return submit(); - } else if (expanded[0] !== '"') { - strArr.push({ - name: "[err]", - value: expanded, - variablesReference: 0 - }); - return submit(); - } - strArr.push({ - name: `[${(arrIndex++)}]`, - value: expanded, - variablesReference: 0 - }); - addOne(); - } else { - strArr.push({ - name: "[err]", - value: expanded, - variablesReference: 0 - }); - submit(); - } - } - } catch (e) { - this.sendErrorResponse(response, 14, `Could not expand variable: ${e}`); - } - }; - addOne(); - } else - this.sendErrorResponse(response, 13, `Unimplemented variable request options: ${JSON.stringify(varReq.options)}`); - } else { - response.body = { - variables: id - }; - this.sendResponse(response); - } - } else { - response.body = { - variables: variables - }; - this.sendResponse(response); - } - } - - protected override pauseRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { - this.miDebugger.interrupt().then(done => { - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 3, `Could not pause: ${msg}`); - }); - } - - protected override reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void { - this.miDebugger.continue(true).then(done => { - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 2, `Could not continue: ${msg}`); - }); - } - - protected override continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { - this.miDebugger.continue().then(done => { - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 2, `Could not continue: ${msg}`); - }); - } - - protected override stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { - this.miDebugger.step(true).then(done => { - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 4, `Could not step back: ${msg} - Try running 'target record-full' before stepping back`); - }); - } - - protected override stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.miDebugger.step().then(done => { - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 4, `Could not step in: ${msg}`); - }); - } - - protected override stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.miDebugger.stepOut().then(done => { - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 5, `Could not step out: ${msg}`); - }); - } - - protected override nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.miDebugger.next().then(done => { - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 6, `Could not step over: ${msg}`); - }); - } - - protected override evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { - const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId); - if (args.context === "watch" || args.context === "hover") { - this.miDebugger.evalExpression(args.expression, threadId, level).then((res) => { - response.body = { - variablesReference: 0, - result: res.result("value") - }; - this.sendResponse(response); - }, msg => { - if (args.context === "hover") { - // suppress error for hover as the user may just play with the mouse - this.sendResponse(response); - } else { - this.sendErrorResponse(response, 7, msg.toString()); - } - }); - } else { - this.miDebugger.sendUserInput(args.expression, threadId, level).then(output => { - if (typeof output === "undefined") - response.body = { - result: "", - variablesReference: 0 - }; - else - response.body = { - result: JSON.stringify(output), - variablesReference: 0 - }; - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 8, msg.toString()); - }); - } - } - - protected override gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, args: DebugProtocol.GotoTargetsArguments): void { - const path: string = this.isSSH ? this.sourceFileMap.toRemotePath(args.source.path) : args.source.path; - this.miDebugger.goto(path, args.line).then(done => { - response.body = { - targets: [{ - id: 1, - label: args.source.name, - column: args.column, - line: args.line - }] - }; - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 16, `Could not jump: ${msg}`); - }); - } - - protected override gotoRequest(response: DebugProtocol.GotoResponse, args: DebugProtocol.GotoArguments): void { - this.sendResponse(response); - } - protected setSourceFileMap(configMap: { [index: string]: string }, fallbackGDB: string, fallbackIDE: string): void { if (configMap === undefined) { - this.sourceFileMap = new SourceFileMap({ [fallbackGDB]: fallbackIDE }); + this.shared.sourceFileMap = new SourceFileMap({ [fallbackGDB]: fallbackIDE }); } else { - this.sourceFileMap = new SourceFileMap(configMap, fallbackGDB); + this.shared.sourceFileMap = new SourceFileMap(configMap, fallbackGDB); } } - } -function prettyStringArray(strings: any) { - if (typeof strings === "object") { - if (strings.length !== undefined) - return strings.join(", "); - else - return JSON.stringify(strings); - } else return strings; -} diff --git a/src/miinferior.ts b/src/miinferior.ts new file mode 100644 index 0000000..ccb3737 --- /dev/null +++ b/src/miinferior.ts @@ -0,0 +1,534 @@ +import { ExtendedVariable, MI2DebugSession, SharedState, VariableScope } from './mibase'; +import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; +import { Breakpoint, IBackend, Variable, VariableObject, ValuesFormattingMode, MIError } from './backend/backend'; +import { expandValue, isExpandable } from './backend/gdb_expansion'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { setFlagsFromString } from 'v8'; +import { MI2 } from './backend/mi2/mi2'; +import { MINode } from './backend/mi_parse'; + +export interface InferiorAttachRequestArguments extends DebugProtocol.AttachRequestArguments { + target: string +} + +export class MI2InferiorSession extends DebugSession { + constructor(shared: SharedState, debuggerLinesStartAt1?: boolean, isServer?: boolean) { + super(debuggerLinesStartAt1, isServer); + this.shared = shared; + } + + protected sessionPid: string | undefined; + protected useVarObjects: boolean; + protected quit: boolean; + protected attached: boolean; + protected started: boolean; + protected crashed: boolean; + protected shared: SharedState; + + protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + // Same capabilities as GDBDebugSession + response.body.supportsGotoTargetsRequest = true; + response.body.supportsHitConditionalBreakpoints = true; + response.body.supportsConfigurationDoneRequest = true; + response.body.supportsConditionalBreakpoints = true; + response.body.supportsFunctionBreakpoints = true; + response.body.supportsEvaluateForHovers = true; + response.body.supportsSetVariable = true; + response.body.supportsStepBack = true; + response.body.supportsLogPoints = true; + this.sendResponse(response); + } + + protected override disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { + this.sendResponse(response); + } + + protected override async setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): Promise { + try { + if (this.useVarObjects) { + let name = args.name; + const parent = this.shared.variableHandles.get(args.variablesReference); + if (parent instanceof VariableScope) { + name = VariableScope.variableName(args.variablesReference, name); + } else if (parent instanceof VariableObject) { + name = `${parent.name}.${name}`; + } + + const res = await this.shared.miDebugger.varAssign(name, args.value); + response.body = { + value: res.result("value") + }; + } else { + await this.shared.miDebugger.changeVariable(args.name, args.value); + response.body = { + value: args.value + }; + } + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 11, `Could not continue: ${err}`); + } + } + + protected override threadsRequest(response: DebugProtocol.ThreadsResponse): void { + if (!this.shared.miDebugger) { + this.sendResponse(response); + return; + } + this.shared.miDebugger.getThreads().then(threads => { + response.body = { + threads: [] + }; + for (const thread of threads) { + const threadName = thread.name || thread.targetId || ""; + + let pid = this.shared.threadToPid.get(thread.id); + + if (pid == this.sessionPid) { + response.body.threads.push(new Thread(thread.id, `${thread.id}:${threadName}`)); + } + } + this.sendResponse(response); + }).catch((error: MIError) => { + if (error.message === 'Selected thread is running.') { + this.sendResponse(response); + return; + } + this.sendErrorResponse(response, 17, `Could not get threads: ${error}`); + }); + } + + // Supports 65535 threads. + protected threadAndLevelToFrameId(threadId: number, level: number) { + return level << 16 | threadId; + } + protected frameIdToThreadAndLevel(frameId: number) { + return [frameId & 0xffff, frameId >> 16]; + } + + protected override stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { + this.shared.miDebugger.getStack(args.startFrame, args.levels, args.threadId).then(stack => { + const ret: StackFrame[] = []; + stack.forEach(element => { + let source = undefined; + let path = element.file; + if (path) { + if (this.shared.isSSH) { + // convert ssh path to local path + path = this.shared.sourceFileMap.toLocalPath(path); + } else if (process.platform === "win32") { + if (path.startsWith("\\cygdrive\\") || path.startsWith("/cygdrive/")) { + path = path[10] + ":" + path.substring(11); // replaces /cygdrive/c/foo/bar.txt with c:/foo/bar.txt + } + } + source = new Source(element.fileName, path); + } + + ret.push(new StackFrame( + this.threadAndLevelToFrameId(args.threadId, element.level), + element.function + (element.address ? "@" + element.address : ""), + source, + element.line, + 0)); + }); + response.body = { + stackFrames: ret + }; + this.sendResponse(response); + }, err => { + this.sendErrorResponse(response, 12, `Failed to get Stack Trace: ${err.toString()}`); + }); + } + + protected override scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { + const scopes = new Array(); + const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId); + + const createScope = (scopeName: string, expensive: boolean): Scope => { + const key: string = scopeName + ":" + threadId + ":" + level; + let handle: number; + + if (this.shared.scopeHandlesReverse.hasOwnProperty(key)) { + handle = this.shared.scopeHandlesReverse[key]; + } else { + handle = this.shared.variableHandles.create(new VariableScope(scopeName, threadId, level)); + this.shared.scopeHandlesReverse[key] = handle; + } + + return new Scope(scopeName, handle, expensive); + }; + + scopes.push(createScope("Locals", false)); + scopes.push(createScope("Registers", false)); + + response.body = { + scopes: scopes + }; + this.sendResponse(response); + } + + protected override async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise { + const variables: DebugProtocol.Variable[] = []; + const id: VariableScope | string | VariableObject | ExtendedVariable = this.shared.variableHandles.get(args.variablesReference); + + const createVariable = (arg: string | VariableObject, options?: any) => { + if (options) + return this.shared.variableHandles.create(new ExtendedVariable(typeof arg === 'string' ? arg : arg.name, options)); + else + return this.shared.variableHandles.create(arg); + }; + + const findOrCreateVariable = (varObj: VariableObject): number => { + let id: number; + if (this.shared.variableHandlesReverse.hasOwnProperty(varObj.name)) { + id = this.shared.variableHandlesReverse[varObj.name]; + } else { + id = createVariable(varObj); + this.shared.variableHandlesReverse[varObj.name] = id; + } + return varObj.isCompound() ? id : 0; + }; + + if (id instanceof VariableScope) { + try { + if (id.name === "Registers") { + const registers = await this.shared.miDebugger.getRegisters(); + for (const reg of registers) { + variables.push({ + name: reg.name, + value: reg.valueStr, + variablesReference: 0 + }); + } + } else { + const stack: Variable[] = await this.shared.miDebugger.getStackVariables(id.threadId, id.level); + for (const variable of stack) { + if (this.useVarObjects) { + try { + const varObjName = VariableScope.variableName(args.variablesReference, variable.name); + let varObj: VariableObject; + try { + const changes = await this.shared.miDebugger.varUpdate(varObjName); + const changelist = changes.result("changelist"); + changelist.forEach((change: any) => { + const name = MINode.valueOf(change, "name"); + const vId = this.shared.variableHandlesReverse[name]; + const v = this.shared.variableHandles.get(vId) as any; + v.applyChanges(change); + }); + const varId = this.shared.variableHandlesReverse[varObjName]; + varObj = this.shared.variableHandles.get(varId) as any; + } catch (err) { + if (err instanceof MIError && (err.message === "Variable object not found" || err.message.endsWith("does not exist"))) { + varObj = await this.shared.miDebugger.varCreate(id.threadId, id.level, variable.name, varObjName); + const varId = findOrCreateVariable(varObj); + varObj.exp = variable.name; + varObj.id = varId; + } else { + throw err; + } + } + variables.push(varObj.toProtocolVariable()); + } catch (err) { + variables.push({ + name: variable.name, + value: `<${err}>`, + variablesReference: 0 + }); + } + } else { + if (variable.valueStr !== undefined) { + let expanded = expandValue(createVariable, `{${variable.name}=${variable.valueStr})`, "", variable.raw); + if (expanded) { + if (typeof expanded[0] === "string") + expanded = [ + { + name: "", + value: prettyStringArray(expanded), + variablesReference: 0 + } + ]; + variables.push(expanded[0]); + } + } else + variables.push({ + name: variable.name, + type: variable.type, + value: variable.type, + variablesReference: createVariable(variable.name) + }); + } + } + } + response.body = { + variables: variables + }; + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); + } + } else if (typeof id === "string") { + // Variable members + let variable; + try { + // TODO: this evaluates on an (effectively) unknown thread for multithreaded programs. + variable = await this.shared.miDebugger.evalExpression(JSON.stringify(id), 0, 0); + try { + let variableValue = variable.result("value"); + const pattern = /'([^']*)' /g; + variableValue = variableValue.replace(pattern, (_: any, char: string, count: string) => { + const repeatCount = parseInt(count, 10) + 1; + const repeatedArray = Array(repeatCount).fill(char); + return `{${repeatedArray.map(item => `'${item}'`).join(', ')}}`; + }); + let expanded = expandValue(createVariable, variableValue, id, variable); + if (!expanded) { + this.sendErrorResponse(response, 2, `Could not expand variable`); + } else { + if (typeof expanded[0] === "string") + expanded = [ + { + name: "", + value: prettyStringArray(expanded), + variablesReference: 0 + } + ]; + response.body = { + variables: expanded + }; + this.sendResponse(response); + } + } catch (e) { + this.sendErrorResponse(response, 2, `Could not expand variable: ${e}`); + } + } catch (err) { + this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); + } + } else if (typeof id === "object") { + if (id instanceof VariableObject) { + // Variable members + let children: VariableObject[]; + try { + children = await this.shared.miDebugger.varListChildren(id.name); + const vars = children.map(child => { + const varId = findOrCreateVariable(child); + child.id = varId; + return child.toProtocolVariable(); + }); + + response.body = { + variables: vars + }; + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); + } + } else if (id instanceof ExtendedVariable) { + const varReq = id; + if (varReq.options.arg) { + const strArr: DebugProtocol.Variable[] = []; + let argsPart = true; + let arrIndex = 0; + const submit = () => { + response.body = { + variables: strArr + }; + this.sendResponse(response); + }; + const addOne = async () => { + // TODO: this evaluates on an (effectively) unknown thread for multithreaded programs. + const variable = await this.shared.miDebugger.evalExpression(JSON.stringify(`${varReq.name}+${arrIndex})`), 0, 0); + try { + const expanded = expandValue(createVariable, variable.result("value"), varReq.name, variable); + if (!expanded) { + this.sendErrorResponse(response, 15, `Could not expand variable`); + } else { + if (typeof expanded === "string") { + if (expanded === "") { + if (argsPart) + argsPart = false; + else + return submit(); + } else if (expanded[0] !== '"') { + strArr.push({ + name: "[err]", + value: expanded, + variablesReference: 0 + }); + return submit(); + } + strArr.push({ + name: `[${(arrIndex++)}]`, + value: expanded, + variablesReference: 0 + }); + addOne(); + } else { + strArr.push({ + name: "[err]", + value: expanded, + variablesReference: 0 + }); + submit(); + } + } + } catch (e) { + this.sendErrorResponse(response, 14, `Could not expand variable: ${e}`); + } + }; + addOne(); + } else + this.sendErrorResponse(response, 13, `Unimplemented variable request options: ${JSON.stringify(varReq.options)}`); + } else { + response.body = { + variables: id + }; + this.sendResponse(response); + } + } else { + response.body = { + variables: variables + }; + this.sendResponse(response); + } + } + + protected override pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments): void { + this.shared.miDebugger.interrupt(args.threadId).then(done => { + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 3, `Could not pause: ${msg}`); + }); + } + + protected override reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void { + this.shared.miDebugger.continue(true, args.threadId).then(done => { + if (!response.hasOwnProperty("body")) { + response.body = Object(); + } + + response.body.allThreadsContinued = false; + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 2, `Could not continue: ${msg}`); + }); + } + + protected override continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + this.shared.miDebugger.continue(false, args.threadId).then(done => { + if (!response.hasOwnProperty("body")) { + response.body = Object(); + } + + response.body.allThreadsContinued = false; + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 2, `Could not continue: ${msg}`); + }); + } + + protected override stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { + this.shared.miDebugger.step(true, args.threadId).then(done => { + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 4, `Could not step back: ${msg} - Try running 'target record-full' before stepping back`); + }); + } + + protected override stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void { + this.shared.miDebugger.step(false, args.threadId).then(done => { + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 4, `Could not step in: ${msg}`); + }); + } + + protected override stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void { + this.shared.miDebugger.stepOut(false, args.threadId).then(done => { + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 5, `Could not step out: ${msg}`); + }); + } + + protected override nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + this.shared.miDebugger.next(false, args.threadId).then(done => { + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 6, `Could not step over: ${msg}`); + }); + } + + protected override evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { + const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId); + if (args.context === "watch" || args.context === "hover") { + this.shared.miDebugger.evalExpression(args.expression, threadId, level).then((res) => { + response.body = { + variablesReference: 0, + result: res.result("value") + }; + this.sendResponse(response); + }, msg => { + if (args.context === "hover") { + // suppress error for hover as the user may just play with the mouse + this.sendResponse(response); + } else { + this.sendErrorResponse(response, 7, msg.toString()); + } + }); + } else { + this.shared.miDebugger.sendUserInput(args.expression, threadId, level).then(output => { + if (typeof output === "undefined") + response.body = { + result: "", + variablesReference: 0 + }; + else + response.body = { + result: JSON.stringify(output), + variablesReference: 0 + }; + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 8, msg.toString()); + }); + } + } + + protected override gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, args: DebugProtocol.GotoTargetsArguments): void { + const path: string = this.shared.isSSH ? this.shared.sourceFileMap.toRemotePath(args.source.path) : args.source.path; + this.shared.miDebugger.goto(path, args.line).then(done => { + response.body = { + targets: [{ + id: 1, + label: args.source.name, + column: args.column, + line: args.line + }] + }; + this.sendResponse(response); + }, msg => { + this.sendErrorResponse(response, 16, `Could not jump: ${msg}`); + }); + } + + protected override gotoRequest(response: DebugProtocol.GotoResponse, args: DebugProtocol.GotoArguments): void { + this.sendResponse(response); + } +} + +function prettyStringArray(strings: any) { + if (typeof strings === "object") { + if (strings.length !== undefined) + return strings.join(", "); + else + return JSON.stringify(strings); + } else return strings; +} + +export class MI2InferiorServer extends MI2InferiorSession { + protected override attachRequest(response: DebugProtocol.AttachResponse, args: InferiorAttachRequestArguments): void { + // Attached to server + this.sessionPid = args.target; + } +} \ No newline at end of file