Debugger Plugin

The c9.ide.run.debug package offers a set of plugins that provide a generic, configurable and extendable user interface for step-through debuggers. The plugin structure is optimized to easily support additional debugger protocols. Each debugger protocol can be implemented in a so called "Debugger Implementation" plugin. This article outlines how to create a debugger implementation. Cloud9 comes with an implementation for the v8 debug protocol and a recent community addition supports gdb, the GNU Project Debugger. An xdebug implementation is forthcoming.

UI

The debugger user interface consists of a panel with a button toolbar and several debug panels. By default there are 4 debug panels:

  • Watch Expressions
  • Call Stack
  • Scope Variables
  • Breakpoints

Other plugins can introduce new debug panels - this is outside of the scope of this article and will be addressed in a future article.

The debugger integrates with the Ace editor and allows users to set (conditional) breakpoints and highlights the current line while stepping through the code or looking through the call stack. In addition, while execution is paused, the user can hover over a variable in the editor to evaluate that variable in the current context or frame.

Approach

The debugger implementation is a plugin that provides a specific API which is used by the UI as described above. This means that in most cases you won't have to add any UI to implement a step-through debugger for a certain runtime or platform.

The interface for the debugger implementation is straightforward and can be implemented mostly stateless. The implementer (you) has the choice to add any state you want. We just made sure that this is not a requirement for the plugin.

It is common practice to keep the plugin stateless and move state information, such as the connection and management of any processes that assist with debugging, to the implementation of a so-called "proxy." This proxy is server-side code that sits between the running process and the debugger implementation and is described in more detail below.

The diagram below visualizes the structure of the debugger. It consists of the following parts:

Running ProcessThe process that was started by the Cloud9 runner.
[Debug Server]Optional. Some platforms may support or require a separate process for debugging (e.g., GDB Server).
Proxy ProcessA process that holds the connection to the debugger (and will survive browser reloads)
Debug SocketThe debug socket plugin provides the connection between the proxy process and the debugger implementation. This plugin is also responsible for starting the proxy process.
Your Debugger ImplementationThis implementation mediates communication between the APIs of running debugger (or debug server) and the Debugger UI.
Debugger UIThe UI for the debugger that the user interacts with.
818818

You should strongly consider using a Debug Server if the debugger process supports it and if typical running processes for your debugger will use console access (e.g., command-line programs). Using such an implementation allows easy separation between the standard streams (i.e., stdin, stdout, and stderr) from the running process and the command interface for the debugger itself. In other words, the Cloud9 runner will start the corresponding client process and allow the standard streams to be visible in the Console window of the UI. Without using such an implementation, you may need to write additional code in the proxy process that parses and separates the debugger's output from the process' output, properly passes user input along to the running process, and prevents the user from intentionally or accidentally writing commands to the debugger.

The focus of this article includes both the orange (debugger implementation) and green (proxy) blocks.

Basic Debugger Implementation Plugin

Scroll to the bottom the article to find a copy and paste-able snippet to get started.

Lets start with the outline of the plugin by consuming the Plugin factory and debugger plugin and give our plugin a name.

define(function(require, exports, module) {
    main.consumes = ["Plugin", "debugger"];
    main.provides = ["mydebugger"];
    return main;
    
    function main(options, imports, register) {
        var Plugin = imports.Plugin;
        var debug = imports["debugger"];

Get local references to the data objects the we'll use later.

        var Frame = debug.Frame;
        var Source = debug.Source;
        var Breakpoint = debug.Breakpoint;
        var Variable = debug.Variable;
        var Scope = debug.Scope;

Define the type of the plugin to be used throughout the plugin. This string is used in the runner to reference this debugger. As examples, for the v8 debugger the string is "v8" and for GDB the string is "gdb".

        var TYPE = "yourtype";

Initialize the plugin and emitters. Make sure to set the maximum number of listeners as there might be many.

        var plugin = new Plugin("Your Name", main.consumes);
        var emit = plugin.getEmitter();
        emit.setMaxListeners(1000);

During load and unload register and unregister the implementation with the debug plugin.

        plugin.on("load", function(){
            debug.registerDebugger(TYPE, plugin);
        });
        plugin.on("unload", function(){
            debug.unregisterDebugger(TYPE, plugin);
        });

Finally define the public API and register your plugin. There will be much more API as is described further down in this article.

        plugin.freezePublicAPI({
            type: TYPE
        });
        
        register(null, {
            "mydebugger" : plugin
        });
    }
});

API

This part of the article will go into the interface that your debugger implementation should support. Implementing support for a debugger protocol is essentially implementing this set of APIs.

These properties and functions should all be defined in the public API.

plugin.freezePublicAPI({
    // The API is defined here
});

All properties, methods and events that must be implemented by your debugger implementation plugin are listed in the reference guide. We'll highlight some of the ones that require more in-depth explanation below. Keep the reference guide as the authoritative source for all members that must be implemented.

Debugger Features

The debugger implementation can hint which features are supported and which are not. Setting (some of) these features to false will disable the corresponding UI and will prevent related functions from being called.

features: {
                                    // Able to:
    scripts: true,                  // download code (if false: disable the scripts button)
    conditionalBreakpoints: true,   // have conditional breakpoints (if false: disable menu item)
    liveUpdate: true,               // update code live (if false: don't do anything when saving)
    updateWatchedVariables: true,   // edit vars in watches (if false: don't show editor)
    updateScopeVariables: true,     // edit vars in variables panel (if false: don't show editor)
    setBreakBehavior: true,         // configure break behavior (if false: disables button)
    executeCode: true               // execute code (if false: disable REPL)
},

👍

Need more control?

The ability to turn off features like this is a recent addition and we appreciate any feedback on this via the mailinglist or in a github issue.

Properties

Besides features and type, there are only four properties for which you should define property getters. These are state, attached, breakOnExceptions and breakOnUncaughtExceptions.

If you are unfamiliar with adding getter properties in javascript, the following example shows how to do this.

plugin.freezePublicAPI({
    get attached(){ return attached; }
});

In that example, the attached variable is a local variable which value is returned when another plugin calls plugin.attached.

Attached

The attached property must be set to true immediately before firing the attach event and set to false immediately before firing the detach event. This property is used throughout the debugger UI plugins to determine whether to interact with the debugger implementation or not.

State

The state property indicates the process' execution state; it might be "stopped" on a breakpoint, "running", or null if it doesn't exist. The stateChange event must be fired immediately after changing the state property.

Value Description
null process doesn't exist
"stopped" paused on breakpoint
"running" process is running

Events

There are 14 events that can be implemented by your plugin. These events must be emitted by the event emitter of your plugin.

emit("attach", { breakpoints: breakpoints });

The following table lists all the events. Several events are required no matter how basic your implementation is, but others are dependent on the features you support. We'll discuss them below.

attachRequired. Fires when the debugger is attached.
detachRequired. Fires when the debugger is detached.
errorRequired. Fires when the socket experiences an error
breakRequired. Fires when the debugger hits a breakpoint.
exceptionFires when the debugger hits an exception.
sourcesCompileFires when a source file is (re-)compiled.
frameActivateRequired. Fires when a frame becomes active.
setScriptSourceFires when the source of a file is updated
getFramesRequired. Fires when the result of the getFrames call comes in.
sourcesFires when the result of the getSources call comes in.
suspendRequired. Fires when execution is suspended (paused)
getBreakpointsFires when the current list of breakpoints is needed
breakpointUpdateFires when a breakpoint is updated.
stateChangeFires when the state property changes

attach, detach, error

These events must fire during the different state transitions of the connection. The attach event should be fired after the debugger is ready to receive commands. In practice this means that the attach event is fired after connecting to the debugger, getting a list of source filenames, setting the breakpoints and detecting whether the debugger is already on a breakpoint.

The detach event must be fired when the debugger gets detached. This is usually in the detach method.

Lastly the error event is fired when the socket object that is passed to the attach method has an error:

socket.on("error", function(err) {
    emit("error", err);
}, plugin);

break, exception, sourcesCompile

These events must fire when the debugger either; hits a breakpoint (break), exceptions or segfaults (exception) or finishes compiling a source file (sourcesCompile - only if applicable).

frameActivate

The frameActivate event must fire when a different frame becomes active (or no frame). Make sure to also call this after attach and detach and during a break event.

setScriptSource, getFrames, sources, suspend

These events must fire in the callback of their related methods.

function getSources(callback) {
    fetchSources(function(sources) {
        callback(null, sources);
        emit("sources", { sources: sources })
    });
}

getBreakpoints

Unlike other events the getBreakpoints event should fire when you need a list of current breakpoints from the UI.

function attach(socket, reconnect, callback){
   var breakpoints = emit("getBreakpoints");
   connect(breakpoints, callback);
}

breakpointUpdate

This event must fire when a breakpoint is moved to a new line. This can happen when it is set at a location which is not an expression. Certain debuggers (such as v8) will move the breakpoint location to the first expression that's next in source order.

function setBreakpoint(breakpoint, callback) {
    setbreakpoint(breakpoint, function(info) {
        if (isDifferentLocation(info, breakpoint)) {
            updateBreakpoint(breakpoint, info);
            emit("breakpointUpdate", { breakpoint: breakpoint });
        }
        callback(null, breakpoint, info);
    });
    return true;
}

Methods

There are 23 methods that must be implemented by your plugin. These methods must be defined on the public API of your plugin.

function attach(){}

plugin.freezePublicAPI({
    attach: attach
});

🚧

Unsupported methods

Some debuggers don't support all the operations needed to implement a function. In that case simply keep the function empty and call the callback with an error.

The following table lists all the methods. We'll discuss them below.

attachAttaches the debugger to the started process.
detachDetaches the debugger from the started process.
getSourcesLoads all the active sources from the process
getSourceRetrieves the contents of a source file
getFramesRetrieves the current stack of frames (aka "the call stack")
getScopeRetrieves the variables from a scope.
getPropertiesRetrieves and sets the properties of a variable.
stepIntoStep into the next statement.
stepOverStep over the next statement.
stepOutStep out of the current statement.
resumeContinues execution of a process after it has hit a breakpoint.
suspendPauses the execution of a process at the next statement.
evaluateEvaluates an expression in a frame or in global space.
setScriptSourceChange a live running source to the latest code state.
setBreakpointAdds a breakpoint to a line in a source file.
changeBreakpointUpdates properties of a breakpoint.
clearBreakpointRemoves a breakpoint from a line in a source file.
listBreakpointsRetrieves a list of all the breakpoints that are set in the debugger.
setVariableSets the value of a variable.
restartFrameStarts a frame (usually a function) from the first expression in that frame.
serializeVariableRetrieve the value of a variable.
setBreakBehaviorDefines how the debugger deals with exceptions.
getProxySourceReturns the source of the proxy process.

attach

The attach method implements the flow for connecting to the debugger through a proxy connection which is setup by the socket. This is arguably the most involved function to implement and the exact implementation will be highly unique for each debugger. After the attach() function has set up the initial connection with the debugger, the other functions will be responsible for sending commands and parsing the response.

var socket;
function attach(_socket, reconnect, callback) {
    socket = _socket;

    // The back event is fired when the socket reconnects
    socket.on("back", function(err) {
        // Get a list of breakpoints
        var breakpoints = emit("getBreakpoints");
        // Redo the state sync
        sync(breakpoints, true);
    }, plugin);
    
    // The error event is fired when the socket fails to connect
    socket.on("error", function(err) {
        emit("error", err);
    }, plugin);
    
    // Attach to the debugger in your own way
    attachToDebugger(socket, function(err) {
        if (err) return callback(err);
    
        // Reset the active frame
        emit("frameActivate", { frame: null });

        // Initialize the connected debugger
        sync(emit("getBreakpoints"), reconnect, callback);
    });
}

The example above portrays the basic flow your implementation of the attach method should follow. The UI depends on the error and frameActivate events to present the correct state to the user. For more information on the socket object click here.

The sync function is a placeholder for a function that fetches the state from the debugger. Check out an example implementation below.

function sync(breakpoints, reconnect, callback){
    // TODO: Fetch sources
    // TODO: Fetch frames (if on a break)
    // TODO: Sync breakpoints
    
    attached = true;
    emit("attach", { breakpoints: breakpoints });
    
    if (frames.length) {
        emit("frameActivate", { frame: frames[0] });
        emit("break", {
            frame: frames[0],
            frames: frames
        });
        emit("stateChange", { state: "stopped" });
    }
    else {
        emit("stateChange", { state: "running" });
    }
}

The sync() (or similar function) should fetch the current state from the debugger. This includes the sources, frames and breakpoints - after setting the list of breakpoints that are known to the UI. The attach, frameActivate, stateChange and break events direct the UI to render the state.

🚧

The 'v8' example debugger

The very first Cloud9 debugger is the v8 debugger and it still serves as an example to anyone implementing their own debugger. If you check out the v8 source code you'll find that the attach function calls out to its sync function which implements the init flow for v8.

detach

The detach() function gracefully disconnects from the debugger. You must emit the frameActivate, stateChange and detach events in order for the UI to update correctly.

function detach() {
    // Clean up the debugger connection
    detachFromDebugger();
    
    emit("frameActivate", { frame: null });
    emit("stateChange", { state: null });
    
    attached = false;
    emit("detach");
}

getSources

The getSources() function retrieves all the source files (paths) known to the debugger and returns them as an array of Source objects.

function getSources(callback) {
    getSourcesFromDebugger(function(err, scripts) {
        if (err) return callback(err);
    
        var sources = scripts.map(function(options){
            return createSource(options);
        });

        callback(null, sources);
        emit("sources", { sources: sources });
    });
}

The createSource() helper function used above returns a Source object.

function createSource(options) {
    return new Source({
        // etc
    });
}

getSource

The getSource() method fetches the source code of a source file. If your debugger doesn't support this feature it should simply call the callback with an error.

function getSource(source, callback) {
    getSourceFromDebugger(source, function(err, code) {
        callback(err, code);
    });
}

getFrames

The getFrames() function retrieves all the frames on the callstack from the debugger and returns them as an array of Frame objects.

function getFrames(callback, silent) {
    getFramesFromDebugger(function(err, items) {
        frames = items.map(function(options, index) {
            return createFrame(frame, index);
        });

        emit("getFrames", { frames: frames });
        callback(null, frames);
    });
}

The createFrame() helper function used above returns a Frame object.

function createFrame(options, index) {
    var frame = new Frame({
        istop: index == 0
        // etc
    });
}

getScope

The getScope() function retrieves all the variables in a frame from the debugger and returns them as an array of Variable objects.

function getScope(frame, scope, callback) {
    getScopeFromDebugger(scope.index, frame.index, function(err, properties) {
        if (err) return callback(err);
        
        var variables = properties.map(function(options) {
            return createVariable(options);
        });
        
        scope.variables = variables;
        callback(null, variables, scope, frame);
    });
}

The createVariable() helper function used above returns a Frame object.

function createVariable(options) {
    return new Variable({
        // etc
    });
}

getProperties

The getProperties() function retrieves all the properties of a variable from the debugger and returns them as an array of Variable objects. For each property that is of a basic data type, you must set the value (i.e. string, int, etc.). The UI will call this function recursively as a user clicks through the datagrid.

function getProperties(variable, callback) {
    getPropertiesFromDebugger(variable.ref, function(err, props) {
        if (err) return callback(err);
        
        if (props.length > 5000) {
            props = [new Variable({
                name: "Too many properties",
                type: "error", 
                value: "Found more than 5000 properties",
                children: false
            })];
            
            variable.properties = props;
            callback(null, props, variable);
            return;
        }
        
        var properties = props.map(function(prop) { 
            return createVariable(prop);
        });
        
        variable.properties = properties;
        callback(null, properties, variable);
    });
}

In the example above we only return 5000 or fewer properties. This is generally a good practice if there are multiple calls to the debugger needed to fetch the requested data - for instance the v8 debugger requires one call for each property to fetch its value.

stepInto

The stepInto() function steps into the current expression.

function stepInto(callback) {
    stepIntoDebugger(function(err){ callback(err); });
}

stepOver

The stepOver() function steps over the current expression.

function stepOver(callback) {
    stepOverDebugger(function(err){ callback(err); });
}

stepOut

The stepOut() function steps out of the current frame.

function stepOut(callback) {
    stepOutDebugger(function(err){ callback(err); });
}

resume

The resume() function continues execution.

function resume(callback) {
    resumeDebugger(function(err){ callback(err); });
}

suspend

The suspend() function pauses expression at the next possible expression.

function suspend(callback) {
    suspendDebugger(function(err){
        if (err) return callback(err);
        
        emit("suspend");
        callback();
    });
}

evaluate

The evaluate() function takes an expression and executes it in the global context or the context of a frame. If your debugger doesn't support evaluation of expressions simply return an error in the callback. This function is used amongst others by the immediate window.

function evaluate(expression, frame, global, disableBreak, callback) {
    var frameIndex = frame && typeof frame == "object" ? frame.index : frame;
    
    evaluateInDebugger(expression, frameIndex, global, disableBreak, 
      function(error, value, properties) {
        var name = expression.trim();
        if (error) {
            var err = new Error(error.message);
            err.name = name;
            err.stack = error.stack;
            return callback(err);
        }
        
        var variable = createVariable({
            name: name,
            value: value
        });
        if (properties)
            variable.properties = properties;
        
        callback(null, variable);
    });
}

setScriptSource

The setScriptSource() function updates the source of a file that is active in the running process. If your debugger doesn't support this simply call the callback with an error.

function setScriptSource(script, newSource, previewOnly, callback) {
    setScriptSourceInDebugger(script.id, newSource, previewOnly, function(err, data) {
        if (err)
            callback(new Error("Could not update source"));
        
        emit("setScriptSource", data);
        callback(null, data);
    });
};

setBreakpoint

The setBreakpoint() function adds a breakpoint to a line and column in a source file.

function setBreakpoint(bp, callback) {
    setBreakpointAtDebugger(bp, function(info) {
        if (isDifferentLocation(info, bp)) {
            updateBreakpoint(bp, info);
            emit("breakpointUpdate", { breakpoint: bp });
        }
        callback(null, bp, info);
    });
    return true;
}

changeBreakpoint

The changeBreakpoint() function updates a breakpoint such as toggling the enabled state, condition or ignoreCount. If your debugger implementation doesn't support updating a breakpoint simply remove the existing one and add a new one.

function changeBreakpoint(bp, callback) {
    changeBreakpointAtDebugger(function(err, data) {
        callback(err, bp, data);
    });
}

clearBreakpoint

The clearBreakpoint() function remove a breakpoint from a certain line and column in a source file.

function clearBreakpoint(bp, callback) {
    clearBreakpointAtDebugger(bp, function(err) {
        callback(err);
    });
}

listBreakpoints

The listBreakpoints() function fetches an array of breakpoints known by the debugger.

function listBreakpoints(callback) {
    listBreakpointsAtDebugger(function(err, breakpoints) {
        if (err) return callback(err);
        
        callback(null, breakpoints.map(function(bp) {
            return createBreakpoint(bp);
        }));
    });
}

setVariable

The setVariable() function sets the value of a variable in a certain frame. This is used by the watch expression and the variables debug panel. The parents argument is an array of variables that are the objects on which this variable is a property. For a local variable to the frame this array is empty, otherwise the 0th element will contain the reference to the object the variable is a property of.

function setVariable(variable, parents, value, frame, callback) {
    setVariableAtDebugger(variable, parents, value, frame, function(err, options){
        if (err) return callback(err)
        callback(null, createVariable(options));
    });
}

restartFrame

The restartFrame() function starts the frame at the first expression in that frame. If your debugger doesn't support this feature simply return an error in the callback.

function restartFrame(frame, callback) {
    var frameIndex = frame && typeof frame == "object" ? frame.index : frame;
    
    restartFrameAtDebugger(frameIndex, function(err, data) {
        callback(err, data);
    });
}

serializeVariable

The serializeVariable() function returns a serialized version of the variable. For a string this would be "value" for a regexp this might be /\w/ and for a function this might be the source of that function or function. You have total freedom in determining the serialized state of a variable. It is wise to choose a format that can be used to set the variable when using setVariable() if your debugger supports that.

function serializeVariable(variable, callback) {
    serializeVariableAtDebugger(variable, function(err, value) {
        callback(err, value);
    });
}

setBreakBehavior

The setBreakBehavior() function tells the debugger on which type of exceptions to break. The type argument can be set to all or uncaught.

function setBreakBehavior(type, enabled, callback) {
    breakOnExceptions = enabled ? type == "all" : false;
    breakOnUncaughtExceptions = enabled ? type == "uncaught" : false;
    
    setBreakBehavior(type, enabled, callback);
}

getProxySource

The getProxySource() function is used to fetch the source code of the proxy for your debugger implementation. You can fetch the default proxy source from debug.proxySource. You can find more information about the proxy below.

function getProxySource(process){
    return debug.proxySource
        .replace(/\/\/.*/g, "")
        .replace(/[\n\r]/g, "")
        .replace(/\{PORT\}/, process.runner[0].debugport);
}

Unimplemented methods

If your implementation doesn't support a feature of the public API, you must still implement the method but you can supply an error to the provided callback, as shown in an example below.

function getSource(source, callback) {
   callback(new Error("Debugger cannot return source code"));
}