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 Process | The 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 Process | A process that holds the connection to the debugger (and will survive browser reloads) |
Debug Socket | The 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 Implementation | This implementation mediates communication between the APIs of running debugger (or debug server) and the Debugger UI. |
Debugger UI | The UI for the debugger that the user interacts with. |
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.
attach | Required. Fires when the debugger is attached. |
---|---|
detach | Required. Fires when the debugger is detached. |
error | Required. Fires when the socket experiences an error |
break | Required. Fires when the debugger hits a breakpoint. |
exception | Fires when the debugger hits an exception. |
sourcesCompile | Fires when a source file is (re-)compiled. |
frameActivate | Required. Fires when a frame becomes active. |
setScriptSource | Fires when the source of a file is updated |
getFrames | Required. Fires when the result of the getFrames call comes in. |
sources | Fires when the result of the getSources call comes in. |
suspend | Required. Fires when execution is suspended (paused) |
getBreakpoints | Fires when the current list of breakpoints is needed |
breakpointUpdate | Fires when a breakpoint is updated. |
stateChange | Fires 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.
attach | Attaches the debugger to the started process. |
---|---|
detach | Detaches the debugger from the started process. |
getSources | Loads all the active sources from the process |
getSource | Retrieves the contents of a source file |
getFrames | Retrieves the current stack of frames (aka "the call stack") |
getScope | Retrieves the variables from a scope. |
getProperties | Retrieves and sets the properties of a variable. |
stepInto | Step into the next statement. |
stepOver | Step over the next statement. |
stepOut | Step out of the current statement. |
resume | Continues execution of a process after it has hit a breakpoint. |
suspend | Pauses the execution of a process at the next statement. |
evaluate | Evaluates an expression in a frame or in global space. |
setScriptSource | Change a live running source to the latest code state. |
setBreakpoint | Adds a breakpoint to a line in a source file. |
changeBreakpoint | Updates properties of a breakpoint. |
clearBreakpoint | Removes a breakpoint from a line in a source file. |
listBreakpoints | Retrieves a list of all the breakpoints that are set in the debugger. |
setVariable | Sets the value of a variable. |
restartFrame | Starts a frame (usually a function) from the first expression in that frame. |
serializeVariable | Retrieve the value of a variable. |
setBreakBehavior | Defines how the debugger deals with exceptions. |
getProxySource | Returns 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 theattach
function calls out to itssync
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"));
}
Updated less than a minute ago