diff --git a/package.json b/package.json index 5454bf6..8da0ba4 100644 --- a/package.json +++ b/package.json @@ -1097,6 +1097,163 @@ } } ] + }, + { + "type": "aliceserver-mi", + "program": "./out/src/aliceserver.js", + "runtime": "node", + "label": "Aliceserver", + "languages": [ + "c", + "cpp", + "d" + ], + "configurationAttributes": { + "launch": { + "required": [ + "target" + ], + "properties": { + "target": { + "type": "string", + "description": "Path of executable" + }, + "arguments": { + "type": "string", + "description": "Arguments to append after the executable" + }, + "cwd": { + "type": "string", + "description": "project path" + }, + "aliceserver_path": { + "type": "string", + "description": "Path to the aliceserver executable or the command if in PATH", + "default": "aliceserver" + }, + "env": { + "type": "object", + "description": "Environment overriding the aliceserver environment variables", + "default": null + }, + "debugger_args": { + "type": "array", + "description": "Additional arguments to pass to specifically to aliceserver", + "default": [] + }, + "printCalls": { + "type": "boolean", + "description": "Prints all aliceserver calls to the console", + "default": false + }, + "showDevDebugOutput": { + "type": "boolean", + "description": "Prints all aliceserver responses to the console", + "default": false + } + } + }, + "attach": { + "required": [ + "target" + ], + "properties": { + "target": { + "type": "string", + "description": "PID of running program or program name" + }, + "valuesFormatting": { + "type": "string", + "description": "Set the way of showing variable values. 'disabled' - show value as is, 'parseText' - parse debuggers output text into structure, 'prettyPrinters' - enable debuggers custom pretty-printers if there are any", + "default": "parseText", + "enum": [ + "disabled", + "parseText", + "prettyPrinters" + ] + }, + "printCalls": { + "type": "boolean", + "description": "Prints all aliceserver calls to the console", + "default": false + }, + "showDevDebugOutput": { + "type": "boolean", + "description": "Prints all aliceserver responses to the console", + "default": false + }, + "executable": { + "type": "string", + "description": "Path of executable for debugging symbols" + }, + "aliceserverpath": { + "type": "string", + "description": "Path to the aliceserver executable or the command if in PATH", + "default": "aliceservere" + }, + "env": { + "type": "object", + "description": "Environment overriding the aliceserver environment variables", + "default": null + }, + "debugger_args": { + "type": "array", + "description": "Additional arguments to pass to mago", + "default": [] + }, + "cwd": { + "type": "string", + "description": "project path", + "default": "${workspaceRoot}" + }, + "autorun": { + "type": "array", + "description": "aliceserver commands to run when starting to debug", + "default": [] + }, + "stopAtConnect": { + "type": "boolean", + "description": "Whether debugger should stop after connecting to target", + "default": false + } + } + } + }, + "initialConfigurations": [ + { + "name": "Debug", + "type": "aliceserver-mi", + "request": "launch", + "target": "./bin/executable", + "cwd": "${workspaceRoot}" + } + ], + "configurationSnippets": [ + { + "label": "Aliceserver: Launch Program", + "description": "Starts the program using aliceserver", + "body": { + "type": "aliceserver", + "request": "launch", + "name": "${2:Launch Program}", + "target": "${1:./executable}", + "cwd": "^\"\\${workspaceRoot}\"", + "valuesFormatting": "parseText" + } + }, + { + "label": "Aliceserver: Attach to PID", + "description": "Attaches to a running program pid using aliceserver", + "body": { + "type": "aliceserver", + "request": "attach", + "name": "${2:Attach to PID}", + "target": "${1:[PID]}", + "cwd": "^\"\\${workspaceRoot}\"", + "valuesFormatting": "parseText" + } + } + ] } ] }, diff --git a/src/aliceserver.ts b/src/aliceserver.ts new file mode 100644 index 0000000..4def9c6 --- /dev/null +++ b/src/aliceserver.ts @@ -0,0 +1,140 @@ +// Manages a debugging session using Aliceserver. +// +// This imports and uses the MI2_ALICE class to manage its session using +// supported commands. + +import { MI2DebugSession, RunCommand } from './mibase'; +import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { MI2_ALICE } from "./backend/mi2/mi2aliceserver"; +import { SSHArguments, ValuesFormattingMode } from './backend/backend'; +import { execSync } from 'child_process'; // Temporary import + +export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { + cwd: string; + target: string; + target_arguments: string; + aliceserver_path: string; + env: any; + debugger_args: string[]; + autorun: string[]; + stopAtEntry: boolean | string; + ssh: SSHArguments; + valuesFormatting: ValuesFormattingMode; + printCalls: boolean; + showDevDebugOutput: boolean; +} + +export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { + cwd: string; + target: string; + aliceserver_path: string; + env: any; + debugger_args: string[]; + executable: string; + autorun: string[]; + stopAtConnect: boolean; + stopAtEntry: boolean | string; + printCalls: boolean; + showDevDebugOutput: boolean; +} + +class AliceserverDebugSession extends MI2DebugSession { + protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + response.body.supportsGotoTargetsRequest = false; + response.body.supportsHitConditionalBreakpoints = false; + response.body.supportsConfigurationDoneRequest = false; + response.body.supportsConditionalBreakpoints = false; + response.body.supportsFunctionBreakpoints = false; + response.body.supportsEvaluateForHovers = false; + this.sendResponse(response); + } + + // NOTE: Temporary fix that allows absolute executable paths outside of PATH + // Until Aliceserver is fully implemented. + // This fix bypasses the PATH check (performed by Windows' WHERE and + // POSIX's command(1)) by directly invoking the compiler. + protected checkCommand(debuggerName: string): boolean { + try { + execSync(`${debuggerName} --version`, { stdio: 'ignore' }); + return true; + } catch (error) { + return false; + } + } + + protected override launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + const dbgCommand = args.aliceserver_path || "aliceserver"; + if (args.aliceserver_path === undefined && this.checkCommand(dbgCommand)) { + this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); + return; + } + + this.miDebugger = new MI2_ALICE(dbgCommand, [ "-a", "mi" ], args.debugger_args, args.env); + this.initDebugger(); + + // Defaults + this.quit = false; + this.attached = false; + this.initialRunCommand = RunCommand.RUN; + this.isSSH = false; + this.started = false; + this.crashed = false; + this.miDebugger.printCalls = !!args.printCalls; + this.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.stopAtEntry = args.stopAtEntry; + + // Initiate session + this.isSSH = args.ssh !== undefined; + if (this.isSSH) { + // Set defaults if these are unset + args.ssh.forwardX11 ??= true; + args.ssh.port ??= 22; + args.ssh.x11port ??= 6000; + args.ssh.x11host ??= "localhost"; + args.ssh.remotex11screen ??= 0; + + this.setSourceFileMap(args.ssh.sourceFileMap, args.ssh.cwd, args.cwd); + this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.target_arguments, undefined, false, args.autorun || []).then(() => { + this.sendResponse(response); + }, err => { + this.sendErrorResponse(response, 106, `Failed to SSH: ${err.toString()}`); + }); + } else { // Local session + this.miDebugger.load(args.cwd, args.target, args.target_arguments, undefined, args.autorun || []).then(() => { + this.sendResponse(response); + }, err => { + this.sendErrorResponse(response, 107, `Failed to load MI Debugger: ${err.toString()}`); + }); + } + } + + protected override attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { + const dbgCommand = args.aliceserver_path || "aliceserver"; + if (args.aliceserver_path === undefined && this.checkCommand(dbgCommand)) { + this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); + return; + } + + this.miDebugger = new MI2_ALICE(dbgCommand, ["-a", "mi"], args.debugger_args, args.env); + this.initDebugger(); + + // Defaults + this.quit = false; + this.attached = true; + this.initialRunCommand = args.stopAtConnect ? RunCommand.NONE : RunCommand.CONTINUE; + this.isSSH = false; + this.miDebugger.printCalls = !!args.printCalls; + this.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.stopAtEntry = args.stopAtEntry; + + // Start session + this.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { + this.sendResponse(response); + }, err => { + this.sendErrorResponse(response, 108, `Failed to attach: ${err.toString()}`); + }); + } +} + +DebugSession.run(AliceserverDebugSession); diff --git a/src/backend/mi2/mi2aliceserver.ts b/src/backend/mi2/mi2aliceserver.ts new file mode 100644 index 0000000..3979135 --- /dev/null +++ b/src/backend/mi2/mi2aliceserver.ts @@ -0,0 +1,133 @@ +// Directly manages an Aliceserver instance by managing MI requests. + +import { MI2, escape } from "./mi2"; +import { Breakpoint } from "../backend"; +import * as ChildProcess from "child_process"; +import * as path from "path"; +import { MINode } from "../mi_parse"; + +export class MI2_ALICE extends MI2 { + protected override initCommands(target: string, cwd: string, attach: boolean = false) { + // We need to account for the possibility of the path type used by the debugger being different + // than the path type where the extension is running (e.g., SSH from Linux to Windows machine). + // Since the CWD is expected to be an absolute path in the debugger's environment, we can test + // that to determine the path type used by the debugger and use the result of that test to + // select the correct API to check whether the target path is an absolute path. + const debuggerPath = path.posix.isAbsolute(cwd) ? path.posix : path.win32; + + if (!debuggerPath.isAbsolute(target)) + target = debuggerPath.join(cwd, target); + + const cmds = [ + // Aliceserver is already async by default + //this.sendCommand("gdb-set target-async on"), + + /* Format unknown since I'm too lazy to compile lldb-mi + new Promise(resolve => { + this.sendCommand("list-features").then(done => { + this.features = done.result("features"); + resolve(undefined); + }, err => { + this.features = []; + resolve(undefined); + }); + }) as Thenable, + */ + + // TODO: environment-directory + // Command not currently supported + //this.sendCommand("environment-directory \"" + escape(cwd) + "\"", true) + ] as Thenable[]; + if (!attach) // When launching + cmds.push(this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\"")); + for (const cmd of this.extraCommands) // For the target process + cmds.push(this.sendCliCommand(cmd)); + return cmds; + } + + // Start debugging target + override start(runToStart: boolean): Thenable { + const options: string[] = []; + if (runToStart) + options.push("--start"); + const startCommand: string = ["exec-run"].concat(options).join(" "); + return new Promise((resolve, reject) => { + this.log("console", "Running executable"); + this.sendCommand(startCommand).then((info) => { + if (info.resultRecords.resultClass == "running") + resolve(undefined); + else + reject(); + }, reject); + }); + } + + override attach(cwd: string, executable: string, target: string, autorun: string[]): Thenable { + return new Promise((resolve, reject) => { + const args = this.preargs.concat(this.extraargs || []); + this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv }); + this.process.stdout.on("data", this.stdout.bind(this)); + this.process.stderr.on("data", this.stderr.bind(this)); + this.process.on("exit", () => this.emit("quit")); + this.process.on("error", err => this.emit("launcherror", err)); + const promises = this.initCommands(target, cwd, true); + promises.push(this.sendCommand("file-exec-and-symbols \"" + escape(executable) + "\"")); + promises.push(this.sendCommand("attach " + target)); + promises.push(...autorun.map(value => { return this.sendUserInput(value); })); + Promise.all(promises).then(() => { + this.emit("debug-ready"); + resolve(undefined); + }, reject); + }); + } + + override stop(): void { + this.sendRaw("-gdb-exit"); + if (this.isSSH) { + const proc = this.stream; + const to = setTimeout(() => { + proc.signal("KILL"); + }, 1000); + this.stream.on("exit", function (code) { + clearTimeout(to); + }); + } else { + const proc = this.process; + const to = setTimeout(() => { + // When tinkering with Aliceserver: + // - the proc.pid field might be undefined (when exited too early) + // - the process could no longer be found after sending requests (crashed or exited) + try + { + process.kill(-proc.pid); + } + catch (error) + { + // Warning, since it does not prevent the intent of + // continuing to shut down the server. + console.warn("Failed to terminate process: " + error); + } + }, 1000); + this.process.on("exit", function (code) { + clearTimeout(to); + }); + } + } + + override setBreakPointCondition(bkptNum: number, condition: string): Thenable { + return this.sendCommand("break-condition " + bkptNum + " \"" + escape(condition) + "\" 1"); + } + + override goto(filename: string, line: number): Thenable { + return new Promise((resolve, reject) => { + // LLDB parses the file differently than GDB... + // GDB doesn't allow quoting only the file but only the whole argument + // LLDB doesn't allow quoting the whole argument but rather only the file + const target: string = (filename ? '"' + escape(filename) + '":' : "") + line; + this.sendCliCommand("jump " + target).then(() => { + this.emit("step-other", undefined); + resolve(true); + }, reject); + }); + } +}