import { Breakpoint, IBackend, Thread, Stack, SSHArguments, Variable, 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 { posix } from "path"; import * as nativePath from "path"; const path = posix; import { Client } from "ssh2"; import { TreeItemCollapsibleState } from "vscode"; import { setFlagsFromString } from "v8"; export function escape(str: string) { return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\""); } //const nonOutput = /^(?:\d*|undefined)[\*\+\=]|[\~\@\&\^]/; const nonOutput = /^(?:\d*|undefined)[\*\=]|[\~\@\&\^]/; // Dubner removed '+' const gdbMatch = /(?:\d*|undefined)\(gdb\)/; const numRegex = /\d+/; //function couldBeOutput(line: string) { // if (nonOutput.exec(line)) // return false; // return true; //} function couldBeOutput(line: string) { // Return false if line starts with GDB/MI keyword const gdbmi = [ "(gdb)", // Result records "^done", "^running", "^connected", "^error", "^exit", // Stream records "~\"", "@\"", "&\"", // Async records "*running", "*stopped", "=thread-", "=library-", "=traceframe-", "=tsv-", "=tsv-", "=breakpoint-", "=record-", "=cmd-", "=memory-", ]; // Skip leading digits: var nstart = 0; var patt = new RegExp("^[0-9]"); while( nstart<line.length ) { var onechar = line.slice(nstart,nstart+1) ; if( !patt.test(onechar) ) { break; } nstart += 1; } for( const possible of gdbmi ) { var chunk = line.slice(nstart,nstart+possible.length); if( possible == chunk ) { 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) { 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 === null) delete env[key]; else env[key] = procEnv[key]; } } this.procEnv = env; } } load(cwd: string, program: string, procArgs: string, separateConsole: string): Thenable<any> { if (!nativePath.isAbsolute(program)) program = nativePath.join(cwd, program); return new Promise((resolve, reject) => { this.isSSH = false; const args = this.preargs.concat(this.extraargs || []); this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv }); this.process.stdout.on("data", this.stdout.bind(this)); this.process.stderr.on("data", this.stderr.bind(this)); this.process.on("exit", (() => { this.emit("quit"); }).bind(this)); this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this)); const promises = this.initCommands(program, 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, program: 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) => { 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 (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..."); const 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 " + program; 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)); const promises = this.initCommands(program, 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); } const 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) + "\"")); if (this.prettyPrint) cmds.push(this.sendCommand("enable-pretty-printing")); return cmds; } attach(cwd: string, executable: string, target: string, iexclause: string, ixscript:string, primdebug:string): Thenable<any> { return new Promise((resolve, reject) => { let args = []; args = args.concat(this.preargs); if (executable && !nativePath.isAbsolute(executable)) { executable = nativePath.join(cwd, executable); } if (!executable && target) { executable = "-p"; } let isExtendedRemote = false; if (target && target.startsWith("extended-remote")) { isExtendedRemote = true; args = this.preargs; } else{ if( iexclause != undefined ){ args = args.concat("-iex"); args = args.concat("set 'solib-search-path " + iexclause + "'") } if( ixscript ) { args = args.concat("-ix"); args = args.concat(ixscript); } if ( primdebug ){ // One of the numerous possibilities is that the user has supplied us // with a path to a file, or fifo, containing a single line: // PID appname\n // // We need to read that file, and turn it into -ex commands: // // Get the contents of the file try { var primdebugstring:string = fs.readFileSync(primdebug,"UTF-8"); // Strip off the newline at the end primdebugstring = primdebugstring.slice(0,-1); var tokens = primdebugstring.split("\t"); if( tokens.length == 2 ){ // Put in the -p PID args = args.concat("-p"); args = args.concat(tokens[0]); // Put in the -ex 'b app_' args = args.concat("-ex"); args = args.concat("b " + tokens[1] + "_"); // Put in the -ex 'signal SIGCONT' args = args.concat("-ex"); args = args.concat("signal SIGCONT"); } } catch(error){ } } if (executable) { args = args.concat(executable); } if (target) { args = args.concat(target); } } 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 commands = [ //this.sendCommand("gdb-set target-async on"), // Dubner eliminated this to prevent crashing 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); }); } convert_mi_variables(buf:string):any { var retval:any = buf; if( typeof buf == "string" ) { // The cobcd.py extension to gdb returns a string when issued the // command "p/m ?". The processing up to this point converts that // string to a string, meaning any embedded control characters // have been converted to '\"', '\\', '\n,' and so on. // I don't know who is doing that, or why. Rather than dig down, // I decided to minimize the impact and just transmogrify it back // here. const leading_variables:string = '~"variables=' ; const leading_value:string = '~"value=' ; if( buf.substr(0,leading_variables.length) == leading_variables || buf.substr(0,leading_value.length) == leading_value) { //console.log("convert_mi_variables(1): ", buf); //DUBNERDEBUG var n1 = buf.indexOf('\n'); if( n1 > -1) { var n2 = buf.indexOf('\n',n1+1); if( n2 > -1) { retval = buf.substr(n1+1,n2-(n1+1)); retval += ',' retval += buf.substr(2,(n1-1)-2); // Skip the leading '~"' and the trailing '"' } } var temp:string = retval; retval = ""; for(var i=0; i<temp.length; ) { var ch = temp.charAt(i++); if( ch == "\\" ) { ch = temp.charAt(i++); if( ch == 'n' ) { retval += '\n'; } else { retval += ch; } } else { retval += ch ; } } temp = retval; retval = ""; for(var i=0; i<temp.length; i++) { var ch = temp.charAt(i); if( ch == '/'){ ch = '/'; // '\u203F'; // undertie } retval += ch ; } console.log("convert_mi_variables(2): ", retval); // DUBNERDEBUG // var nfound = retval.indexOf('['); // if(nfound != -1){ // var newr = retval.substr(0,nfound) // newr = newr + "[{name=\"L01\",value=\"{_groupvalue=\\\"Snap!\\\",fullname=\\\"RobertDubner\\\",L02={_groupvalue=\\\"Crackle!\\\",firstname = \\\"Robert\\\", lastname = \\\"Dubner\\\",L03={_groupvalue=\\\"Pop!\\\",firstname = \\\"Judy\\\", lastname = \\\"Ruderman\\\"}}}\"}]\n"; // console.log("convert_mi_variables(3): ", newr); // DUBNERDEBUG // retval = newr; // } } } return retval; } stdout(data) { if( this.buffer == undefined ) { this.buffer = ""; } if (trace) this.log("stderr", "stdout: " + data); if (typeof data == "string") { this.buffer += data; //console.log("(A): ", data); //DUBNERDEBUG } else { this.buffer += data.toString("utf8"); //console.log("(B): ", data.toString("utf8")); //DUBNERDEBUG } var end = this.buffer.lastIndexOf('\n'); // The 'p/m ?' request is not an MI request, so sometimes the // p/m response comes in, and then a little while later the token^done // response comes in. This following code only runs when a // 'p/m ?' request has been sent. It will strip out the '&p/m ?' response // that the machine-interface sends back. And then it'll accumulate // strings until it sees ^done if( this.recent_variables_request > -1 ) { // We did a 'p/m ?' request. if( this.buffer.length>0 && this.buffer.charAt(0) == '&') { // Strip out the '&...' echo end = this.buffer.indexOf('\n'); if( end != -1 ) { this.buffer = this.buffer.substr(end + 1); } end = this.buffer.lastIndexOf('\n'); } // Just keep accumulating characters until we get to the ^done\n, which // the machine-interface will send out after the ~response if( this.buffer.indexOf("^done\n") == -1 ) { end = -1; } } if (end != -1) { this.buffer = this.convert_mi_variables(this.buffer); // We need to find end again, in case convert_mi_variables // changed the length. end = this.buffer.lastIndexOf('\n'); 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"); const 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 { 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 (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)); } 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(): 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) { 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); }); } changeVariableOriginal(name: string, rawValue: string): Thenable<any> { if (trace) this.log("stderr", "changeVariable"); return this.sendCommand("gdb-set var " + name + "=" + rawValue); } changeVariable(name: string, rawValue: string): Thenable<any> { if (trace) this.log("stderr", "changeVariable"); return this.sendStraightCommand("p/m " + name.substr(3) + "=" + 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, 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, undefined]); let location = ""; if (breakpoint.countCondition) { if (breakpoint.countCondition[0] == ">") location += "-i " + numRegex.exec(breakpoint.countCondition.substr(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: 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, 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(): 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); }); }); } 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[] = []; return threads.map(element => { const ret: Thread = { id: parseInt(MINode.valueOf(element, "id")), targetId: MINode.valueOf(element, "target-id") }; const name = MINode.valueOf(element, "name"); if (name) { ret.name = name; } return ret; }); } async getStack(maxLevels: number, thread: number): Promise<Stack[]> { if (trace) this.log("stderr", "getStack"); let command = "stack-list-frames"; if (thread != 0) { command += ` --thread ${thread}`; } if (maxLevels) { command += " 0 " + maxLevels; } const result = await this.sendCommand(command); const stack = result.result("stack"); const ret: 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 = posix.normalize(file); else file = nativePath.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 getStackVariablesOriginal(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 getStackVariables(thread: number, frame: number): Promise<Variable[]> { if (trace) this.log("stderr", "getStackVariables"); const result = await this.sendStraightCommand(`p/m ?`); 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; } examineMemory(from: number, length: number): Thenable<any> {7 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 = "p/m "; command += name; //console.log("evalExpression(): ", command); // DUBNERDEBUG return await this.sendStraightCommand(command); // let command = "data-evaluate-expression "; // if (thread != 0) { // command += `--thread ${thread} --frame ${frame} `; // } // command += name; // //console.log(command); // DUBNERDEBUG // return await this.sendCommand(command); } 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") || []; 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 ${name}`); } async varAssign(name: string, rawValue: string): Promise<MINode> { if (trace) this.log("stderr", "varAssign"); return this.sendCommand(`var-assign ${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<any> { if (command.startsWith("-")) { return this.sendCommand(command.substr(1)); } else { return this.sendCliCommand(command, threadId, frameLevel); } } sendRaw(raw: string) { //console.log("sendRaw():", raw); // DUBNERDEBUG if (this.printCalls) this.log("log", raw); if (this.isSSH) this.stream.write(raw + "\n"); else this.process.stdin.write(raw + "\n"); } async sendCliCommand(command: string, threadId: number = 0, frameLevel: number = 0) { // For some reason, somebody, somewhere, is sending a console command "process.pid", which // just confuses everybody and creates an error message the user can see. // Let's nip this one in the bud. if( command == "process.pid" ){ return; } let miCommand = "interpreter-exec "; if (threadId != 0) { miCommand += `--thread ${threadId} --frame ${frameLevel} `; } miCommand += `console "${command.replace(/[\\"']/g, '\\$&')}"`; await this.sendCommand(miCommand); } sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> { const sel = this.currentToken++; this.recent_variables_request = -1; 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); }); } sendStraightCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> { const sel = this.currentToken++; this.recent_variables_request = sel; 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); }); } isReady(): boolean { return this.isSSH ? this.sshReady : !!this.process; } prettyPrint: boolean = true; 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; protected recent_variables_request: number; // Most recent MI token for 'p/m ?' variables request }