diff --git a/README.md b/README.md index 9602203..31b6d89 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,20 @@ Native VSCode debugger. Currently only using GDB. ## Installation -Run `ext install debug` (Ctrl-Shift-P -> install extension + make sure its just called `Debug` and at the right it should say `webfreak`) in visual studio code and install GDB and add the executable to your PATH variable. If you have changed your PATH, don't forget to restart vscode. Then follow the usage tutorial below. +Run `ext install debug` (Ctrl-Shift-P -> install extension + make sure its just called `Debug` and at the right it should say `webfreak`) in visual studio code and install GDB/LLDB and add the executable to your PATH variable. If you have changed your PATH, don't forget to restart vscode. Then follow the usage tutorial below. ![Preview](images/preview.png) ## Usage -![Image with red circle around a cog and an orange arrow pointing at GDB](images/tutorial1.png) +![Image with red circle around a gear and a red arrow pointing at GDB and LLDB](images/tutorial1.png) Open your project and click the debug button in your sidebar. At the top right press -the little gear icon and select GDB. It will automatically generate the configuration +the little gear icon and select GDB or LLDB. It will automatically generate the configuration you need. +*Note: for LLDB you need to have lldb-mi in your PATH* + ![Default config with a red circle around the target](images/tutorial2.png) Now you need to change `target` to the application you want to debug relative @@ -35,12 +37,12 @@ while its paused works as expected. Extending variables is very limited as it does not support child values of variables. Watching expressions works partially but the result does not get properly parsed and -it shows the raw GDB output of the command. It will run `data-evaluate-expression` +it shows the raw output of the command. It will run `data-evaluate-expression` to check for variables. -While running you will get a console where you can manually type GDB commands or GDB/MI -commands prepended with a hyphen `-`. The console shows all output GDB gives separated -in `stdout` for the application, `stderr` for errors and `log` for GDB log messages. +While running you will get a console where you can manually type GDB/LLDB commands or MI +commands prepended with a hyphen `-`. The console shows all output separated +in `stdout` for the application, `stderr` for errors and `log` for log messages. Some exceptions/signals like segmentation faults will be catched and displayed but it does not support for example most D exceptions. @@ -49,7 +51,7 @@ it does not support for example most D exceptions. Attaching to existing processes currently only works by specifying the PID in the `launch.json` and setting `request` to `"attach"`. You also need to specify the executable -path for GDB to find the debug symbols. +path for the debugger to find the debug symbols. ``` "request": "attach", @@ -57,9 +59,9 @@ path for GDB to find the debug symbols. "target": "4285" ``` -This will attach to PID 4285 which should already run. GDB will pause the program on entering. +This will attach to PID 4285 which should already run. GDB will pause the program on entering and LLDB will keep it running. -### Using `gdbserver` for remote debugging +### Using `gdbserver` for remote debugging (GDB only) You can also connect to a gdbserver instance and debug using that. For that modify the `launch.json` by setting `request` to `"attach"` and `remote` to `true` and specifing the diff --git a/images/preview.png b/images/preview.png index ea8335d..dfbcd81 100644 Binary files a/images/preview.png and b/images/preview.png differ diff --git a/images/tutorial1.png b/images/tutorial1.png index fd96d9a..73920d3 100644 Binary files a/images/tutorial1.png and b/images/tutorial1.png differ diff --git a/images/tutorial2.png b/images/tutorial2.png index dd08443..2540f0d 100644 Binary files a/images/tutorial2.png and b/images/tutorial2.png differ diff --git a/package.json b/package.json index e1ed13b..546332b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "debug", "displayName": "Debug", - "description": "Native debugging for VSCode - Currently in GDB only beta", + "description": "GDB & LLDB support for VSCode", "version": "0.5.0", "publisher": "webfreak", "icon": "images/icon-plain.svg", @@ -172,7 +172,170 @@ "name": "Debug", "type": "gdb", "request": "launch", - "target": "./output", + "target": "./bin/executable", + "cwd": "${workspaceRoot}" + } + ] + }, + { + "type": "lldb-mi", + "extensions": [], + "program": "./out/src/lldb.js", + "runtime": "node", + "label": "LLDB", + "enableBreakpointsFor": { + "languageIds": [ + "c", + "ada", + "cpp", + "cobol", + "fortran", + "pascal", + "modula", + "java", + "pli", + "objective-c", + "objective-cpp", + "d", + "python", + "opencl", + "modula3", + "haskell", + "ocaml", + "swift", + "julia", + "dylan", + "mips", + "renderscript" + ] + }, + "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": "Path of project" + }, + "printCalls": { + "type": "boolean", + "description": "Prints all lldb calls to console", + "default": false + }, + "autorun": { + "type": "array", + "description": "lldb commands to run when starting to debug", + "default": [] + }, + "ssh": { + "required": [ + "host", + "cwd", + "user" + ], + "type": "object", + "description": "If this is set then the extension will connect to an ssh host and run lldb there", + "properties": { + "host": { + "type": "string", + "description": "Remote host name/ip to connect to" + }, + "cwd": { + "type": "string", + "description": "Path of project on the remote" + }, + "port": { + "type": "number", + "description": "Remote port number", + "default": 22 + }, + "user": { + "type": "string", + "description": "Username to connect as" + }, + "password": { + "type": "string", + "description": "Plain text password (unsafe; if possible use keyfile instead)" + }, + "keyfile": { + "type": "string", + "description": "Absolute path to private key" + }, + "forwardX11": { + "type": "boolean", + "description": "If true, the server will redirect x11 to the local host", + "default": true + }, + "x11port": { + "type": "number", + "description": "Port to redirect X11 data to (by default port = display + 6000)", + "default": 6000 + }, + "x11host": { + "type": "string", + "description": "Hostname/ip to redirect X11 data to", + "default": "localhost" + }, + "remotex11screen": { + "type": "number", + "description": "Screen to start the application on the remote side", + "default": 0 + }, + "bootstrap": { + "type": "string", + "description": "Content will be executed on the SSH host before the debugger call." + } + } + } + } + }, + "attach": { + "required": [ + "target" + ], + "properties": { + "target": { + "type": "string", + "description": "PID of running program or program name" + }, + "printCalls": { + "type": "boolean", + "description": "Prints all LLDB calls to console", + "default": false + }, + "executable": { + "type": "string", + "description": "Path of executable for debugging symbols" + }, + "cwd": { + "type": "string", + "description": "Path of project", + "default": "${workspaceRoot}" + }, + "autorun": { + "type": "array", + "description": "LLDB commands to run when starting to debug", + "default": [] + } + } + } + }, + "initialConfigurations": [ + { + "name": "Debug", + "type": "lldb-mi", + "request": "launch", + "target": "./bin/executable", "cwd": "${workspaceRoot}" } ] @@ -193,4 +356,4 @@ "typescript": "^1.7.5", "vscode": "0.11.x" } -} +} \ No newline at end of file diff --git a/src/backend/gdb_expansion.ts b/src/backend/gdb_expansion.ts index 7cf4174..db9e187 100644 --- a/src/backend/gdb_expansion.ts +++ b/src/backend/gdb_expansion.ts @@ -2,6 +2,8 @@ const resultRegex = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*)\s*=\s*/; const variableRegex = /^[a-zA-Z_\-][a-zA-Z0-9_\-]*/; const errorRegex = /^\<.+?\>/; const referenceRegex = /^0x[0-9a-fA-F]+/; +const nullpointerRegex = /^0x0+\b/; +const charRegex = /^([0-9]+) ['"]/; const numberRegex = /^[0-9]+/; const pointerCombineChar = "."; @@ -10,11 +12,13 @@ export function isExpandable(value: string): number { let match; value = value.trim(); if (value.length == 0) return 0; + else if (value.startsWith("{...}")) return 2; // lldb string/array else if (value[0] == '{') return 1; // object else if (value.startsWith("true")) return 0; else if (value.startsWith("false")) return 0; - else if (value.startsWith("0x0")) return 0; + else if (match = nullpointerRegex.exec(value)) return 0; else if (match = referenceRegex.exec(value)) return 2; // reference + else if (match = charRegex.exec(value)) return 0; else if (match = numberRegex.exec(value)) return 0; else if (match = variableRegex.exec(value)) return 0; else if (match = errorRegex.exec(value)) return 0; @@ -24,10 +28,11 @@ export function isExpandable(value: string): number { export function expandValue(variableCreate: Function, value: string, root: string = ""): any { let parseCString = () => { value = value.trim(); - if (value[0] != '"') + if (value[0] != '"' && value[0] != '\'') return ""; let stringEnd = 1; let inString = true; + let charStr = value[0]; let remaining = value.substr(1); let escaped = false; while (inString) { @@ -35,7 +40,7 @@ export function expandValue(variableCreate: Function, value: string, root: strin escaped = false; else if (remaining[0] == '\\') escaped = true; - else if (remaining[0] == '"') + else if (remaining[0] == charStr) inString = false; remaining = remaining.substr(1); @@ -82,8 +87,17 @@ export function expandValue(variableCreate: Function, value: string, root: strin return undefined; let oldContent = value; value = value.substr(1).trim(); - if (value[0] == '}') + if (value[0] == '}') { + value = value.substr(1).trim(); return []; + } + if (value.startsWith("...")) { + value = value.substr(3).trim(); + if (value[0] == '}') { + value = value.substr(1).trim(); + return "<...>"; + } + } let eqPos = value.indexOf("="); let newValPos1 = value.indexOf("{"); let newValPos2 = value.indexOf(","); @@ -138,7 +152,7 @@ export function expandValue(variableCreate: Function, value: string, root: strin primitive = "false"; value = value.substr(5).trim(); } - else if (value.startsWith("0x0")) { + else if (match = nullpointerRegex.exec(value)) { primitive = ""; value = value.substr(3).trim(); } @@ -146,6 +160,11 @@ export function expandValue(variableCreate: Function, value: string, root: strin primitive = "*" + match[0]; value = value.substr(match[0].length).trim(); } + else if (match = charRegex.exec(value)) { + primitive = match[1]; + value = value.substr(match[0].length - 1); + primitive += " " + parseCString(); + } else if (match = numberRegex.exec(value)) { primitive = match[0]; value = value.substr(match[0].length).trim(); @@ -199,6 +218,10 @@ export function expandValue(variableCreate: Function, value: string, root: strin ref = variableCreate(getNamespace("*" + name)); val = "Object@" + val; } + if (typeof val == "string" && val.startsWith("<...>")) { + ref = variableCreate(getNamespace(name)); + val = "..."; + } return { name: name, value: val, diff --git a/src/backend/mi2/mi2.ts b/src/backend/mi2/mi2.ts index dd480cb..805e13e 100644 --- a/src/backend/mi2/mi2.ts +++ b/src/backend/mi2/mi2.ts @@ -10,7 +10,7 @@ import * as nativePath from "path" let path = posix; var Client = require("ssh2").Client; -function escape(str: string) { +export function escape(str: string) { return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\""); } @@ -32,14 +32,11 @@ export class MI2 extends EventEmitter implements IBackend { target = nativePath.join(cwd, target); return new Promise((resolve, reject) => { this.isSSH = false; - this.process = ChildProcess.spawn(this.application, this.preargs.concat([target]), { cwd: cwd }); + this.process = ChildProcess.spawn(this.application, this.preargs, { cwd: cwd }); this.process.stdout.on("data", this.stdout.bind(this)); this.process.stderr.on("data", this.stdout.bind(this)); this.process.on("exit", (() => { this.emit("quit"); }).bind(this)); - let promises = [ - this.sendCommand("gdb-set target-async on"), - this.sendCommand("environment-directory \"" + escape(cwd) + "\"") - ]; + let promises = this.initCommands(target, cwd); if (procArgs && procArgs.length) promises.push(this.sendCommand("exec-arguments " + procArgs)); if (process.platform == "win32") { @@ -139,12 +136,8 @@ export class MI2 extends EventEmitter implements IBackend { this.emit("quit"); this.sshConn.end(); }).bind(this)); - let promises = [ - this.sendCommand("gdb-set target-async on"), - this.sendCommand("environment-directory \"" + escape(cwd) + "\""), - this.sendCommand("environment-cd \"" + escape(cwd) + "\""), - this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\"") - ]; + let promises = this.initCommands(target, cwd); + promises.push(this.sendCommand("environment-cd \"" + escape(cwd) + "\"")); if (procArgs && procArgs.length) promises.push(this.sendCommand("exec-arguments " + procArgs)); Promise.all(promises).then(() => { @@ -161,6 +154,14 @@ export class MI2 extends EventEmitter implements IBackend { }); } + protected initCommands(target: string, cwd: string) { + return [ + this.sendCommand("gdb-set target-async on"), + this.sendCommand("environment-directory \"" + escape(cwd) + "\""), + this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\"") + ]; + } + attach(cwd: string, executable: string, target: string): Thenable { return new Promise((resolve, reject) => { let args = []; @@ -380,6 +381,10 @@ export class MI2 extends EventEmitter implements IBackend { return Promise.all(promisses); } + setBreakPointCondition(bkptNum, condition): Thenable { + return this.sendCommand("break-condition " + bkptNum + " " + condition); + } + addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]> { return new Promise((resolve, reject) => { if (this.breakpoints.has(breakpoint)) @@ -393,7 +398,7 @@ export class MI2 extends EventEmitter implements IBackend { condition: breakpoint.condition }; if (breakpoint.condition) { - this.sendCommand("break-condition " + bkptNum + " " + breakpoint.condition).then((result) => { + this.setBreakPointCondition(bkptNum, breakpoint.condition).then((result) => { if (result.resultRecords.resultClass == "done") { this.breakpoints.set(newBrk, bkptNum); resolve([true, newBrk]); @@ -467,7 +472,7 @@ export class MI2 extends EventEmitter implements IBackend { }); }); resolve(ret); - }); + }, reject); }); } @@ -520,18 +525,15 @@ export class MI2 extends EventEmitter implements IBackend { } sendCommand(command: string): Thenable { + let sel = this.currentToken++; return new Promise((resolve, reject) => { - this.handlers[this.currentToken] = (node: MINode) => { - if (node.resultRecords && node.resultRecords.resultClass == "error") { - let msg = node.result("msg") || "Internal error"; - this.log("stderr", "Failed to run command `" + command + "`: " + msg); - reject(msg); - } + this.handlers[sel] = (node: MINode) => { + if (node && node.resultRecords && node.resultRecords.resultClass === "error") + reject(node.result("msg") || "Internal error"); else resolve(node); }; - this.sendRaw(this.currentToken + "-" + command); - this.currentToken++; + this.sendRaw(sel + "-" + command); }); } @@ -540,13 +542,13 @@ export class MI2 extends EventEmitter implements IBackend { } printCalls: boolean; - private isSSH: boolean; - private sshReady: boolean; - private currentToken: number = 1; - private handlers: { [index: number]: (info: MINode) => any } = {}; - private breakpoints: Map = new Map(); - private buffer: string; - private process: ChildProcess.ChildProcess; - private stream; - private sshConn; + protected isSSH: boolean; + protected sshReady: boolean; + protected currentToken: number = 1; + protected handlers: { [index: number]: (info: MINode) => any } = {}; + protected breakpoints: Map = new Map(); + protected buffer: string; + protected process: ChildProcess.ChildProcess; + protected stream; + protected sshConn; } \ No newline at end of file diff --git a/src/backend/mi2/mi2lldb.ts b/src/backend/mi2/mi2lldb.ts new file mode 100644 index 0000000..9afc053 --- /dev/null +++ b/src/backend/mi2/mi2lldb.ts @@ -0,0 +1,50 @@ +import { MI2, escape } from "./mi2" +import { Breakpoint } from "../backend" +import * as ChildProcess from "child_process" +import { posix } from "path" +import * as nativePath from "path" +let path = posix; + +export class MI2_LLDB extends MI2 { + protected initCommands(target: string, cwd: string) { + return [ + this.sendCommand("gdb-set target-async on"), + this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\"") + ]; + } + + attach(cwd: string, executable: string, target: string): Thenable { + return new Promise((resolve, reject) => { + this.process = ChildProcess.spawn(this.application, this.preargs, { cwd: cwd }); + this.process.stdout.on("data", this.stdout.bind(this)); + this.process.stderr.on("data", this.stdout.bind(this)); + this.process.on("exit", (() => { this.emit("quit"); }).bind(this)); + Promise.all([ + this.sendCommand("gdb-set target-async on"), + this.sendCommand("file-exec-and-symbols \"" + escape(executable) + "\""), + this.sendCommand("target-attach " + target) + ]).then(() => { + this.emit("debug-ready"); + resolve(); + }, reject); + }); + } + + clearBreakPoints(): Thenable { + return new Promise((resolve, reject) => { + let promises = []; + for (let k in this.breakpoints.values) { + promises.push(this.sendCommand("break-delete " + k).then((result) => { + if (result.resultRecords.resultClass == "done") resolve(true); + else resolve(false); + })); + } + this.breakpoints.clear(); + Promise.all(promises).then(resolve, reject); + }); + } + + setBreakPointCondition(bkptNum, condition): Thenable { + return this.sendCommand("break-condition " + bkptNum + " \"" + escape(condition) + "\" 1"); + } +} \ No newline at end of file diff --git a/src/gdb.ts b/src/gdb.ts index e34d4f5..11c1b73 100644 --- a/src/gdb.ts +++ b/src/gdb.ts @@ -1,14 +1,8 @@ +import { MI2DebugSession } from './mibase'; import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { Breakpoint, IBackend, SSHArguments } from './backend/backend' -import { MINode } from './backend/mi_parse' -import { expandValue, isExpandable } from './backend/gdb_expansion' -import { MI2 } from './backend/mi2/mi2' -import { posix } from "path" -import * as systemPath from "path" - -let resolve = posix.resolve; -let relative = posix.relative; +import { MI2 } from "./backend/mi2/mi2"; +import { SSHArguments } from './backend/backend'; export interface LaunchRequestArguments { cwd: string; @@ -29,70 +23,14 @@ export interface AttachRequestArguments { printCalls: boolean; } -class MI2DebugSession extends DebugSession { - private static THREAD_ID = 1; - private gdbDebugger: MI2; - private variableHandles = new Handles(); - private quit: boolean; - private attached: boolean; - private needContinue: boolean; - private isSSH: boolean; - private trimCWD: string; - private switchCWD: string; - private started: boolean; - private crashed: boolean; - - public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { - super(debuggerLinesStartAt1, isServer); - } - +class GDBDebugSession extends MI2DebugSession { protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { response.body.supportsConfigurationDoneRequest = true; response.body.supportsEvaluateForHovers = true; // Assume working in future releases response.body.supportsFunctionBreakpoints = true; // TODO: Implement in future release this.sendResponse(response); - this.gdbDebugger = new MI2("gdb", ["-q", "--interpreter=mi2"]); - this.gdbDebugger.on("quit", this.quitEvent.bind(this)); - this.gdbDebugger.on("exited-normally", this.quitEvent.bind(this)); - this.gdbDebugger.on("stopped", this.stopEvent.bind(this)); - this.gdbDebugger.on("msg", this.handleMsg.bind(this)); - this.gdbDebugger.on("breakpoint", this.handleBreakpoint.bind(this)); - this.gdbDebugger.on("step-end", this.handleBreak.bind(this)); - this.gdbDebugger.on("step-out-end", this.handleBreak.bind(this)); - this.gdbDebugger.on("signal-stop", this.handlePause.bind(this)); - this.sendEvent(new InitializedEvent()); - } - - private handleMsg(type: string, msg: string) { - if (type == "target") - type = "stdout"; - if (type == "log") - type = "stderr"; - this.sendEvent(new OutputEvent(msg, type)); - } - - private handleBreakpoint(info: MINode) { - this.sendEvent(new StoppedEvent("breakpoint", MI2DebugSession.THREAD_ID)); - } - - private handleBreak(info: MINode) { - this.sendEvent(new StoppedEvent("step", MI2DebugSession.THREAD_ID)); - } - - private handlePause(info: MINode) { - this.sendEvent(new StoppedEvent("user request", MI2DebugSession.THREAD_ID)); - } - - private stopEvent(info: MINode) { - if (!this.started) - this.crashed = true; - if (!this.quit) - this.sendEvent(new StoppedEvent("exception", MI2DebugSession.THREAD_ID)); - } - - private quitEvent() { - this.quit = true; - this.sendEvent(new TerminatedEvent()); + this.miDebugger = new MI2("gdb", ["-q", "--interpreter=mi2"]); + this.initDebugger(); } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { @@ -102,7 +40,7 @@ class MI2DebugSession extends DebugSession { this.isSSH = false; this.started = false; this.crashed = false; - this.gdbDebugger.printCalls = !!args.printCalls; + this.miDebugger.printCalls = !!args.printCalls; if (args.ssh !== undefined) { if (args.ssh.forwardX11 === undefined) args.ssh.forwardX11 = true; @@ -117,28 +55,34 @@ class MI2DebugSession extends DebugSession { this.isSSH = true; this.trimCWD = args.cwd.replace(/\\/g, "/"); this.switchCWD = args.ssh.cwd; - this.gdbDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, args.terminal).then(() => { + this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, args.terminal).then(() => { if (args.autorun) args.autorun.forEach(command => { - this.gdbDebugger.sendUserInput(command); + this.miDebugger.sendUserInput(command); }); - this.gdbDebugger.start().then(() => { + setTimeout(() => { + this.miDebugger.emit("ui-break-done"); + }, 50); + this.sendResponse(response); + this.miDebugger.start().then(() => { this.started = true; - this.sendResponse(response); if (this.crashed) this.handlePause(undefined); }); }); } else { - this.gdbDebugger.load(args.cwd, args.target, args.arguments, args.terminal).then(() => { + this.miDebugger.load(args.cwd, args.target, args.arguments, args.terminal).then(() => { if (args.autorun) args.autorun.forEach(command => { - this.gdbDebugger.sendUserInput(command); + this.miDebugger.sendUserInput(command); }); - this.gdbDebugger.start().then(() => { + setTimeout(() => { + this.miDebugger.emit("ui-break-done"); + }, 50); + this.sendResponse(response); + this.miDebugger.start().then(() => { this.started = true; - this.sendResponse(response); if (this.crashed) this.handlePause(undefined); }); @@ -151,267 +95,26 @@ class MI2DebugSession extends DebugSession { this.attached = !args.remote; this.needContinue = true; this.isSSH = false; - this.gdbDebugger.printCalls = !!args.printCalls; + this.miDebugger.printCalls = !!args.printCalls; if (args.remote) { - this.gdbDebugger.connect(args.cwd, args.executable, args.target).then(() => { + this.miDebugger.connect(args.cwd, args.executable, args.target).then(() => { if (args.autorun) args.autorun.forEach(command => { - this.gdbDebugger.sendUserInput(command); + this.miDebugger.sendUserInput(command); }); this.sendResponse(response); }); } else { - this.gdbDebugger.attach(args.cwd, args.executable, args.target).then(() => { + this.miDebugger.attach(args.cwd, args.executable, args.target).then(() => { if (args.autorun) args.autorun.forEach(command => { - this.gdbDebugger.sendUserInput(command); + this.miDebugger.sendUserInput(command); }); this.sendResponse(response); }); } } - - protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { - if (this.attached) - this.gdbDebugger.detach(); - else - this.gdbDebugger.stop(); - this.sendResponse(response); - } - - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { - this.gdbDebugger.once("debug-ready", (() => { - this.gdbDebugger.clearBreakPoints().then(() => { - let path = args.source.path; - if (this.isSSH) { - path = relative(this.trimCWD.replace(/\\/g, "/"), path.replace(/\\/g, "/")); - path = resolve(this.switchCWD.replace(/\\/g, "/"), path.replace(/\\/g, "/")); - } - let all = []; - args.breakpoints.forEach(brk => { - all.push(this.gdbDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition })); - }); - Promise.all(all).then(brkpoints => { - let finalBrks = []; - brkpoints.forEach(brkp => { - if (brkp[0]) - finalBrks.push({ line: brkp[1].line }); - }); - response.body = { - breakpoints: finalBrks - }; - setTimeout(() => { - this.gdbDebugger.emit("ui-break-done"); - }, 50); - this.sendResponse(response); - }); - }); - }).bind(this)); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - response.body = { - threads: [ - new Thread(MI2DebugSession.THREAD_ID, "Thread 1") - ] - }; - this.sendResponse(response); - } - - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { - this.gdbDebugger.getStack(args.levels).then(stack => { - let ret: StackFrame[] = []; - stack.forEach(element => { - let file = element.file; - if (this.isSSH) { - file = relative(this.switchCWD.replace(/\\/g, "/"), file.replace(/\\/g, "/")); - file = systemPath.resolve(this.trimCWD.replace(/\\/g, "/"), file.replace(/\\/g, "/")); - } - ret.push(new StackFrame(element.level, element.function + "@" + element.address, new Source(element.fileName, file), element.line, 0)); - }); - response.body = { - stackFrames: ret - }; - this.sendResponse(response); - }); - } - - protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { - // FIXME: Does not seem to get called in january release - if (this.needContinue) { - this.gdbDebugger.continue().then(done => { - this.sendResponse(response); - }, msg => { - this.sendResponse(response); - this.sendEvent(new OutputEvent(`Could not continue: ${msg}\n`, 'stderr')); - }); - } - else - this.sendResponse(response); - } - - protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { - const scopes = new Array(); - scopes.push(new Scope("Local", this.variableHandles.create("@frame:" + args.frameId), false)); - - response.body = { - scopes: scopes - }; - this.sendResponse(response); - } - - protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { - const variables = []; - const id = this.variableHandles.get(args.variablesReference); - - let createVariable = (arg) => { - return this.variableHandles.create(arg); - }; - - if (typeof id == "string") { - if (id.startsWith("@frame:")) { - this.gdbDebugger.getStackVariables(1, 0).then(stack => { - stack.forEach(variable => { - if (variable[1] !== undefined) { - let expanded = expandValue(createVariable, `{${variable[0]} = ${variable[1]}}`); - if (!expanded) - new OutputEvent("Could not expand " + variable[1] + "\n", "stderr"); - else if (typeof expanded[0] == "string") - expanded = [ - { - name: "", - value: prettyStringArray(expanded), - variablesReference: 0 - } - ]; - variables.push(expanded[0]); - } else - variables.push({ - name: variable[0], - value: "", - variablesReference: createVariable(variable[0]) - }); - }); - response.body = { - variables: variables - }; - this.sendResponse(response); - }, err => { - this.handleMsg("stderr", "Could not expand variable\n"); - response.body = { - variables: [] - }; - this.sendResponse(response); - }); - } - else { - // Variable members - this.gdbDebugger.evalExpression(JSON.stringify(id)).then(variable => { - let expanded = expandValue(createVariable, variable.result("value"), id); - if (!expanded) - this.sendEvent(new OutputEvent("Could not expand " + variable.result("value") + "\n", "stderr")); - else if (typeof expanded[0] == "string") - expanded = [ - { - name: "", - value: prettyStringArray(expanded), - variablesReference: 0 - } - ]; - response.body = { - variables: expanded - }; - this.sendResponse(response); - }, err => { - this.handleMsg("stderr", "Could not expand variable\n"); - response.body = { - variables: [] - }; - this.sendResponse(response); - }); - } - } - else if (typeof id == "object") { - response.body = { - variables: id - }; - this.sendResponse(response); - } - else { - response.body = { - variables: variables - }; - this.sendResponse(response); - } - } - - protected pauseRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { - this.gdbDebugger.interrupt().then(done => { - this.sendResponse(response); - }, msg => { - this.sendResponse(response); - this.sendEvent(new OutputEvent(`Could not pause: ${msg}\n`, 'stderr')); - }); - } - - protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { - this.gdbDebugger.continue().then(done => { - this.sendResponse(response); - }, msg => { - this.sendResponse(response); - this.sendEvent(new OutputEvent(`Could not continue: ${msg}\n`, 'stderr')); - }); - } - - protected stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.gdbDebugger.step().then(done => { - this.sendResponse(response); - }, msg => { - this.sendResponse(response); - this.sendEvent(new OutputEvent(`Could not step in: ${msg}\n`, 'stderr')); - }); - } - - protected stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.gdbDebugger.stepOut().then(done => { - this.sendResponse(response); - }, msg => { - this.sendResponse(response); - this.sendEvent(new OutputEvent(`Could not step out: ${msg}\n`, 'stderr')); - }); - } - - protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.gdbDebugger.next().then(done => { - this.sendResponse(response); - }, msg => { - this.sendResponse(response); - this.sendEvent(new OutputEvent(`Could not step over: ${msg}\n`, 'stderr')); - }); - } - - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { - if (args.context == "watch" || args.context == "hover") - this.gdbDebugger.evalExpression(args.expression).then((res) => { - response.body = { - variablesReference: 0, - result: res.result("value") - } - this.sendResponse(response); - }); - else { - this.gdbDebugger.sendUserInput(args.expression).then(output => { - if (output) - response.body.result = JSON.stringify(output); - this.sendResponse(response); - }); - } - } } -function prettyStringArray(strings: string[]) { - return strings.join(", "); -} - -DebugSession.run(MI2DebugSession); \ No newline at end of file +DebugSession.run(GDBDebugSession); \ No newline at end of file diff --git a/src/lldb.ts b/src/lldb.ts new file mode 100644 index 0000000..1e229d3 --- /dev/null +++ b/src/lldb.ts @@ -0,0 +1,107 @@ +import { MI2DebugSession } 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"; +import { SSHArguments } from './backend/backend'; + +export interface LaunchRequestArguments { + cwd: string; + target: string; + arguments: string; + autorun: string[]; + ssh: SSHArguments; + printCalls: boolean; +} + +export interface AttachRequestArguments { + cwd: string; + target: string; + executable: string; + autorun: string[]; + printCalls: boolean; +} + +class LLDBDebugSession extends MI2DebugSession { + protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + response.body.supportsConfigurationDoneRequest = true; + response.body.supportsEvaluateForHovers = true; // Assume working in future releases + response.body.supportsFunctionBreakpoints = true; // TODO: Implement in future release + this.sendResponse(response); + this.miDebugger = new MI2_LLDB("lldb-mi", []); + this.initDebugger(); + } + + protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + this.quit = false; + this.attached = false; + this.needContinue = false; + this.isSSH = false; + this.started = false; + this.crashed = false; + this.miDebugger.printCalls = !!args.printCalls; + if (args.ssh !== undefined) { + if (args.ssh.forwardX11 === undefined) + args.ssh.forwardX11 = true; + if (args.ssh.port === undefined) + args.ssh.port = 22; + if (args.ssh.x11port === undefined) + args.ssh.x11port = 6000; + if (args.ssh.x11host === undefined) + args.ssh.x11host = "localhost"; + if (args.ssh.remotex11screen === undefined) + args.ssh.remotex11screen = 0; + this.isSSH = true; + this.trimCWD = args.cwd.replace(/\\/g, "/"); + this.switchCWD = args.ssh.cwd; + this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, undefined).then(() => { + if (args.autorun) + args.autorun.forEach(command => { + this.miDebugger.sendUserInput(command); + }); + setTimeout(() => { + this.miDebugger.emit("ui-break-done"); + }, 50); + this.sendResponse(response); + this.miDebugger.start().then(() => { + this.started = true; + if (this.crashed) + this.handlePause(undefined); + }); + }); + } + else { + this.miDebugger.load(args.cwd, args.target, args.arguments, undefined).then(() => { + if (args.autorun) + args.autorun.forEach(command => { + this.miDebugger.sendUserInput(command); + }); + setTimeout(() => { + this.miDebugger.emit("ui-break-done"); + }, 50); + this.sendResponse(response); + this.miDebugger.start().then(() => { + this.started = true; + if (this.crashed) + this.handlePause(undefined); + }); + }); + } + } + + protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { + this.quit = false; + this.attached = true; + this.needContinue = true; + this.isSSH = false; + this.miDebugger.printCalls = !!args.printCalls; + this.miDebugger.attach(args.cwd, args.executable, args.target).then(() => { + if (args.autorun) + args.autorun.forEach(command => { + this.miDebugger.sendUserInput(command); + }); + this.sendResponse(response); + }); + } +} + +DebugSession.run(LLDBDebugSession); \ No newline at end of file diff --git a/src/mibase.ts b/src/mibase.ts new file mode 100644 index 0000000..2284e5e --- /dev/null +++ b/src/mibase.ts @@ -0,0 +1,314 @@ +import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { Breakpoint, IBackend } from './backend/backend'; +import { MINode } from './backend/mi_parse'; +import { expandValue, isExpandable } from './backend/gdb_expansion'; +import { MI2 } from './backend/mi2/mi2'; +import { posix } from "path"; +import * as systemPath from "path"; + +let resolve = posix.resolve; +let relative = posix.relative; + +export class MI2DebugSession extends DebugSession { + protected static THREAD_ID = 1; + protected variableHandles = new Handles(); + protected quit: boolean; + protected attached: boolean; + protected needContinue: boolean; + protected isSSH: boolean; + protected trimCWD: string; + protected switchCWD: string; + protected started: boolean; + protected crashed: boolean; + protected miDebugger: MI2; + + public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { + super(debuggerLinesStartAt1, isServer); + } + + protected initDebugger() { + 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("step-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.sendEvent(new InitializedEvent()); + } + + protected handleMsg(type: string, msg: string) { + if (type == "target") + type = "stdout"; + if (type == "log") + type = "stderr"; + this.sendEvent(new OutputEvent(msg, type)); + } + + protected handleBreakpoint(info: MINode) { + this.sendEvent(new StoppedEvent("breakpoint", MI2DebugSession.THREAD_ID)); + } + + protected handleBreak(info: MINode) { + this.sendEvent(new StoppedEvent("step", MI2DebugSession.THREAD_ID)); + } + + protected handlePause(info: MINode) { + this.sendEvent(new StoppedEvent("user request", MI2DebugSession.THREAD_ID)); + } + + protected stopEvent(info: MINode) { + if (!this.started) + this.crashed = true; + if (!this.quit) + this.sendEvent(new StoppedEvent("exception", MI2DebugSession.THREAD_ID)); + } + + protected quitEvent() { + this.quit = true; + this.sendEvent(new TerminatedEvent()); + } + + protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { + if (this.attached) + this.miDebugger.detach(); + else + this.miDebugger.stop(); + this.sendResponse(response); + } + + protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { + this.miDebugger.once("debug-ready", (() => { + this.miDebugger.clearBreakPoints().then(() => { + let path = args.source.path; + if (this.isSSH) { + path = relative(this.trimCWD.replace(/\\/g, "/"), path.replace(/\\/g, "/")); + path = resolve(this.switchCWD.replace(/\\/g, "/"), path.replace(/\\/g, "/")); + } + let all = []; + args.breakpoints.forEach(brk => { + all.push(this.miDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition })); + }); + Promise.all(all).then(brkpoints => { + let finalBrks = []; + brkpoints.forEach(brkp => { + if (brkp[0]) + finalBrks.push({ line: brkp[1].line }); + }); + response.body = { + breakpoints: finalBrks + }; + setTimeout(() => { + this.miDebugger.emit("ui-break-done"); + }, 50); + this.sendResponse(response); + }); + }); + }).bind(this)); + } + + protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { + response.body = { + threads: [ + new Thread(MI2DebugSession.THREAD_ID, "Thread 1") + ] + }; + this.sendResponse(response); + } + + protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { + this.miDebugger.getStack(args.levels).then(stack => { + let ret: StackFrame[] = []; + stack.forEach(element => { + let file = element.file; + if (this.isSSH) { + file = relative(this.switchCWD.replace(/\\/g, "/"), file.replace(/\\/g, "/")); + file = systemPath.resolve(this.trimCWD.replace(/\\/g, "/"), file.replace(/\\/g, "/")); + } + ret.push(new StackFrame(element.level, element.function + "@" + element.address, new Source(element.fileName, file), element.line, 0)); + }); + response.body = { + stackFrames: ret + }; + this.sendResponse(response); + }); + } + + protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { + // FIXME: Does not seem to get called in january release + if (this.needContinue) { + this.miDebugger.continue().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not continue: ${msg}\n`, 'stderr')); + }); + } + else + this.sendResponse(response); + } + + protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { + const scopes = new Array(); + scopes.push(new Scope("Local", this.variableHandles.create("@frame:" + args.frameId), false)); + + response.body = { + scopes: scopes + }; + this.sendResponse(response); + } + + protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { + const variables = []; + const id = this.variableHandles.get(args.variablesReference); + + let createVariable = (arg) => { + return this.variableHandles.create(arg); + }; + + if (typeof id == "string") { + if (id.startsWith("@frame:")) { + this.miDebugger.getStackVariables(1, 0).then(stack => { + stack.forEach(variable => { + if (variable[1] !== undefined) { + let expanded = expandValue(createVariable, `{${variable[0]} = ${variable[1]}}`); + if (!expanded) + new OutputEvent("Could not expand " + variable[1] + "\n", "stderr"); + else if (typeof expanded[0] == "string") + expanded = [ + { + name: "", + value: prettyStringArray(expanded), + variablesReference: 0 + } + ]; + variables.push(expanded[0]); + } else + variables.push({ + name: variable[0], + value: "", + variablesReference: createVariable(variable[0]) + }); + }); + response.body = { + variables: variables + }; + this.sendResponse(response); + }, err => { + this.handleMsg("stderr", "Could not expand variable\n"); + response.body = { + variables: [] + }; + this.sendResponse(response); + }); + } + else { + // Variable members + this.miDebugger.evalExpression(JSON.stringify(id)).then(variable => { + let expanded = expandValue(createVariable, variable.result("value"), id); + if (!expanded) + this.sendEvent(new OutputEvent("Could not expand " + variable.result("value") + "\n", "stderr")); + else if (typeof expanded[0] == "string") + expanded = [ + { + name: "", + value: prettyStringArray(expanded), + variablesReference: 0 + } + ]; + response.body = { + variables: expanded + }; + this.sendResponse(response); + }, err => { + this.handleMsg("stderr", "Could not expand variable\n"); + response.body = { + variables: [] + }; + this.sendResponse(response); + }); + } + } + else if (typeof id == "object") { + response.body = { + variables: id + }; + this.sendResponse(response); + } + else { + response.body = { + variables: variables + }; + this.sendResponse(response); + } + } + + protected pauseRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + this.miDebugger.interrupt().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not pause: ${msg}\n`, 'stderr')); + }); + } + + protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + this.miDebugger.continue().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not continue: ${msg}\n`, 'stderr')); + }); + } + + protected stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + this.miDebugger.step().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not step in: ${msg}\n`, 'stderr')); + }); + } + + protected stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + this.miDebugger.stepOut().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not step out: ${msg}\n`, 'stderr')); + }); + } + + protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + this.miDebugger.next().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not step over: ${msg}\n`, 'stderr')); + }); + } + + protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { + if (args.context == "watch" || args.context == "hover") + this.miDebugger.evalExpression(args.expression).then((res) => { + response.body = { + variablesReference: 0, + result: res.result("value") + } + this.sendResponse(response); + }); + else { + this.miDebugger.sendUserInput(args.expression).then(output => { + if (output) + response.body.result = JSON.stringify(output); + this.sendResponse(response); + }); + } + } +} + +function prettyStringArray(strings: string[]) { + return strings.join(", "); +} \ No newline at end of file diff --git a/test/gdb_expansion.test.ts b/test/gdb_expansion.test.ts index 2bcd9f3..5e790de 100644 --- a/test/gdb_expansion.test.ts +++ b/test/gdb_expansion.test.ts @@ -12,8 +12,12 @@ suite("GDB Value Expansion", () => { assert.equal(expandValue(variableCreate, `"hello world!"`), `"hello world!"`); assert.strictEqual(isExpandable(`0x0`), 0); assert.equal(expandValue(variableCreate, `0x0`), ""); - assert.strictEqual(isExpandable(`0xabc`), 2); - assert.equal(expandValue(variableCreate, `0x7ffff7ecb480`), "*0x7ffff7ecb480"); + assert.strictEqual(isExpandable(`0x000000`), 0); + assert.equal(expandValue(variableCreate, `0x000000`), ""); + assert.strictEqual(isExpandable(`{...}`), 2); + assert.equal(expandValue(variableCreate, `{...}`), "<...>"); + assert.strictEqual(isExpandable(`0x00abc`), 2); + assert.equal(expandValue(variableCreate, `0x007ffff7ecb480`), "*0x007ffff7ecb480"); assert.strictEqual(isExpandable(`{a = b, c = d}`), 1); assert.deepEqual(expandValue(variableCreate, `{a = b, c = d}`), [ { @@ -262,5 +266,17 @@ suite("GDB Value Expansion", () => { variablesReference: 0 } ]); - }) + }); + test("lldb strings", () => { + let node = `{ name = {...} }`; + assert.strictEqual(isExpandable(node), 1); + let variables = expandValue(variableCreate, node); + assert.deepEqual(variables, [ + { + name: "name", + value: "...", + variablesReference: { expanded: "name" } + } + ]); + }); }); \ No newline at end of file