From 348bb2e2a9963a4a3e90c9cf3c67d15761ccb063 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Fri, 22 Jul 2016 20:22:27 +0000 Subject: [PATCH 01/20] rearchitect netproxy as shim --- debuggers/gdb/gdbdebugger.js | 34 +- debuggers/gdb/netproxy.js | 1020 +------------------------------- debuggers/gdb/shim.js | 1083 ++++++++++++++++++++++++++++++++++ 3 files changed, 1098 insertions(+), 1039 deletions(-) mode change 100755 => 100644 debuggers/gdb/netproxy.js create mode 100755 debuggers/gdb/shim.js diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index 1228103..9e1c5fc 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -5,15 +5,16 @@ */ define(function(require, exports, module) { main.consumes = [ - "Plugin", "debugger", "c9", "panels", "settings", "dialog.error" + "Plugin", "c9", "debugger", "dialog.error", "fs", "panels", "settings" ]; main.provides = ["gdbdebugger"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; - var debug = imports["debugger"]; var c9 = imports.c9; + var debug = imports["debugger"]; + var fs = imports["fs"]; var panels = imports.panels; var settings = imports.settings; var showError = imports["dialog.error"].show; @@ -60,8 +61,15 @@ define(function(require, exports, module) { ["autoshow", "true"] ]); }); - - debug.registerDebugger(TYPE, plugin); + + var shim = require("text!./shim.js"); + fs.writeFile("~/bin/c9gdbshim.js", shim, function(err) { + if (err) + return console.error("Error writing gdb shim: " + err); + + // register the debugger only if shim is in place + debug.registerDebugger(TYPE, plugin); + }); } /***** Helper Functions *****/ @@ -206,24 +214,9 @@ define(function(require, exports, module) { /***** Methods *****/ function getProxySource(process){ - var max_depth = (process.runner[0].maxdepth) ? - process.runner[0].maxdepth : 50; - - var bin; - try { - bin = process.insertVariables(process.runner[0].executable); - } - catch(e) { - bin = "!"; - } - return PROXY .replace(/\/\/.*/g, "") - .replace(/[\n\r]/g, "") - .replace(/\{PATH\}/, c9.workspaceDir) - .replace(/\{MAX_DEPTH\}/, max_depth) - .replace(/\{BIN\}/, bin) - .replace(/\{PORT\}/, process.runner[0].debugport); + .replace(/[\n\r]/g, ""); } function attach(s, reconnect, callback) { @@ -471,6 +464,7 @@ define(function(require, exports, module) { } function setBreakpoint(bp, callback) { + bp.data.fullpath = Path.join(c9.workspaceDir, bp.data.path); proxy.sendCommand("bp-set", bp.data, function(err, reply) { if (err) return callback && callback(err); diff --git a/debuggers/gdb/netproxy.js b/debuggers/gdb/netproxy.js old mode 100755 new mode 100644 index 1e9b288..6e97286 --- a/debuggers/gdb/netproxy.js +++ b/debuggers/gdb/netproxy.js @@ -1,1019 +1 @@ -/** - * 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; -var exec = require('child_process').exec; - -var executable = "{BIN}"; -var dirname = "{PATH}"; -var gdb_port = parseInt("{PORT}", 10); -var proxy_port = gdb_port + 1; - -var MAX_STACK_DEPTH = parseInt("{MAX_DEPTH}", 10); -var STACK_RANGE = "0 " + MAX_STACK_DEPTH; - -var MAX_RETRY = 300; - -var client = null, // Client class instance with connection to browser - gdb = null; // GDB class instance with spawned gdb process - -var DEBUG = false; - -var old_console = console.log; -var log_file = null; -var log = function() {}; - -console.warn = console.log = function() { - if (DEBUG) { - var args = Array.prototype.slice.call(arguments); - log_file.write(args.join(" ") + "\n"); - } - return console.error.apply(console, arguments); -}; -function send() { - old_console.apply(console, arguments); -} - -if (DEBUG) { - log_file = fs.createWriteStream("./.gdb_proxy.log"); - log = function(str) { - console.log(str); - }; -} - -// problem! -if (executable === "!") { - console.log("The debugger provided bad data. Please try again."); - process.exit(0); -} - -//////////////////////////////////////////////////////////////////////////////// -// 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) { - if (!gdb) { - callback(new Error("GDB not yet initialized")); - } - - 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 -//////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////////// -// GDB class; connecting, parsing, issuing commands - -function GDB() { - this.sequence_id = 0; - this.callbacks = {}; - this.state = {}; - this.framecache = {}; - this.varcache = {}; - this.running = 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', executable], { - detached: true, - cwd: dirname - }); - - 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("end", function() { - log("gdb proc ended"); - process.exit(); - }); - - this.proc.on("close", function(code, signal) { - self.proc.stdin.end(); - log("GDB terminated with code " + code + " and signal " + signal); - client.send({ err:"killed", code:code, signal:signal }); - process.exit(); - }); - }; - - this.connect = function(callback) { - // ask GDB to retry connections to server with a given timeout - this.issue("set tcp connect-timeout", MAX_RETRY, function() { - // now connect - 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)); - }.bind(this)); - }; - - // spawn GDB client only after gdbserver is ready - this.waitConnect = function(callback) { - function wait(retries, callback) { - if (retries < 0) - return callback(null, "Waited for gdbserver beyond timeout"); - - // determine if gdbserver has opened the port yet - exec("lsof -i :"+gdb_port+" -sTCP:LISTEN|grep -q gdbserver", function(err) { - // if we get an error code back, gdbserver is not yet running - if (err !== null) - return setTimeout(wait.bind(this, --retries, callback), 1000); - - // success! load gdb and connect to server - this.spawn(); - this.connect(callback); - }.bind(this)); - } - wait.call(this, MAX_RETRY, callback); - }; - - // 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); - }); - }; - - - ////// - // Parsing via: - // https://github.com/besnardjb/ngdbmi/blob/master/ngdbmi.js#L1025 - - String.prototype.setCharAt = function(idx, chr) { - if (idx > this.length - 1) { - return this.toString(); - } - else { - return this.substr(0, idx) + chr + this.substr(idx + 1); - } - }; - - this._removeArrayLabels = function(args) { - /* We now have to handle labels inside arrays */ - - var t_in_array = []; - var in_array = 0; - for (var i = 0; i < args.length; i++) { - /* This is a small state handling - * in order to see if we are in an array - * and therefore if we have to remove labels */ - if (args[i] == "[") - t_in_array.push(1); - - if (args[i] == "{") - t_in_array.push(0); - - if (args[i] == "]" || args[i] == "}") - t_in_array.pop(); - - /* in_array == 1 if we are in an array =) */ - in_array = t_in_array[t_in_array.length - 1]; - - /* If we encounter ',"' inside an array delete until '":' or '"=' */ - if (in_array - && (args[i] == "," || args[i] == "[") - && args[i+1] == "\"") { - var k = i; - - /* Walk the label */ - while ((k < args.length) - && (args[k] != ":") - && (args[k] != "=") - && (args[k] != "]")) { - k++; - } - - /* if we end on a label end (= or :) then clear it up */ - if (args[k] == ":" || args[k] == "=") { - for (var l = (i+1); l <= k; l++) { - args = args.setCharAt(l,' '); - } - } - } - } - return args; - }; - - this._parseStateArgs = function(args) { - /* This is crazy but GDB almost provides a JSON output */ - args = args.replace(/=(?=["|{|\[])/g, '!:'); - args = args.replace(/([a-zA-Z0-9-_]*)!:/g, "\"$1\":"); - - /* Remove array labels */ - args = this._removeArrayLabels(args); - - /* And wrap in an object */ - args = "{" + args + "}"; - - var ret = {}; - - try { - ret = JSON.parse(args); - } - catch(e) { - /* We lamentably failed =( */ - log("JSON ERROR: " + e + "\nJSON: " + args); - } - - return ret; - }; - - this._getState = function(line) { - var m = line.match("^([a-z-]*),"); - - if (m && m.length == 2) - return m[1].trim(); - - /* Couldn't we merge this with the previous one ? */ - m = line.match("^([a-z-]*)$"); - - if (m && m.length == 2) - return m[1].trim(); - - return undefined; - }; - - this._parseState = function(line) { - line = line.trim(); - - var gdb_state = {}; - - /* Handle state */ - var state = this._getState(line); - - if (state) - gdb_state.state = state; - - /* Handle args if present */ - var m = line.match("^[a-z-]*,(.*)"); - if (m && m.length == 2) - gdb_state.status = this._parseStateArgs(m[1]); - - return gdb_state; - }; - - //// - // 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(segfault, thread) { - // don't send state updates on reconnect, wait for plugin to request - if (this.clientReconnect) return; - - this.state.err = (segfault === true)? "segfault" : null; - this.state.thread = (thread)? thread : null; - - if (segfault === true) - // 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 this function - if (!this.memoized_files[file] || !this.memoized_files[file].exists && !this.state.err) { - 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") - this._updateState((state.status['signal-name']=="SIGSEGV"), 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) { - if (line.trim() === "(gdb)") - return; - - // status line: ^status or id^status - var line_split = line.match(/^([0-9]*)\^(.*)$/); - - var state = null; - var token = "^"; - - // line split will be true if it's a status line - if (line_split) { - state = this._parseState(line_split[2]); - - // line_id is present if the initiating command had a _seq - if (line_split[1]) - state._seq = line_split[1]; - } - else { - token = line[0]; - state = this._parseState(line.slice(1)); - } - - log("GDB: " + line); - - // first character of output determines line meaning - switch (token) { - case '^': this._handleRecordsResult(state); - break; - case '*': this._handleRecordsAsync(state); - break; - case '+': break; // Ongoing status information about slow operation - case '=': break; // Notify async output - case '&': break; // Log stream; gdb internal debug messages - case '~': break; // Console output stream - case '@': break; // Remote target output stream - default: - } - }; - - ///// - // 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") { - console.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': - 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 + '"'); - } - - var path = dirname + command.path; - args.push('"' + path + ':' + (command.line+1) + '"'); - - this.post(id, "-break-insert", args.join(" ")); - break; - - case "bp-list": - this.post(id, "-break-list"); - break; - - case "eval": - var args = ["--thread", command.t, "--frame", command.f]; - // replace quotes with escaped quotes - args.push('"' + command.exp.replace(/"/g, '\\"') + '"'); - this.post(id, "-data-evaluate-expression", args.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 - -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(); - } - }); - -}); - -gdb = new GDB(); - -gdb.waitConnect(function(reply, err) { - if (err) { - log(err); - process.exit(); - } - start(); -}); - -// handle process events -// pass along SIGINT to suspend gdb, only if program is running -process.on('SIGINT', function() { - console.log("\b\bSIGINT: "); - if (gdb.running) { - console.log("SUSPENDING\n"); - gdb.suspend(); - } - else { - console.log("CANNOT SUSPEND (program not running)\n"); - } -}); - -process.on("SIGHUP", function() { - log("Received SIGHUP"); - process.exit(); -}); - -process.on("exit", function() { - log("quitting!"); - if (gdb) gdb.cleanup(); - if (client) client.cleanup(); - if (DEBUG) log_file.end(); -}); - -process.on("uncaughtException", function(e) { - log("uncaught exception (" + e + ")"); - process.exit(); -}); - -// 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(err); - } - process.exit(); -}); - -// Start listening for browser clients -var host = "127.0.0.1"; -server.listen(proxy_port, host, function() { - start(); -}); - -var I=0; -function start() { - if (++I == 2) - send("ß"); -} +setTimeout(function() {console.log('ß');}, 3000); \ No newline at end of file diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js new file mode 100755 index 0000000..6c8dacb --- /dev/null +++ b/debuggers/gdb/shim.js @@ -0,0 +1,1083 @@ +/** + * 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 + " [-d=depth] [-g=gdb] [-p=proxy] BIN [args]\n", + " 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 15471)", + " 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 GDB_PORT = 15470; +var PROXY_PORT = 15471; +var MAX_STACK_DEPTH = 50; +var DEBUG = false; +var BIN = ""; + +// parse middle arguments +function intArg(str) { + if (str == null || str === "") printUsage(); + var val = parseInt(str, 10); + if (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 "-d": + case "--depth": + MAX_STACK_DEPTH = intArg(val); + break; + case "-g": + case "--gdb": + GDB_PORT = intArg(val); + break; + case "-p": + case "--proxy": + if (!val || val.length == 0) printUsage(); + PROXY_PORT = val; + break; + case "--debug": + DEBUG = (val === "true"); + 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; + +var log = function() {}; + +if (DEBUG) { + var log_file = fs.createWriteStream("./.gdb_proxy.log"); + log = function(str) { + var args = Array.prototype.slice.call(arguments); + log_file.write(args.join(" ") + "\n"); + 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; + + /** + * 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, { + detached: false, + cwd: process.cwd(), + stdio: [process.stdin, process.stdout, 'pipe'] + }); + + 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 }); + process.exit(); + }); + + // wait for gdbserver to listen before executing callback + function handleStderr(data) { + var str = data.toString(); + if (str.indexOf("Listening") > -1) { + // perform callback when gdbserver is ready + callback(); + } + else if (str.indexOf("127.0.0.1") > -1) { + // soak up final gdbserver message before sharing i/o stream + this.proc.stderr.removeListener("data", handleStderr); + this.proc.stderr.pipe(process.stderr, {end: false}); + } + + } + this.proc.stderr.on("data", handleStderr.bind(this)); + }; + + /** + * 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.callbacks = {}; + this.state = {}; + this.framecache = {}; + this.varcache = {}; + this.running = 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() + }); + + 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 }); + process.exit(); + }); + }; + + 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); + }); + }; + + + ////// + // Parsing via: + // https://github.com/besnardjb/ngdbmi/blob/master/ngdbmi.js#L1025 + + String.prototype.setCharAt = function(idx, chr) { + if (idx > this.length - 1) { + return this.toString(); + } + else { + return this.substr(0, idx) + chr + this.substr(idx + 1); + } + }; + + this._removeArrayLabels = function(args) { + /* We now have to handle labels inside arrays */ + + var t_in_array = []; + var in_array = 0; + for (var i = 0; i < args.length; i++) { + /* This is a small state handling + * in order to see if we are in an array + * and therefore if we have to remove labels */ + if (args[i] == "[") + t_in_array.push(1); + + if (args[i] == "{") + t_in_array.push(0); + + if (args[i] == "]" || args[i] == "}") + t_in_array.pop(); + + /* in_array == 1 if we are in an array =) */ + in_array = t_in_array[t_in_array.length - 1]; + + /* If we encounter ',"' inside an array delete until '":' or '"=' */ + if (in_array + && (args[i] == "," || args[i] == "[") + && args[i+1] == "\"") { + var k = i; + + /* Walk the label */ + while ((k < args.length) + && (args[k] != ":") + && (args[k] != "=") + && (args[k] != "]")) { + k++; + } + + /* if we end on a label end (= or :) then clear it up */ + if (args[k] == ":" || args[k] == "=") { + for (var l = (i+1); l <= k; l++) { + args = args.setCharAt(l,' '); + } + } + } + } + return args; + }; + + this._parseStateArgs = function(args) { + /* This is crazy but GDB almost provides a JSON output */ + args = args.replace(/=(?=["|{|\[])/g, '!:'); + args = args.replace(/([a-zA-Z0-9-_]*)!:/g, "\"$1\":"); + + /* Remove array labels */ + args = this._removeArrayLabels(args); + + /* And wrap in an object */ + args = "{" + args + "}"; + + var ret = {}; + + try { + ret = JSON.parse(args); + } + catch(e) { + /* We lamentably failed =( */ + log("JSON ERROR: " + e + "\nJSON: " + args); + } + + return ret; + }; + + this._getState = function(line) { + var m = line.match("^([a-z-]*),"); + + if (m && m.length == 2) + return m[1].trim(); + + /* Couldn't we merge this with the previous one ? */ + m = line.match("^([a-z-]*)$"); + + if (m && m.length == 2) + return m[1].trim(); + + return undefined; + }; + + this._parseState = function(line) { + line = line.trim(); + + var gdb_state = {}; + + /* Handle state */ + var state = this._getState(line); + + if (state) + gdb_state.state = state; + + /* Handle args if present */ + var m = line.match("^[a-z-]*,(.*)"); + if (m && m.length == 2) + gdb_state.status = this._parseStateArgs(m[1]); + + return gdb_state; + }; + + //// + // 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(segfault, thread) { + // don't send state updates on reconnect, wait for plugin to request + if (this.clientReconnect) return; + + this.state.err = (segfault === true)? "segfault" : null; + this.state.thread = (thread)? thread : null; + + if (segfault === true) + // 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 this function + if (!this.memoized_files[file] || !this.memoized_files[file].exists && !this.state.err) { + 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") + this._updateState((state.status['signal-name']=="SIGSEGV"), 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) { + if (line.trim() === "(gdb)") + return; + + // status line: ^status or id^status + var line_split = line.match(/^([0-9]*)\^(.*)$/); + + var state = null; + var token = "^"; + + // line split will be true if it's a status line + if (line_split) { + state = this._parseState(line_split[2]); + + // line_id is present if the initiating command had a _seq + if (line_split[1]) + state._seq = line_split[1]; + } + else { + token = line[0]; + state = this._parseState(line.slice(1)); + } + + log("GDB: " + line); + + // first character of output determines line meaning + switch (token) { + case '^': this._handleRecordsResult(state); + break; + case '*': this._handleRecordsAsync(state); + break; + case '+': break; // Ongoing status information about slow operation + case '=': break; // Notify async output + case '&': break; // Log stream; gdb internal debug messages + case '~': break; // Console output stream + case '@': break; // Remote target output stream + default: + } + }; + + ///// + // 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': + 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.post(id, "-break-insert", args.join(" ")); + break; + + case "bp-list": + this.post(id, "-break-list"); + break; + + case "eval": + var args = ["--thread", command.t, "--frame", command.f]; + // replace quotes with escaped quotes + args.push('"' + command.exp.replace(/"/g, '\\"') + '"'); + this.post(id, "-data-evaluate-expression", args.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 +// pass along SIGINT to suspend gdb, only if program is running +process.on('SIGINT', function() { + log("\b\bSIGINT"); + if (gdb.running) { + log("SUSPENDING\n"); + gdb.suspend(); + } + else { + log("CANNOT SUSPEND (program not running)\n"); + } +}); + +process.on("SIGHUP", function() { + log("Received SIGHUP"); + process.exit(); +}); + +process.on("exit", function() { + log("quitting!"); + if (gdb) gdb.cleanup(); + if (client) client.cleanup(); + if (executable) executable.cleanup(); + if (DEBUG) log_file.end(); +}); + +process.on("uncaughtException", function(e) { + log("uncaught exception (" + e + ")"); + process.exit(); +}); + +// 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(err); + } + process.exit(); +}); + +// 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 + var portNum = parseInt(PROXY_PORT, 10); + if (isNaN(portNum)) + server.listen(PROXY_PORT); + else + server.listen(portNum, "127.0.0.1"); + }); +}); \ No newline at end of file From 2b0bdbb40eed4b2278d424060bdec32e97b3f6a1 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Wed, 27 Jul 2016 15:17:01 +0000 Subject: [PATCH 02/20] improved handling of gdbserver errors on load --- debuggers/gdb/shim.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index 6c8dacb..2f21d7e 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -242,6 +242,11 @@ function Executable() { // wait for gdbserver to listen before executing callback function handleStderr(data) { var str = data.toString(); + // handle cases of error (eg, failure to disable address space rand) + if (str.indexOf("Error") > -1) { + console.log(str); + process.exit(1); + } if (str.indexOf("Listening") > -1) { // perform callback when gdbserver is ready callback(); From 62603a00e51f7712c24b32da977304701e7f9e04 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Wed, 27 Jul 2016 21:33:11 +0000 Subject: [PATCH 03/20] support direct communication to shim via socket --- debuggers/gdb/gdbdebugger.js | 12 ++++++------ debuggers/gdb/netproxy.js | 1 - debuggers/gdb/shim.js | 32 ++++++++++++++++++++------------ 3 files changed, 26 insertions(+), 19 deletions(-) delete mode 100644 debuggers/gdb/netproxy.js diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index 9e1c5fc..ad49ac3 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -36,9 +36,6 @@ define(function(require, exports, module) { var TYPE = "gdb"; - // proxy location - var PROXY = require("text!./netproxy.js"); - var attached = false; var state, // debugger state @@ -214,9 +211,12 @@ define(function(require, exports, module) { /***** Methods *****/ function getProxySource(process){ - return PROXY - .replace(/\/\/.*/g, "") - .replace(/[\n\r]/g, ""); + return { + source: null, + socketpath: "/home/ubuntu/.c9/gdbdebugger.socket", + retryInverval: 300, + retries: 1000 + }; } function attach(s, reconnect, callback) { diff --git a/debuggers/gdb/netproxy.js b/debuggers/gdb/netproxy.js deleted file mode 100644 index 6e97286..0000000 --- a/debuggers/gdb/netproxy.js +++ /dev/null @@ -1 +0,0 @@ -setTimeout(function() {console.log('ß');}, 3000); \ No newline at end of file diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index 2f21d7e..8c8c6bc 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -17,7 +17,7 @@ function printUsage() { "Usage: " + p + " [-d=depth] [-g=gdb] [-p=proxy] BIN [args]\n", " 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 15471)", + " 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" ]; @@ -29,17 +29,17 @@ var argc = process.argv.length; if (argc < 3) printUsage(); // defaults +var PROXY = { sock: "/home/ubuntu/.c9/gdbdebugger.socket" }; var GDB_PORT = 15470; -var PROXY_PORT = 15471; var MAX_STACK_DEPTH = 50; var DEBUG = false; var BIN = ""; // parse middle arguments -function intArg(str) { +function parseArg(str, allowNonInt) { if (str == null || str === "") printUsage(); var val = parseInt(str, 10); - if (isNaN(val)) printUsage(); + if (!allowNonInt && isNaN(val)) printUsage(); return val; } @@ -54,16 +54,20 @@ for(i = 2; i < argc && BIN === ""; i++) { switch (key) { case "-d": case "--depth": - MAX_STACK_DEPTH = intArg(val); + MAX_STACK_DEPTH = parseArg(val); break; case "-g": case "--gdb": - GDB_PORT = intArg(val); + GDB_PORT = parseArg(val); break; case "-p": case "--proxy": - if (!val || val.length == 0) printUsage(); - PROXY_PORT = val; + var portNum = parseArg(val, true); + + if (isNaN(portNum)) + PROXY = { sock: val }; + else + PROXY = { host: "127.0.0.1", port: portNum }; break; case "--debug": DEBUG = (val === "true"); @@ -1027,6 +1031,8 @@ process.on("exit", function() { if (gdb) gdb.cleanup(); if (client) client.cleanup(); if (executable) executable.cleanup(); + if (server) server.close(); + if (PROXY.sock) fs.unlinkSync(PROXY.sock); if (DEBUG) log_file.end(); }); @@ -1079,10 +1085,12 @@ executable.spawn(function() { } // Finally ready: start listening for browser clients on port or sock - var portNum = parseInt(PROXY_PORT, 10); - if (isNaN(portNum)) - server.listen(PROXY_PORT); + if (PROXY.sock) { + fs.unlink(PROXY.sock, function() { + server.listen(PROXY.sock); + }); + } else - server.listen(portNum, "127.0.0.1"); + server.listen(PROXY.port, PROXY.host); }); }); \ No newline at end of file From 704c0a33a3b9eeecdc98eb619464209257df3cb6 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 01:09:31 +0000 Subject: [PATCH 04/20] add support for receiving more signals than just SIGSEGV --- debuggers/gdb/gdbdebugger.js | 13 +++++++++---- debuggers/gdb/shim.js | 11 +++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index ad49ac3..f6b1bff 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -169,9 +169,14 @@ define(function(require, exports, module) { setState("stopped"); emit("frameActivate", { frame: topFrame }); - if (content.err === "segfault") { - showError("GDB has detected a segmentation fault and execution has stopped!"); - emit("exception", { frame: topFrame }, new Error("Segfault!")); + var frameObj = { frame: topFrame, frames: stack }; + + if (content.err === "signal") { + if (content.signal === "SIGSEGV") + showError("Execution has stopped due to a segmentation fault!"); + else + showError("Process received signal " + content.signal + "!"); + emit("exception", frameObj, new Error("Segfault!")); btnResume.$ext.style.display = "none"; btnSuspend.$ext.style.display = "inline-block"; btnSuspend.setAttribute("disabled", true); @@ -180,7 +185,7 @@ define(function(require, exports, module) { btnStepOver.setAttribute("disabled", true); } else { - emit("break", { frame: topFrame, frames: stack }); + emit("break", frameObj); if (stack.length == 1) btnStepOut.setAttribute("disabled", true); } diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index 8c8c6bc..73bf513 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -531,14 +531,17 @@ function GDB() { }; // Stack State Step 0; initiate request - this._updateState = function(segfault, thread) { + this._updateState = function(signal, thread) { // don't send state updates on reconnect, wait for plugin to request if (this.clientReconnect) return; - this.state.err = (segfault === true)? "segfault" : null; + if (signal) { + this.state.err = "signal"; + this.state.signal = signal; + } this.state.thread = (thread)? thread : null; - if (segfault === true) + if (signal === "SIGSEGV") // dump the varobj cache in segfault so var-updates don't crash GDB this._flushVarCache(); else @@ -813,7 +816,7 @@ function GDB() { var thread = state.status['thread-id']; if (cause == "signal-received") - this._updateState((state.status['signal-name']=="SIGSEGV"), thread); + this._updateState(state.status['signal-name'], thread); else if (cause === "breakpoint-hit" || cause === "end-stepping-range" || cause === "function-finished") // update GUI state at breakpoint or after a step in/out From 280eb8e3189a8bfeaf827e0f595ff3ffa76be386 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 01:38:33 +0000 Subject: [PATCH 05/20] provide signal value to exception error --- debuggers/gdb/gdbdebugger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index f6b1bff..69e04a2 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -176,7 +176,7 @@ define(function(require, exports, module) { showError("Execution has stopped due to a segmentation fault!"); else showError("Process received signal " + content.signal + "!"); - emit("exception", frameObj, new Error("Segfault!")); + emit("exception", frameObj, new Error(content.signal)); btnResume.$ext.style.display = "none"; btnSuspend.$ext.style.display = "inline-block"; btnSuspend.setAttribute("disabled", true); From 7866fae232726a8ac8d95995ed0726fe09e48f17 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 03:55:59 +0000 Subject: [PATCH 06/20] allow sigint to pause execution --- debuggers/gdb/gdbdebugger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index 69e04a2..c5708f1 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -171,7 +171,7 @@ define(function(require, exports, module) { var frameObj = { frame: topFrame, frames: stack }; - if (content.err === "signal") { + if (content.err === "signal" && content.signal !== "SIGINT") { if (content.signal === "SIGSEGV") showError("Execution has stopped due to a segmentation fault!"); else From affef2cd55f00182cac79897117b7c33ddf5677b Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 03:57:48 +0000 Subject: [PATCH 07/20] catch unlink errors --- debuggers/gdb/shim.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index 73bf513..7391974 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -1035,7 +1035,14 @@ process.on("exit", function() { if (client) client.cleanup(); if (executable) executable.cleanup(); if (server) server.close(); - if (PROXY.sock) fs.unlinkSync(PROXY.sock); + if (PROXY.sock) { + try { + fs.unlinkSync(PROXY.sock); + } + catch(e) { + log("Unable to delete socket: " + e.code); + } + } if (DEBUG) log_file.end(); }); From 6dda14d4d360d9c257812f8f8bb79eaf9783ba36 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 03:58:52 +0000 Subject: [PATCH 08/20] detach gdbserver spawn process, pipe stdin, and better sigint handling --- debuggers/gdb/shim.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index 7391974..4fc5942 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -232,9 +232,9 @@ function Executable() { this.spawn = function(callback) { var args = ["--once", ":"+GDB_PORT, BIN].concat(ARGS); this.proc = spawn("gdbserver", args, { - detached: false, + detached: true, cwd: process.cwd(), - stdio: [process.stdin, process.stdout, 'pipe'] + stdio: ['pipe', process.stdout, 'pipe'] }); this.proc.on("exit", function(code, signal) { @@ -263,6 +263,9 @@ function Executable() { } this.proc.stderr.on("data", handleStderr.bind(this)); + + // necessary to redirect stdin this way or child receives SIGTTIN + process.stdin.pipe(this.proc.stdin); }; /** @@ -1013,15 +1016,8 @@ executable = new Executable(); // handle process events // pass along SIGINT to suspend gdb, only if program is running -process.on('SIGINT', function() { - log("\b\bSIGINT"); - if (gdb.running) { - log("SUSPENDING\n"); - gdb.suspend(); - } - else { - log("CANNOT SUSPEND (program not running)\n"); - } +process.on("SIGINT", function() { + log("SIGINT"); }); process.on("SIGHUP", function() { From 1c0e45019ca81585fa6dede33ad2f88ae0b9e20d Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 13:29:06 +0000 Subject: [PATCH 09/20] improve paths --- debuggers/gdb/gdbdebugger.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index c5708f1..1fc7786 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -59,13 +59,17 @@ define(function(require, exports, module) { ]); }); + debug.registerDebugger(TYPE, plugin); + + // writeFile root is workspace directory, unless given ~ + var shimPath = "~/bin/c9gdbshim.js"; var shim = require("text!./shim.js"); - fs.writeFile("~/bin/c9gdbshim.js", shim, function(err) { - if (err) - return console.error("Error writing gdb shim: " + err); - - // register the debugger only if shim is in place - debug.registerDebugger(TYPE, plugin); + fs.writeFile(shimPath, shim, "utf8", function(err) { + if (err) { + // unregister the debugger on error + debug.unregisterDebugger(TYPE, plugin); + return console.log("Error writing gdb shim: " + err); + } }); } @@ -218,7 +222,7 @@ define(function(require, exports, module) { function getProxySource(process){ return { source: null, - socketpath: "/home/ubuntu/.c9/gdbdebugger.socket", + socketpath: Path.join(c9.home, "/.c9/gdbdebugger.socket"), retryInverval: 300, retries: 1000 }; From abb4d540d76c9651015c83232cafc3d7d33da2e7 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 13:29:40 +0000 Subject: [PATCH 10/20] further generalize signal handling --- debuggers/gdb/gdbdebugger.js | 24 +++++++++--------------- debuggers/gdb/shim.js | 13 +++++++++---- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index 1fc7786..2f1918a 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -1,7 +1,7 @@ /** * GDB Debugger plugin for Cloud9 * - * @author Dan Armendariz + * @author Dan Armendariz */ define(function(require, exports, module) { main.consumes = [ @@ -175,24 +175,18 @@ define(function(require, exports, module) { var frameObj = { frame: topFrame, frames: stack }; - if (content.err === "signal" && content.signal !== "SIGINT") { - if (content.signal === "SIGSEGV") - showError("Execution has stopped due to a segmentation fault!"); - else - showError("Process received signal " + content.signal + "!"); - emit("exception", frameObj, new Error(content.signal)); - btnResume.$ext.style.display = "none"; - btnSuspend.$ext.style.display = "inline-block"; - btnSuspend.setAttribute("disabled", true); - btnStepOut.setAttribute("disabled", true); - btnStepInto.setAttribute("disabled", true); - btnStepOver.setAttribute("disabled", true); + if (content.err === "signal" && content.signal.name !== "SIGINT") { + var e = "Process received " + content.signal.text.toLowerCase() + + " (" + content.signal.name + ")!"; + showError(e); + emit("exception", frameObj, new Error(content.signal.name)); } else { emit("break", frameObj); - if (stack.length == 1) - btnStepOut.setAttribute("disabled", true); } + + if (stack.length == 1) + btnStepOut.setAttribute("disabled", true); } /* diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index 4fc5942..a040fdd 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -1,7 +1,7 @@ /** * GDB Debugger plugin for Cloud9 * - * @author Dan Armendariz + * @author Dan Armendariz * @author Rob Bowden */ @@ -544,7 +544,7 @@ function GDB() { } this.state.thread = (thread)? thread : null; - if (signal === "SIGSEGV") + if (signal && signal.name === "SIGSEGV") // dump the varobj cache in segfault so var-updates don't crash GDB this._flushVarCache(); else @@ -818,8 +818,13 @@ function GDB() { var cause = state.status.reason; var thread = state.status['thread-id']; - if (cause == "signal-received") - this._updateState(state.status['signal-name'], thread); + 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 From 335a379e41c5afb434e61b5edfc96e47c3895d91 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 13:34:36 +0000 Subject: [PATCH 11/20] improved signal error message grammar --- debuggers/gdb/gdbdebugger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index 2f1918a..8cf03d0 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -176,8 +176,8 @@ define(function(require, exports, module) { var frameObj = { frame: topFrame, frames: stack }; if (content.err === "signal" && content.signal.name !== "SIGINT") { - var e = "Process received " + content.signal.text.toLowerCase() - + " (" + content.signal.name + ")!"; + var e = "Process received " + content.signal.name + ": " + + content.signal.text; showError(e); emit("exception", frameObj, new Error(content.signal.name)); } From b077644100619f084f0a10f201002e9aaff23732 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 14:09:52 +0000 Subject: [PATCH 12/20] improve comments --- debuggers/gdb/gdbdebugger.js | 1 + debuggers/gdb/shim.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index 8cf03d0..4f9cc81 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -59,6 +59,7 @@ define(function(require, exports, module) { ]); }); + // must register ASAP, or debugger won't be ready for reconnects debug.registerDebugger(TYPE, plugin); // writeFile root is workspace directory, unless given ~ diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index a040fdd..c92d9c8 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -1020,7 +1020,7 @@ gdb = new GDB(); executable = new Executable(); // handle process events -// pass along SIGINT to suspend gdb, only if program is running +// catch and ignore SIGINT, allow gdb to handle process.on("SIGINT", function() { log("SIGINT"); }); From 7933eda6be9a488d8016f1081e71c9f503251dcc Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Thu, 28 Jul 2016 22:03:12 +0000 Subject: [PATCH 13/20] add stale binary warning and flag to disable --- debuggers/gdb/shim.js | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index c92d9c8..ae13243 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -14,7 +14,8 @@ function printUsage() { var p = [process.argv[0], process.argv[1]].join(" "); var msg = [ "Cloud9 GDB Debugger shim", - "Usage: " + p + " [-d=depth] [-g=gdb] [-p=proxy] BIN [args]\n", + "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)", @@ -34,6 +35,7 @@ var GDB_PORT = 15470; var MAX_STACK_DEPTH = 50; var DEBUG = false; var BIN = ""; +var BP_WARN = true; // parse middle arguments function parseArg(str, allowNonInt) { @@ -52,6 +54,10 @@ for(i = 2; i < argc && BIN === ""; i++) { 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); @@ -287,11 +293,13 @@ function Executable() { 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 = []; @@ -906,6 +914,19 @@ function GDB() { 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: No breakpoints were successfully", + "set, even though some were sent to\nthe debugger.", + "If you are sure that you have set breakpoints in", + "the source code\nfor this binary, your symbol table", + "may be old (say, if you move the binary and\nsource", + "to a different directory). If this is the case,", + "force-recompile it to\nresolve this warning.\n"); + } this.clientReconnect = false; this.running = true; this.post(id, "-exec-" + command.command); @@ -959,7 +980,13 @@ function GDB() { args.push('"' + command.fullpath + ':' + (command.line+1) + '"'); - this.post(id, "-break-insert", args.join(" ")); + 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": @@ -967,10 +994,10 @@ function GDB() { break; case "eval": - var args = ["--thread", command.t, "--frame", command.f]; + var eargs = ["--thread", command.t, "--frame", command.f]; // replace quotes with escaped quotes - args.push('"' + command.exp.replace(/"/g, '\\"') + '"'); - this.post(id, "-data-evaluate-expression", args.join(" ")); + eargs.push('"' + command.exp.replace(/"/g, '\\"') + '"'); + this.post(id, "-data-evaluate-expression", eargs.join(" ")); break; case "reconnect": From 49ae97808fb1884969416c71827ed808a30ea7f8 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Fri, 29 Jul 2016 13:00:52 +0000 Subject: [PATCH 14/20] allow runner to specify socketpath, retry count and interval --- debuggers/gdb/gdbdebugger.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index 4f9cc81..5bde3e9 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -215,11 +215,12 @@ define(function(require, exports, module) { /***** Methods *****/ function getProxySource(process){ + var socketpath = Path.join(c9.home, "/.c9/gdbdebugger.socket"); return { source: null, - socketpath: Path.join(c9.home, "/.c9/gdbdebugger.socket"), - retryInverval: 300, - retries: 1000 + socketpath: process.runner[0].socketpath || socketpath, + retryInverval: process.runner[0].retryInterval || 300, + retries: process.runner[0].retryCount || 1000 }; } From 25eb37603e88231b8ab2a76453b2081a0877ca7f Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Fri, 29 Jul 2016 17:37:48 +0000 Subject: [PATCH 15/20] ctrl-c pauses execution if running, quits on pause --- debuggers/gdb/shim.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index ae13243..1a30d0b 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -1047,9 +1047,11 @@ gdb = new GDB(); executable = new Executable(); // handle process events -// catch and ignore SIGINT, allow gdb to handle +// 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() { From 748c9fb4346e150e5c8d3c7682ad54f7f11005ba Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Sun, 31 Jul 2016 01:23:21 +0000 Subject: [PATCH 16/20] gdb evaluate make more robust against null frames --- debuggers/gdb/gdbdebugger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debuggers/gdb/gdbdebugger.js b/debuggers/gdb/gdbdebugger.js index 5bde3e9..f5156fc 100755 --- a/debuggers/gdb/gdbdebugger.js +++ b/debuggers/gdb/gdbdebugger.js @@ -437,8 +437,8 @@ define(function(require, exports, module) { function evaluate(expression, frame, global, disableBreak, callback) { var args = { "exp": expression, - "f": (frame.index == null) ? 0 : frame.index, - "t": (frame.thread == null) ? 1 : frame.thread, + "f": (!frame || frame.index == null) ? 0 : frame.index, + "t": (!frame || frame.thread == null) ? 1 : frame.thread, }; proxy.sendCommand("eval", args, function(err, reply) { if (err) From 72a184fcfd79344b958ec68ebf5a87cf5616b445 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Sun, 31 Jul 2016 01:26:02 +0000 Subject: [PATCH 17/20] gdbproxyservice make robust against unexpected socket disappearance --- debuggers/gdb/lib/GDBProxyService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debuggers/gdb/lib/GDBProxyService.js b/debuggers/gdb/lib/GDBProxyService.js index 0c35eec..ce3bd2e 100644 --- a/debuggers/gdb/lib/GDBProxyService.js +++ b/debuggers/gdb/lib/GDBProxyService.js @@ -74,7 +74,7 @@ var GDBProxyService = module.exports = function(socket, haltHandler) { this.$send = function(args) { args = JSON.stringify(args); var msg = ["Content-Length:", args.length, "\r\n\r\n", args].join(""); - this.$socket.send(msg); + this.$socket && this.$socket.send(msg); }; /* From b119449eea27298728417166a7c75b5c228ec019 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Sun, 31 Jul 2016 16:09:34 +0000 Subject: [PATCH 18/20] gdb shim dump queued stderr if gdbserver exits prematurely --- debuggers/gdb/shim.js | 45 +++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index 1a30d0b..a4caed7 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -228,6 +228,7 @@ function Client(c) { function Executable() { this.proc = null; + this.running = false; /** * Spawn GDB server which will in turn run executable, sharing @@ -243,30 +244,50 @@ function Executable() { stdio: ['pipe', process.stdout, 'pipe'] }); + var errqueue = []; + var quit = null; 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 }); - process.exit(); + + // if stderr is still buffering data, don't quit yet + if (errqueue !== null) + quit = code; + else + process.exit(code); + }.bind(this)); + + this.proc.stderr.on("end", function() { + // dump queued stderr data, if it exists + if (errqueue !== null) { + console.error(errqueue.join("")); + errqueue = null; + } + + // quit now if gdbserver ended before stderr buffer flushed + if (quit !== null) + process.exit(quit); }); // wait for gdbserver to listen before executing callback function handleStderr(data) { + // once listening, forward stderr to process + if (this.running) + return process.stderr.write(data); + + // consume and store stderr until gdbserver is listening var str = data.toString(); - // handle cases of error (eg, failure to disable address space rand) - if (str.indexOf("Error") > -1) { - console.log(str); - process.exit(1); - } + errqueue.push(str); + if (str.indexOf("Listening") > -1) { // perform callback when gdbserver is ready callback(); } - else if (str.indexOf("127.0.0.1") > -1) { + if (str.indexOf("127.0.0.1") > -1) { // soak up final gdbserver message before sharing i/o stream - this.proc.stderr.removeListener("data", handleStderr); - this.proc.stderr.pipe(process.stderr, {end: false}); + errqueue = null; + this.running = true; } - } this.proc.stderr.on("data", handleStderr.bind(this)); @@ -356,7 +377,7 @@ function GDB() { 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 }); - process.exit(); + process.exit(code); }); }; @@ -1064,7 +1085,7 @@ process.on("exit", function() { if (gdb) gdb.cleanup(); if (client) client.cleanup(); if (executable) executable.cleanup(); - if (server) server.close(); + if (server && server.listening) server.close(); if (PROXY.sock) { try { fs.unlinkSync(PROXY.sock); From c38a8fd8dc6adbbf0275c2337c8d838ce3136da6 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Sun, 31 Jul 2016 17:35:32 +0000 Subject: [PATCH 19/20] gdb shim pass along abnormal exit codes or signals from children to user --- debuggers/gdb/shim.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index a4caed7..1443c50 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -93,6 +93,9 @@ 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) { @@ -245,15 +248,13 @@ function Executable() { }); var errqueue = []; - var quit = null; 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 }; - // if stderr is still buffering data, don't quit yet - if (errqueue !== null) - quit = code; - else + // only quit if stderr has finished buffering data + if (errqueue === null) process.exit(code); }.bind(this)); @@ -265,8 +266,8 @@ function Executable() { } // quit now if gdbserver ended before stderr buffer flushed - if (quit !== null) - process.exit(quit); + if (exit !== null) + process.exit(exit.code); }); // wait for gdbserver to listen before executing callback @@ -377,6 +378,7 @@ function GDB() { 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); }); }; @@ -1082,6 +1084,14 @@ process.on("SIGHUP", function() { 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(); @@ -1133,7 +1143,7 @@ server.on("error", function(err) { else { console.log(err); } - process.exit(); + process.exit(1); }); // begin debug process From 89b26d32ed9712384dfade75123c5e6f18e9fb69 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Wed, 3 Aug 2016 00:44:13 +0000 Subject: [PATCH 20/20] remove unnecessary socketfile unlink --- debuggers/gdb/shim.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/debuggers/gdb/shim.js b/debuggers/gdb/shim.js index 1443c50..f29a586 100755 --- a/debuggers/gdb/shim.js +++ b/debuggers/gdb/shim.js @@ -1096,14 +1096,6 @@ process.on("exit", function() { if (client) client.cleanup(); if (executable) executable.cleanup(); if (server && server.listening) server.close(); - if (PROXY.sock) { - try { - fs.unlinkSync(PROXY.sock); - } - catch(e) { - log("Unable to delete socket: " + e.code); - } - } if (DEBUG) log_file.end(); });