michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const { Cu } = require("chrome"); michael@0: const { DebuggerServer } = require("devtools/server/main"); michael@0: const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); michael@0: michael@0: Cu.import("resource://gre/modules/jsdebugger.jsm"); michael@0: addDebuggerToGlobal(this); michael@0: michael@0: // TODO bug 943125: remove this polyfill and use Debugger.Frame.prototype.depth michael@0: // once it is implemented. michael@0: if (!Object.getOwnPropertyDescriptor(Debugger.Frame.prototype, "depth")) { michael@0: Debugger.Frame.prototype._depth = null; michael@0: Object.defineProperty(Debugger.Frame.prototype, "depth", { michael@0: get: function () { michael@0: if (this._depth === null) { michael@0: if (!this.older) { michael@0: this._depth = 0; michael@0: } else { michael@0: // Hide depth from self-hosted frames. michael@0: const increment = this.script && this.script.url == "self-hosted" michael@0: ? 0 michael@0: : 1; michael@0: this._depth = increment + this.older.depth; michael@0: } michael@0: } michael@0: michael@0: return this._depth; michael@0: } michael@0: }); michael@0: } michael@0: michael@0: const { setTimeout } = require("sdk/timers"); michael@0: michael@0: /** michael@0: * The number of milliseconds we should buffer frame enter/exit packets before michael@0: * sending. michael@0: */ michael@0: const BUFFER_SEND_DELAY = 50; michael@0: michael@0: /** michael@0: * The maximum number of arguments we will send for any single function call. michael@0: */ michael@0: const MAX_ARGUMENTS = 3; michael@0: michael@0: /** michael@0: * The maximum number of an object's properties we will serialize. michael@0: */ michael@0: const MAX_PROPERTIES = 3; michael@0: michael@0: /** michael@0: * The complete set of trace types supported. michael@0: */ michael@0: const TRACE_TYPES = new Set([ michael@0: "time", michael@0: "return", michael@0: "throw", michael@0: "yield", michael@0: "name", michael@0: "location", michael@0: "callsite", michael@0: "parameterNames", michael@0: "arguments", michael@0: "depth" michael@0: ]); michael@0: michael@0: /** michael@0: * Creates a TraceActor. TraceActor provides a stream of function michael@0: * call/return packets to a remote client gathering a full trace. michael@0: */ michael@0: function TraceActor(aConn, aParentActor) michael@0: { michael@0: this._attached = false; michael@0: this._activeTraces = new MapStack(); michael@0: this._totalTraces = 0; michael@0: this._startTime = 0; michael@0: michael@0: // Keep track of how many different trace requests have requested what kind of michael@0: // tracing info. This way we can minimize the amount of data we are collecting michael@0: // at any given time. michael@0: this._requestsForTraceType = Object.create(null); michael@0: for (let type of TRACE_TYPES) { michael@0: this._requestsForTraceType[type] = 0; michael@0: } michael@0: michael@0: this._sequence = 0; michael@0: this._bufferSendTimer = null; michael@0: this._buffer = []; michael@0: this.onExitFrame = this.onExitFrame.bind(this); michael@0: michael@0: // aParentActor.window might be an Xray for a window, but it might also be a michael@0: // double-wrapper for a Sandbox. We want to unwrap the latter but not the michael@0: // former. michael@0: this.global = aParentActor.window; michael@0: if (!Cu.isXrayWrapper(this.global)) { michael@0: this.global = this.global.wrappedJSObject; michael@0: } michael@0: } michael@0: michael@0: TraceActor.prototype = { michael@0: actorPrefix: "trace", michael@0: michael@0: get attached() { return this._attached; }, michael@0: get idle() { return this._attached && this._activeTraces.size === 0; }, michael@0: get tracing() { return this._attached && this._activeTraces.size > 0; }, michael@0: michael@0: /** michael@0: * Buffer traces and only send them every BUFFER_SEND_DELAY milliseconds. michael@0: */ michael@0: _send: function(aPacket) { michael@0: this._buffer.push(aPacket); michael@0: if (this._bufferSendTimer === null) { michael@0: this._bufferSendTimer = setTimeout(() => { michael@0: this.conn.send({ michael@0: from: this.actorID, michael@0: type: "traces", michael@0: traces: this._buffer.splice(0, this._buffer.length) michael@0: }); michael@0: this._bufferSendTimer = null; michael@0: }, BUFFER_SEND_DELAY); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Initializes a Debugger instance and adds listeners to it. michael@0: */ michael@0: _initDebugger: function() { michael@0: this.dbg = new Debugger(); michael@0: this.dbg.onEnterFrame = this.onEnterFrame.bind(this); michael@0: this.dbg.onNewGlobalObject = this.globalManager.onNewGlobal.bind(this); michael@0: this.dbg.enabled = false; michael@0: }, michael@0: michael@0: /** michael@0: * Add a debuggee global to the Debugger object. michael@0: */ michael@0: _addDebuggee: function(aGlobal) { michael@0: try { michael@0: this.dbg.addDebuggee(aGlobal); michael@0: } catch (e) { michael@0: // Ignore attempts to add the debugger's compartment as a debuggee. michael@0: DevToolsUtils.reportException("TraceActor", michael@0: new Error("Ignoring request to add the debugger's " michael@0: + "compartment as a debuggee")); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add the provided window and all windows in its frame tree as debuggees. michael@0: */ michael@0: _addDebuggees: function(aWindow) { michael@0: this._addDebuggee(aWindow); michael@0: let frames = aWindow.frames; michael@0: if (frames) { michael@0: for (let i = 0; i < frames.length; i++) { michael@0: this._addDebuggees(frames[i]); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * An object used by TraceActors to tailor their behavior depending michael@0: * on the debugging context required (chrome or content). michael@0: */ michael@0: globalManager: { michael@0: /** michael@0: * Adds all globals in the global object as debuggees. michael@0: */ michael@0: findGlobals: function() { michael@0: this._addDebuggees(this.global); michael@0: }, michael@0: michael@0: /** michael@0: * A function that the engine calls when a new global object has been michael@0: * created. Adds the global object as a debuggee if it is in the content michael@0: * window. michael@0: * michael@0: * @param aGlobal Debugger.Object michael@0: * The new global object that was created. michael@0: */ michael@0: onNewGlobal: function(aGlobal) { michael@0: // Content debugging only cares about new globals in the content michael@0: // window, like iframe children. michael@0: if (aGlobal.hostAnnotations && michael@0: aGlobal.hostAnnotations.type == "document" && michael@0: aGlobal.hostAnnotations.element === this.global) { michael@0: this._addDebuggee(aGlobal); michael@0: } michael@0: }, michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to attach to the trace actor. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onAttach: function(aRequest) { michael@0: if (this.attached) { michael@0: return { michael@0: error: "wrongState", michael@0: message: "Already attached to a client" michael@0: }; michael@0: } michael@0: michael@0: if (!this.dbg) { michael@0: this._initDebugger(); michael@0: this.globalManager.findGlobals.call(this); michael@0: } michael@0: michael@0: this._attached = true; michael@0: michael@0: return { michael@0: type: "attached", michael@0: traceTypes: Object.keys(this._requestsForTraceType) michael@0: .filter(k => !!this._requestsForTraceType[k]) michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to detach from the trace actor. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onDetach: function() { michael@0: while (this.tracing) { michael@0: this.onStopTrace(); michael@0: } michael@0: michael@0: this.dbg = null; michael@0: michael@0: this._attached = false; michael@0: return { type: "detached" }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to start a new trace. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onStartTrace: function(aRequest) { michael@0: for (let traceType of aRequest.trace) { michael@0: if (!TRACE_TYPES.has(traceType)) { michael@0: return { michael@0: error: "badParameterType", michael@0: message: "No such trace type: " + traceType michael@0: }; michael@0: } michael@0: } michael@0: michael@0: if (this.idle) { michael@0: this.dbg.enabled = true; michael@0: this._sequence = 0; michael@0: this._startTime = Date.now(); michael@0: } michael@0: michael@0: // Start recording all requested trace types. michael@0: for (let traceType of aRequest.trace) { michael@0: this._requestsForTraceType[traceType]++; michael@0: } michael@0: michael@0: this._totalTraces++; michael@0: let name = aRequest.name || "Trace " + this._totalTraces; michael@0: this._activeTraces.push(name, aRequest.trace); michael@0: michael@0: return { type: "startedTrace", why: "requested", name: name }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to end a trace. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onStopTrace: function(aRequest) { michael@0: if (!this.tracing) { michael@0: return { michael@0: error: "wrongState", michael@0: message: "No active traces" michael@0: }; michael@0: } michael@0: michael@0: let stoppedTraceTypes, name; michael@0: if (aRequest && aRequest.name) { michael@0: name = aRequest.name; michael@0: if (!this._activeTraces.has(name)) { michael@0: return { michael@0: error: "noSuchTrace", michael@0: message: "No active trace with name: " + name michael@0: }; michael@0: } michael@0: stoppedTraceTypes = this._activeTraces.delete(name); michael@0: } else { michael@0: name = this._activeTraces.peekKey(); michael@0: stoppedTraceTypes = this._activeTraces.pop(); michael@0: } michael@0: michael@0: for (let traceType of stoppedTraceTypes) { michael@0: this._requestsForTraceType[traceType]--; michael@0: } michael@0: michael@0: if (this.idle) { michael@0: this.dbg.enabled = false; michael@0: } michael@0: michael@0: return { type: "stoppedTrace", why: "requested", name: name }; michael@0: }, michael@0: michael@0: // JS Debugger API hooks. michael@0: michael@0: /** michael@0: * Called by the engine when a frame is entered. Sends an unsolicited packet michael@0: * to the client carrying requested trace information. michael@0: * michael@0: * @param aFrame Debugger.frame michael@0: * The stack frame that was entered. michael@0: */ michael@0: onEnterFrame: function(aFrame) { michael@0: if (aFrame.script && aFrame.script.url == "self-hosted") { michael@0: return; michael@0: } michael@0: michael@0: let packet = { michael@0: type: "enteredFrame", michael@0: sequence: this._sequence++ michael@0: }; michael@0: michael@0: if (this._requestsForTraceType.name) { michael@0: packet.name = aFrame.callee michael@0: ? aFrame.callee.displayName || "(anonymous function)" michael@0: : "(" + aFrame.type + ")"; michael@0: } michael@0: michael@0: if (this._requestsForTraceType.location && aFrame.script) { michael@0: // We should return the location of the start of the script, but michael@0: // Debugger.Script does not provide complete start locations (bug michael@0: // 901138). Instead, return the current offset (the location of the first michael@0: // statement in the function). michael@0: packet.location = { michael@0: url: aFrame.script.url, michael@0: line: aFrame.script.getOffsetLine(aFrame.offset), michael@0: column: getOffsetColumn(aFrame.offset, aFrame.script) michael@0: }; michael@0: } michael@0: michael@0: if (this._requestsForTraceType.callsite michael@0: && aFrame.older michael@0: && aFrame.older.script) { michael@0: let older = aFrame.older; michael@0: packet.callsite = { michael@0: url: older.script.url, michael@0: line: older.script.getOffsetLine(older.offset), michael@0: column: getOffsetColumn(older.offset, older.script) michael@0: }; michael@0: } michael@0: michael@0: if (this._requestsForTraceType.time) { michael@0: packet.time = Date.now() - this._startTime; michael@0: } michael@0: michael@0: if (this._requestsForTraceType.parameterNames && aFrame.callee) { michael@0: packet.parameterNames = aFrame.callee.parameterNames; michael@0: } michael@0: michael@0: if (this._requestsForTraceType.arguments && aFrame.arguments) { michael@0: packet.arguments = []; michael@0: let i = 0; michael@0: for (let arg of aFrame.arguments) { michael@0: if (i++ > MAX_ARGUMENTS) { michael@0: break; michael@0: } michael@0: packet.arguments.push(createValueSnapshot(arg, true)); michael@0: } michael@0: } michael@0: michael@0: if (this._requestsForTraceType.depth) { michael@0: packet.depth = aFrame.depth; michael@0: } michael@0: michael@0: const onExitFrame = this.onExitFrame; michael@0: aFrame.onPop = function (aCompletion) { michael@0: onExitFrame(this, aCompletion); michael@0: }; michael@0: michael@0: this._send(packet); michael@0: }, michael@0: michael@0: /** michael@0: * Called by the engine when a frame is exited. Sends an unsolicited packet to michael@0: * the client carrying requested trace information. michael@0: * michael@0: * @param Debugger.Frame aFrame michael@0: * The Debugger.Frame that was just exited. michael@0: * @param aCompletion object michael@0: * The debugger completion value for the frame. michael@0: */ michael@0: onExitFrame: function(aFrame, aCompletion) { michael@0: let packet = { michael@0: type: "exitedFrame", michael@0: sequence: this._sequence++, michael@0: }; michael@0: michael@0: if (!aCompletion) { michael@0: packet.why = "terminated"; michael@0: } else if (aCompletion.hasOwnProperty("return")) { michael@0: packet.why = "return"; michael@0: } else if (aCompletion.hasOwnProperty("yield")) { michael@0: packet.why = "yield"; michael@0: } else { michael@0: packet.why = "throw"; michael@0: } michael@0: michael@0: if (this._requestsForTraceType.time) { michael@0: packet.time = Date.now() - this._startTime; michael@0: } michael@0: michael@0: if (this._requestsForTraceType.depth) { michael@0: packet.depth = aFrame.depth; michael@0: } michael@0: michael@0: if (aCompletion) { michael@0: if (this._requestsForTraceType.return && "return" in aCompletion) { michael@0: packet.return = createValueSnapshot(aCompletion.return, true); michael@0: } michael@0: michael@0: else if (this._requestsForTraceType.throw && "throw" in aCompletion) { michael@0: packet.throw = createValueSnapshot(aCompletion.throw, true); michael@0: } michael@0: michael@0: else if (this._requestsForTraceType.yield && "yield" in aCompletion) { michael@0: packet.yield = createValueSnapshot(aCompletion.yield, true); michael@0: } michael@0: } michael@0: michael@0: this._send(packet); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The request types this actor can handle. michael@0: */ michael@0: TraceActor.prototype.requestTypes = { michael@0: "attach": TraceActor.prototype.onAttach, michael@0: "detach": TraceActor.prototype.onDetach, michael@0: "startTrace": TraceActor.prototype.onStartTrace, michael@0: "stopTrace": TraceActor.prototype.onStopTrace michael@0: }; michael@0: michael@0: exports.register = function(handle) { michael@0: handle.addTabActor(TraceActor, "traceActor"); michael@0: }; michael@0: michael@0: exports.unregister = function(handle) { michael@0: handle.removeTabActor(TraceActor, "traceActor"); michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * MapStack is a collection of key/value pairs with stack ordering, michael@0: * where keys are strings and values are any JS value. In addition to michael@0: * the push and pop stack operations, supports a "delete" operation, michael@0: * which removes the value associated with a given key from any michael@0: * location in the stack. michael@0: */ michael@0: function MapStack() michael@0: { michael@0: // Essentially a MapStack is just sugar-coating around a standard JS michael@0: // object, plus the _stack array to track ordering. michael@0: this._stack = []; michael@0: this._map = Object.create(null); michael@0: } michael@0: michael@0: MapStack.prototype = { michael@0: get size() { return this._stack.length; }, michael@0: michael@0: /** michael@0: * Return the key for the value on the top of the stack, or michael@0: * undefined if the stack is empty. michael@0: */ michael@0: peekKey: function() { michael@0: return this._stack[this.size - 1]; michael@0: }, michael@0: michael@0: /** michael@0: * Return true iff a value has been associated with the given key. michael@0: * michael@0: * @param aKey string michael@0: * The key whose presence is to be tested. michael@0: */ michael@0: has: function(aKey) { michael@0: return Object.prototype.hasOwnProperty.call(this._map, aKey); michael@0: }, michael@0: michael@0: /** michael@0: * Return the value associated with the given key, or undefined if michael@0: * no value is associated with the key. michael@0: * michael@0: * @param aKey string michael@0: * The key whose associated value is to be returned. michael@0: */ michael@0: get: function(aKey) { michael@0: return this._map[aKey]; michael@0: }, michael@0: michael@0: /** michael@0: * Push a new value onto the stack. If another value with the same michael@0: * key is already on the stack, it will be removed before the new michael@0: * value is pushed onto the top of the stack. michael@0: * michael@0: * @param aKey string michael@0: * The key of the object to push onto the stack. michael@0: * michael@0: * @param aValue michael@0: * The value to push onto the stack. michael@0: */ michael@0: push: function(aKey, aValue) { michael@0: this.delete(aKey); michael@0: this._stack.push(aKey); michael@0: this._map[aKey] = aValue; michael@0: }, michael@0: michael@0: /** michael@0: * Remove the value from the top of the stack and return it. michael@0: * Returns undefined if the stack is empty. michael@0: */ michael@0: pop: function() { michael@0: let key = this.peekKey(); michael@0: let value = this.get(key); michael@0: this._stack.pop(); michael@0: delete this._map[key]; michael@0: return value; michael@0: }, michael@0: michael@0: /** michael@0: * Remove the value associated with the given key from the stack and michael@0: * return it. Returns undefined if no value is associated with the michael@0: * given key. michael@0: * michael@0: * @param aKey string michael@0: * The key for the value to remove from the stack. michael@0: */ michael@0: delete: function(aKey) { michael@0: let value = this.get(aKey); michael@0: if (this.has(aKey)) { michael@0: let keyIndex = this._stack.lastIndexOf(aKey); michael@0: this._stack.splice(keyIndex, 1); michael@0: delete this._map[aKey]; michael@0: } michael@0: return value; michael@0: } michael@0: }; michael@0: michael@0: // TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when michael@0: // it is implemented. michael@0: function getOffsetColumn(aOffset, aScript) { michael@0: return 0; michael@0: } michael@0: michael@0: // Serialization helper functions. Largely copied from script.js and modified michael@0: // for use in serialization rather than object actor requests. michael@0: michael@0: /** michael@0: * Create a grip for the given debuggee value. michael@0: * michael@0: * @param aValue Debugger.Object|primitive michael@0: * The value to describe with the created grip. michael@0: * michael@0: * @param aDetailed boolean michael@0: * If true, capture slightly more detailed information, like some michael@0: * properties on an object. michael@0: * michael@0: * @return Object michael@0: * A primitive value or a snapshot of an object. michael@0: */ michael@0: function createValueSnapshot(aValue, aDetailed=false) { michael@0: switch (typeof aValue) { michael@0: case "boolean": michael@0: return aValue; michael@0: case "string": michael@0: if (aValue.length >= DebuggerServer.LONG_STRING_LENGTH) { michael@0: return { michael@0: type: "longString", michael@0: initial: aValue.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), michael@0: length: aValue.length michael@0: }; michael@0: } michael@0: return aValue; michael@0: case "number": michael@0: if (aValue === Infinity) { michael@0: return { type: "Infinity" }; michael@0: } else if (aValue === -Infinity) { michael@0: return { type: "-Infinity" }; michael@0: } else if (Number.isNaN(aValue)) { michael@0: return { type: "NaN" }; michael@0: } else if (!aValue && 1 / aValue === -Infinity) { michael@0: return { type: "-0" }; michael@0: } michael@0: return aValue; michael@0: case "undefined": michael@0: return { type: "undefined" }; michael@0: case "object": michael@0: if (aValue === null) { michael@0: return { type: "null" }; michael@0: } michael@0: return aDetailed michael@0: ? detailedObjectSnapshot(aValue) michael@0: : objectSnapshot(aValue); michael@0: default: michael@0: DevToolsUtils.reportException("TraceActor", michael@0: new Error("Failed to provide a grip for: " + aValue)); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Create a very minimal snapshot of the given debuggee object. michael@0: * michael@0: * @param aObject Debugger.Object michael@0: * The object to describe with the created grip. michael@0: */ michael@0: function objectSnapshot(aObject) { michael@0: return { michael@0: "type": "object", michael@0: "class": aObject.class, michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Create a (slightly more) detailed snapshot of the given debuggee object. michael@0: * michael@0: * @param aObject Debugger.Object michael@0: * The object to describe with the created descriptor. michael@0: */ michael@0: function detailedObjectSnapshot(aObject) { michael@0: let desc = objectSnapshot(aObject); michael@0: let ownProperties = desc.ownProperties = Object.create(null); michael@0: michael@0: if (aObject.class == "DeadObject") { michael@0: return desc; michael@0: } michael@0: michael@0: let i = 0; michael@0: for (let name of aObject.getOwnPropertyNames()) { michael@0: if (i++ > MAX_PROPERTIES) { michael@0: break; michael@0: } michael@0: let desc = propertySnapshot(name, aObject); michael@0: if (desc) { michael@0: ownProperties[name] = desc; michael@0: } michael@0: } michael@0: michael@0: return desc; michael@0: } michael@0: michael@0: /** michael@0: * A helper method that creates a snapshot of the object's |aName| property. michael@0: * michael@0: * @param aName string michael@0: * The property of which the snapshot is taken. michael@0: * michael@0: * @param aObject Debugger.Object michael@0: * The object whose property the snapshot is taken of. michael@0: * michael@0: * @return Object michael@0: * The snapshot of the property. michael@0: */ michael@0: function propertySnapshot(aName, aObject) { michael@0: let desc; michael@0: try { michael@0: desc = aObject.getOwnPropertyDescriptor(aName); michael@0: } catch (e) { michael@0: // Calling getOwnPropertyDescriptor on wrapped native prototypes is not michael@0: // allowed (bug 560072). Inform the user with a bogus, but hopefully michael@0: // explanatory, descriptor. michael@0: return { michael@0: configurable: false, michael@0: writable: false, michael@0: enumerable: false, michael@0: value: e.name michael@0: }; michael@0: } michael@0: michael@0: // Only create descriptors for simple values. We skip objects and properties michael@0: // that have getters and setters; ain't nobody got time for that! michael@0: if (!desc michael@0: || typeof desc.value == "object" && desc.value !== null michael@0: || !("value" in desc)) { michael@0: return undefined; michael@0: } michael@0: michael@0: return { michael@0: configurable: desc.configurable, michael@0: enumerable: desc.enumerable, michael@0: writable: desc.writable, michael@0: value: createValueSnapshot(desc.value) michael@0: }; michael@0: }