define(function(require, exports, module) { "use strict"; module.exports = DbgpSession; var inherits = require("util").inherits; var Stream = require("stream").Stream; var xmlToObject = require("./util").xmlToObject; var base64Decode = require("./util").base64Decode; function noop() {} function toArray(value) { if (value == null) return []; if (!Array.isArray(value)) return [ value ]; else return value; } /** * @class debugger.xdebug.DbgpSession * * The `DbgpSession` handles communication between the IDE and the debugger * engine. It is created when a new connection is established and destroyed * when the debugger engine stops. * * @constructor */ function DbgpSession() { Stream.call(this); this.writable = true; this.status = "starting"; this.initialized = false; this._seq = 0; this._callbacks = {}; var session = this; this.on("stopping", function() { session.end(); }); } inherits(DbgpSession, Stream); /** * @event init * Emitted when the init handshake is complete and the session is ready to send * and receive commands. */ /** * @event end * Emitted when the debugging session ends and the IDE cannot continue sending * commands. This happens when the debugger engine stops, the IDE detaches, or * the connection to the engine is lost. */ /** * @event error * Emitted when an unexpected error occurs. * * @param {Error} err */ // message handling /////////////////////////////////////////////////////////// /** * Send a raw command with the given arguments and payload data to the debugger * engine. * * @param {string} command The command name. * @param {Object=} args A hash of arguments. * @param {*=} data The payload data. * * @return {number} The sequence number (`transaction_id`) of the sent * command. */ DbgpSession.prototype.sendCommand = function(command, args, data, callback) { var seq = this._seq++; this.emit("data", { seq: seq, command: command, args: args, data: data }); this._callbacks["" + seq] = callback; return seq; }; DbgpSession.prototype.write = function(xml) { var type = Object.keys(xml)[0]; switch (type) { case "init": return this._handleInit(xml[type]); case "response": return this._handleResponse(xml[type]); default: this.emit("error", new Error("Unhandled message type: " + type)); } }; DbgpSession.prototype.end = function() { this.emit("end"); }; DbgpSession.prototype._handleInit = function(init) { this.protocolVersion = init["@protocol_version"]; this.appId = init["@appid"]; this.ideKey = init["@idekey"]; this.sessionId = init["@session"]; this.threadId = init["@thread"]; this.parentAppId = init["@parent"]; this.language = init["@language"]; this.fileURI = init["@fileuri"]; this.engine = { name: init.engine && init.engine["$"], version: init.engine && init.engine["@version"], info: {}, }; for (var key in init) { if (key[0] === "@" || key === "engine") continue; this.engine.info[key] = init[key]; } var _self = this; // verify connection this.getStatus(function(err) { if (err) { _self.emit("error", err); return; } _self.emit("init"); }); this.initialized = true; }; DbgpSession.prototype._handleResponse = function(response) { // command response var command = response["@command"]; var seq = response["@transaction_id"]; if (command) { this._handleCommandResponse(command, seq, response); } // status var status = response["@status"]; var reason = response["@reason"]; if (status) { this._handleStatus(status); } }; DbgpSession.prototype._handleCommandResponse = function(command, seq, response) { var callback = this._callbacks[seq]; delete this._callbacks[seq]; // error if (response.error) { var errorCode = response.error["@code"]; var errorMessage = response.error.message; var err = new Error(errorMessage || "Debugger error"); err.code = errorCode; callback && callback(err, {}); return; } // success callback && callback(null, response); }; DbgpSession.prototype._handleStatus = function(status) { if (this.status !== status) { this.status = status; this.emit("status", status); this.emit(status); } }; // misc /////////////////////////////////////////////////////////////////////// /** @method getStatus */ DbgpSession.prototype.getStatus = function(callback) { callback = callback || noop; this.sendCommand("status", null, null, callback); }; /** @method eval */ DbgpSession.prototype.eval = function(script, callback) { callback = callback || noop; var _self = this; var params = { l: script.length }; _self.sendCommand("eval", params, script, function(err, response) { callback(err, response.property); }); }; /** * Get the content of the given source file. * * @param {string} fileURI The file URI of the source. * @param {Function} callback * @param {Error=} callback.err * @param {string} callback.source The content of the source. */ DbgpSession.prototype.getSource = function(fileURI, callback) { callback = callback || noop; var _self = this; var params = { f: fileURI }; _self.sendCommand("source", params, null, function(err, response) { if (err) return callback(err); var source = response["$"]; if (response["@encoding"] === "base64") { source = base64Decode(source); } callback(null, source); }); }; // continuation //////////////////////////////////////////////////////////////// DbgpSession.prototype.run = function(callback) { this._continue("run", callback); }; DbgpSession.prototype.stepInto = function(callback) { this._continue("step_into", callback); }; DbgpSession.prototype.stepOver = function(callback) { this._continue("step_over", callback); }; DbgpSession.prototype.stepOut = function(callback) { this._continue("step_out", callback); }; DbgpSession.prototype.stop = function(callback) { this._continue("stop", callback); }; DbgpSession.prototype._continue = function(command, callback) { callback = callback || noop; this._handleStatus("running"); this.sendCommand(command, null, null, noop); callback(); }; // feature negotiation ///////////////////////////////////////////////////////// /** * The feature commands are used to request feature support from the debugger * engine. * * @param {string} feature The name of the feature flag. * * @param {Function} callback * @param {Error=} callback.err * @param {*} callback.value The current value of the feature flag. */ DbgpSession.prototype.getFeature = function(feature, callback) { callback = callback || noop; var _self = this; var params = { n: feature }; _self.sendCommand("feature_get", params, null, function(err, response) { if (err) return callback(err); if (response["@supported"] !== 1) return callback(new Error("No support for debugger feature: " + feature)); callback(null, response["$"]); }); }; /** * The feature set command allows a IDE to tell the debugger engine what * additional capabilities it has. * * @param {string} feature The name of the feature flag. * @param {*} value The new value of the feature flag. * * @param {Function} callback * @param {Error=} callback.err */ DbgpSession.prototype.setFeature = function(feature, value, callback) { callback = callback || noop; var _self = this; var params = { n: feature, v: value }; _self.sendCommand("feature_set", params, null, function(err, response) { if (err) return callback(err); if (response["@success"] !== 1) return callback(new Error("Could not set debugger feature: " + feature)); callback(); }); }; // stacks ////////////////////////////////////////////////////////////////////// /** * Returns stack information for a given stack depth. * * If the stack depth is specified, only one stack element is returned, for the * depth requested, though child elements may be returned also. The current * context is stack depth of zero, the 'oldest' context (in some languages * known as 'main') is the highest numbered context. * * @param {number} stackDepth The depth to retrieve. * * @param {Function} callback * @param {Error=} callback.err * @param {Object[]} callback.frames */ DbgpSession.prototype.getStackFrames = function(stackDepth, callback) { callback = callback || noop; var _self = this; var params = { d: stackDepth }; _self.sendCommand("stack_get", params, null, function(err, response) { callback(err, toArray(response.stack)); }); }; // contexts //////////////////////////////////////////////////////////////////// /** * Returns an array of properties in a given context at a given stack depth. * * If the stack depth is omitted, the current stack depth is used. If the * context name is omitted, the context with an id zero is used (generally the * 'locals' context). * * @param {number} stackDepth The depth to retrieve. * @param {number} contextId The context to retrieve. * * @param {Function} callback * @param {Error=} callback.err * @param {Object[]} callback.properties */ DbgpSession.prototype.getContextProperties = function(stackDepth, contextId, callback) { callback = callback || noop; var _self = this; var params = { d: stackDepth, c: contextId }; _self.setFeature("max_depth", 0, function() { _self.sendCommand("context_get", params, null, function(err, response) { callback(err, toArray(response.property)); }); }); }; // properties ////////////////////////////////////////////////////////////////// /** * Get the attributes and value of the given property. * * @param {string} propertyName The long name of the property, as given by the debugger engine. * @param {number} stackDepth The depth of the stack the property exists in. * @param {number} contextId The context the property exists in. * * @param {Function} callback * @param {Error=} callback.err * @param {Object} callback.property The property object. * @param {*} callback.property.$ The property value. */ DbgpSession.prototype.getProperty = function(propertyName, stackDepth, contextId, callback) { callback = callback || noop; var _self = this; var params = { d: stackDepth, c: contextId, n: propertyName }; _self.setFeature("max_depth", 0, function() { _self.sendCommand("property_get", params, null, function(err, response) { if (err) return callback(err); var property = response.property; _self.sendCommand("property_value", params, null, function(err, response) { property["$"] = response["$"]; callback(err, property); }); }); }); }; /** * Set the value of the given property. * * @param {string} propertyName The long name of the property, as given by the debugger engine. * @param {number} stackDepth The depth of the stack the property exists in. * @param {number} contextId The context the property exists in. * @param {*} value The new property value. * * @param {Function} callback * @param {Error=} callback.err * @param {Object} callback.property The property object. */ DbgpSession.prototype.setPropertyValue = function(propertyName, stackDepth, contextId, value, callback) { callback = callback || noop; var _self = this; var params = { d: stackDepth, c: contextId, n: propertyName }; _self.sendCommand("property_set", params, value, function(err, response) { if (err) return callback(err); if (response["@success"] !== 1) return callback(new Error("Could not set property value")); callback(); }); }; /** * Get the child properties of the given property. * * @param {string} propertyName The long name of the property, as given by the debugger engine. * @param {number} stackDepth The depth of the stack the property exists in. * @param {number} contextId The context the property exists in. * * @param {Function} callback * @param {Error=} callback.err * @param {Object[]} callback.properties The child properties. */ DbgpSession.prototype.getPropertyChildren = function(propertyName, stackDepth, contextId, callback) { callback = callback || noop; var _self = this; var params = { d: stackDepth, c: contextId, n: propertyName }; _self.setFeature("max_depth", 1, function() { _self.sendCommand("property_get", params, null, function(err, response) { callback(err, toArray(response.property.property)); }); }); }; // breakpoints ///////////////////////////////////////////////////////////////// /** * Set a breakpoint in the given file with the given options. * * To allow a breakpoint's condition to be updated using `updateBreakpoint()`, * all breakpoints are set as the `conditional` type. If no break condition is * defined in `options.condition`, the condition is set to `true` so it always * triggers. * * @param {string} fileURI The file URI of the source. * * @param {Object} options * @param {number} options.line The line number to break on, starting from line 1. * @param {boolean=} [options.enabled=true] If the breakpoint is active or not. * @param {string=} options.condition Only break if this expression evaluates * to `true`. * @param {number=} options.ignoreCount Only break after the breakpoint * triggered at least _n_ times. * * @param {Function} callback * @param {Error=} callback.err * @param {string} callback.breakpointId An arbitrary string that uniquely * identifies this breakpoint in the debugger engine. */ DbgpSession.prototype.setBreakpoint = function(fileURI, options, callback) { callback = callback || noop; var _self = this; var params = { t: "conditional", s: options.enabled !== false ? "enabled" : "disabled", f: fileURI, n: options.line, h: options.ignoreCount }; var condition = options.condition || "true"; _self.sendCommand("breakpoint_set", params, condition, function(err, response) { callback(err, response["@id"]); }); }; /** * Update the given breakpoint with attributes with the given options. * * @param {string} breakpointId The unique identifier given by * {@link #method-setBreakpoint setBreakpoint()}. * @param {Object} options * @param {number=} options.line The line number to break on, starting from line 1. * @param {boolean=} options.enabled If the breakpoint is active or not. * @param {string=} options.condition Only break if this expression evaluates * to `true`. * @param {number=} options.ignoreCount Only break after the breakpoint * triggered at least _n_ times. * * @param {Function} callback * @param {Error=} callback.err */ DbgpSession.prototype.updateBreakpoint = function(breakpointId, options, callback) { callback = callback || noop; var _self = this; var params = { d: breakpointId }; if (options.line != null) { params.n = options.line; } if (options.enabled != null) { params.s = options.enabled !== false ? "enabled" : "disabled"; } if (options.ignoreCount != null) { params.h = options.ignoreCount; } var condition; if (options.condition != null) { condition = options.condition; } _self.sendCommand("breakpoint_update", params, condition, function(err, response) { callback(err); }); }; /** * Remove the given breakpoint. * * @param {string} breakpointId The unique identifier given by * {@link #method-setBreakpoint setBreakpoint()}. * * @param {Function} callback * @param {Error=} callback.err */ DbgpSession.prototype.removeBreakpoint = function(breakpointId, callback) { callback = callback || noop; var _self = this; var params = { d: breakpointId }; _self.sendCommand("breakpoint_remove", params, null, function(err, response) { callback(err); }); }; });