/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is * Mozilla Foundation * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Dave Camp * Panos Astithas * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ "use strict"; /** * JSD2 actors. */ /** * Creates a ThreadActor. * * ThreadActors manage a JSInspector object and manage execution/inspection * of debuggees. * * @param aHooks object * An object with preNest and postNest methods for calling when entering * and exiting a nested event loop. */ function ThreadActor(aHooks) { this._state = "detached"; this._frameActors = []; this._environmentActors = []; this._hooks = aHooks ? aHooks : {}; } ThreadActor.prototype = { actorPrefix: "context", get state() { return this._state; }, get dbg() { return this._dbg; }, get threadLifetimePool() { if (!this._threadLifetimePool) { this._threadLifetimePool = new ActorPool(this.conn); this.conn.addActorPool(this._threadLifetimePool); } return this._threadLifetimePool; }, _breakpointPool: null, get breakpointActorPool() { if (!this._breakpointPool) { this._breakpointPool = new ActorPool(this.conn); this.conn.addActorPool(this._breakpointPool); } return this._breakpointPool; }, _scripts: {}, /** * Add a debuggee global to the JSInspector. */ addDebuggee: function TA_addDebuggee(aGlobal) { // Use the inspector xpcom component to turn on debugging // for aGlobal's compartment. Ideally this won't be necessary // medium- to long-term, and will be managed by the engine // instead. if (!this._dbg) { this._dbg = new Debugger(); } this.dbg.addDebuggee(aGlobal); this.dbg.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this); this.dbg.onDebuggerStatement = this.onDebuggerStatement.bind(this); this.dbg.onNewScript = this.onNewScript.bind(this); // Keep the debugger disabled until a client attaches. this.dbg.enabled = false; }, /** * Remove a debuggee global from the JSInspector. */ removeDebugee: function TA_removeDebuggee(aGlobal) { try { this.dbg.removeDebuggee(aGlobal); } catch(ex) { // XXX: This debuggee has code currently executing on the stack, // we need to save this for later. } }, disconnect: function TA_disconnect() { if (this._state == "paused") { this.onResume(); } this._state = "exited"; if (this.dbg) { this.dbg.enabled = false; this._dbg = null; } this.conn.removeActorPool(this._threadLifetimePool || undefined); this._threadLifetimePool = null; this.conn.removeActorPool(this._breakpointPool); this._breakpointPool = null; for (let url in this._scripts) { delete this._scripts[url]; } this._scripts = {}; }, /** * Disconnect the debugger and put the actor in the exited state. */ exit: function TA_exit() { this.disconnect(); }, // Request handlers onAttach: function TA_onAttach(aRequest) { if (this.state === "exited") { return { type: "exited" }; } if (this.state !== "detached") { return { error: "wrongState" }; } this._state = "attached"; this.dbg.enabled = true; try { // Put ourselves in the paused state. // XXX: We need to put the debuggee in a paused state too. let packet = this._paused(); if (!packet) { return { error: "notAttached" }; } packet.why = { type: "attached" }; // Send the response to the attach request now (rather than // returning it), because we're going to start a nested event loop // here. this.conn.send(packet); // Start a nested event loop. this._nest(); // We already sent a response to this request, don't send one // now. return null; } catch(e) { Cu.reportError(e); return { error: "notAttached", message: e.toString() }; } }, onDetach: function TA_onDetach(aRequest) { this.disconnect(); return { type: "detached" }; }, /** * Pause the debuggee, by entering a nested event loop, and return a 'paused' * packet to the client. * * @param Debugger.Frame aFrame * The newest debuggee frame in the stack. * @param object aReason * An object with a 'type' property containing the reason for the pause. */ _pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason) { try { let packet = this._paused(aFrame); if (!packet) { return undefined; } packet.why = aReason; this.conn.send(packet); return this._nest(); } catch(e) { Cu.reportError("Got an exception during TA__pauseAndRespond: " + e + ": " + e.stack); return undefined; } }, /** * Handle a protocol request to resume execution of the debuggee. */ onResume: function TA_onResume(aRequest) { if (aRequest && aRequest.forceCompletion) { // TODO: remove this when Debugger.Frame.prototype.pop is implemented in // bug 736733. if (typeof this.frame.pop != "function") { return { error: "notImplemented", message: "forced completion is not yet implemented." }; } this.dbg.getNewestFrame().pop(aRequest.completionValue); let packet = this._resumed(); DebuggerServer.xpcInspector.exitNestedEventLoop(); return { type: "resumeLimit", frameFinished: aRequest.forceCompletion }; } if (aRequest && aRequest.resumeLimit) { // Bind these methods because some of the hooks are called with 'this' // set to the current frame. let pauseAndRespond = this._pauseAndRespond.bind(this); let createValueGrip = this.createValueGrip.bind(this); let startFrame = this._youngestFrame; let startLine; if (this._youngestFrame.script) { let offset = this._youngestFrame.offset; startLine = this._youngestFrame.script.getOffsetLine(offset); } // Define the JS hook functions for stepping. let onEnterFrame = function TA_onEnterFrame(aFrame) { return pauseAndRespond(aFrame, { type: "resumeLimit" }); }; let onPop = function TA_onPop(aCompletion) { // onPop is called with 'this' set to the current frame. // Note that we're popping this frame; we need to watch for // subsequent step events on its caller. this.reportedPop = true; return pauseAndRespond(this, { type: "resumeLimit" }); } let onStep = function TA_onStep() { // onStep is called with 'this' set to the current frame. // If we've changed frame or line, then report that. if (this !== startFrame || (this.script && this.script.getOffsetLine(this.offset) != startLine)) { return pauseAndRespond(this, { type: "resumeLimit" }); } // Otherwise, let execution continue. return undefined; } switch (aRequest.resumeLimit.type) { case "step": this.dbg.onEnterFrame = onEnterFrame; // Fall through. case "next": let stepFrame = this._getNextStepFrame(startFrame); if (stepFrame) { stepFrame.onStep = onStep; stepFrame.onPop = onPop; } break; case "finish": stepFrame = this._getNextStepFrame(startFrame); if (stepFrame) { stepFrame.onPop = onPop; } break; default: return { error: "badParameterType", message: "Unknown resumeLimit type" }; } } let packet = this._resumed(); DebuggerServer.xpcInspector.exitNestedEventLoop(); return packet; }, /** * Helper method that returns the next frame when stepping. */ _getNextStepFrame: function TA__getNextStepFrame(aFrame) { let stepFrame = aFrame.reportedPop ? aFrame.older : aFrame; if (!stepFrame || !stepFrame.script) { stepFrame = null; } return stepFrame; }, onClientEvaluate: function TA_onClientEvaluate(aRequest) { if (this.state !== "paused") { return { error: "wrongState", message: "Debuggee must be paused to evaluate code." }; }; let frame = this._requestFrame(aRequest.frame); if (!frame) { return { error: "unknownFrame", message: "Evaluation frame not found" }; } if (!frame.environment) { return { error: "notDebuggee", message: "cannot access the environment of this frame." }; }; // We'll clobber the youngest frame if the eval causes a pause, so // save our frame now to be restored after eval returns. // XXX: or we could just start using dbg.getNewestFrame() now that it // works as expected. let youngest = this._youngestFrame; // Put ourselves back in the running state and inform the client. let resumedPacket = this._resumed(); this.conn.send(resumedPacket); // Run the expression. // XXX: test syntax errors let completion = frame.eval(aRequest.expression); // Put ourselves back in the pause state. let packet = this._paused(youngest); packet.why = { type: "clientEvaluated", frameFinished: this.createProtocolCompletionValue(completion) }; // Return back to our previous pause's event loop. return packet; }, onFrames: function TA_onFrames(aRequest) { if (this.state !== "paused") { return { error: "wrongState", message: "Stack frames are only available while the debuggee is paused."}; } let start = aRequest.start ? aRequest.start : 0; let count = aRequest.count; // Find the starting frame... let frame = this._youngestFrame; let i = 0; while (frame && (i < start)) { frame = frame.older; i++; } // Return request.count frames, or all remaining // frames if count is not defined. let frames = []; for (; frame && (!count || i < (start + count)); i++) { let form = this._createFrameActor(frame).form(); form.depth = i; frames.push(form); frame = frame.older; } return { frames: frames }; }, onReleaseMany: function TA_onReleaseMany(aRequest) { if (!aRequest.actors) { return { error: "missingParameter", message: "no actors were specified" }; } for each (let actorID in aRequest.actors) { let actor = this.threadLifetimePool.get(actorID); this.threadLifetimePool.objectActors.delete(actor.obj); this.threadLifetimePool.removeActor(actorID); } return {}; }, /** * Handle a protocol request to set a breakpoint. */ onSetBreakpoint: function TA_onSetBreakpoint(aRequest) { if (this.state !== "paused") { return { error: "wrongState", message: "Breakpoints can only be set while the debuggee is paused."}; } let location = aRequest.location; if (!this._scripts[location.url] || location.line < 0) { return { error: "noScript" }; } // Fetch the list of scripts in that url. let scripts = this._scripts[location.url]; // Fetch the specified script in that list. let script = null; for (let i = location.line; i >= 0; i--) { // Stop when the first script that contains this location is found. if (scripts[i]) { // If that first script does not contain the line specified, it's no // good. if (i + scripts[i].lineCount < location.line) { continue; } script = scripts[i]; break; } } if (!script) { return { error: "noScript" }; } script = this._getInnermostContainer(script, location.line); let bpActor = new BreakpointActor(script, this); this.breakpointActorPool.addActor(bpActor); let offsets = script.getLineOffsets(location.line); let codeFound = false; for (let i = 0; i < offsets.length; i++) { script.setBreakpoint(offsets[i], bpActor); codeFound = true; } let actualLocation; if (offsets.length == 0) { // No code at that line in any script, skipping forward. let lines = script.getAllOffsets(); for (let line = location.line; line < lines.length; ++line) { if (lines[line]) { for (let i = 0; i < lines[line].length; i++) { script.setBreakpoint(lines[line][i], bpActor); codeFound = true; } actualLocation = location; actualLocation.line = line; break; } } } if (!codeFound) { bpActor.onDelete(); return { error: "noCodeAtLineColumn" }; } return { actor: bpActor.actorID, actualLocation: actualLocation }; }, /** * Get the innermost script that contains this line, by looking through child * scripts of the supplied script. * * @param aScript Debugger.Script * The source script. * @param aLine number * The line number. */ _getInnermostContainer: function TA__getInnermostContainer(aScript, aLine) { let children = aScript.getChildScripts(); if (children.length > 0) { for (let i = 0; i < children.length; i++) { let child = children[i]; // Stop when the first script that contains this location is found. if (child.startLine <= aLine && child.startLine + child.lineCount > aLine) { return this._getInnermostContainer(child, aLine); } } } // Location not found in children, this is the innermost containing script. return aScript; }, /** * Handle a protocol request to return the list of loaded scripts. */ onScripts: function TA_onScripts(aRequest) { // Get the script list from the debugger. for (let s of this.dbg.findScripts()) { if (s.url.indexOf("chrome://") != 0) { this._addScript(s); } } // Build the cache. let scripts = []; for (let url in this._scripts) { for (let i = 0; i < this._scripts[url].length; i++) { if (!this._scripts[url][i]) { continue; } let script = { url: url, startLine: i, lineCount: this._scripts[url][i].lineCount }; scripts.push(script); } } let packet = { from: this.actorID, scripts: scripts }; return packet; }, /** * Handle a protocol request to pause the debuggee. */ onInterrupt: function TA_onScripts(aRequest) { if (this.state == "exited") { return { type: "exited" }; } else if (this.state == "paused") { // TODO: return the actual reason for the existing pause. return { type: "paused", why: { type: "alreadyPaused" } }; } else if (this.state != "running") { return { error: "wrongState", message: "Received interrupt request in " + this.state + " state." }; } try { // Put ourselves in the paused state. let packet = this._paused(); if (!packet) { return { error: "notInterrupted" }; } packet.why = { type: "interrupted" }; // Send the response to the interrupt request now (rather than // returning it), because we're going to start a nested event loop // here. this.conn.send(packet); // Start a nested event loop. this._nest(); // We already sent a response to this request, don't send one // now. return null; } catch(e) { Cu.reportError(e); return { error: "notInterrupted", message: e.toString() }; } }, /** * Return the Debug.Frame for a frame mentioned by the protocol. */ _requestFrame: function TA_requestFrame(aFrameID) { if (!aFrameID) { return this._youngestFrame; } if (this._framePool.has(aFrameID)) { return this._framePool.get(aFrameID).frame; } return undefined; }, _paused: function TA_paused(aFrame) { // We don't handle nested pauses correctly. Don't try - if we're // paused, just continue running whatever code triggered the pause. // We don't want to actually have nested pauses (although we // have nested event loops). If code runs in the debuggee during // a pause, it should cause the actor to resume (dropping // pause-lifetime actors etc) and then repause when complete. if (this.state === "paused") { return undefined; } // Clear stepping hooks. this.dbg.onEnterFrame = undefined; if (aFrame) { aFrame.onStep = undefined; aFrame.onPop = undefined; } this._state = "paused"; // Save the pause frame (if any) as the youngest frame for // stack viewing. this._youngestFrame = aFrame; // Create the actor pool that will hold the pause actor and its // children. dbg_assert(!this._pausePool); this._pausePool = new ActorPool(this.conn); this.conn.addActorPool(this._pausePool); // Give children of the pause pool a quick link back to the // thread... this._pausePool.threadActor = this; // Create the pause actor itself... dbg_assert(!this._pauseActor); this._pauseActor = new PauseActor(this._pausePool); this._pausePool.addActor(this._pauseActor); // Update the list of frames. let poppedFrames = this._updateFrames(); // Send off the paused packet and spin an event loop. let packet = { from: this.actorID, type: "paused", actor: this._pauseActor.actorID }; if (aFrame) { packet.frame = this._createFrameActor(aFrame).form(); } if (poppedFrames) { packet.poppedFrames = poppedFrames; } return packet; }, _nest: function TA_nest() { if (this._hooks.preNest) { var nestData = this._hooks.preNest(); } DebuggerServer.xpcInspector.enterNestedEventLoop(); dbg_assert(this.state === "running"); if (this._hooks.postNest) { this._hooks.postNest(nestData) } // "continue" resumption value. return undefined; }, _resumed: function TA_resumed() { this._state = "running"; // Drop the actors in the pause actor pool. this.conn.removeActorPool(this._pausePool); this._pausePool = null; this._pauseActor = null; this._youngestFrame = null; return { from: this.actorID, type: "resumed" }; }, /** * Expire frame actors for frames that have been popped. * * @returns A list of actor IDs whose frames have been popped. */ _updateFrames: function TA_updateFrames() { let popped = []; // Create the actor pool that will hold the still-living frames. let framePool = new ActorPool(this.conn); let frameList = []; for each (let frameActor in this._frameActors) { if (frameActor.frame.live) { framePool.addActor(frameActor); frameList.push(frameActor); } else { popped.push(frameActor.actorID); } } // Remove the old frame actor pool, this will expire // any actors that weren't added to the new pool. if (this._framePool) { this.conn.removeActorPool(this._framePool); } this._frameActors = frameList; this._framePool = framePool; this.conn.addActorPool(framePool); return popped; }, _createFrameActor: function TA_createFrameActor(aFrame) { if (aFrame.actor) { return aFrame.actor; } let actor = new FrameActor(aFrame, this); this._frameActors.push(actor); this._framePool.addActor(actor); aFrame.actor = actor; return actor; }, /** * Create and return an environment actor that corresponds to the * Debugger.Environment for the provided object. * @param Debugger.Object aObject * The object whose lexical environment we want to extract. * @param object aPool * The pool where the newly-created actor will be placed. * @return The EnvironmentActor for aObject or undefined for host functions or * functions scoped to a non-debuggee global. */ createEnvironmentActor: function TA_createEnvironmentActor(aObject, aPool) { let environment = aObject.environment; if (!environment) { return undefined; } if (environment.actor) { return environment.actor; } let actor = new EnvironmentActor(aObject, this); this._environmentActors.push(actor); aPool.addActor(actor); environment.actor = actor; return actor; }, /** * Create a grip for the given debuggee value. If the value is an * object, will create a pause-lifetime actor. */ createValueGrip: function TA_createValueGrip(aValue) { let type = typeof(aValue); if (type === "boolean" || type === "string" || type === "number") { return aValue; } if (aValue === null) { return { type: "null" }; } if (aValue === undefined) { return { type: "undefined" } } if (typeof(aValue) === "object") { return this.pauseObjectGrip(aValue); } dbg_assert(false, "Failed to provide a grip for: " + aValue); return null; }, /** * Return a protocol completion value representing the given * Debugger-provided completion value. */ createProtocolCompletionValue: function TA_createProtocolCompletionValue(aCompletion) { let protoValue = {}; if ("return" in aCompletion) { protoValue.return = this.createValueGrip(aCompletion.return); } else if ("yield" in aCompletion) { protoValue.return = this.createValueGrip(aCompletion.yield); } else if ("throw" in aCompletion) { protoValue.throw = this.createValueGrip(aCompletion.throw); } else { protoValue.terminated = true; } return protoValue; }, /** * Create a grip for the given debuggee object. * * @param aValue Debugger.Object * The debuggee object value. * @param aPool ActorPool * The actor pool where the new object actor will be added. */ objectGrip: function TA_objectGrip(aValue, aPool) { if (!aPool.objectActors) { aPool.objectActors = new WeakMap(); } if (aPool.objectActors.has(aValue)) { return aPool.objectActors.get(aValue).grip(); } let actor = new ObjectActor(aValue, this); aPool.addActor(actor); aPool.objectActors.set(aValue, actor); return actor.grip(); }, /** * Create a grip for the given debuggee object with a pause lifetime. * * @param aValue Debugger.Object * The debuggee object value. */ pauseObjectGrip: function TA_pauseObjectGrip(aValue) { if (!this._pausePool) { throw "Object grip requested while not paused."; } return this.objectGrip(aValue, this._pausePool); }, /** * Create a grip for the given debuggee object with a thread lifetime. * * @param aValue Debugger.Object * The debuggee object value. */ threadObjectGrip: function TA_threadObjectGrip(aValue) { return this.objectGrip(aValue, this.threadLifetimePool); }, // JS Debugger API hooks. /** * A function that the engine calls when a call to a debug event hook, * breakpoint handler, watchpoint handler, or similar function throws some * exception. * * @param aException exception * The exception that was thrown in the debugger code. */ uncaughtExceptionHook: function TA_uncaughtExceptionHook(aException) { dumpn("Got an exception:" + aException); }, /** * A function that the engine calls when a debugger statement has been * executed in the specified frame. * * @param aFrame Debugger.Frame * The stack frame that contained the debugger statement. */ onDebuggerStatement: function TA_onDebuggerStatement(aFrame) { return this._pauseAndRespond(aFrame, { type: "debuggerStatement" }); }, /** * A function that the engine calls when a new script has been loaded into the * scope of the specified debuggee global. * * @param aScript Debugger.Script * The source script that has been loaded into a debuggee compartment. * @param aGlobal Debugger.Object * A Debugger.Object instance whose referent is the global object. */ onNewScript: function TA_onNewScript(aScript, aGlobal) { this._addScript(aScript); // Notify the client. this.conn.send({ from: this.actorID, type: "newScript", url: aScript.url, startLine: aScript.startLine }); }, /** * Add the provided script to the server cache. * * @param aScript Debugger.Script * The source script that will be stored. */ _addScript: function TA__addScript(aScript) { // Use a sparse array for storing the scripts for each URL in order to // optimize retrieval. if (!this._scripts[aScript.url]) { this._scripts[aScript.url] = []; } this._scripts[aScript.url][aScript.startLine] = aScript; } }; ThreadActor.prototype.requestTypes = { "attach": ThreadActor.prototype.onAttach, "detach": ThreadActor.prototype.onDetach, "resume": ThreadActor.prototype.onResume, "clientEvaluate": ThreadActor.prototype.onClientEvaluate, "frames": ThreadActor.prototype.onFrames, "interrupt": ThreadActor.prototype.onInterrupt, "releaseMany": ThreadActor.prototype.onReleaseMany, "setBreakpoint": ThreadActor.prototype.onSetBreakpoint, "scripts": ThreadActor.prototype.onScripts }; /** * Creates a PauseActor. * * PauseActors exist for the lifetime of a given debuggee pause. Used to * scope pause-lifetime grips. * * @param ActorPool aPool * The actor pool created for this pause. */ function PauseActor(aPool) { this.pool = aPool; } PauseActor.prototype = { actorPrefix: "pause" }; /** * Creates an actor for the specified object. * * @param aObj Debugger.Object * The debuggee object. * @param aThreadActor ThreadActor * The parent thread actor for this object. */ function ObjectActor(aObj, aThreadActor) { this.obj = aObj; this.threadActor = aThreadActor; } ObjectActor.prototype = { actorPrefix: "obj", WRONG_STATE_RESPONSE: { error: "wrongState", message: "Object actors can only be accessed while the thread is paused." }, /** * Returns a grip for this actor for returning in a protocol message. */ grip: function OA_grip() { return { "type": "object", "class": this.obj.class, "actor": this.actorID }; }, /** * Releases this actor from the pool. */ release: function OA_release() { this.registeredPool.objectActors.delete(this.obj); this.registeredPool.removeActor(this.actorID); }, /** * Handle a protocol request to provide the names of the properties defined on * the object and not its prototype. * * @param aRequest object * The protocol request object. */ onOwnPropertyNames: function OA_onOwnPropertyNames(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } return { from: this.actorID, ownPropertyNames: this.obj.getOwnPropertyNames() }; }, /** * Handle a protocol request to provide the prototype and own properties of * the object. * * @param aRequest object * The protocol request object. */ onPrototypeAndProperties: function OA_onPrototypeAndProperties(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } let ownProperties = {}; for each (let name in this.obj.getOwnPropertyNames()) { try { let desc = this.obj.getOwnPropertyDescriptor(name); ownProperties[name] = this._propertyDescriptor(desc); } catch (e if e.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO") { // Calling getOwnPropertyDescriptor on wrapped native prototypes is not // allowed. dumpn("Error while getting the property descriptor for " + name + ": " + e.name); } } return { from: this.actorID, prototype: this.threadActor.createValueGrip(this.obj.proto), ownProperties: ownProperties }; }, /** * Handle a protocol request to provide the prototype of the object. * * @param aRequest object * The protocol request object. */ onPrototype: function OA_onPrototype(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } return { from: this.actorID, prototype: this.threadActor.createValueGrip(this.obj.proto) }; }, /** * Handle a protocol request to provide the property descriptor of the * object's specified property. * * @param aRequest object * The protocol request object. */ onProperty: function OA_onProperty(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } if (!aRequest.name) { return { error: "missingParameter", message: "no property name was specified" }; } let desc = this.obj.getOwnPropertyDescriptor(aRequest.name); return { from: this.actorID, descriptor: this._propertyDescriptor(desc) }; }, /** * A helper method that creates a property descriptor for the provided object, * properly formatted for sending in a protocol response. * * @param aObject object * The object that the descriptor is generated for. */ _propertyDescriptor: function OA_propertyDescriptor(aObject) { let descriptor = {}; descriptor.configurable = aObject.configurable; descriptor.enumerable = aObject.enumerable; if (aObject.value) { descriptor.writable = aObject.writable; descriptor.value = this.threadActor.createValueGrip(aObject.value); } else { descriptor.get = this.threadActor.createValueGrip(aObject.get); descriptor.set = this.threadActor.createValueGrip(aObject.set); } return descriptor; }, /** * Handle a protocol request to provide the source code of a function. * * @param aRequest object * The protocol request object. */ onDecompile: function OA_onDecompile(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } if (this.obj.class !== "Function") { return { error: "objectNotFunction", message: "decompile request is only valid for object grips " + "with a 'Function' class." }; } return { from: this.actorID, decompiledCode: this.obj.decompile(!!aRequest.pretty) }; }, /** * Handle a protocol request to provide the lexical scope of a function. * * @param aRequest object * The protocol request object. */ onScope: function OA_onScope(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } if (this.obj.class !== "Function") { return { error: "objectNotFunction", message: "scope request is only valid for object grips with a" + " 'Function' class." }; } let envActor = this.threadActor.createEnvironmentActor(this.obj, this.registeredPool); if (!envActor) { return { error: "notDebuggee", message: "cannot access the environment of this function." }; } return { name: this.obj.name || null, scope: envActor.form() }; }, /** * Handle a protocol request to provide the name and parameters of a function. * * @param aRequest object * The protocol request object. */ onNameAndParameters: function OA_onNameAndParameters(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } if (this.obj.class !== "Function") { return { error: "objectNotFunction", message: "nameAndParameters request is only valid for object " + "grips with a 'Function' class." }; } return { name: this.obj.name || null, parameters: this.obj.parameterNames }; }, /** * Handle a protocol request to promote a pause-lifetime grip to a * thread-lifetime grip. * * @param aRequest object * The protocol request object. */ onThreadGrip: function OA_onThreadGrip(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } return { threadGrip: this.threadActor.threadObjectGrip(this.obj) }; }, /** * Handle a protocol request to release a thread-lifetime grip. * * @param aRequest object * The protocol request object. */ onRelease: function OA_onRelease(aRequest) { if (this.threadActor.state !== "paused") { return this.WRONG_STATE_RESPONSE; } if (this.registeredPool !== this.threadActor.threadLifetimePool) { return { error: "notReleasable", message: "only thread-lifetime actors can be released." }; } this.release(); return {}; }, }; ObjectActor.prototype.requestTypes = { "nameAndParameters": ObjectActor.prototype.onNameAndParameters, "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, "prototype": ObjectActor.prototype.onPrototype, "property": ObjectActor.prototype.onProperty, "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, "scope": ObjectActor.prototype.onScope, "decompile": ObjectActor.prototype.onDecompile, "threadGrip": ObjectActor.prototype.onThreadGrip, "release": ObjectActor.prototype.onRelease, }; /** * Creates an actor for the specified stack frame. * * @param aFrame Debugger.Frame * The debuggee frame. * @param aThreadActor ThreadActor * The parent thread actor for this frame. */ function FrameActor(aFrame, aThreadActor) { this.frame = aFrame; this.threadActor = aThreadActor; } FrameActor.prototype = { actorPrefix: "frame", /** * A pool that contains frame-lifetime objects, like the environment. */ _frameLifetimePool: null, get frameLifetimePool() { if (!this._frameLifetimePool) { this._frameLifetimePool = new ActorPool(this.conn); this.conn.addActorPool(this._frameLifetimePool); } return this._frameLifetimePool; }, /** * Finalization handler that is called when the actor is being evicted from * the pool. */ disconnect: function FA_disconnect() { this.conn.removeActorPool(this._frameLifetimePool); this._frameLifetimePool = null; }, /** * Returns a frame form for use in a protocol message. */ form: function FA_form() { let form = { actor: this.actorID, type: this.frame.type }; if (this.frame.type === "call") { form.callee = this.threadActor.createValueGrip(this.frame.callee); if (this.frame.callee.name) { form.calleeName = this.frame.callee.name; } else { let desc = this.frame.callee.getOwnPropertyDescriptor("displayName"); if (desc && desc.value && typeof desc.value == "string") { form.calleeName = desc.value; } } } let envActor = this.threadActor .createEnvironmentActor(this.frame, this.frameLifetimePool); form.environment = envActor ? envActor.form() : envActor; form.this = this.threadActor.createValueGrip(this.frame.this); form.arguments = this._args(); if (this.frame.script) { form.where = { url: this.frame.script.url, line: this.frame.script.getOffsetLine(this.frame.offset) }; } if (!this.frame.older) { form.oldest = true; } return form; }, _args: function FA__args() { if (!this.frame.arguments) { return []; } return [this.threadActor.createValueGrip(arg) for each (arg in this.frame.arguments)]; }, /** * Handle a protocol request to pop this frame from the stack. * * @param aRequest object * The protocol request object. */ onPop: function FA_onPop(aRequest) { // TODO: remove this when Debugger.Frame.prototype.pop is implemented if (typeof this.frame.pop != "function") { return { error: "notImplemented", message: "Popping frames is not yet implemented." }; } while (this.frame != this.threadActor.dbg.getNewestFrame()) { this.threadActor.dbg.getNewestFrame().pop(); } this.frame.pop(aRequest.completionValue); // TODO: return the watches property when frame pop watch actors are // implemented. return { from: this.actorID }; } }; FrameActor.prototype.requestTypes = { "pop": FrameActor.prototype.onPop, }; /** * Creates a BreakpointActor. BreakpointActors exist for the lifetime of their * containing thread and are responsible for deleting breakpoints, handling * breakpoint hits and associating breakpoints with scripts. * * @param Debugger.Script aScript * The script this breakpoint is set on. * @param ThreadActor aThreadActor * The parent thread actor that contains this breakpoint. */ function BreakpointActor(aScript, aThreadActor) { this.script = aScript; this.threadActor = aThreadActor; } BreakpointActor.prototype = { actorPrefix: "breakpoint", /** * A function that the engine calls when a breakpoint has been hit. * * @param aFrame Debugger.Frame * The stack frame that contained the breakpoint. */ hit: function BA_hit(aFrame) { // TODO: add the rest of the breakpoints on that line (bug 676602). let reason = { type: "breakpoint", actors: [ this.actorID ] }; return this.threadActor._pauseAndRespond(aFrame, reason); }, /** * Handle a protocol request to remove this breakpoint. * * @param aRequest object * The protocol request object. */ onDelete: function BA_onDelete(aRequest) { this.threadActor.breakpointActorPool.removeActor(this.actorID); this.script.clearBreakpoint(this); this.script = null; return { from: this.actorID }; } }; BreakpointActor.prototype.requestTypes = { "delete": BreakpointActor.prototype.onDelete }; /** * Creates an EnvironmentActor. EnvironmentActors are responsible for listing * the bindings introduced by a lexical environment and assigning new values to * those identifier bindings. * * @param Debugger.Object aObject * The object whose lexical environment will be used to create the actor. * @param ThreadActor aThreadActor * The parent thread actor that contains this environment. */ function EnvironmentActor(aObject, aThreadActor) { this.obj = aObject; this.threadActor = aThreadActor; } EnvironmentActor.prototype = { actorPrefix: "environment", /** * Returns an environment form for use in a protocol message. */ form: function EA_form() { // Debugger.Frame might be dead by the time we get here, which will cause // accessing its properties to throw. if (!this.obj.live) { return undefined; } let parent; if (this.obj.environment.parent) { parent = this.threadActor .createEnvironmentActor(this.obj.environment.parent, this.registeredPool); } let form = { actor: this.actorID, parent: parent ? parent.form() : parent }; if (this.obj.environment.type == "object") { if (this.obj.environment.parent) { form.type = "with"; } else { form.type = "object"; } form.object = this.threadActor.createValueGrip(this.obj.environment.object); } else { if (this.obj.class == "Function") { form.type = "function"; form.function = this.threadActor.createValueGrip(this.obj); form.functionName = this.obj.name; } else { form.type = "block"; } form.bindings = this._bindings(); } return form; }, /** * Return the identifier bindings object as required by the remote protocol * specification. */ _bindings: function EA_bindings() { let bindings = { arguments: [], variables: {} }; // TODO: this will be redundant after bug 692984 is fixed. if (typeof this.obj.environment.getVariableDescriptor != "function") { return bindings; } for (let name in this.obj.parameterNames) { let arg = {}; let desc = this.obj.environment.getVariableDescriptor(name); let descForm = { enumerable: true, configurable: desc.configurable }; if ("value" in desc) { descForm.value = this.threadActor.createValueGrip(desc.value); descForm.writable = desc.writable; } else { descForm.get = this.threadActor.createValueGrip(desc.get); descForm.set = this.threadActor.createValueGrip(desc.set); } arg[name] = descForm; bindings.arguments.push(arg); } for (let name in this.obj.environment.names()) { if (bindings.arguments.some(function exists(element) { return !!element[name]; })) { continue; } let desc = this.obj.environment.getVariableDescriptor(name); let descForm = { enumerable: true, configurable: desc.configurable }; if ("value" in desc) { descForm.value = this.threadActor.createValueGrip(desc.value); descForm.writable = desc.writable; } else { descForm.get = this.threadActor.createValueGrip(desc.get); descForm.set = this.threadActor.createValueGrip(desc.set); } bindings.variables[name] = descForm; } return bindings; }, /** * Handle a protocol request to change the value of a variable bound in this * lexical environment. * * @param aRequest object * The protocol request object. */ onAssign: function EA_onAssign(aRequest) { let desc = this.obj.environment.getVariableDescriptor(aRequest.name); if (!desc.writable) { return { error: "immutableBinding", message: "Changing the value of an immutable binding is not " + "allowed" }; } try { this.obj.environment.setVariable(aRequest.name, aRequest.value); } catch (e) { if (e instanceof Debugger.DebuggeeWouldRun) { return { error: "threadWouldRun", cause: e.cause ? e.cause : "setter", message: "Assigning a value would cause the debuggee to run" }; } // This should never happen, so let it complain loudly if it does. throw e; } return { from: this.actorID }; }, /** * Handle a protocol request to fully enumerate the bindings introduced by the * lexical environment. * * @param aRequest object * The protocol request object. */ onBindings: function EA_onBindings(aRequest) { return { from: this.actorID, bindings: this._bindings() }; } }; EnvironmentActor.prototype.requestTypes = { "assign": EnvironmentActor.prototype.onAssign, "bindings": EnvironmentActor.prototype.onBindings };