/** * GDB Debugger plugin for Cloud9 * * @author Dan Armendariz */ define(function(require, exports, module) { main.consumes = [ "Plugin", "c9", "debugger", "dialog.error", "fs", "panels", "settings" ]; main.provides = ["gdbdebugger"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; 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; var Frame = debug.Frame; var Source = debug.Source; var Breakpoint = debug.Breakpoint; var Variable = debug.Variable; var Scope = debug.Scope; var Path = require("path"); var GDBProxyService = require("./lib/GDBProxyService"); /***** Initialization *****/ var plugin = new Plugin("CS50", main.consumes); var emit = plugin.getEmitter(); emit.setMaxListeners(1000); // increment when shim is updated to force rewrite var SHIM_VERSION = 2; var TYPE = "gdb"; var attached = false; var state, // debugger state socket, // socket to proxy stack, // always up-to-date frame stack proxy = null; // GDB service proxy // GUI buttons var btnResume, btnSuspend, btnStepOver, btnStepInto, btnStepOut; var SCOPES = ["Arguments", "Locals"]; var loaded = false; function load() { if (loaded) return false; loaded = true; settings.on("read", function() { settings.setDefaults("user/debug", [ ["autoshow", true] ]); }); // must register ASAP, or debugger won't be ready for reconnects debug.registerDebugger(TYPE, plugin); // writeFile root is workspace directory, unless given ~ var shimVersion = "~/.c9/bin/.c9gdbshim" + SHIM_VERSION; var shim = require("text!./shim.js"); fs.exists(shimVersion, function(exists) { if (exists) return; fs.writeFile("~/.c9/bin/c9gdbshim.js", shim, "utf8", function(err) { if (err) { // unregister the debugger on error debug.unregisterDebugger(TYPE, plugin); return console.log("Error writing gdb shim: " + err); } // remember we wrote current version fs.writeFile(shimVersion, "", function(err) { if (err) return console.log("Error writing shim version: " + err); }); }); }); } /***** Helper Functions *****/ /* * Build individual variable objects from GDB output */ function buildVariable(variable, scope) { if (variable == null) return; return new Variable({ ref: (variable.objname) ? variable.objname : variable.name, name: (variable.exp) ? variable.exp : variable.name, value: variable.value, type: variable.type, children: (variable.numchild && variable.numchild > 0), properties: null, scope: scope }); } /* * Create a scope and variables from data received from GDB */ function buildScopeVariables(frame_vars, scope_index, frame_index, vars) { var scope = new Scope({ index: scope_index, type: SCOPES[scope_index], frameIndex: frame_index }); for (var i = 0, j = vars.length; i < j; i++) { frame_vars.push(buildVariable(vars[i], scope)); } } /* * Create a frame object, scope, and variables from a GDB frame */ function buildFrame(thread, topIndex, frame, i) { var variables = []; // build scopes and variables for this frame buildScopeVariables(variables, 0, i, frame.args); if (typeof frame.locals !== "undefined") { buildScopeVariables(variables, 1, i, frame.locals); } // get file path from GDB output var fullpath = (frame.fullname) ? frame.fullname : ((frame.from) ? frame.from : c9.workspaceDir + "/?"); // get file name and relative path from full path var file = Path.basename(fullpath); var relative = "/" + ((fullpath.indexOf(c9.workspaceDir) == 0) ? Path.relative(c9.workspaceDir, fullpath) : fullpath); var line = parseInt(frame.line, 10); line = (isNaN(line)) ? 0 : line - 1; return new Frame({ index: i, name: frame.func, column: 0, id: file + ":" + frame.func + i + line, line: line, script: file, path: relative, sourceId: file, thread: thread, istop: (i === topIndex), variables: variables }); } /* * Process out-of-sequence information received from proxy, * like errors or frames on breakpoint hit. */ function processHalt(content) { if (content.err === "killed") { // GDB was killed return detach(); } else if (content.err === "corrupt") { showError("GDB has detected a corrupt execution environment and has shut down!"); return detach(); } // process frames var frames = content.frames; var topIndex = Math.max(0, frames.findIndex(function (frame) { return frame.exists; })); stack = frames.map(buildFrame.bind(this, content.thread, topIndex)); var topFrame = stack[topIndex]; setState("stopped"); emit("frameActivate", { frame: topFrame }); var frameObj = { frame: topFrame, frames: stack }; if (content.err === "signal" && content.signal.name !== "SIGINT") { var e = "Process received " + content.signal.name + ": " + content.signal.text; showError(e); emit("exception", frameObj, new Error(content.signal.name)); } else { emit("break", frameObj); } if (stack.length == 1) btnStepOut.setAttribute("disabled", true); } /* * A special case of sendCommand that demands a status update on reply. */ function sendExecutionCommand(command, callback) { proxy.sendCommand(command, {}, function(err, reply) { if (err) return callback && callback(err); setState(reply.state); callback && callback(); }); } /* * Set the debugger state and emit state change */ function setState(_state) { if (state === _state) return; state = _state; emit("stateChange", { state: state }); } /***** Methods *****/ function getProxySource(process) { var socketpath = Path.join(c9.home, "/.c9/gdbdebugger.socket"); return { source: null, socketpath: process.runner.socketpath || socketpath, retryInverval: process.runner.retryInterval || 300, retries: process.runner.retryCount || 1000 }; } function attach(s, reconnect, callback) { if (proxy) proxy.detach(); socket = s; socket.on("back", function() { reconnectSync(); }, plugin); socket.on("error", function(err) { console.log("gdbdebugger err: ", err); emit("error", err); }, plugin); proxy = new GDBProxyService(socket, processHalt); proxy.attach(function() { emit("connect"); // if we're reconnecting, check GDB's state if (reconnect) reconnectSync(callback); else sync(true, callback); }); // show the debug panel immediately if (settings.getBool("user/debug/@autoshow")) panels.activate("debugger"); // attach to GUI elements btnResume = debug.getElement("btnResume"); btnStepOver = debug.getElement("btnStepOver"); btnStepInto = debug.getElement("btnStepInto"); btnStepOut = debug.getElement("btnStepOut"); btnSuspend = debug.getElement("btnSuspend"); } function detach() { if (proxy) proxy.detach(); emit("frameActivate", { frame: null }); setState(null); socket = null; attached = false; proxy = null; btnResume.$ext.style.display = "inline-block"; btnSuspend.$ext.style.display = "none"; btnSuspend.setAttribute("disabled", false); btnStepOut.setAttribute("disabled", false); btnStepInto.setAttribute("disabled", false); btnStepOver.setAttribute("disabled", false); emit("detach"); } function sync(begin, callback) { // send breakpoints to gdb and attach when done var localBkpts = emit("getBreakpoints"); listBreakpoints(function(err, remoteBkpts) { if (err) return callback(err); /* There exist two sets of breakpoints. One local as shown * in the GUI, L, and one "remote" that already exists in * GDB's state, R. * Syncing L and R must prioritize L's elements. We'll * create three sets: * to_remove = R\L (or {x∈R|x∉L}) * BPs present in R but not in L, must be removed from R * to_add = L/R (or {x∈L|x∉R}) * BPs present in L but not in R, must be added to R * synced = L∩R * BPs already in both. */ var to_add = []; var synced = []; // compare the GUI breakpoints to those already created for (var i = 0, j = localBkpts.length; i < j; i++) { var bp = localBkpts[i]; var missing = true; // test for membership of bp in remoteBkpts for (var x = 0, y = remoteBkpts.length; x < y; x++) { var rbp = remoteBkpts[x]; if (bp.text == rbp.text && bp.line == rbp.line && bp.condition == rbp.condition) { // make sure synced BP has correct id bp.id = rbp.id; // track necessary removals by removing used BPs remoteBkpts.splice(x, 1); missing = false; break; } } if (missing) to_add.push(bp); else synced.push(bp); } // notify GDB of new breakpoints manyBreakpoints(to_add, setBreakpoint, function(added, fail) { // successfully created BPs are now synced synced = synced.concat(added); // now remove extraneous BPs manyBreakpoints(remoteBkpts, clearBreakpoint, function(cleared, clrfail) { // BPs that failed to remove need to be present locally synced = synced.concat(clrfail); attached = true; emit("attach", { breakpoints: synced }); if (begin) resume(callback); else sendExecutionCommand("status", callback); }); }); }); } /* * Not applicable. */ function getSources(callback) { var sources = [new Source()]; callback(null, sources); emit("sources", { sources: sources }); } /* * Not applicable. */ function getSource(source, callback) { callback(null, new Source()); } function getFrames(callback, silent) { emit("getFrames", { frames: stack }); callback(null, stack); } function getScope(frame, scope, callback) { callback(null, scope.variables, scope, frame); } function getProperties(variable, callback) { // request children of a variable var args = { name: variable.ref }; proxy.sendCommand("var-children", args, function(err, reply) { if (err) return callback && callback(err); else if (typeof reply.children === "undefined") return callback && callback(new Error("No children")); var children = []; reply.children.forEach(function (child) { children.push(buildVariable(child, variable.scope)); }); variable.properties = children; callback && callback(null, children, variable); }); } function stepInto(callback) { sendExecutionCommand("step", callback); } function stepOver(callback) { sendExecutionCommand("next", callback); } function stepOut(callback) { // step out only works in GDB if we're not inside main() if (stack.length > 1) sendExecutionCommand("finish", callback); } function resume(callback) { sendExecutionCommand("continue", callback); } function suspend(callback) { proxy.sendCommand("suspend", {}, function(err) { if (err) return callback && callback(err); emit("suspend"); callback && callback(); }); } function reconnectSync(callback) { // If a program is executing when debugger reconnects, GDB must // be paused to fetch the state and then restarted or it will hang if (!callback) callback = function() {}; proxy.sendCommand("reconnect", {}, function(err, reply) { var restart = !err && reply.state == "running"; sync(restart, callback); }); } function evaluate(expression, frame, global, disableBreak, callback) { var args = { "exp": expression, "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) return callback(new Error("No value")); else if (typeof reply.status === "undefined") return callback(new Error(reply.status.msg)); callback(null, new Variable({ name: expression, value: reply.status.value, type: undefined, /* type info is not provided by GDB */ children: false })); }); } function setVariable(variable, value, frame, callback) { var args = { "name": variable.ref, "val": value }; proxy.sendCommand("var-set", args, function(err, reply) { if (err) return callback && callback(err); callback && callback(null, variable); }); } 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); if (!reply.status || !reply.status.bkpt) return callback && callback(new Error("Can't set breakpoint")); bp.id = reply.status.bkpt.number; callback && callback(null, bp, {}); }); } function manyBreakpoints(breakpoints, command, callback) { function _setBPs(breakpoints, failed, callback, i) { // run callback once we've exhausted setting breakpoints if (i == breakpoints.length) { callback(breakpoints, failed); return; } command(breakpoints[i], function(err, bp) { if (err) { // breakpoint failure, remove it before going on failed.push(breakpoints.splice(i, 1)); _setBPs(breakpoints, failed, callback, i); } else { breakpoints[i].id = bp.id; _setBPs(breakpoints, failed, callback, i + 1); } }); } _setBPs(breakpoints, [], callback, 0); } function changeBreakpoint(bp, callback) { proxy.sendCommand("bp-change", bp.data, function(err) { callback && callback(err, bp); }); } function clearBreakpoint(bp, callback) { proxy.sendCommand("bp-clear", bp.data, function(err) { callback && callback(err, bp); }); } function listBreakpoints(callback) { proxy.sendCommand("bp-list", {}, function(err, reply) { if (err) return callback && callback(err); var bps = reply.status.BreakpointTable.body.map(function (bp) { return new Breakpoint({ id: bp.number, path: bp.fullname, line: parseInt(bp.line, 10) - 1, ignoreCount: (bp.hasOwnProperty("ignore")) ? bp.ignore : undefined, condition: (bp.hasOwnProperty("cond")) ? bp.cond : undefined, enabled: (bp.enabled == "y") ? true : false, text: bp.file }); }); callback(null, bps); }); } function serializeVariable(variable, callback) { callback(variable.value); } /***** Lifecycle *****/ plugin.on("load", function() { load(); }); plugin.on("enable", function() { }); plugin.on("disable", function() { }); plugin.on("unload", function() { debug.unregisterDebugger(TYPE, plugin); state = null; socket = null; reader = null; stack = null; proxy = null; btnResume = btnSuspend = btnStepOver = btnStepInto = btnStepOut = null; loaded = false; attached = false; }); /***** Register and define API *****/ /** * Debugger implementation for Cloud9. When you are implementing a * custom debugger, implement this API. If you are looking for the * debugger interface of Cloud9, check out the {@link debugger}. * * This interface is defined to be as stateless as possible. By * implementing these methods and events you'll be able to hook your * debugger seamlessly into the Cloud9 debugger UI. * * See also {@link debugger#registerDebugger}. * * @class debugger.implementation */ plugin.freezePublicAPI({ /** * Specifies the features that this debugger implementation supports * @property {Object} features * @property {Boolean} features.scripts Able to download code (disable the scripts button) * @property {Boolean} features.conditionalBreakpoints Able to have conditional breakpoints (disable menu item) * @property {Boolean} features.liveUpdate Able to update code live (don't do anything when saving) * @property {Boolean} features.updateWatchedVariables Able to edit variables in watches (don't show editor) * @property {Boolean} features.updateScopeVariables Able to edit variables in variables panel (don't show editor) * @property {Boolean} features.setBreakBehavior Able to configure break behavior (disable break behavior button) * @property {Boolean} features.executeCode Able to execute code (disable REPL) */ features: { scripts: false, conditionalBreakpoints: true, liveUpdate: false, updateWatchedVariables: true, updateScopeVariables: true, setBreakBehavior: false, executeCode: true }, /** * The type of the debugger implementation. This is the identifier * with which the runner selects the debugger implementation. * @property {String} type * @readonly */ type: TYPE, /** * @property {null|"running"|"stopped"} state The state of the debugger process * * * * * *
Value Description
null process doesn't exist
"stopped" paused on breakpoint
"running" process is running
* @readonly */ get state() { return state; }, /** * */ get attached() { return attached; }, /** * Whether the debugger will break when it encounters any exception. * This includes exceptions in try/catch blocks. * @property {Boolean} breakOnExceptions * @readonly */ get breakOnExceptions() { return false; }, /** * Whether the debugger will break when it encounters an uncaught * exception. * @property {Boolean} breakOnUncaughtExceptions * @readonly */ get breakOnUncaughtExceptions() { return false; }, _events: [ /** * Fires when the debugger hits a breakpoint. * @event break * @param {Object} e * @param {debugger.Frame} e.frame The frame where the debugger has breaked at. * @param {debugger.Frame[]} [e.frames] The callstack frames. */ "break", /** * Fires when the {@link #state} property changes * @event stateChange * @param {Object} e * @param {debugger.Frame} e.state The new value of the state property. */ "stateChange", /** * Fires when the debugger hits an exception. * @event exception * @param {Object} e * @param {debugger.Frame} e.frame The frame where the debugger has breaked at. * @param {Error} e.exception The exception that the debugger breaked at. */ "exception", /** * Fires when a frame becomes active. This happens when the debugger * hits a breakpoint, or when it starts running again. * @event frameActivate * @param {Object} e * @param {debugger.Frame/null} e.frame The current frame or null if there is no active frame. */ "frameActivate", /** * Fires when the result of the {@link #method-getFrames} call comes in. * @event getFrames * @param {Object} e * @param {debugger.Frame[]} e.frames The frames that were retrieved. */ "getFrames", /** * Fires when the result of the {@link #getSources} call comes in. * @event sources * @param {Object} e * @param {debugger.Source[]} e.sources The sources that were retrieved. */ "sources", /** * Fires when a source file is (re-)compiled. In your event * handler, make sure you check against the sources you already * have collected to see if you need to update or add your source. * @event sourcesCompile * @param {Object} e * @param {debugger.Source} e.file the source file that is compiled. **/ "sourcesCompile" ], /** * Attaches the debugger to the started process. * @param {Object} runner A runner as specified by {@link run#run}. * @param {debugger.Breakpoint[]} breakpoints The set of breakpoints that should be set from the start */ attach: attach, /** * Detaches the debugger from the started process. */ detach: detach, /** * Loads all the active sources from the process * * @param {Function} callback Called when the sources are retrieved. * @param {Error} callback.err The error object if an error occured. * @param {debugger.Source[]} callback.sources A list of the active sources. * @fires sources */ getSources: getSources, /** * Retrieves the contents of a source file * @param {debugger.Source} source The source to retrieve the contents for * @param {Function} callback Called when the contents is retrieved * @param {Error} callback.err The error object if an error occured. * @param {String} callback.contents The contents of the source file */ getSource: getSource, /** * Retrieves the current stack of frames (aka "the call stack") * from the debugger. * @param {Function} callback Called when the frame are retrieved. * @param {Error} callback.err The error object if an error occured. * @param {debugger.Frame[]} callback.frames A list of frames, where index 0 is the frame where the debugger has breaked in. * @fires getFrames */ getFrames: getFrames, /** * Retrieves the variables from a scope. * @param {debugger.Frame} frame The frame to which the scope is related. * @param {debugger.Scope} scope The scope from which to load the variables. * @param {Function} callback Called when the variables are loaded * @param {Error} callback.err The error object if an error occured. * @param {debugger.Variable[]} callback.variables A list of variables defined in the `scope`. * @param {debugger.Scope} callback.scope The scope to which these variables belong * @param {debugger.Frame} callback.frame The frame related to the scope. */ getScope: getScope, /** * Retrieves and sets the properties of a variable. * @param {debugger.Variable} variable The variable for which to retrieve the properties. * @param {Function} callback Called when the properties are loaded * @param {Error} callback.err The error object if an error occured. * @param {debugger.Variable[]} callback.properties A list of properties of the variable. * @param {debugger.Variable} callback.variable The variable to which the properties belong. */ getProperties: getProperties, /** * Step into the next statement. */ stepInto: stepInto, /** * Step over the next statement. */ stepOver: stepOver, /** * Step out of the current statement. */ stepOut: stepOut, /** * Continues execution of a process after it has hit a breakpoint. */ resume: resume, /** * Pauses the execution of a process at the next statement. */ suspend: suspend, /** * Evaluates an expression in a frame or in global space. * @param {String} expression The expression. * @param {debugger.Frame} frame The stack frame which serves as the contenxt of the expression. * @param {Boolean} global Specifies whether to execute the expression in global space. * @param {Boolean} disableBreak Specifies whether to disabled breaking when executing this expression. * @param {Function} callback Called after the expression has executed. * @param {Error} callback.err The error if any error occured. * @param {debugger.Variable} callback.variable The result of the expression. */ evaluate: evaluate, /** * Change a live running source to the latest code state * @param {debugger.Source} source The source file to update. * @param {String} value The new contents of the source file. * @param {Boolean} previewOnly * @param {Function} callback Called after the expression has executed. * @param {Error} callback.err The error if any error occured. */ setScriptSource: function() {}, /** * Adds a breakpoint to a line in a source file. * @param {debugger.Breakpoint} breakpoint The breakpoint to add. * @param {Function} callback Called after the expression has executed. * @param {Error} callback.err The error if any error occured. * @param {debugger.Breakpoint} callback.breakpoint The added breakpoint * @param {Object} callback.data Additional debugger specific information. */ setBreakpoint: setBreakpoint, /** * Updates properties of a breakpoint * @param {debugger.Breakpoint} breakpoint The breakpoint to update. * @param {Function} callback Called after the expression has executed. * @param {Error} callback.err The error if any error occured. * @param {debugger.Breakpoint} callback.breakpoint The updated breakpoint */ changeBreakpoint: changeBreakpoint, /** * Removes a breakpoint from a line in a source file. * @param {debugger.Breakpoint} breakpoint The breakpoint to remove. * @param {Function} callback Called after the expression has executed. * @param {Error} callback.err The error if any error occured. * @param {debugger.Breakpoint} callback.breakpoint The removed breakpoint */ clearBreakpoint: clearBreakpoint, /** * Retrieves a list of all the breakpoints that are set in the * debugger. * @param {Function} callback Called when the breakpoints are retrieved. * @param {Error} callback.err The error if any error occured. * @param {debugger.Breakpoint[]} callback.breakpoints A list of breakpoints */ listBreakpoints: listBreakpoints, /** * Sets the value of a variable. * @param {debugger.Variable} variable The variable to set the value of. * @param {debugger.Variable[]} parents The parent variables (i.e. the objects of which the variable is the property). * @param {Mixed} value The new value of the variable. * @param {debugger.Frame} frame The frame to which the variable belongs. * @param {Function} callback * @param {Function} callback Called when the breakpoints are retrieved. * @param {Error} callback.err The error if any error occured. * @param {Object} callback.data Additional debugger specific information. */ setVariable: setVariable, /** * */ restartFrame: function() {}, /** * */ serializeVariable: serializeVariable, /** * Defines how the debugger deals with exceptions. * @param {"all"/"uncaught"} type Specifies which errors to break on. * @param {Boolean} enabled Specifies whether to enable breaking on exceptions. * @param {Function} callback Called after the setting is changed. * @param {Error} callback.err The error if any error occured. */ setBreakBehavior: function() {}, /** * Returns the source of the proxy */ getProxySource: getProxySource }); register(null, { gdbdebugger: plugin }); } });