si-debug-1
Version:
GDB, LLDB & Mago-MI Debugger support for SuperIDE
961 lines (883 loc) • 32.3 kB
text/typescript
import { Breakpoint, IBackend, Thread, Stack, SSHArguments, Variable, RegisterValue, VariableObject, MIError } 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 * as path from "path";
import { Client } from "ssh2";
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, public extraCommands: string[] = []) {
super();
if (procEnv) {
const env = {};
// Duplicate process.env so we don't override it
for (const key in process.env)
if (process.env.hasOwnProperty(key))
env[key] = process.env[key];
// Overwrite with user specified variables
for (const key in procEnv) {
if (procEnv.hasOwnProperty(key)) {
if (procEnv === undefined)
delete env[key];
else
env[key] = procEnv[key];
}
}
this.procEnv = env;
}
}
load(cwd: string, target: string, procArgs: string, separateConsole: string, autorun: string[]): Thenable<any> {
if (!path.isAbsolute(target))
target = path.join(cwd, target);
return new Promise((resolve, reject) => {
this.isSSH = false;
const args = this.preargs.concat(this.extraargs || []);
const pythonInterpreter = '/home/denk/.superide/penv/bin/python3'; // 或者 'python',取决于你的系统和配置
//const pythonInterpreter = 'super-ide'; // 或者 'python',取决于你的系统和配置
// const pythonScript = ['/home/denk/.superide/penv/lib/python3.10/site-packages/superide/__main__.py',
// 'debug', '-d', '/home/denk/PlatformIO/Projects/hello',
// '--interface=gdb', '--interpreter=mi2', '-q'];
const pythonScript = ['-m','superide','debug', '-d', '/home/denk/PlatformIO/Projects/hello',
'--interface=gdb', '--interpreter=mi2', '-q'];
//this.process = ChildProcess.spawn(pythonInterpreter, pythonScript, spawnOptions1);
this.process = ChildProcess.spawn(pythonInterpreter, pythonScript, { cwd: '/home/denk/PlatformIO/Projects/hello', env: this.procEnv });
//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));
const 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"));
promises.push(...autorun.map(value => { return this.sendUserInput(value); }));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
} else {
if (separateConsole !== undefined) {
linuxTerm.spawnTerminalEmulator(separateConsole).then(tty => {
promises.push(this.sendCommand("inferior-tty-set " + tty));
promises.push(...autorun.map(value => { return this.sendUserInput(value); }));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
} else {
promises.push(...autorun.map(value => { return this.sendUserInput(value); }));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
}
}
});
}
ssh(args: SSHArguments, cwd: string, target: string, procArgs: string, separateConsole: string, attach: boolean, autorun: string[]): 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) => {
const 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", () => {
const xclientsock = accept();
xclientsock.pipe(xserversock).pipe(xclientsock);
});
xserversock.connect(args.x11port, args.x11host);
});
}
const connectionArgs: any = {
host: args.host,
port: args.port,
username: args.user
};
if (args.useAgent) {
connectionArgs.agent = process.env.SSH_AUTH_SOCK;
} else if (args.keyfile) {
if (fs.existsSync(args.keyfile))
connectionArgs.privateKey = 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...");
const execArgs: any = {};
if (args.forwardX11) {
execArgs.x11 = {
single: false,
screen: args.remotex11screen
};
}
let sshCMD = this.application + " " + this.preargs.concat(this.extraargs || []).join(" ");
if (args.bootstrap) sshCMD = args.bootstrap + " && " + sshCMD;
this.sshConn.exec(sshCMD, execArgs, (err, stream) => {
if (err) {
this.log("stderr", "Could not run " + this.application + "(" + sshCMD + ") over ssh!");
if (err === undefined) {
err = "<reason unknown>";
}
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));
const promises = this.initCommands(target, cwd, attach);
promises.push(this.sendCommand("environment-cd \"" + escape(cwd) + "\""));
if (attach) {
// Attach to local process
promises.push(this.sendCommand("target-attach " + target));
} else if (procArgs && procArgs.length)
promises.push(this.sendCommand("exec-arguments " + procArgs));
promises.push(...autorun.map(value => { return this.sendUserInput(value); }));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
}).on("error", (err) => {
this.log("stderr", "Error running " + this.application + " over ssh!");
if (err === undefined) {
err = "<reason unknown>";
}
this.log("stderr", err.toString());
this.emit("quit");
reject();
}).connect(connectionArgs);
});
}
protected initCommands(target: string, cwd: string, attach: boolean = false) {
// We need to account for the possibility of the path type used by the debugger being different
// from the path type where the extension is running (e.g., SSH from Linux to Windows machine).
// Since the CWD is expected to be an absolute path in the debugger's environment, we can test
// that to determine the path type used by the debugger and use the result of that test to
// select the correct API to check whether the target path is an absolute path.
const debuggerPath = path.posix.isAbsolute(cwd) ? path.posix : path.win32;
if (!debuggerPath.isAbsolute(target))
target = debuggerPath.join(cwd, target);
const cmds = [
this.sendCommand("gdb-set target-async on", true),
new Promise(resolve => {
this.sendCommand("list-features").then(done => {
this.features = done.result("features");
resolve(undefined);
}, () => {
// Default to no supported features on error
this.features = [];
resolve(undefined);
});
}),
this.sendCommand("environment-directory \"" + escape(cwd) + "\"", true)
];
if (!attach)
cmds.push(this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\""));
if (this.prettyPrint)
cmds.push(this.sendCommand("enable-pretty-printing"));
for (const cmd of this.extraCommands) {
cmds.push(this.sendCommand(cmd));
}
return cmds;
}
attach(cwd: string, executable: string, target: string, autorun: string[]): Thenable<any> {
return new Promise((resolve, reject) => {
let args = [];
if (executable && !path.isAbsolute(executable))
executable = path.join(cwd, executable);
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));
const promises = this.initCommands(target, cwd, true);
if (target.startsWith("extended-remote")) {
promises.push(this.sendCommand("target-select " + target));
if (executable)
promises.push(this.sendCommand("file-symbol-file \"" + escape(executable) + "\""));
} else {
// Attach to local process
if (executable)
promises.push(this.sendCommand("file-exec-and-symbols \"" + escape(executable) + "\""));
promises.push(this.sendCommand("target-attach " + target));
}
promises.push(...autorun.map(value => { return this.sendUserInput(value); }));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
}
connect(cwd: string, executable: string, target: string, autorun: string[]): Thenable<any> {
return new Promise((resolve, reject) => {
let args = [];
if (executable && !path.isAbsolute(executable))
executable = path.join(cwd, executable);
args = this.preargs.concat(this.extraargs || []);
if (executable)
args = args.concat([executable]);
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));
const promises = this.initCommands(target, cwd, true);
promises.push(this.sendCommand("target-select remote " + target));
promises.push(...autorun.map(value => { return this.sendUserInput(value); }));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
}
stdout(data) {
if (trace)
this.log("stderr", "stdout: " + data);
if (typeof data == "string")
this.buffer += data;
else
this.buffer += data.toString("utf8");
const end = this.buffer.lastIndexOf('\n');
if (end != -1) {
this.onOutput(this.buffer.substring(0, end));
this.buffer = this.buffer.substring(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");
const end = this.errbuf.lastIndexOf('\n');
if (end != -1) {
this.onOutputStderr(this.errbuf.substring(0, end));
this.errbuf = this.errbuf.substring(end + 1);
}
if (this.errbuf.length) {
this.logNoNewLine("stderr", this.errbuf);
this.errbuf = "";
}
}
onOutputStderr(str: string) {
const lines = str.split('\n');
lines.forEach(line => {
this.log("stderr", line);
});
}
onOutputPartial(line: string) {
if (couldBeOutput(line)) {
this.logNoNewLine("stdout", line);
return true;
}
return false;
}
onOutput(str: string) {
const lines = str.split('\n');
lines.forEach(line => {
if (couldBeOutput(line)) {
if (!gdbMatch.exec(line))
this.log("stdout", line);
} else {
const 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") {
const reason = parsed.record("reason");
if (reason === undefined) {
if (trace)
this.log("stderr", "stop (no reason given)");
// attaching to a process stops, but does not provide a reason
// also python generated interrupt seems to only produce this
this.emit("step-other", parsed);
} else {
if (trace)
this.log("stderr", "stop: " + reason);
switch (reason) {
case "breakpoint-hit":
this.emit("breakpoint", parsed);
break;
case "watchpoint-trigger":
case "read-watchpoint-trigger":
case "access-watchpoint-trigger":
this.emit("watchpoint", parsed);
break;
case "function-finished":
// identical result → send step-end
// this.emit("step-out-end", parsed);
// break;
case "location-reached":
case "end-stepping-range":
this.emit("step-end", parsed);
break;
case "watchpoint-scope":
case "solib-event":
case "syscall-entry":
case "syscall-return":
// TODO: inform the user
this.emit("step-end", parsed);
break;
case "fork":
case "vfork":
case "exec":
// TODO: inform the user, possibly add second inferior
this.emit("step-end", parsed);
break;
case "signal-received":
this.emit("signal-stop", parsed);
break;
case "exited-normally":
this.emit("exited-normally", parsed);
break;
case "exited": // exit with error code != 0
this.log("stderr", "Program exited with code " + parsed.record("exit-code"));
this.emit("exited-normally", parsed);
break;
// case "exited-signalled": // consider handling that explicit possible
// this.log("stderr", "Program exited because of signal " + parsed.record("signal"));
// this.emit("stopped", parsed);
// break;
default:
this.log("console", "Not implemented stop reason (assuming exception): " + reason);
this.emit("stopped", parsed);
break;
}
}
} else
this.log("log", JSON.stringify(parsed));
} else if (record.type == "notify") {
if (record.asyncClass == "thread-created") {
this.emit("thread-created", parsed);
} else if (record.asyncClass == "thread-exited") {
this.emit("thread-exited", 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(runToStart: boolean): Thenable<boolean> {
const options: string[] = [];
if (runToStart)
options.push("--start");
const startCommand: string = ["exec-run"].concat(options).join(" ");
return new Promise((resolve, reject) => {
this.log("console", "Running executable");
this.sendCommand(startCommand).then((info) => {
if (info.resultRecords.resultClass == "running")
resolve(undefined);
else
reject();
}, reject);
});
}
stop() {
if (this.isSSH) {
const proc = this.stream;
const to = setTimeout(() => {
proc.signal("KILL");
}, 1000);
this.stream.on("exit", function (code) {
clearTimeout(to);
});
this.sendRaw("-gdb-exit");
} else {
const proc = this.process;
const to = setTimeout(() => {
process.kill(-proc.pid);
}, 1000);
this.process.on("exit", function (code) {
clearTimeout(to);
});
this.sendRaw("-gdb-exit");
}
}
detach() {
const proc = this.process;
const 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);
});
}
goto(filename: string, line: number): Thenable<Boolean> {
if (trace)
this.log("stderr", "goto");
return new Promise((resolve, reject) => {
const target: string = '"' + (filename ? escape(filename) + ":" : "") + line + '"';
this.sendCommand("break-insert -t " + target).then(() => {
this.sendCommand("exec-jump " + target).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
}, 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");
const promisses = [];
breakpoints.forEach(breakpoint => {
promisses.push(this.addBreakPoint(breakpoint));
});
return Promise.all(promisses);
}
setBreakPointCondition(bkptNum: number, condition: string): Thenable<any> {
if (trace)
this.log("stderr", "setBreakPointCondition");
return this.sendCommand("break-condition " + bkptNum + " " + condition);
}
setEntryBreakPoint(entryPoint: string): Thenable<any> {
return this.sendCommand("break-insert -t -f " + entryPoint);
}
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, undefined]);
let location = "";
if (breakpoint.countCondition) {
if (breakpoint.countCondition[0] == ">")
location += "-i " + numRegex.exec(breakpoint.countCondition.substring(1))[0] + " ";
else {
const 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") {
const bkptNum = parseInt(result.result("bkpt.number"));
const newBrk = {
file: breakpoint.file ? breakpoint.file : result.result("bkpt.file"),
raw: breakpoint.raw,
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, undefined]);
}
}, 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(source?: string): Thenable<any> {
if (trace)
this.log("stderr", "clearBreakPoints");
return new Promise((resolve, reject) => {
const promises = [];
const breakpoints = this.breakpoints;
this.breakpoints = new Map();
breakpoints.forEach((k, index) => {
if (index.file === source) {
promises.push(this.sendCommand("break-delete " + k).then((result) => {
if (result.resultRecords.resultClass == "done") resolve(true);
else resolve(false);
}));
} else {
this.breakpoints.set(index, k);
}
});
Promise.all(promises).then(resolve, reject);
});
}
async getThreads(): Promise<Thread[]> {
if (trace) this.log("stderr", "getThreads");
const command = "thread-info";
const result = await this.sendCommand(command);
const threads = result.result("threads");
const ret: Thread[] = [];
if (!Array.isArray(threads)) { // workaround for lldb-mi bug: `'^done,threads="[]"'`
return ret;
}
return threads.map(element => {
const ret: Thread = {
id: parseInt(MINode.valueOf(element, "id")),
targetId: MINode.valueOf(element, "target-id"),
name: MINode.valueOf(element, "name") || MINode.valueOf(element, "details")
};
return ret;
});
}
async getStack(startFrame: number, maxLevels: number, thread: number): Promise<Stack[]> {
if (trace) this.log("stderr", "getStack");
const options: string[] = [];
if (thread != 0)
options.push("--thread " + thread);
const depth: number = (await this.sendCommand(["stack-info-depth"].concat(options).join(" "))).result("depth").valueOf();
const lowFrame: number = startFrame ? startFrame : 0;
const highFrame: number = (maxLevels ? Math.min(depth, lowFrame + maxLevels) : depth) - 1;
if (highFrame < lowFrame)
return [];
options.push(lowFrame.toString());
options.push(highFrame.toString());
const result = await this.sendCommand(["stack-list-frames"].concat(options).join(" "));
const stack = result.result("stack");
return stack.map(element => {
const level = MINode.valueOf(element, "@frame.level");
const addr = MINode.valueOf(element, "@frame.addr");
const func = MINode.valueOf(element, "@frame.func");
const filename = MINode.valueOf(element, "@frame.file");
let file: string = MINode.valueOf(element, "@frame.fullname");
if (file) {
if (this.isSSH)
file = path.posix.normalize(file);
else
file = path.normalize(file);
}
let line = 0;
const lnstr = MINode.valueOf(element, "@frame.line");
if (lnstr)
line = parseInt(lnstr);
const from = parseInt(MINode.valueOf(element, "@frame.from"));
return {
address: addr,
fileName: filename,
file: file,
function: func || from,
level: level,
line: line
};
});
}
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");
const 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;
}
async getRegisters(): Promise<Variable[]> {
if (trace)
this.log("stderr", "getRegisters");
// Getting register names and values are separate GDB commands.
// We first retrieve the register names and then the values.
// The register names should never change, so we could cache and reuse them,
// but for now we just retrieve them every time to keep it simple.
const names = await this.getRegisterNames();
const values = await this.getRegisterValues();
const ret: Variable[] = [];
for (const val of values) {
const key = names[val.index];
const value = val.value;
const type = "string";
ret.push({
name: key,
valueStr: value,
type: type
});
}
return ret;
}
async getRegisterNames(): Promise<string[]> {
if (trace)
this.log("stderr", "getRegisterNames");
const result = await this.sendCommand("data-list-register-names");
const names = result.result('register-names');
if (!Array.isArray(names)) {
throw new Error('Failed to retrieve register names.');
}
return names.map(name => name.toString());
}
async getRegisterValues(): Promise<RegisterValue[]> {
if (trace)
this.log("stderr", "getRegisterValues");
const result = await this.sendCommand("data-list-register-values N");
const nodes = result.result('register-values');
if (!Array.isArray(nodes)) {
throw new Error('Failed to retrieve register values.');
}
const ret: RegisterValue[] = nodes.map(node => {
const index = parseInt(MINode.valueOf(node, "number"));
const value = MINode.valueOf(node, "value");
return { index: index, value: value };
});
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);
});
}
async evalExpression(name: string, thread: number, frame: number): Promise<MINode> {
if (trace)
this.log("stderr", "evalExpression");
let command = "data-evaluate-expression ";
if (thread != 0) {
command += `--thread ${thread} --frame ${frame} `;
}
command += name;
return await this.sendCommand(command);
}
async varCreate(threadId: number, frameLevel: number, expression: string, name: string = "-", frame: string = "@"): Promise<VariableObject> {
if (trace)
this.log("stderr", "varCreate");
let miCommand = "var-create ";
if (threadId != 0) {
miCommand += `--thread ${threadId} --frame ${frameLevel}`;
}
const res = await this.sendCommand(`${miCommand} ${this.quote(name)} ${frame} "${expression}"`);
return new VariableObject(res.result(""));
}
async varEvalExpression(name: string): Promise<MINode> {
if (trace)
this.log("stderr", "varEvalExpression");
return this.sendCommand(`var-evaluate-expression ${this.quote(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 ${this.quote(name)}`);
const children = res.result("children") || [];
const 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 ${this.quote(name)}`);
}
async varAssign(name: string, rawValue: string): Promise<MINode> {
if (trace)
this.log("stderr", "varAssign");
return this.sendCommand(`var-assign ${this.quote(name)} ${rawValue}`);
}
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, threadId: number = 0, frameLevel: number = 0): Thenable<MINode> {
if (command.startsWith("-")) {
return this.sendCommand(command.substring(1));
} else {
return this.sendCliCommand(command, threadId, frameLevel);
}
}
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");
}
sendCliCommand(command: string, threadId: number = 0, frameLevel: number = 0): Thenable<MINode> {
let miCommand = "interpreter-exec ";
if (threadId != 0) {
miCommand += `--thread ${threadId} --frame ${frameLevel} `;
}
miCommand += `console "${command.replace(/[\\"']/g, '\\$&')}"`;
return this.sendCommand(miCommand);
}
sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
const 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(new MIError(node.result("msg") || "Internal error", command));
} else
resolve(node);
};
this.sendRaw(sel + "-" + command);
this.log("stdout",sel + "-" + command);
});
}
isReady(): boolean {
return this.isSSH ? this.sshReady : !!this.process;
}
protected quote(text: string): string {
// only escape if text contains non-word or non-path characters such as whitespace or quotes
return /^-|[^\w\d\/_\-\.]/g.test(text) ? ('"' + escape(text) + '"') : text;
}
prettyPrint: boolean = true;
printCalls: boolean;
debugOutput: boolean;
features: string[];
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;
}