Added LLDB support (fix #39)

This commit is contained in:
WebFreak001 2016-03-06 12:02:11 +01:00
commit 35afccfd77
12 changed files with 756 additions and 376 deletions

View file

@ -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 <any>"<...>";
}
}
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 = "<nullptr>";
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,

View file

@ -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<any> {
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<any> {
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<MINode> {
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<Breakpoint, Number> = 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<Breakpoint, Number> = new Map();
protected buffer: string;
protected process: ChildProcess.ChildProcess;
protected stream;
protected sshConn;
}

View file

@ -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<any> {
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<any> {
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<any> {
return this.sendCommand("break-condition " + bkptNum + " \"" + escape(condition) + "\" 1");
}
}

View file

@ -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<any>();
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<Scope>();
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>",
value: prettyStringArray(expanded),
variablesReference: 0
}
];
variables.push(expanded[0]);
} else
variables.push({
name: variable[0],
value: "<unknown>",
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>",
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);
DebugSession.run(GDBDebugSession);

107
src/lldb.ts Normal file
View file

@ -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);

314
src/mibase.ts Normal file
View file

@ -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<any>();
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<Scope>();
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>",
value: prettyStringArray(expanded),
variablesReference: 0
}
];
variables.push(expanded[0]);
} else
variables.push({
name: variable[0],
value: "<unknown>",
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>",
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(", ");
}