/** * gdb-mi-parser * https://github.com/llop/gdb-mi-parser */ //---------------------------------------------------------- // // Helper string functions // //---------------------------------------------------------- function isEmpty(str) { return !str || 0===str.length; } function trim(str) { if (str) str = str.trim(); return str; } //---------------------------------------------------------- // // prefix mappings // //---------------------------------------------------------- var recordTypeMapping = { '^': 'result', '~': 'stream', '&': 'stream', '@': 'stream', '*': 'async', '+': 'async', '=': 'async' }; var outputTypeMapping = { '^': 'result', '~': 'console', '&': 'log', '@': 'target', '*': 'exec', '+': 'status', '=': 'notify' }; /* * About gdb output * https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Syntax.html * * output ==> * ( out-of-band-record )* [ result-record ] "(gdb)" nl * result-record ==> * [ token ] "^" result-class ( "," result )* nl * out-of-band-record ==> * async-record | stream-record * async-record ==> * exec-async-output | status-async-output | notify-async-output * exec-async-output ==> * [ token ] "*" async-output nl * status-async-output ==> * [ token ] "+" async-output nl * notify-async-output ==> * [ token ] "=" async-output nl * async-output ==> * async-class ( "," result )* * result-class ==> * "done" | "running" | "connected" | "error" | "exit" * async-class ==> * "stopped" | others (where others will be added depending on the needs—this is still in development). * result ==> * variable "=" value * variable ==> * string * value ==> * const | tuple | list * const ==> * c-string * tuple ==> * "{}" | "{" result ( "," result )* "}" * list ==> * "[]" | "[" value ( "," value )* "]" | "[" result ( "," result )* "]" * stream-record ==> * console-stream-output | target-stream-output | log-stream-output * console-stream-output ==> * "~" c-string nl * target-stream-output ==> * "@" c-string nl * log-stream-output ==> * "&" c-string nl * nl ==> * CR | CR-LF * token ==> * any sequence of digits. * * Notes: * * All output sequences end in a single line containing a period. * * The token is from the corresponding request. Note that for all async output, while the token is allowed by * the grammar and may be output by future versions of gdb for select async output messages, it is generally omitted. * Frontends should treat all async output as reporting general changes in the state of the target * and there should be no need to associate async output to any prior command. * * status-async-output contains on-going status information about the progress of a slow operation. It can be discarded. * All status output is prefixed by ‘+’. * * exec-async-output contains asynchronous state change on the target (stopped, started, disappeared). * All async output is prefixed by ‘*’. * * notify-async-output contains supplementary information that the client should handle (e.g., a new breakpoint information). * All notify output is prefixed by ‘=’. * * console-stream-output is output that should be displayed as is in the console. It is the textual response to a CLI command. * All the console output is prefixed by ‘~’. * * target-stream-output is the output produced by the target program. All the target output is prefixed by ‘@’. * * log-stream-output is output text coming from gdb's internals, for instance messages that should be displayed * as part of an error log. All the log output is prefixed by ‘&’. * * New gdb/mi commands should only output lists containing values */ function eatWhitespace(line, i) { while (i='0' && line[j]<='9') ++j; return [j, line.substring(i, j)]; } function nextClass(line, i) { var j = i+1; while (j ( out-of-band-record )* [ result-record ] "(gdb)" nl return { hasTerminator: hasTerminator, outOfBandRecords: outOfBandRecords, resultRecord: resultRecord }; } /** * GDB Debugger plugin for Cloud9 * * @author Dan Armendariz * @author Rob Bowden */ var net = require('net'); var fs = require('fs'); var spawn = require('child_process').spawn; // process arguments function printUsage() { var p = [process.argv[0], process.argv[1]].join(" "); var msg = [ "Cloud9 GDB Debugger shim", "Usage: " + p + " [-b=bp] [-d=depth] [-g=gdb] [-p=proxy] BIN [args]\n", " bp: warn when BPs are sent but none are set (default true)", " depth: maximum stack depth computed (default 50)", " gdb: port that GDB client and server communicate (default 15470)", " proxy: port or socket that this shim listens for connections (default ~/.c9/gdbdebugger.socket)", " BIN: the binary to debug with GDB", " args: optional arguments for BIN\n" ]; console.error(msg.join("\n")); process.exit(1); } var argc = process.argv.length; if (argc < 3) printUsage(); // defaults var PROXY = { sock: process.env.HOME + "/.c9/gdbdebugger.socket" }; var GDB_PORT = 15470; var MAX_STACK_DEPTH = 50; var DEBUG = 0; var BIN = ""; var BP_WARN = true; // parse middle arguments function parseArg(str, allowNonInt) { if (str == null || str === "") printUsage(); var val = parseInt(str, 10); if (!allowNonInt && isNaN(val)) printUsage(); return val; } // attempt to parse shim arguments var i = 0; for (i = 2; i < argc && BIN === ""; i++) { var arg = process.argv[i]; var a = arg.split("="); var key = a[0]; var val = (a.length == 2) ? a[1] : null; switch (key) { case "-b": case "--bp": BP_WARN = (val === "true"); break; case "-d": case "--depth": MAX_STACK_DEPTH = parseArg(val); break; case "-g": case "--gdb": GDB_PORT = parseArg(val); break; case "-p": case "--proxy": var portNum = parseArg(val, true); if (isNaN(portNum)) PROXY = { sock: val }; else PROXY = { host: "127.0.0.1", port: portNum }; break; case "--debug": DEBUG = parseInt(val) || 0; break; default: BIN = arg; } } // all executable's arguments exist after executable path var ARGS = process.argv.slice(i); var STACK_RANGE = "0 " + MAX_STACK_DEPTH; // class instances var client = null; var gdb = null; var executable = null; // store abnormal exit state to relay to user var exit = null; var log = function() {}; if (DEBUG) { var log_file = fs.createWriteStream(process.env.HOME + "/.c9/gdb_proxy.log", { flags: "a" }); log_file.write("\n\n" + new Date()); log = function(str) { var args = Array.prototype.slice.call(arguments); log_file.write(args.join(" ") + "\n"); if (DEBUG > 1) console.log(str); }; } //////////////////////////////////////////////////////////////////////////////// // Client class to buffer and parse full JSON objects from plugin function Client(c) { this.connection = c; this.buffer = []; this.reconnect = function(c) { // replace old connection this.cleanup(); this.connection = c; }; this.connect = function(callback) { var parser = this._parse(); this.connection.on("data", function(data) { log("PLUGIN: " + data.toString()); // parse commands and begin processing queue var commands = parser(data); if (commands.length > 0) { gdb.command_queue = gdb.command_queue.concat(commands); gdb.handleCommands(); } }); this.connection.on("error", function (e) { log(e); process.exit(0); }); this.connection.on("end", function() { this.connection = null; }); callback(); }; // flush response buffer this.flush = function() { if (!this.connection) return; if (this.buffer.length == 0) return; this.buffer.forEach(function(msg) { this.connection.write(msg); }); this.buffer = []; }; this.cleanup = function() { if (this.connection) this.connection.end(); }; this._parse = function() { var data_buffer = ""; var data_length = false; var json_objects = []; function parser(data) { data = data_buffer + data.toString(); function abort() { var ret = json_objects; json_objects = []; return ret; } if (data_length === false) { var idx = data.indexOf("\r\n\r\n"); if (idx === -1) { data_buffer = data; return abort(); } data_length = parseInt(data.substr(15, idx), 10); data = data.slice(idx + 4); } // haven't gotten the full JSON object yet if (data.length < data_length) { return abort(); } data_buffer = data.slice(data_length); data = data.substr(0, data_length); try { data = JSON.parse(data); } catch (ex) { console.log("There was an error parsing data from the plugin."); log("JSON (Parse error): " + data); return abort(); } json_objects.push(data); data_length = false; return parser(""); } return parser; }; this.send = function(args) { args = JSON.stringify(args); var msg = ["Content-Length:", args.length, "\r\n\r\n", args].join(""); log("SENDING: " + msg); if (this.connection) this.connection.write(msg); else this.buffer.push(msg); }; } // End of Client class //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // Process class; initiating gdbserver, which in turn begins the executable function Executable() { this.proc = null; this.running = false; /** * Spawn GDB server which will in turn run executable, sharing * stdio with the shim. * * @param {Function} callback Called when gdbserver is listening */ this.spawn = function(callback) { var args = ["--once", ":" + GDB_PORT, BIN].concat(ARGS); this.proc = spawn("gdbserver", args, { cwd: process.cwd(), // stdio: "inherit", stdio: ['pipe', process.stdout, 'pipe'], }); var errqueue = []; this.proc.on("exit", function(code, signal) { log("GDB server terminated with code " + code + " and signal " + signal); client && client.send({ err: "killed", code: code, signal: signal }); exit = { proc: "GDB server", code: code, signal: signal }; // only quit if stderr has finished buffering data if (errqueue === null) process.exit(code); }.bind(this)); this.proc.on("error", function(e) { console.error("ERROR while launching the debugger:"); if (e.code == "ENOENT") { console.log("\t\"gdbserver\" is not installed"); } else { console.error(e); } process.exit(1); }); this.proc.stderr.on("end", function() { // dump queued stderr data, if it exists if (errqueue !== null) { log(errqueue.join("")); errqueue = null; } // quit now if gdbserver ended before stderr buffer flushed if (exit !== null) process.exit(exit.code); }); // wait for gdbserver to listen before executing callback function handleStderr(data) { // once listening, forward stderr to process if (this.running) return log(data.toString()); // consume and store stderr until gdbserver is listening var str = data.toString(); errqueue.push(str); if (str.indexOf("Listening") > -1) { // perform callback when gdbserver is ready callback(); } if (str.indexOf("127.0.0.1") > -1) { // soak up final gdbserver message before sharing i/o stream errqueue = null; this.running = true; } } this.proc.stderr.on("data", handleStderr.bind(this)); // necessary to redirect stdin this way or child receives SIGTTIN process.stdin.pipe(this.proc.stdin); }; /** * Dismantle the GDB server process. */ this.cleanup = function() { if (this.proc) { this.proc.kill("SIGHUP"); this.proc = null; } }; } // End of Client class //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // GDB class; connecting, parsing, issuing commands to the debugger function GDB() { this.sequence_id = 0; this.bp_set = null; this.callbacks = {}; this.state = {}; this.framecache = {}; this.varcache = {}; this.running = false; this.started = false; this.clientReconnect = false; this.memoized_files = []; this.command_queue = []; this.proc = null; ///// // Private methods // Create a buffer function that sends full lines to a callback function buffers() { var last_buffer = ""; return function(data, callback) { var full_output = last_buffer + data; var lines = full_output.split("\n"); // populate the stream's last buffer if the last line is incomplete last_buffer = (full_output.slice(-1) == "\n") ? "" : lines.pop; for (var i = 0; i < lines.length; i++) { if (lines[i].length === 0) continue; callback(lines[i]); } }; } //// // Public Methods // spawn the GDB client process this.spawn = function() { this.proc = spawn('gdb', ['-q', '--interpreter=mi2', BIN], { detached: false, cwd: process.cwd() }); this.proc.on("error", function(e) { console.error("ERROR while launching the debugger:"); if (e.code == "ENOENT") { console.log("\t\"gdb\" is not installed"); } else { console.error(e); } }); var self = this; // handle gdb output var stdout_buff = buffers(); this.proc.stdout.on("data", function(stdout_data) { stdout_buff(stdout_data, self._handleLine.bind(self)); }); // handle gdb stderr var stderr_buff = buffers(); this.proc.stderr.on("data", function(stderr_data) { stderr_buff(stderr_data, function(line) { log("GDB STDERR: " + line); }); }); this.proc.on("exit", function(code, signal) { log("GDB terminated with code " + code + " and signal " + signal); client && client.send({ err: "killed", code: code, signal: signal }); exit = { proc: "GDB", code: code, signal: signal }; process.exit(code); }); }; this.connect = function(callback) { this.issue("-target-select", "remote localhost:" + GDB_PORT, function(reply) { if (reply.state != "connected") return callback(reply, "Cannot connect to gdbserver"); // connected! set eval of conditional breakpoints on server this.issue("set breakpoint", "condition-evaluation host", callback); }.bind(this)); }; // Suspend program operation by sending sigint and prepare for state update this.suspend = function() { this.proc.kill('SIGINT'); }; this.cleanup = function() { if (this.proc) { this.proc.kill("SIGHUP"); this.proc = null; } }; // issue a command to GDB this.issue = function(cmd, args, callback) { var seq = ""; if (!args) args = ""; if (typeof callback === "function") { seq = ++this.sequence_id; this.callbacks[seq] = callback; } var msg = [seq, cmd, " ", args, "\n"].join(""); log(msg); this.proc.stdin.write(msg); }; this.post = function(client_seq, command, args) { this.issue(command, args, function(output) { output._id = client_seq; client.send(output); }); }; //// // GDB Output handling //// // stack frame cache getter function this._cachedFrame = function(frame, frameNum, create) { // the uniqueness of a frame is determined by the function and its depth var depth = this.state.frames.length - 1 - frameNum; var key = frame.file + frame.line + frame.func + depth; if (!this.framecache.hasOwnProperty(key)) { if (create) this.framecache[key] = create; else return false; } return this.framecache[key]; }; // Stack State Step 0; initiate request this._updateState = function(signal, thread) { // don't send state updates on reconnect, wait for plugin to request if (this.clientReconnect) return; if (signal) { this.state.err = "signal"; this.state.signal = signal; } this.state.thread = (thread) ? thread : null; if (signal && signal.name === "SIGSEGV") // dump the varobj cache in segfault so var-updates don't crash GDB this._flushVarCache(); else this._updateThreadId(); }; // Stack State Step 0a; flush var objects in event of a segfault this._flushVarCache = function() { // determine all the varobj names by pulling keys from the cache var keys = []; for (var key in this.varcache) { if (this.varcache.hasOwnProperty(key)) keys.push(key); } this.varcache = {}; function __flush(varobjs) { // once we've run out of keys, resume state compilation if (varobjs.length == 0) return this._updateThreadId(); // pop a key from the varobjs stack and delete it var v = varobjs.pop(); this.issue("-var-delete", v, __flush.bind(this, varobjs)); } // begin flushing the keys __flush.call(this, keys); }; // Stack State Step 1; find the thread ID this._updateThreadId = function() { if (this.state.thread !== null) return this._updateStack(); this.issue("-thread-info", null, function(state) { this.state.thread = state.status["current-thread-id"]; this._updateStack(); }.bind(this)); }; // Stack State Step 2; process stack frames and request arguments this._updateStack = function() { this.issue("-stack-list-frames", STACK_RANGE, function(state) { this.state.frames = state.status.stack; // provide relative path of script to IDE for (var i = 0, j = this.state.frames.length; i < j; i++) { // if file name is not here a stack overflow has probably occurred if (this.state.frames[i].func == "??" || !this.state.frames[i].hasOwnProperty("fullname")) { // go code often has "??" at the top of the stack, ignore that this.state.frames[i].exists = false; continue; } var file = this.state.frames[i].fullname; if (!file) { continue; } // remember if we can view the source for this frame if (!(file in this.memoized_files)) { this.memoized_files[file] = { exists: fs.existsSync(file) }; } // we must abort step if we cannot show source for top function if (!this.memoized_files[file] || !this.memoized_files[file].exists && !this.state.err) { if (i != 0) continue; this.state = {}; this.issue("-exec-finish"); return; } // notify IDE if file exists this.state.frames[i].exists = this.memoized_files[file].exists; } this._updateStackArgs(); }.bind(this)); }; // Stack State Step 3; append stack args to frames; request top frame locals this._updateStackArgs = function() { this.issue("-stack-list-arguments", "--simple-values " + STACK_RANGE, function(state) { var args = state.status['stack-args']; for (var i = 0; i < args.length; i++) { if (this.state.frames[i]) this.state.frames[i].args = args[i].args; } this._updateLocals(); }.bind(this)); }; // Stack State Step 4: fetch each frame's locals & send all to proxy this._updateLocals = function() { function requestLocals(frame) { // skip this frame if we have its variables cached if (this._cachedFrame(this.state.frames[frame], frame)) return frameLocals.call(this, frame, null, true); var args = [ "--thread", this.state.thread, "--frame", frame, "--simple-values" ].join(" "); this.issue("-stack-list-locals", args, frameLocals.bind(this, frame)); } function frameLocals(i, state, cache) { var f = this.state.frames[i]; if (cache) f.locals = this._cachedFrame(f, i).locals; else f.locals = state.status.locals; if (--i >= 0) requestLocals.call(this, i); else // update vars and fetch remaining this._updateCachedVars(); } // work from bottom of stack; upon completion, active frame should be 0 requestLocals.call(this, this.state.frames.length - 1); }; // Stack State Step 5: update cached vars this._updateCachedVars = function() { this.issue("-var-update", "--all-values *", function(reply) { //update cache for (var i = 0; i < reply.status.changelist.length; i++) { var obj = reply.status.changelist[i]; // updates to out-of-scope vars are irrelevant if (obj.in_scope != "true") { if (obj.in_scope == "invalid") this.issue("-var-delete", obj.name); continue; } if (!this.varcache[obj.name]) { console.log("FATAL: varcache miss for varobj " + obj.name); process.exit(1); } this.varcache[obj.name].value = obj.value; if (obj.type_changed == "true") this.varcache[obj.name].type = obj.new_type; } // stitch cache together in state for (var i = 0; i < this.state.frames.length; i++) { var frame = this.state.frames[i]; var cache = this._cachedFrame(frame, i); // cache miss if (cache === false) continue; // rebuild from cache frame.args = []; for (var j = 0; j < cache.args.length; j++) frame.args.push(this.varcache[cache.args[j]]); frame.locals = []; for (var j = 0; j < cache.locals.length; j++) frame.locals.push(this.varcache[cache.locals[j]]); } this._fetchVars(); }.bind(this)); }; // Stack State Step 6 (final): fetch information for all non-trivial vars this._fetchVars = function() { var newvars = []; function __iterVars(vars, varstack, f) { if (!vars) return; for (var i = 0; i < vars.length; i++) { var vari = vars[i]; if (!vari.type) continue; // TODO how to properly display this? if (vari.type.slice(-1) === '*') { // variable is a pointer, store its address vari.address = parseInt(vari.value, 16); if (!vari.address) { // don't allow null pointers' children to be evaluated vari.address = 0; vari.value = "NULL"; continue; } } varstack.push({ frame: f, item: vari }); } } function __createVars(varstack) { if (varstack.length == 0) { // DONE: set stack frame to topmost; send & flush compiled data this.issue("-stack-select-frame", "0"); client.send(this.state); this.state = {}; return; } var obj = varstack.pop(); var item = obj.item; var frame = obj.frame; // if this variable already has a corresponding varobj, advance if (item.objname) return __createVars.call(this, varstack); // no corresponding varobj for this variable, create one var args = ["-", "*", item.name].join(" "); this.issue("-var-create", args, function(item, state) { // allow the item to remember the varobj's ID item.objname = state.status.name; item.numchild = state.status.numchild; // store this varobj in caches this.varcache[item.objname] = item; // notify the frame of this variable frame.push(item.objname); __createVars.call(this, varstack); }.bind(this, item)); } // iterate over all locals and args and push complex vars onto stack for (var i = 0; i < this.state.frames.length; i++) { var frame = this.state.frames[i]; // skip the frame if it's already cached if (this._cachedFrame(frame, i) !== false) continue; var cache = this._cachedFrame(frame, i, { args: [], locals: []}); __iterVars(frame.args, newvars, cache.args); __iterVars(frame.locals, newvars, cache.locals); } __createVars.call(this, newvars); }; // Received a result set from GDB; initiate callback on that request this._handleRecordsResult = function(state) { if (typeof state._seq === "undefined") return; // command is awaiting result, issue callback and remove from queue if (this.callbacks[state._seq]) { this.callbacks[state._seq](state); delete this.callbacks[state._seq]; } this.handleCommands(); }; // Handle program status update this._handleRecordsAsync = function(state) { if (typeof state.status === "undefined") return; if (state.state === "stopped") this.running = false; var cause = state.status.reason; var thread = state.status['thread-id']; if (cause == "signal-received") { var signal = { name: state.status['signal-name'], text: state.status['signal-meaning'] }; this._updateState(signal, thread); } else if (cause === "breakpoint-hit" || cause === "end-stepping-range" || cause === "function-finished") // update GUI state at breakpoint or after a step in/out this._updateState(false, thread); else if (cause === "exited-normally") // program has quit process.exit(); }; // handle a line of stdout from gdb this._handleLine = function(line) { var output = parseGdbMiOut(line); if (output.hasTerminator) return; log("GDB: " + line); output = output.resultRecord || output.outOfBandRecords[0]; var state = { _seq: output.token, state: output.class, status: output.result }; // first character of output determines line meaning switch (output.outputType) { case "result": this._handleRecordsResult(state); break; case "exec": this._handleRecordsAsync(state); break; case "status": break; // Ongoing status information about slow operation case "notify": break; // Notify async output case "log": break; // Log stream; gdb internal debug messages case "console": break; // Console output stream case "target": break; // Remote target output stream } }; ///// // Incoming command handling ///// this.handleCommands = function() { // command queue is empty if (this.command_queue.length < 1) return; // get the next command in the queue var command = this.command_queue.shift(); if (typeof command.command === "undefined") { log("ERROR: Received an empty request, ignoring."); } if (typeof command._id !== "number") command._id = ""; var id = command._id; // fix some condition syntax if (command.condition) command.condition = command.condition.replace(/=(["|{|\[])/g, "= $1"); switch (command.command) { case 'run': case 'continue': case 'step': case 'next': case 'finish': if (this.started === false) { this.started = true; // provide a warning if BPs sent but not set if (this.bp_set === false && BP_WARN) console.error("\nWARNING: Could not set any", "breakpoints. Try deleting", BIN, "and", "re-compiling\nyour code. Be sure to compile with", "-ggdb3.\n"); } this.clientReconnect = false; this.running = true; this.post(id, "-exec-" + command.command); break; case "var-set": this.post(id, "-var-assign", command.name + " " + command.val); break; case "var-children": // if passed a single var name, we want to fetch its children var largs = ["--simple-values", command.name].join(" "); this.issue("-var-list-children", largs, function(state) { var children = []; if (parseInt(state.status.numchild, 10) > 0) state.status.children.forEach(function(child) { child.objname = child.name; this.varcache[child.name] = child; children.push(child); }.bind(this)); client.send({ _id: id, children: children, state: "done" }); }.bind(this)); break; case "bp-change": if (command.enabled === false) this.post(id, "-break-disable", command.id); else if (command.condition) this.post(id, "-break-condition", command.id + " " + command.condition); else this.post(id, "-break-enable", command.id); break; case "bp-clear": // include filename for multiple files this.post(id, "-break-delete", command.id); break; case "bp-set": var args = []; // create a disabled breakpoint if requested if (command.enabled === false) args.push("-d"); if (command.condition) { command.condition = command.condition.replace(/"/g, '\\"'); args.push("-c"); args.push('"' + command.condition + '"'); } args.push('"' + command.fullpath + ':' + (command.line + 1) + '"'); this.issue("-break-insert", args.join(" "), function(output) { // record whether we've successfully set any BPs this.bp_set = this.bp_set || (output.state === "done"); output._id = id; client.send(output); }.bind(this)); break; case "bp-list": this.post(id, "-break-list"); break; case "eval": var eargs = ["--thread", command.t, "--frame", command.f]; // replace quotes with escaped quotes eargs.push('"' + command.exp.replace(/"/g, '\\"') + '"'); this.post(id, "-data-evaluate-expression", eargs.join(" ")); break; case "reconnect": if (this.running) { this.clientReconnect = true; this.suspend(); client.send({ _id: id, state: "running" }); } else client.send({ _id: id, state: "stopped" }); break; case "suspend": this.suspend(); client.send({ _id: id, state: "stopped" }); break; case "status": if (this.running) { client.send({ _id: id, state: "running" }); } else { client.send({ _id: id, state: "stopped" }); this._updateState(); } break; case "detach": client.cleanup(); this.issue("monitor", "exit", function() { log("shutdown requested"); process.exit(); }); break; default: log("PROXY: received unknown request: " + command.command); } }; } // End GDB class //////////////////////////////////////////////////////////////////////////////// // Proxy initialization gdb = new GDB(); executable = new Executable(); // handle process events // catch SIGINT, allowing GDB to pause if running, quit otherwise process.on("SIGINT", function() { log("SIGINT"); if (!gdb || !gdb.running) process.exit(); }); process.on("SIGHUP", function() { log("Received SIGHUP"); process.exit(); }); process.on("exit", function() { log("quitting!"); // provide context for exit if child process died if (exit) { if (exit.code !== null && exit.code > 0) console.error(exit.proc, "terminated with code", exit.code); else if (exit.signal !== null) console.error(exit.proc, "killed with signal", exit.signal); } // cleanup if (gdb) gdb.cleanup(); if (client) client.cleanup(); if (executable) executable.cleanup(); if (server && server.listening) server.close(); if (DEBUG) log_file.end(); }); process.on("uncaughtException", function(e) { log("uncaught exception (" + e + ")" + "\n" + e.stack); process.exit(1); }); // create the proxy server var server = net.createServer(function(c) { if (client) client.reconnect(c); else client = new Client(c); client.connect(function(err) { if (err) { log("PROXY: Could not connect to client; " + err); } else { log("PROXY: server connected"); client.send("connect"); // flush buffer of pending requests client.flush(); } }); }); // handle server events server.on("error", function(err) { if (err.errno == "EADDRINUSE") { console.log("It looks like the debugger is already in use!"); console.log("Try stopping the existing instance first."); } else { console.log("server error"); console.log(err); } process.exit(1); }); // begin debug process executable.spawn(function() { gdb.spawn(); gdb.connect(function (reply, err) { if (err) { log(err); process.exit(); } // Finally ready: start listening for browser clients on port or sock if (PROXY.sock) { fs.unlink(PROXY.sock, function(err) { if (err && err.code != "ENOENT") console.error(err); server.listen(PROXY.sock); }); } else server.listen(PROXY.port, PROXY.host); }); });