code-debug/src/backend/mi2/mi2.ts
gentoo90 2551dba725 Reuse variable objects, utilize var-update
Same variables now have same ids in each VariablesResponse.
This allows vscode to keep track of changes properly and prevents
collapsing of all variables after every step.
2017-05-25 23:49:19 +03:00

756 lines
23 KiB
TypeScript

import { Breakpoint, IBackend, Stack, SSHArguments, Variable, VariableObject } from "../backend"
import * as ChildProcess from "child_process"
import { EventEmitter } from "events"
import { parseMI, MINode } from '../mi_parse';
import * as linuxTerm from '../linux/console';
import * as net from "net"
import * as fs from "fs"
import { posix } from "path"
import * as nativePath from "path"
let path = posix;
var Client = require("ssh2").Client;
export function escape(str: string) {
return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
}
const nonOutput = /^(?:\d*|undefined)[\*\+\=]|[\~\@\&\^]/;
const gdbMatch = /(?:\d*|undefined)\(gdb\)/;
const numRegex = /\d+/;
function couldBeOutput(line: string) {
if (nonOutput.exec(line))
return false;
return true;
}
const trace = false;
export class MI2 extends EventEmitter implements IBackend {
constructor(public application: string, public preargs: string[], public extraargs: string[], procEnv: any) {
super();
if (procEnv) {
var env = {};
// Duplicate process.env so we don't override it
for (var key in process.env)
if (process.env.hasOwnProperty(key))
env[key] = process.env[key];
// Overwrite with user specified variables
for (var key in procEnv) {
if (procEnv.hasOwnProperty(key)) {
if (procEnv === null)
delete env[key];
else
env[key] = procEnv[key];
}
}
this.procEnv = env;
}
}
load(cwd: string, target: string, procArgs: string, separateConsole: string): Thenable<any> {
if (!nativePath.isAbsolute(target))
target = nativePath.join(cwd, target);
return new Promise((resolve, reject) => {
this.isSSH = false;
let 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"); }).bind(this));
this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
let promises = this.initCommands(target, cwd);
if (procArgs && procArgs.length)
promises.push(this.sendCommand("exec-arguments " + procArgs));
if (process.platform == "win32") {
if (separateConsole !== undefined)
promises.push(this.sendCommand("gdb-set new-console on"))
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve();
}, reject);
}
else {
if (separateConsole !== undefined) {
linuxTerm.spawnTerminalEmulator(separateConsole).then(tty => {
promises.push(this.sendCommand("inferior-tty-set " + tty));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve();
}, reject);
});
}
else {
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve();
}, reject);
}
}
});
}
ssh(args: SSHArguments, cwd: string, target: string, procArgs: string, separateConsole: string, attach: boolean): Thenable<any> {
return new Promise((resolve, reject) => {
this.isSSH = true;
this.sshReady = false;
this.sshConn = new Client();
if (separateConsole !== undefined)
this.log("stderr", "WARNING: Output to terminal emulators are not supported over SSH");
if (args.forwardX11) {
this.sshConn.on("x11", (info, accept, reject) => {
var xserversock = new net.Socket();
xserversock.on("error", (err) => {
this.log("stderr", "Could not connect to local X11 server! Did you enable it in your display manager?\n" + err);
});
xserversock.on("connect", () => {
let xclientsock = accept();
xclientsock.pipe(xserversock).pipe(xclientsock);
});
xserversock.connect(args.x11port, args.x11host);
});
}
let connectionArgs: any = {
host: args.host,
port: args.port,
username: args.user
};
if (args.keyfile) {
if (require("fs").existsSync(args.keyfile))
connectionArgs.privateKey = require("fs").readFileSync(args.keyfile);
else {
this.log("stderr", "SSH key file does not exist!");
this.emit("quit");
reject();
return;
}
} else {
connectionArgs.password = args.password;
}
this.sshConn.on("ready", () => {
this.log("stdout", "Running " + this.application + " over ssh...");
let execArgs: any = {};
if (args.forwardX11) {
execArgs.x11 = {
single: false,
screen: args.remotex11screen
};
}
let sshCMD = this.application + " " + this.preargs.join(" ");
if (args.bootstrap) sshCMD = args.bootstrap + " && " + sshCMD;
if (attach)
sshCMD += " -p " + target;
this.sshConn.exec(sshCMD, execArgs, (err, stream) => {
if (err) {
this.log("stderr", "Could not run " + this.application + " over ssh!");
this.log("stderr", err.toString());
this.emit("quit");
reject();
return;
}
this.sshReady = true;
this.stream = stream;
stream.on("data", this.stdout.bind(this));
stream.stderr.on("data", this.stderr.bind(this));
stream.on("exit", (() => {
this.emit("quit");
this.sshConn.end();
}).bind(this));
let promises = this.initCommands(target, cwd, true, attach);
promises.push(this.sendCommand("environment-cd \"" + escape(cwd) + "\""));
if (procArgs && procArgs.length && !attach)
promises.push(this.sendCommand("exec-arguments " + procArgs));
Promise.all(promises).then(() => {
this.emit("debug-ready")
resolve();
}, reject);
});
}).on("error", (err) => {
this.log("stderr", "Could not run " + this.application + " over ssh!");
this.log("stderr", err.toString());
this.emit("quit");
reject();
}).connect(connectionArgs);
});
}
protected initCommands(target: string, cwd: string, ssh: boolean = false, attach: boolean = false) {
if (ssh) {
if (!path.isAbsolute(target))
target = path.join(cwd, target);
}
else {
if (!nativePath.isAbsolute(target))
target = nativePath.join(cwd, target);
}
var cmds = [
this.sendCommand("gdb-set target-async on", true),
this.sendCommand("environment-directory \"" + escape(cwd) + "\"", true)
];
if (!attach)
cmds.push(this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\""));
// TODO: add extension parameter for enabling/disabling pretty printers
cmds.push(this.sendCommand("enable-pretty-printing"));
return cmds;
}
attach(cwd: string, executable: string, target: string): Thenable<any> {
return new Promise((resolve, reject) => {
let args = [];
if (executable && !nativePath.isAbsolute(executable))
executable = nativePath.join(cwd, executable);
if (!executable)
executable = "-p";
var isExtendedRemote = false;
if (target.startsWith("extended-remote")) {
isExtendedRemote = true;
args = this.preargs;
} else
args = args.concat([executable, target], this.preargs);
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"); }).bind(this));
this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
var commands = [
this.sendCommand("gdb-set target-async on"),
this.sendCommand("environment-directory \"" + escape(cwd) + "\"")
];
if (isExtendedRemote) {
commands.push(this.sendCommand("target-select " + target));
commands.push(this.sendCommand("file-symbol-file \"" + escape(executable) + "\""));
}
Promise.all(commands).then(() => {
this.emit("debug-ready")
resolve();
}, reject);
});
}
connect(cwd: string, executable: string, target: string): Thenable<any> {
return new Promise((resolve, reject) => {
let args = [];
if (executable && !nativePath.isAbsolute(executable))
executable = nativePath.join(cwd, executable);
if (executable)
args = args.concat([executable], this.preargs);
else
args = this.preargs;
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"); }).bind(this));
this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
Promise.all([
this.sendCommand("gdb-set target-async on"),
this.sendCommand("environment-directory \"" + escape(cwd) + "\""),
this.sendCommand("target-select remote " + target)
]).then(() => {
this.emit("debug-ready")
resolve();
}, reject);
});
}
stdout(data) {
if (trace)
this.log("stderr", "stdout: " + data);
if (typeof data == "string")
this.buffer += data;
else
this.buffer += data.toString("utf8");
let end = this.buffer.lastIndexOf('\n');
if (end != -1) {
this.onOutput(this.buffer.substr(0, end));
this.buffer = this.buffer.substr(end + 1);
}
if (this.buffer.length) {
if (this.onOutputPartial(this.buffer)) {
this.buffer = "";
}
}
}
stderr(data) {
if (typeof data == "string")
this.errbuf += data;
else
this.errbuf += data.toString("utf8");
let end = this.errbuf.lastIndexOf('\n');
if (end != -1) {
this.onOutputStderr(this.errbuf.substr(0, end));
this.errbuf = this.errbuf.substr(end + 1);
}
if (this.errbuf.length) {
this.logNoNewLine("stderr", this.errbuf);
this.errbuf = "";
}
}
onOutputStderr(lines) {
lines = <string[]>lines.split('\n');
lines.forEach(line => {
this.log("stderr", line);
});
}
onOutputPartial(line) {
if (couldBeOutput(line)) {
this.logNoNewLine("stdout", line);
return true;
}
return false;
}
onOutput(lines) {
lines = <string[]>lines.split('\n');
lines.forEach(line => {
if (couldBeOutput(line)) {
if (!gdbMatch.exec(line))
this.log("stdout", line);
}
else {
let parsed = parseMI(line);
if (this.debugOutput)
this.log("log", "GDB -> App: " + JSON.stringify(parsed));
let handled = false;
if (parsed.token !== undefined) {
if (this.handlers[parsed.token]) {
this.handlers[parsed.token](parsed);
delete this.handlers[parsed.token];
handled = true;
}
}
if (!handled && parsed.resultRecords && parsed.resultRecords.resultClass == "error") {
this.log("stderr", parsed.result("msg") || line);
}
if (parsed.outOfBandRecord) {
parsed.outOfBandRecord.forEach(record => {
if (record.isStream) {
this.log(record.type, record.content);
} else {
if (record.type == "exec") {
this.emit("exec-async-output", parsed);
if (record.asyncClass == "running")
this.emit("running", parsed);
else if (record.asyncClass == "stopped") {
let reason = parsed.record("reason");
if (trace)
this.log("stderr", "stop: " + reason);
if (reason == "breakpoint-hit")
this.emit("breakpoint", parsed);
else if (reason == "end-stepping-range")
this.emit("step-end", parsed);
else if (reason == "function-finished")
this.emit("step-out-end", parsed);
else if (reason == "signal-received")
this.emit("signal-stop", parsed);
else if (reason == "exited-normally")
this.emit("exited-normally", parsed);
else if (reason == "exited") { // exit with error code != 0
this.log("stderr", "Program exited with code " + parsed.record("exit-code"));
this.emit("exited-normally", parsed);
}
else {
this.log("console", "Not implemented stop reason (assuming exception): " + reason);
this.emit("stopped", parsed);
}
} else
this.log("log", JSON.stringify(parsed));
}
}
});
handled = true;
}
if (parsed.token == undefined && parsed.resultRecords == undefined && parsed.outOfBandRecord.length == 0)
handled = true;
if (!handled)
this.log("log", "Unhandled: " + JSON.stringify(parsed));
}
});
}
start(): Thenable<boolean> {
return new Promise((resolve, reject) => {
this.once("ui-break-done", () => {
this.log("console", "Running executable");
this.sendCommand("exec-run").then((info) => {
if (info.resultRecords.resultClass == "running")
resolve();
else
reject();
}, reject);
});
});
}
stop() {
if (this.isSSH) {
let proc = this.stream;
let to = setTimeout(() => {
proc.signal("KILL");
}, 1000);
this.stream.on("exit", function (code) {
clearTimeout(to);
})
this.sendRaw("-gdb-exit");
}
else {
let proc = this.process;
let to = setTimeout(() => {
process.kill(-proc.pid);
}, 1000);
this.process.on("exit", function (code) {
clearTimeout(to);
});
this.sendRaw("-gdb-exit");
}
}
detach() {
let proc = this.process;
let to = setTimeout(() => {
process.kill(-proc.pid);
}, 1000);
this.process.on("exit", function (code) {
clearTimeout(to);
});
this.sendRaw("-target-detach");
}
interrupt(): Thenable<boolean> {
if (trace)
this.log("stderr", "interrupt");
return new Promise((resolve, reject) => {
this.sendCommand("exec-interrupt").then((info) => {
resolve(info.resultRecords.resultClass == "done");
}, reject);
});
}
continue(reverse: boolean = false): Thenable<boolean> {
if (trace)
this.log("stderr", "continue");
return new Promise((resolve, reject) => {
this.sendCommand("exec-continue" + (reverse ? " --reverse" : "")).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
});
}
next(reverse: boolean = false): Thenable<boolean> {
if (trace)
this.log("stderr", "next");
return new Promise((resolve, reject) => {
this.sendCommand("exec-next" + (reverse ? " --reverse" : "")).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
});
}
step(reverse: boolean = false): Thenable<boolean> {
if (trace)
this.log("stderr", "step");
return new Promise((resolve, reject) => {
this.sendCommand("exec-step" + (reverse ? " --reverse" : "")).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
});
}
stepOut(reverse: boolean = false): Thenable<boolean> {
if (trace)
this.log("stderr", "stepOut");
return new Promise((resolve, reject) => {
this.sendCommand("exec-finish" + (reverse ? " --reverse" : "")).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
});
}
changeVariable(name: string, rawValue: string): Thenable<any> {
if (trace)
this.log("stderr", "changeVariable");
return this.sendCommand("gdb-set var " + name + "=" + rawValue);
}
loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]> {
if (trace)
this.log("stderr", "loadBreakPoints");
let promisses = [];
breakpoints.forEach(breakpoint => {
promisses.push(this.addBreakPoint(breakpoint));
});
return Promise.all(promisses);
}
setBreakPointCondition(bkptNum, condition): Thenable<any> {
if (trace)
this.log("stderr", "setBreakPointCondition");
return this.sendCommand("break-condition " + bkptNum + " " + condition);
}
addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]> {
if (trace)
this.log("stderr", "addBreakPoint");
return new Promise((resolve, reject) => {
if (this.breakpoints.has(breakpoint))
return resolve(false);
let location = "";
if (breakpoint.countCondition) {
if (breakpoint.countCondition[0] == ">")
location += "-i " + numRegex.exec(breakpoint.countCondition.substr(1))[0] + " ";
else {
let match = numRegex.exec(breakpoint.countCondition)[0];
if (match.length != breakpoint.countCondition.length) {
this.log("stderr", "Unsupported break count expression: '" + breakpoint.countCondition + "'. Only supports 'X' for breaking once after X times or '>X' for ignoring the first X breaks");
location += "-t ";
}
else if (parseInt(match) != 0)
location += "-t -i " + parseInt(match) + " ";
}
}
if (breakpoint.raw)
location += '"' + escape(breakpoint.raw) + '"';
else
location += '"' + escape(breakpoint.file) + ":" + breakpoint.line + '"';
this.sendCommand("break-insert -f " + location).then((result) => {
if (result.resultRecords.resultClass == "done") {
let bkptNum = parseInt(result.result("bkpt.number"));
let newBrk = {
file: result.result("bkpt.file"),
line: parseInt(result.result("bkpt.line")),
condition: breakpoint.condition
};
if (breakpoint.condition) {
this.setBreakPointCondition(bkptNum, breakpoint.condition).then((result) => {
if (result.resultRecords.resultClass == "done") {
this.breakpoints.set(newBrk, bkptNum);
resolve([true, newBrk]);
} else {
resolve([false, null]);
}
}, reject);
}
else {
this.breakpoints.set(newBrk, bkptNum);
resolve([true, newBrk]);
}
}
else {
reject(result);
}
}, reject);
});
}
removeBreakPoint(breakpoint: Breakpoint): Thenable<boolean> {
if (trace)
this.log("stderr", "removeBreakPoint");
return new Promise((resolve, reject) => {
if (!this.breakpoints.has(breakpoint))
return resolve(false);
this.sendCommand("break-delete " + this.breakpoints.get(breakpoint)).then((result) => {
if (result.resultRecords.resultClass == "done") {
this.breakpoints.delete(breakpoint);
resolve(true);
}
else resolve(false);
});
});
}
clearBreakPoints(): Thenable<any> {
if (trace)
this.log("stderr", "clearBreakPoints");
return new Promise((resolve, reject) => {
this.sendCommand("break-delete").then((result) => {
if (result.resultRecords.resultClass == "done") {
this.breakpoints.clear();
resolve(true);
}
else resolve(false);
}, () => {
resolve(false);
});
});
}
getStack(maxLevels: number): Thenable<Stack[]> {
if (trace)
this.log("stderr", "getStack");
return new Promise((resolve, reject) => {
let command = "stack-list-frames";
if (maxLevels) {
command += " 0 " + maxLevels;
}
this.sendCommand(command).then((result) => {
let stack = result.result("stack");
let ret: Stack[] = [];
stack.forEach(element => {
let level = MINode.valueOf(element, "@frame.level");
let addr = MINode.valueOf(element, "@frame.addr");
let func = MINode.valueOf(element, "@frame.func");
let filename = MINode.valueOf(element, "@frame.file");
let file = MINode.valueOf(element, "@frame.fullname");
let line = 0;
let lnstr = MINode.valueOf(element, "@frame.line");
if (lnstr)
line = parseInt(lnstr);
let from = parseInt(MINode.valueOf(element, "@frame.from"));
ret.push({
address: addr,
fileName: filename,
file: file,
function: func || from,
level: level,
line: line
});
});
resolve(ret);
}, reject);
});
}
async getStackVariables(thread: number, frame: number): Promise<Variable[]> {
if (trace)
this.log("stderr", "getStackVariables");
const result = await this.sendCommand(`stack-list-variables --thread ${thread} --frame ${frame} --simple-values`);
const variables = result.result("variables");
let ret: Variable[] = [];
for (const element of variables) {
const key = MINode.valueOf(element, "name");
const value = MINode.valueOf(element, "value");
const type = MINode.valueOf(element, "type");
ret.push({
name: key,
valueStr: value,
type: type,
raw: element
});
}
return ret;
}
examineMemory(from: number, length: number): Thenable<any> {
if (trace)
this.log("stderr", "examineMemory");
return new Promise((resolve, reject) => {
this.sendCommand("data-read-memory-bytes 0x" + from.toString(16) + " " + length).then((result) => {
resolve(result.result("memory[0].contents"));
}, reject);
});
}
evalExpression(name: string): Thenable<any> {
if (trace)
this.log("stderr", "evalExpression");
return new Promise((resolve, reject) => {
this.sendCommand("data-evaluate-expression " + name).then((result) => {
resolve(result);
}, reject);
});
}
async varCreate(expression: string, name: string = "-"): Promise<VariableObject> {
if (trace)
this.log("stderr", "varCreate");
const res = await this.sendCommand(`var-create ${name} @ "${expression}"`);
return new VariableObject(res.result(""));
}
async varEvalExpression(name: string): Promise < MINode > {
if (trace)
this.log("stderr", "varEvalExpression");
return this.sendCommand(`var-evaluate-expression ${name}`);
}
async varListChildren(name: string): Promise<VariableObject[]> {
if (trace)
this.log("stderr", "varListChildren");
//TODO: add `from` and `to` arguments
const res = await this.sendCommand(`var-list-children --all-values ${name}`);
const children = res.result("children");
let omg: VariableObject[] = children.map(child => new VariableObject(child[1]));
return omg;
}
async varUpdate(name: string = "*"): Promise<MINode> {
if (trace)
this.log("stderr", "varUpdate");
return this.sendCommand(`var-update --all-values ${name}`)
}
logNoNewLine(type: string, msg: string) {
this.emit("msg", type, msg);
}
log(type: string, msg: string) {
this.emit("msg", type, msg[msg.length - 1] == '\n' ? msg : (msg + "\n"));
}
sendUserInput(command: string): Thenable<any> {
if (command.startsWith("-")) {
return this.sendCommand(command.substr(1));
}
else {
this.sendRaw(command);
return Promise.resolve(undefined);
}
}
sendRaw(raw: string) {
if (this.printCalls)
this.log("log", raw);
if (this.isSSH)
this.stream.write(raw + "\n");
else
this.process.stdin.write(raw + "\n");
}
sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
let sel = this.currentToken++;
return new Promise((resolve, reject) => {
this.handlers[sel] = (node: MINode) => {
if (node && node.resultRecords && node.resultRecords.resultClass === "error") {
if (suppressFailure) {
this.log("stderr", "WARNING: Error executing command '" + command + "'");
resolve(node);
}
else
reject((node.result("msg") || "Internal error") + " (from " + command + ")");
}
else
resolve(node);
};
this.sendRaw(sel + "-" + command);
});
}
isReady(): boolean {
return this.isSSH ? this.sshReady : !!this.process;
}
printCalls: boolean;
debugOutput: boolean;
public procEnv: any;
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 errbuf: string;
protected process: ChildProcess.ChildProcess;
protected stream;
protected sshConn;
}