summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/script.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/server/actors/script.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/server/actors/script.js')
-rw-r--r--devtools/server/actors/script.js2360
1 files changed, 2360 insertions, 0 deletions
diff --git a/devtools/server/actors/script.js b/devtools/server/actors/script.js
new file mode 100644
index 000000000..e8e39546c
--- /dev/null
+++ b/devtools/server/actors/script.js
@@ -0,0 +1,2360 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+const { Cc, Ci, Cu, Cr, components, ChromeWorker } = require("chrome");
+const { ActorPool, OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
+const { BreakpointActor, setBreakpointAtEntryPoints } = require("devtools/server/actors/breakpoint");
+const { EnvironmentActor } = require("devtools/server/actors/environment");
+const { FrameActor } = require("devtools/server/actors/frame");
+const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object");
+const { SourceActor, getSourceURL } = require("devtools/server/actors/source");
+const { DebuggerServer } = require("devtools/server/main");
+const { ActorClassWithSpec } = require("devtools/shared/protocol");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+const { assert, dumpn, update, fetch } = DevToolsUtils;
+const promise = require("promise");
+const xpcInspector = require("xpcInspector");
+const { DevToolsWorker } = require("devtools/shared/worker/worker");
+const object = require("sdk/util/object");
+const { threadSpec } = require("devtools/shared/specs/script");
+
+const { defer, resolve, reject, all } = promise;
+
+loader.lazyGetter(this, "Debugger", () => {
+ let Debugger = require("Debugger");
+ hackDebugger(Debugger);
+ return Debugger;
+});
+loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
+
+/**
+ * A BreakpointActorMap is a map from locations to instances of BreakpointActor.
+ */
+function BreakpointActorMap() {
+ this._size = 0;
+ this._actors = {};
+}
+
+BreakpointActorMap.prototype = {
+ /**
+ * Return the number of BreakpointActors in this BreakpointActorMap.
+ *
+ * @returns Number
+ * The number of BreakpointActor in this BreakpointActorMap.
+ */
+ get size() {
+ return this._size;
+ },
+
+ /**
+ * Generate all BreakpointActors that match the given location in
+ * this BreakpointActorMap.
+ *
+ * @param OriginalLocation location
+ * The location for which matching BreakpointActors should be generated.
+ */
+ findActors: function* (location = new OriginalLocation()) {
+ // Fast shortcut for when we know we won't find any actors. Surprisingly
+ // enough, this speeds up refreshing when there are no breakpoints set by
+ // about 2x!
+ if (this.size === 0) {
+ return;
+ }
+
+ function* findKeys(object, key) {
+ if (key !== undefined) {
+ if (key in object) {
+ yield key;
+ }
+ }
+ else {
+ for (let key of Object.keys(object)) {
+ yield key;
+ }
+ }
+ }
+
+ let query = {
+ sourceActorID: location.originalSourceActor ? location.originalSourceActor.actorID : undefined,
+ line: location.originalLine,
+ };
+
+ // If location contains a line, assume we are searching for a whole line
+ // breakpoint, and set begin/endColumn accordingly. Otherwise, we are
+ // searching for all breakpoints, so begin/endColumn should be left unset.
+ if (location.originalLine) {
+ query.beginColumn = location.originalColumn ? location.originalColumn : 0;
+ query.endColumn = location.originalColumn ? location.originalColumn + 1 : Infinity;
+ } else {
+ query.beginColumn = location.originalColumn ? query.originalColumn : undefined;
+ query.endColumn = location.originalColumn ? query.originalColumn + 1 : undefined;
+ }
+
+ for (let sourceActorID of findKeys(this._actors, query.sourceActorID))
+ for (let line of findKeys(this._actors[sourceActorID], query.line))
+ for (let beginColumn of findKeys(this._actors[sourceActorID][line], query.beginColumn))
+ for (let endColumn of findKeys(this._actors[sourceActorID][line][beginColumn], query.endColumn)) {
+ yield this._actors[sourceActorID][line][beginColumn][endColumn];
+ }
+ },
+
+ /**
+ * Return the BreakpointActor at the given location in this
+ * BreakpointActorMap.
+ *
+ * @param OriginalLocation location
+ * The location for which the BreakpointActor should be returned.
+ *
+ * @returns BreakpointActor actor
+ * The BreakpointActor at the given location.
+ */
+ getActor: function (originalLocation) {
+ for (let actor of this.findActors(originalLocation)) {
+ return actor;
+ }
+
+ return null;
+ },
+
+ /**
+ * Set the given BreakpointActor to the given location in this
+ * BreakpointActorMap.
+ *
+ * @param OriginalLocation location
+ * The location to which the given BreakpointActor should be set.
+ *
+ * @param BreakpointActor actor
+ * The BreakpointActor to be set to the given location.
+ */
+ setActor: function (location, actor) {
+ let { originalSourceActor, originalLine, originalColumn } = location;
+
+ let sourceActorID = originalSourceActor.actorID;
+ let line = originalLine;
+ let beginColumn = originalColumn ? originalColumn : 0;
+ let endColumn = originalColumn ? originalColumn + 1 : Infinity;
+
+ if (!this._actors[sourceActorID]) {
+ this._actors[sourceActorID] = [];
+ }
+ if (!this._actors[sourceActorID][line]) {
+ this._actors[sourceActorID][line] = [];
+ }
+ if (!this._actors[sourceActorID][line][beginColumn]) {
+ this._actors[sourceActorID][line][beginColumn] = [];
+ }
+ if (!this._actors[sourceActorID][line][beginColumn][endColumn]) {
+ ++this._size;
+ }
+ this._actors[sourceActorID][line][beginColumn][endColumn] = actor;
+ },
+
+ /**
+ * Delete the BreakpointActor from the given location in this
+ * BreakpointActorMap.
+ *
+ * @param OriginalLocation location
+ * The location from which the BreakpointActor should be deleted.
+ */
+ deleteActor: function (location) {
+ let { originalSourceActor, originalLine, originalColumn } = location;
+
+ let sourceActorID = originalSourceActor.actorID;
+ let line = originalLine;
+ let beginColumn = originalColumn ? originalColumn : 0;
+ let endColumn = originalColumn ? originalColumn + 1 : Infinity;
+
+ if (this._actors[sourceActorID]) {
+ if (this._actors[sourceActorID][line]) {
+ if (this._actors[sourceActorID][line][beginColumn]) {
+ if (this._actors[sourceActorID][line][beginColumn][endColumn]) {
+ --this._size;
+ }
+ delete this._actors[sourceActorID][line][beginColumn][endColumn];
+ if (Object.keys(this._actors[sourceActorID][line][beginColumn]).length === 0) {
+ delete this._actors[sourceActorID][line][beginColumn];
+ }
+ }
+ if (Object.keys(this._actors[sourceActorID][line]).length === 0) {
+ delete this._actors[sourceActorID][line];
+ }
+ }
+ }
+ }
+};
+
+exports.BreakpointActorMap = BreakpointActorMap;
+
+/**
+ * Keeps track of persistent sources across reloads and ties different
+ * source instances to the same actor id so that things like
+ * breakpoints survive reloads. ThreadSources uses this to force the
+ * same actorID on a SourceActor.
+ */
+function SourceActorStore() {
+ // source identifier --> actor id
+ this._sourceActorIds = Object.create(null);
+}
+
+SourceActorStore.prototype = {
+ /**
+ * Lookup an existing actor id that represents this source, if available.
+ */
+ getReusableActorId: function (aSource, aOriginalUrl) {
+ let url = this.getUniqueKey(aSource, aOriginalUrl);
+ if (url && url in this._sourceActorIds) {
+ return this._sourceActorIds[url];
+ }
+ return null;
+ },
+
+ /**
+ * Update a source with an actorID.
+ */
+ setReusableActorId: function (aSource, aOriginalUrl, actorID) {
+ let url = this.getUniqueKey(aSource, aOriginalUrl);
+ if (url) {
+ this._sourceActorIds[url] = actorID;
+ }
+ },
+
+ /**
+ * Make a unique URL from a source that identifies it across reloads.
+ */
+ getUniqueKey: function (aSource, aOriginalUrl) {
+ if (aOriginalUrl) {
+ // Original source from a sourcemap.
+ return aOriginalUrl;
+ }
+ else {
+ return getSourceURL(aSource);
+ }
+ }
+};
+
+exports.SourceActorStore = SourceActorStore;
+
+/**
+ * Manages pushing event loops and automatically pops and exits them in the
+ * correct order as they are resolved.
+ *
+ * @param ThreadActor thread
+ * The thread actor instance that owns this EventLoopStack.
+ * @param DebuggerServerConnection connection
+ * The remote protocol connection associated with this event loop stack.
+ * @param Object hooks
+ * An object with the following properties:
+ * - url: The URL string of the debuggee we are spinning an event loop
+ * for.
+ * - preNest: function called before entering a nested event loop
+ * - postNest: function called after exiting a nested event loop
+ */
+function EventLoopStack({ thread, connection, hooks }) {
+ this._hooks = hooks;
+ this._thread = thread;
+ this._connection = connection;
+}
+
+EventLoopStack.prototype = {
+ /**
+ * The number of nested event loops on the stack.
+ */
+ get size() {
+ return xpcInspector.eventLoopNestLevel;
+ },
+
+ /**
+ * The URL of the debuggee who pushed the event loop on top of the stack.
+ */
+ get lastPausedUrl() {
+ let url = null;
+ if (this.size > 0) {
+ try {
+ url = xpcInspector.lastNestRequestor.url;
+ } catch (e) {
+ // The tab's URL getter may throw if the tab is destroyed by the time
+ // this code runs, but we don't really care at this point.
+ dumpn(e);
+ }
+ }
+ return url;
+ },
+
+ /**
+ * The DebuggerServerConnection of the debugger who pushed the event loop on
+ * top of the stack
+ */
+ get lastConnection() {
+ return xpcInspector.lastNestRequestor._connection;
+ },
+
+ /**
+ * Push a new nested event loop onto the stack.
+ *
+ * @returns EventLoop
+ */
+ push: function () {
+ return new EventLoop({
+ thread: this._thread,
+ connection: this._connection,
+ hooks: this._hooks
+ });
+ }
+};
+
+/**
+ * An object that represents a nested event loop. It is used as the nest
+ * requestor with nsIJSInspector instances.
+ *
+ * @param ThreadActor thread
+ * The thread actor that is creating this nested event loop.
+ * @param DebuggerServerConnection connection
+ * The remote protocol connection associated with this event loop.
+ * @param Object hooks
+ * The same hooks object passed into EventLoopStack during its
+ * initialization.
+ */
+function EventLoop({ thread, connection, hooks }) {
+ this._thread = thread;
+ this._hooks = hooks;
+ this._connection = connection;
+
+ this.enter = this.enter.bind(this);
+ this.resolve = this.resolve.bind(this);
+}
+
+EventLoop.prototype = {
+ entered: false,
+ resolved: false,
+ get url() { return this._hooks.url; },
+
+ /**
+ * Enter this nested event loop.
+ */
+ enter: function () {
+ let nestData = this._hooks.preNest
+ ? this._hooks.preNest()
+ : null;
+
+ this.entered = true;
+ xpcInspector.enterNestedEventLoop(this);
+
+ // Keep exiting nested event loops while the last requestor is resolved.
+ if (xpcInspector.eventLoopNestLevel > 0) {
+ const { resolved } = xpcInspector.lastNestRequestor;
+ if (resolved) {
+ xpcInspector.exitNestedEventLoop();
+ }
+ }
+
+ if (this._hooks.postNest) {
+ this._hooks.postNest(nestData);
+ }
+ },
+
+ /**
+ * Resolve this nested event loop.
+ *
+ * @returns boolean
+ * True if we exited this nested event loop because it was on top of
+ * the stack, false if there is another nested event loop above this
+ * one that hasn't resolved yet.
+ */
+ resolve: function () {
+ if (!this.entered) {
+ throw new Error("Can't resolve an event loop before it has been entered!");
+ }
+ if (this.resolved) {
+ throw new Error("Already resolved this nested event loop!");
+ }
+ this.resolved = true;
+ if (this === xpcInspector.lastNestRequestor) {
+ xpcInspector.exitNestedEventLoop();
+ return true;
+ }
+ return false;
+ },
+};
+
+/**
+ * JSD2 actors.
+ */
+
+/**
+ * Creates a ThreadActor.
+ *
+ * ThreadActors manage a JSInspector object and manage execution/inspection
+ * of debuggees.
+ *
+ * @param aParent object
+ * This |ThreadActor|'s parent actor. It must implement the following
+ * properties:
+ * - url: The URL string of the debuggee.
+ * - window: The global window object.
+ * - preNest: Function called before entering a nested event loop.
+ * - postNest: Function called after exiting a nested event loop.
+ * - makeDebugger: A function that takes no arguments and instantiates
+ * a Debugger that manages its globals on its own.
+ * @param aGlobal object [optional]
+ * An optional (for content debugging only) reference to the content
+ * window.
+ */
+const ThreadActor = ActorClassWithSpec(threadSpec, {
+ initialize: function (aParent, aGlobal) {
+ this._state = "detached";
+ this._frameActors = [];
+ this._parent = aParent;
+ this._dbg = null;
+ this._gripDepth = 0;
+ this._threadLifetimePool = null;
+ this._tabClosed = false;
+ this._scripts = null;
+ this._pauseOnDOMEvents = null;
+
+ this._options = {
+ useSourceMaps: false,
+ autoBlackBox: false
+ };
+
+ this.breakpointActorMap = new BreakpointActorMap();
+ this.sourceActorStore = new SourceActorStore();
+
+ this._debuggerSourcesSeen = null;
+
+ // A map of actorID -> actor for breakpoints created and managed by the
+ // server.
+ this._hiddenBreakpoints = new Map();
+
+ this.global = aGlobal;
+
+ this._allEventsListener = this._allEventsListener.bind(this);
+ this.onNewGlobal = this.onNewGlobal.bind(this);
+ this.onSourceEvent = this.onSourceEvent.bind(this);
+ this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
+ this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
+ this.onNewScript = this.onNewScript.bind(this);
+ this.objectGrip = this.objectGrip.bind(this);
+ this.pauseObjectGrip = this.pauseObjectGrip.bind(this);
+ this._onWindowReady = this._onWindowReady.bind(this);
+ events.on(this._parent, "window-ready", this._onWindowReady);
+ // Set a wrappedJSObject property so |this| can be sent via the observer svc
+ // for the xpcshell harness.
+ this.wrappedJSObject = this;
+ },
+
+ // Used by the ObjectActor to keep track of the depth of grip() calls.
+ _gripDepth: null,
+
+ get dbg() {
+ if (!this._dbg) {
+ this._dbg = this._parent.makeDebugger();
+ this._dbg.uncaughtExceptionHook = this.uncaughtExceptionHook;
+ this._dbg.onDebuggerStatement = this.onDebuggerStatement;
+ this._dbg.onNewScript = this.onNewScript;
+ this._dbg.on("newGlobal", this.onNewGlobal);
+ // Keep the debugger disabled until a client attaches.
+ this._dbg.enabled = this._state != "detached";
+ }
+ return this._dbg;
+ },
+
+ get globalDebugObject() {
+ if (!this._parent.window) {
+ return null;
+ }
+ return this.dbg.makeGlobalObjectReference(this._parent.window);
+ },
+
+ get state() {
+ return this._state;
+ },
+
+ get attached() {
+ return this.state == "attached" ||
+ this.state == "running" ||
+ this.state == "paused";
+ },
+
+ get threadLifetimePool() {
+ if (!this._threadLifetimePool) {
+ this._threadLifetimePool = new ActorPool(this.conn);
+ this.conn.addActorPool(this._threadLifetimePool);
+ this._threadLifetimePool.objectActors = new WeakMap();
+ }
+ return this._threadLifetimePool;
+ },
+
+ get sources() {
+ return this._parent.sources;
+ },
+
+ get youngestFrame() {
+ if (this.state != "paused") {
+ return null;
+ }
+ return this.dbg.getNewestFrame();
+ },
+
+ _prettyPrintWorker: null,
+ get prettyPrintWorker() {
+ if (!this._prettyPrintWorker) {
+ this._prettyPrintWorker = new DevToolsWorker(
+ "resource://devtools/server/actors/pretty-print-worker.js",
+ { name: "pretty-print",
+ verbose: flags.wantLogging }
+ );
+ }
+ return this._prettyPrintWorker;
+ },
+
+ /**
+ * Keep track of all of the nested event loops we use to pause the debuggee
+ * when we hit a breakpoint/debugger statement/etc in one place so we can
+ * resolve them when we get resume packets. We have more than one (and keep
+ * them in a stack) because we can pause within client evals.
+ */
+ _threadPauseEventLoops: null,
+ _pushThreadPause: function () {
+ if (!this._threadPauseEventLoops) {
+ this._threadPauseEventLoops = [];
+ }
+ const eventLoop = this._nestedEventLoops.push();
+ this._threadPauseEventLoops.push(eventLoop);
+ eventLoop.enter();
+ },
+ _popThreadPause: function () {
+ const eventLoop = this._threadPauseEventLoops.pop();
+ assert(eventLoop, "Should have an event loop.");
+ eventLoop.resolve();
+ },
+
+ /**
+ * Remove all debuggees and clear out the thread's sources.
+ */
+ clearDebuggees: function () {
+ if (this._dbg) {
+ this.dbg.removeAllDebuggees();
+ }
+ this._sources = null;
+ this._scripts = null;
+ },
+
+ /**
+ * Listener for our |Debugger|'s "newGlobal" event.
+ */
+ onNewGlobal: function (aGlobal) {
+ // Notify the client.
+ this.conn.send({
+ from: this.actorID,
+ type: "newGlobal",
+ // TODO: after bug 801084 lands see if we need to JSONify this.
+ hostAnnotations: aGlobal.hostAnnotations
+ });
+ },
+
+ disconnect: function () {
+ dumpn("in ThreadActor.prototype.disconnect");
+ if (this._state == "paused") {
+ this.onResume();
+ }
+
+ // Blow away our source actor ID store because those IDs are only
+ // valid for this connection. This is ok because we never keep
+ // things like breakpoints across connections.
+ this._sourceActorStore = null;
+
+ events.off(this._parent, "window-ready", this._onWindowReady);
+ this.sources.off("newSource", this.onSourceEvent);
+ this.sources.off("updatedSource", this.onSourceEvent);
+ this.clearDebuggees();
+ this.conn.removeActorPool(this._threadLifetimePool);
+ this._threadLifetimePool = null;
+
+ if (this._prettyPrintWorker) {
+ this._prettyPrintWorker.destroy();
+ this._prettyPrintWorker = null;
+ }
+
+ if (!this._dbg) {
+ return;
+ }
+ this._dbg.enabled = false;
+ this._dbg = null;
+ },
+
+ /**
+ * Disconnect the debugger and put the actor in the exited state.
+ */
+ exit: function () {
+ this.disconnect();
+ this._state = "exited";
+ },
+
+ // Request handlers
+ onAttach: function (aRequest) {
+ if (this.state === "exited") {
+ return { type: "exited" };
+ }
+
+ if (this.state !== "detached") {
+ return { error: "wrongState",
+ message: "Current state is " + this.state };
+ }
+
+ this._state = "attached";
+ this._debuggerSourcesSeen = new WeakSet();
+
+ Object.assign(this._options, aRequest.options || {});
+ this.sources.setOptions(this._options);
+ this.sources.on("newSource", this.onSourceEvent);
+ this.sources.on("updatedSource", this.onSourceEvent);
+
+ // Initialize an event loop stack. This can't be done in the constructor,
+ // because this.conn is not yet initialized by the actor pool at that time.
+ this._nestedEventLoops = new EventLoopStack({
+ hooks: this._parent,
+ connection: this.conn,
+ thread: this
+ });
+
+ this.dbg.addDebuggees();
+ this.dbg.enabled = true;
+ try {
+ // Put ourselves in the paused state.
+ 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._pushThreadPause();
+
+ // We already sent a response to this request, don't send one
+ // now.
+ return null;
+ } catch (e) {
+ reportError(e);
+ return { error: "notAttached", message: e.toString() };
+ }
+ },
+
+ onDetach: function (aRequest) {
+ this.disconnect();
+ this._state = "detached";
+ this._debuggerSourcesSeen = null;
+
+ dumpn("ThreadActor.prototype.onDetach: returning 'detached' packet");
+ return {
+ type: "detached"
+ };
+ },
+
+ onReconfigure: function (aRequest) {
+ if (this.state == "exited") {
+ return { error: "wrongState" };
+ }
+ const options = aRequest.options || {};
+
+ if ("observeAsmJS" in options) {
+ this.dbg.allowUnobservedAsmJS = !options.observeAsmJS;
+ }
+
+ Object.assign(this._options, options);
+
+ // Update the global source store
+ this.sources.setOptions(options);
+
+ return {};
+ },
+
+ /**
+ * 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.
+ * @param function onPacket
+ * Hook to modify the packet before it is sent. Feel free to return a
+ * promise.
+ */
+ _pauseAndRespond: function (aFrame, aReason, onPacket = function (k) { return k; }) {
+ try {
+ let packet = this._paused(aFrame);
+ if (!packet) {
+ return undefined;
+ }
+ packet.why = aReason;
+
+ let generatedLocation = this.sources.getFrameLocation(aFrame);
+ this.sources.getOriginalLocation(generatedLocation)
+ .then((originalLocation) => {
+ if (!originalLocation.originalSourceActor) {
+ // The only time the source actor will be null is if there
+ // was a sourcemap and it tried to look up the original
+ // location but there was no original URL. This is a strange
+ // scenario so we simply don't pause.
+ DevToolsUtils.reportException(
+ "ThreadActor",
+ new Error("Attempted to pause in a script with a sourcemap but " +
+ "could not find original location.")
+ );
+
+ return undefined;
+ }
+
+ packet.frame.where = {
+ source: originalLocation.originalSourceActor.form(),
+ line: originalLocation.originalLine,
+ column: originalLocation.originalColumn
+ };
+ resolve(onPacket(packet))
+ .then(null, error => {
+ reportError(error);
+ return {
+ error: "unknownError",
+ message: error.message + "\n" + error.stack
+ };
+ })
+ .then(packet => {
+ this.conn.send(packet);
+ });
+ });
+
+ this._pushThreadPause();
+ } catch (e) {
+ reportError(e, "Got an exception during TA__pauseAndRespond: ");
+ }
+
+ // If the browser tab has been closed, terminate the debuggee script
+ // instead of continuing. Executing JS after the content window is gone is
+ // a bad idea.
+ return this._tabClosed ? null : undefined;
+ },
+
+ _makeOnEnterFrame: function ({ pauseAndRespond }) {
+ return aFrame => {
+ const generatedLocation = this.sources.getFrameLocation(aFrame);
+ let { originalSourceActor } = this.unsafeSynchronize(this.sources.getOriginalLocation(
+ generatedLocation));
+ let url = originalSourceActor.url;
+
+ return this.sources.isBlackBoxed(url)
+ ? undefined
+ : pauseAndRespond(aFrame);
+ };
+ },
+
+ _makeOnPop: function ({ thread, pauseAndRespond, createValueGrip }) {
+ return function (aCompletion) {
+ // onPop is called with 'this' set to the current frame.
+
+ const generatedLocation = thread.sources.getFrameLocation(this);
+ const { originalSourceActor } = thread.unsafeSynchronize(thread.sources.getOriginalLocation(
+ generatedLocation));
+ const url = originalSourceActor.url;
+
+ if (thread.sources.isBlackBoxed(url)) {
+ return undefined;
+ }
+
+ // Note that we're popping this frame; we need to watch for
+ // subsequent step events on its caller.
+ this.reportedPop = true;
+
+ return pauseAndRespond(this, aPacket => {
+ aPacket.why.frameFinished = {};
+ if (!aCompletion) {
+ aPacket.why.frameFinished.terminated = true;
+ } else if (aCompletion.hasOwnProperty("return")) {
+ aPacket.why.frameFinished.return = createValueGrip(aCompletion.return);
+ } else if (aCompletion.hasOwnProperty("yield")) {
+ aPacket.why.frameFinished.return = createValueGrip(aCompletion.yield);
+ } else {
+ aPacket.why.frameFinished.throw = createValueGrip(aCompletion.throw);
+ }
+ return aPacket;
+ });
+ };
+ },
+
+ _makeOnStep: function ({ thread, pauseAndRespond, startFrame,
+ startLocation, steppingType }) {
+ // Breaking in place: we should always pause.
+ if (steppingType === "break") {
+ return function () {
+ return pauseAndRespond(this);
+ };
+ }
+
+ // Otherwise take what a "step" means into consideration.
+ return function () {
+ // onStep is called with 'this' set to the current frame.
+
+ // Only allow stepping stops at entry points for the line, when
+ // the stepping occurs in a single frame. The "same frame"
+ // check makes it so a sequence of steps can step out of a frame
+ // and into subsequent calls in the outer frame. E.g., if there
+ // is a call "a(b())" and the user steps into b, then this
+ // condition makes it possible to step out of b and into a.
+ if (this === startFrame &&
+ !this.script.getOffsetLocation(this.offset).isEntryPoint) {
+ return undefined;
+ }
+
+ const generatedLocation = thread.sources.getFrameLocation(this);
+ const newLocation = thread.unsafeSynchronize(thread.sources.getOriginalLocation(
+ generatedLocation));
+
+ // Cases when we should pause because we have executed enough to consider
+ // a "step" to have occured:
+ //
+ // 1.1. We change frames.
+ // 1.2. We change URLs (can happen without changing frames thanks to
+ // source mapping).
+ // 1.3. We change lines.
+ //
+ // Cases when we should always continue execution, even if one of the
+ // above cases is true:
+ //
+ // 2.1. We are in a source mapped region, but inside a null mapping
+ // (doesn't correlate to any region of original source)
+ // 2.2. The source we are in is black boxed.
+
+ // Cases 2.1 and 2.2
+ if (newLocation.originalUrl == null
+ || thread.sources.isBlackBoxed(newLocation.originalUrl)) {
+ return undefined;
+ }
+
+ // Cases 1.1, 1.2 and 1.3
+ if (this !== startFrame
+ || startLocation.originalUrl !== newLocation.originalUrl
+ || startLocation.originalLine !== newLocation.originalLine) {
+ return pauseAndRespond(this);
+ }
+
+ // Otherwise, let execution continue (we haven't executed enough code to
+ // consider this a "step" yet).
+ return undefined;
+ };
+ },
+
+ /**
+ * Define the JS hook functions for stepping.
+ */
+ _makeSteppingHooks: function (aStartLocation, steppingType) {
+ // Bind these methods and state because some of the hooks are called
+ // with 'this' set to the current frame. Rather than repeating the
+ // binding in each _makeOnX method, just do it once here and pass it
+ // in to each function.
+ const steppingHookState = {
+ pauseAndRespond: (aFrame, onPacket = k=>k) => {
+ return this._pauseAndRespond(aFrame, { type: "resumeLimit" }, onPacket);
+ },
+ createValueGrip: v => createValueGrip(v, this._pausePool,
+ this.objectGrip),
+ thread: this,
+ startFrame: this.youngestFrame,
+ startLocation: aStartLocation,
+ steppingType: steppingType
+ };
+
+ return {
+ onEnterFrame: this._makeOnEnterFrame(steppingHookState),
+ onPop: this._makeOnPop(steppingHookState),
+ onStep: this._makeOnStep(steppingHookState)
+ };
+ },
+
+ /**
+ * Handle attaching the various stepping hooks we need to attach when we
+ * receive a resume request with a resumeLimit property.
+ *
+ * @param Object aRequest
+ * The request packet received over the RDP.
+ * @returns A promise that resolves to true once the hooks are attached, or is
+ * rejected with an error packet.
+ */
+ _handleResumeLimit: function (aRequest) {
+ let steppingType = aRequest.resumeLimit.type;
+ if (["break", "step", "next", "finish"].indexOf(steppingType) == -1) {
+ return reject({ error: "badParameterType",
+ message: "Unknown resumeLimit type" });
+ }
+
+ const generatedLocation = this.sources.getFrameLocation(this.youngestFrame);
+ return this.sources.getOriginalLocation(generatedLocation)
+ .then(originalLocation => {
+ const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation,
+ steppingType);
+
+ // Make sure there is still a frame on the stack if we are to continue
+ // stepping.
+ let stepFrame = this._getNextStepFrame(this.youngestFrame);
+ if (stepFrame) {
+ switch (steppingType) {
+ case "step":
+ this.dbg.onEnterFrame = onEnterFrame;
+ // Fall through.
+ case "break":
+ case "next":
+ if (stepFrame.script) {
+ stepFrame.onStep = onStep;
+ }
+ stepFrame.onPop = onPop;
+ break;
+ case "finish":
+ stepFrame.onPop = onPop;
+ }
+ }
+
+ return true;
+ });
+ },
+
+ /**
+ * Clear the onStep and onPop hooks from the given frame and all of the frames
+ * below it.
+ *
+ * @param Debugger.Frame aFrame
+ * The frame we want to clear the stepping hooks from.
+ */
+ _clearSteppingHooks: function (aFrame) {
+ if (aFrame && aFrame.live) {
+ while (aFrame) {
+ aFrame.onStep = undefined;
+ aFrame.onPop = undefined;
+ aFrame = aFrame.older;
+ }
+ }
+ },
+
+ /**
+ * Listen to the debuggee's DOM events if we received a request to do so.
+ *
+ * @param Object aRequest
+ * The resume request packet received over the RDP.
+ */
+ _maybeListenToEvents: function (aRequest) {
+ // Break-on-DOMEvents is only supported in content debugging.
+ let events = aRequest.pauseOnDOMEvents;
+ if (this.global && events &&
+ (events == "*" ||
+ (Array.isArray(events) && events.length))) {
+ this._pauseOnDOMEvents = events;
+ let els = Cc["@mozilla.org/eventlistenerservice;1"]
+ .getService(Ci.nsIEventListenerService);
+ els.addListenerForAllEvents(this.global, this._allEventsListener, true);
+ }
+ },
+
+ /**
+ * If we are tasked with breaking on the load event, we have to add the
+ * listener early enough.
+ */
+ _onWindowReady: function () {
+ this._maybeListenToEvents({
+ pauseOnDOMEvents: this._pauseOnDOMEvents
+ });
+ },
+
+ /**
+ * Handle a protocol request to resume execution of the debuggee.
+ */
+ onResume: function (aRequest) {
+ if (this._state !== "paused") {
+ return {
+ error: "wrongState",
+ message: "Can't resume when debuggee isn't paused. Current state is '"
+ + this._state + "'",
+ state: this._state
+ };
+ }
+
+ // In case of multiple nested event loops (due to multiple debuggers open in
+ // different tabs or multiple debugger clients connected to the same tab)
+ // only allow resumption in a LIFO order.
+ if (this._nestedEventLoops.size && this._nestedEventLoops.lastPausedUrl
+ && (this._nestedEventLoops.lastPausedUrl !== this._parent.url
+ || this._nestedEventLoops.lastConnection !== this.conn)) {
+ return {
+ error: "wrongOrder",
+ message: "trying to resume in the wrong order.",
+ lastPausedUrl: this._nestedEventLoops.lastPausedUrl
+ };
+ }
+
+ let resumeLimitHandled;
+ if (aRequest && aRequest.resumeLimit) {
+ resumeLimitHandled = this._handleResumeLimit(aRequest);
+ } else {
+ this._clearSteppingHooks(this.youngestFrame);
+ resumeLimitHandled = resolve(true);
+ }
+
+ return resumeLimitHandled.then(() => {
+ if (aRequest) {
+ this._options.pauseOnExceptions = aRequest.pauseOnExceptions;
+ this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions;
+ this.maybePauseOnExceptions();
+ this._maybeListenToEvents(aRequest);
+ }
+
+ let packet = this._resumed();
+ this._popThreadPause();
+ // Tell anyone who cares of the resume (as of now, that's the xpcshell
+ // harness)
+ if (Services.obs) {
+ Services.obs.notifyObservers(this, "devtools-thread-resumed", null);
+ }
+ return packet;
+ }, error => {
+ return error instanceof Error
+ ? { error: "unknownError",
+ message: DevToolsUtils.safeErrorString(error) }
+ // It is a known error, and the promise was rejected with an error
+ // packet.
+ : error;
+ });
+ },
+
+ /**
+ * Spin up a nested event loop so we can synchronously resolve a promise.
+ *
+ * DON'T USE THIS UNLESS YOU ABSOLUTELY MUST! Nested event loops suck: the
+ * world's state can change out from underneath your feet because JS is no
+ * longer run-to-completion.
+ *
+ * @param aPromise
+ * The promise we want to resolve.
+ * @returns The promise's resolution.
+ */
+ unsafeSynchronize: function (aPromise) {
+ let needNest = true;
+ let eventLoop;
+ let returnVal;
+
+ aPromise
+ .then((aResolvedVal) => {
+ needNest = false;
+ returnVal = aResolvedVal;
+ })
+ .then(null, (aError) => {
+ reportError(aError, "Error inside unsafeSynchronize:");
+ })
+ .then(() => {
+ if (eventLoop) {
+ eventLoop.resolve();
+ }
+ });
+
+ if (needNest) {
+ eventLoop = this._nestedEventLoops.push();
+ eventLoop.enter();
+ }
+
+ return returnVal;
+ },
+
+ /**
+ * Set the debugging hook to pause on exceptions if configured to do so.
+ */
+ maybePauseOnExceptions: function () {
+ if (this._options.pauseOnExceptions) {
+ this.dbg.onExceptionUnwind = this.onExceptionUnwind.bind(this);
+ }
+ },
+
+ /**
+ * A listener that gets called for every event fired on the page, when a list
+ * of interesting events was provided with the pauseOnDOMEvents property. It
+ * is used to set server-managed breakpoints on any existing event listeners
+ * for those events.
+ *
+ * @param Event event
+ * The event that was fired.
+ */
+ _allEventsListener: function (event) {
+ if (this._pauseOnDOMEvents == "*" ||
+ this._pauseOnDOMEvents.indexOf(event.type) != -1) {
+ for (let listener of this._getAllEventListeners(event.target)) {
+ if (event.type == listener.type || this._pauseOnDOMEvents == "*") {
+ this._breakOnEnter(listener.script);
+ }
+ }
+ }
+ },
+
+ /**
+ * Return an array containing all the event listeners attached to the
+ * specified event target and its ancestors in the event target chain.
+ *
+ * @param EventTarget eventTarget
+ * The target the event was dispatched on.
+ * @returns Array
+ */
+ _getAllEventListeners: function (eventTarget) {
+ let els = Cc["@mozilla.org/eventlistenerservice;1"]
+ .getService(Ci.nsIEventListenerService);
+
+ let targets = els.getEventTargetChainFor(eventTarget, true);
+ let listeners = [];
+
+ for (let target of targets) {
+ let handlers = els.getListenerInfoFor(target);
+ for (let handler of handlers) {
+ // Null is returned for all-events handlers, and native event listeners
+ // don't provide any listenerObject, which makes them not that useful to
+ // a JS debugger.
+ if (!handler || !handler.listenerObject || !handler.type)
+ continue;
+ // Create a listener-like object suitable for our purposes.
+ let l = Object.create(null);
+ l.type = handler.type;
+ let listener = handler.listenerObject;
+ let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener);
+ // If the listener is not callable, assume it is an event handler object.
+ if (!listenerDO.callable) {
+ // For some events we don't have permission to access the
+ // 'handleEvent' property when running in content scope.
+ if (!listenerDO.unwrap()) {
+ continue;
+ }
+ let heDesc;
+ while (!heDesc && listenerDO) {
+ heDesc = listenerDO.getOwnPropertyDescriptor("handleEvent");
+ listenerDO = listenerDO.proto;
+ }
+ if (heDesc && heDesc.value) {
+ listenerDO = heDesc.value;
+ }
+ }
+ // When the listener is a bound function, we are actually interested in
+ // the target function.
+ while (listenerDO.isBoundFunction) {
+ listenerDO = listenerDO.boundTargetFunction;
+ }
+ l.script = listenerDO.script;
+ // Chrome listeners won't be converted to debuggee values, since their
+ // compartment is not added as a debuggee.
+ if (!l.script)
+ continue;
+ listeners.push(l);
+ }
+ }
+ return listeners;
+ },
+
+ /**
+ * Set a breakpoint on the first line of the given script that has an entry
+ * point.
+ */
+ _breakOnEnter: function (script) {
+ let offsets = script.getAllOffsets();
+ for (let line = 0, n = offsets.length; line < n; line++) {
+ if (offsets[line]) {
+ // N.B. Hidden breakpoints do not have an original location, and are not
+ // stored in the breakpoint actor map.
+ let actor = new BreakpointActor(this);
+ this.threadLifetimePool.addActor(actor);
+
+ let scripts = this.dbg.findScripts({ source: script.source, line: line });
+ let entryPoints = findEntryPointsForLine(scripts, line);
+ setBreakpointAtEntryPoints(actor, entryPoints);
+ this._hiddenBreakpoints.set(actor.actorID, actor);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Helper method that returns the next frame when stepping.
+ */
+ _getNextStepFrame: function (aFrame) {
+ let stepFrame = aFrame.reportedPop ? aFrame.older : aFrame;
+ if (!stepFrame || !stepFrame.script) {
+ stepFrame = null;
+ }
+ return stepFrame;
+ },
+
+ onClientEvaluate: function (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." };
+ }
+
+ 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 (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 promises = [];
+ for (; frame && (!count || i < (start + count)); i++, frame = frame.older) {
+ let form = this._createFrameActor(frame).form();
+ form.depth = i;
+
+ let promise = this.sources.getOriginalLocation(new GeneratedLocation(
+ this.sources.createNonSourceMappedActor(frame.script.source),
+ form.where.line,
+ form.where.column
+ )).then((originalLocation) => {
+ if (!originalLocation.originalSourceActor) {
+ return null;
+ }
+
+ let sourceForm = originalLocation.originalSourceActor.form();
+ form.where = {
+ source: sourceForm,
+ line: originalLocation.originalLine,
+ column: originalLocation.originalColumn
+ };
+ form.source = sourceForm;
+ return form;
+ });
+ promises.push(promise);
+ }
+
+ return all(promises).then(function (frames) {
+ // Filter null values because sourcemapping may have failed.
+ return { frames: frames.filter(x => !!x) };
+ });
+ },
+
+ onReleaseMany: function (aRequest) {
+ if (!aRequest.actors) {
+ return { error: "missingParameter",
+ message: "no actors were specified" };
+ }
+
+ let res;
+ for (let actorID of aRequest.actors) {
+ let actor = this.threadLifetimePool.get(actorID);
+ if (!actor) {
+ if (!res) {
+ res = { error: "notReleasable",
+ message: "Only thread-lifetime actors can be released." };
+ }
+ continue;
+ }
+ actor.onRelease();
+ }
+ return res ? res : {};
+ },
+
+ /**
+ * Get the script and source lists from the debugger.
+ */
+ _discoverSources: function () {
+ // Only get one script per Debugger.Source.
+ const sourcesToScripts = new Map();
+ const scripts = this.dbg.findScripts();
+
+ for (let i = 0, len = scripts.length; i < len; i++) {
+ let s = scripts[i];
+ if (s.source) {
+ sourcesToScripts.set(s.source, s);
+ }
+ }
+
+ return all([...sourcesToScripts.values()].map(script => {
+ return this.sources.createSourceActors(script.source);
+ }));
+ },
+
+ onSources: function (aRequest) {
+ return this._discoverSources().then(() => {
+ // No need to flush the new source packets here, as we are sending the
+ // list of sources out immediately and we don't need to invoke the
+ // overhead of an RDP packet for every source right now. Let the default
+ // timeout flush the buffered packets.
+
+ return {
+ sources: this.sources.iter().map(s => s.form())
+ };
+ });
+ },
+
+ /**
+ * Disassociate all breakpoint actors from their scripts and clear the
+ * breakpoint handlers. This method can be used when the thread actor intends
+ * to keep the breakpoint store, but needs to clear any actual breakpoints,
+ * e.g. due to a page navigation. This way the breakpoint actors' script
+ * caches won't hold on to the Debugger.Script objects leaking memory.
+ */
+ disableAllBreakpoints: function () {
+ for (let bpActor of this.breakpointActorMap.findActors()) {
+ bpActor.removeScripts();
+ }
+ },
+
+
+ /**
+ * Handle a protocol request to pause the debuggee.
+ */
+ onInterrupt: function (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 {
+ // If execution should pause just before the next JavaScript bytecode is
+ // executed, just set an onEnterFrame handler.
+ if (aRequest.when == "onNext") {
+ let onEnterFrame = (aFrame) => {
+ return this._pauseAndRespond(aFrame, { type: "interrupted", onNext: true });
+ };
+ this.dbg.onEnterFrame = onEnterFrame;
+
+ return { type: "willInterrupt" };
+ }
+
+ // If execution should pause immediately, just 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._pushThreadPause();
+
+ // We already sent a response to this request, don't send one
+ // now.
+ return null;
+ } catch (e) {
+ reportError(e);
+ return { error: "notInterrupted", message: e.toString() };
+ }
+ },
+
+ /**
+ * Handle a protocol request to retrieve all the event listeners on the page.
+ */
+ onEventListeners: function (aRequest) {
+ // This request is only supported in content debugging.
+ if (!this.global) {
+ return {
+ error: "notImplemented",
+ message: "eventListeners request is only supported in content debugging"
+ };
+ }
+
+ let els = Cc["@mozilla.org/eventlistenerservice;1"]
+ .getService(Ci.nsIEventListenerService);
+
+ let nodes = this.global.document.getElementsByTagName("*");
+ nodes = [this.global].concat([].slice.call(nodes));
+ let listeners = [];
+
+ for (let node of nodes) {
+ let handlers = els.getListenerInfoFor(node);
+
+ for (let handler of handlers) {
+ // Create a form object for serializing the listener via the protocol.
+ let listenerForm = Object.create(null);
+ let listener = handler.listenerObject;
+ // Native event listeners don't provide any listenerObject or type and
+ // are not that useful to a JS debugger.
+ if (!listener || !handler.type) {
+ continue;
+ }
+
+ // There will be no tagName if the event listener is set on the window.
+ let selector = node.tagName ? CssLogic.findCssSelector(node) : "window";
+ let nodeDO = this.globalDebugObject.makeDebuggeeValue(node);
+ listenerForm.node = {
+ selector: selector,
+ object: createValueGrip(nodeDO, this._pausePool, this.objectGrip)
+ };
+ listenerForm.type = handler.type;
+ listenerForm.capturing = handler.capturing;
+ listenerForm.allowsUntrusted = handler.allowsUntrusted;
+ listenerForm.inSystemEventGroup = handler.inSystemEventGroup;
+ let handlerName = "on" + listenerForm.type;
+ listenerForm.isEventHandler = false;
+ if (typeof node.hasAttribute !== "undefined") {
+ listenerForm.isEventHandler = !!node.hasAttribute(handlerName);
+ }
+ if (!!node[handlerName]) {
+ listenerForm.isEventHandler = !!node[handlerName];
+ }
+ // Get the Debugger.Object for the listener object.
+ let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener);
+ // If the listener is an object with a 'handleEvent' method, use that.
+ if (listenerDO.class == "Object" || listenerDO.class == "XULElement") {
+ // For some events we don't have permission to access the
+ // 'handleEvent' property when running in content scope.
+ if (!listenerDO.unwrap()) {
+ continue;
+ }
+ let heDesc;
+ while (!heDesc && listenerDO) {
+ heDesc = listenerDO.getOwnPropertyDescriptor("handleEvent");
+ listenerDO = listenerDO.proto;
+ }
+ if (heDesc && heDesc.value) {
+ listenerDO = heDesc.value;
+ }
+ }
+ // When the listener is a bound function, we are actually interested in
+ // the target function.
+ while (listenerDO.isBoundFunction) {
+ listenerDO = listenerDO.boundTargetFunction;
+ }
+ listenerForm.function = createValueGrip(listenerDO, this._pausePool,
+ this.objectGrip);
+ listeners.push(listenerForm);
+ }
+ }
+ return { listeners: listeners };
+ },
+
+ /**
+ * Return the Debug.Frame for a frame mentioned by the protocol.
+ */
+ _requestFrame: function (aFrameID) {
+ if (!aFrameID) {
+ return this.youngestFrame;
+ }
+
+ if (this._framePool.has(aFrameID)) {
+ return this._framePool.get(aFrameID).frame;
+ }
+
+ return undefined;
+ },
+
+ _paused: function (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;
+ this.dbg.onExceptionUnwind = undefined;
+ if (aFrame) {
+ aFrame.onStep = undefined;
+ aFrame.onPop = undefined;
+ }
+
+ // Clear DOM event breakpoints.
+ // XPCShell tests don't use actual DOM windows for globals and cause
+ // removeListenerForAllEvents to throw.
+ if (!isWorker && this.global && !this.global.toString().includes("Sandbox")) {
+ let els = Cc["@mozilla.org/eventlistenerservice;1"]
+ .getService(Ci.nsIEventListenerService);
+ els.removeListenerForAllEvents(this.global, this._allEventsListener, true);
+ for (let [, bp] of this._hiddenBreakpoints) {
+ bp.delete();
+ }
+ this._hiddenBreakpoints.clear();
+ }
+
+ this._state = "paused";
+
+ // Create the actor pool that will hold the pause actor and its
+ // children.
+ assert(!this._pausePool, "No pause pool should exist yet");
+ 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...
+ assert(!this._pauseActor, "No pause actor should exist yet");
+ 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;
+ },
+
+ _resumed: function () {
+ this._state = "running";
+
+ // Drop the actors in the pause actor pool.
+ this.conn.removeActorPool(this._pausePool);
+
+ this._pausePool = null;
+ this._pauseActor = 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 () {
+ let popped = [];
+
+ // Create the actor pool that will hold the still-living frames.
+ let framePool = new ActorPool(this.conn);
+ let frameList = [];
+
+ for (let frameActor of 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 (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 provided
+ * Debugger.Environment.
+ * @param Debugger.Environment aEnvironment
+ * The lexical environment we want to extract.
+ * @param object aPool
+ * The pool where the newly-created actor will be placed.
+ * @return The EnvironmentActor for aEnvironment or undefined for host
+ * functions or functions scoped to a non-debuggee global.
+ */
+ createEnvironmentActor: function (aEnvironment, aPool) {
+ if (!aEnvironment) {
+ return undefined;
+ }
+
+ if (aEnvironment.actor) {
+ return aEnvironment.actor;
+ }
+
+ let actor = new EnvironmentActor(aEnvironment, this);
+ aPool.addActor(actor);
+ aEnvironment.actor = actor;
+
+ return actor;
+ },
+
+ /**
+ * Return a protocol completion value representing the given
+ * Debugger-provided completion value.
+ */
+ createProtocolCompletionValue: function (aCompletion) {
+ let protoValue = {};
+ if (aCompletion == null) {
+ protoValue.terminated = true;
+ } else if ("return" in aCompletion) {
+ protoValue.return = createValueGrip(aCompletion.return,
+ this._pausePool, this.objectGrip);
+ } else if ("throw" in aCompletion) {
+ protoValue.throw = createValueGrip(aCompletion.throw,
+ this._pausePool, this.objectGrip);
+ } else {
+ protoValue.return = createValueGrip(aCompletion.yield,
+ this._pausePool, this.objectGrip);
+ }
+ 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 (aValue, aPool) {
+ if (!aPool.objectActors) {
+ aPool.objectActors = new WeakMap();
+ }
+
+ if (aPool.objectActors.has(aValue)) {
+ return aPool.objectActors.get(aValue).grip();
+ } else if (this.threadLifetimePool.objectActors.has(aValue)) {
+ return this.threadLifetimePool.objectActors.get(aValue).grip();
+ }
+
+ let actor = new PauseScopedObjectActor(aValue, {
+ getGripDepth: () => this._gripDepth,
+ incrementGripDepth: () => this._gripDepth++,
+ decrementGripDepth: () => this._gripDepth--,
+ createValueGrip: v => createValueGrip(v, this._pausePool,
+ this.pauseObjectGrip),
+ sources: () => this.sources,
+ createEnvironmentActor: (env, pool) =>
+ this.createEnvironmentActor(env, pool),
+ promote: () => this.threadObjectGrip(actor),
+ isThreadLifetimePool: () =>
+ actor.registeredPool !== this.threadLifetimePool,
+ getGlobalDebugObject: () => this.globalDebugObject
+ });
+ 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 (aValue) {
+ if (!this._pausePool) {
+ throw "Object grip requested while not paused.";
+ }
+
+ return this.objectGrip(aValue, this._pausePool);
+ },
+
+ /**
+ * Extend the lifetime of the provided object actor to thread lifetime.
+ *
+ * @param aActor object
+ * The object actor.
+ */
+ threadObjectGrip: function (aActor) {
+ // We want to reuse the existing actor ID, so we just remove it from the
+ // current pool's weak map and then let pool.addActor do the rest.
+ aActor.registeredPool.objectActors.delete(aActor.obj);
+ this.threadLifetimePool.addActor(aActor);
+ this.threadLifetimePool.objectActors.set(aActor.obj, aActor);
+ },
+
+ /**
+ * Handle a protocol request to promote multiple pause-lifetime grips to
+ * thread-lifetime grips.
+ *
+ * @param aRequest object
+ * The protocol request object.
+ */
+ onThreadGrips: function (aRequest) {
+ if (this.state != "paused") {
+ return { error: "wrongState" };
+ }
+
+ if (!aRequest.actors) {
+ return { error: "missingParameter",
+ message: "no actors were specified" };
+ }
+
+ for (let actorID of aRequest.actors) {
+ let actor = this._pausePool.get(actorID);
+ if (actor) {
+ this.threadObjectGrip(actor);
+ }
+ }
+ return {};
+ },
+
+ /**
+ * Create a long string grip that is scoped to a pause.
+ *
+ * @param aString String
+ * The string we are creating a grip for.
+ */
+ pauseLongStringGrip: function (aString) {
+ return longStringGrip(aString, this._pausePool);
+ },
+
+ /**
+ * Create a long string grip that is scoped to a thread.
+ *
+ * @param aString String
+ * The string we are creating a grip for.
+ */
+ threadLongStringGrip: function (aString) {
+ return longStringGrip(aString, 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 (aException) {
+ dumpn("Got an exception: " + aException.message + "\n" + aException.stack);
+ },
+
+ /**
+ * 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 (aFrame) {
+ // Don't pause if we are currently stepping (in or over) or the frame is
+ // black-boxed.
+ const generatedLocation = this.sources.getFrameLocation(aFrame);
+ const { originalSourceActor } = this.unsafeSynchronize(this.sources.getOriginalLocation(
+ generatedLocation));
+ const url = originalSourceActor ? originalSourceActor.url : null;
+
+ return this.sources.isBlackBoxed(url) || aFrame.onStep
+ ? undefined
+ : this._pauseAndRespond(aFrame, { type: "debuggerStatement" });
+ },
+
+ /**
+ * A function that the engine calls when an exception has been thrown and has
+ * propagated to the specified frame.
+ *
+ * @param aFrame Debugger.Frame
+ * The youngest remaining stack frame.
+ * @param aValue object
+ * The exception that was thrown.
+ */
+ onExceptionUnwind: function (aFrame, aValue) {
+ let willBeCaught = false;
+ for (let frame = aFrame; frame != null; frame = frame.older) {
+ if (frame.script.isInCatchScope(frame.offset)) {
+ willBeCaught = true;
+ break;
+ }
+ }
+
+ if (willBeCaught && this._options.ignoreCaughtExceptions) {
+ return undefined;
+ }
+
+ // NS_ERROR_NO_INTERFACE exceptions are a special case in browser code,
+ // since they're almost always thrown by QueryInterface functions, and
+ // handled cleanly by native code.
+ if (aValue == Cr.NS_ERROR_NO_INTERFACE) {
+ return undefined;
+ }
+
+ const generatedLocation = this.sources.getFrameLocation(aFrame);
+ const { originalSourceActor } = this.unsafeSynchronize(this.sources.getOriginalLocation(
+ generatedLocation));
+ const url = originalSourceActor ? originalSourceActor.url : null;
+
+ if (this.sources.isBlackBoxed(url)) {
+ return undefined;
+ }
+
+ try {
+ let packet = this._paused(aFrame);
+ if (!packet) {
+ return undefined;
+ }
+
+ packet.why = { type: "exception",
+ exception: createValueGrip(aValue, this._pausePool,
+ this.objectGrip)
+ };
+ this.conn.send(packet);
+
+ this._pushThreadPause();
+ } catch (e) {
+ reportError(e, "Got an exception during TA_onExceptionUnwind: ");
+ }
+
+ return undefined;
+ },
+
+ /**
+ * 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 (aScript, aGlobal) {
+ this._addSource(aScript.source);
+ },
+
+ /**
+ * A function called when there's a new or updated source from a thread actor's
+ * sources. Emits `newSource` and `updatedSource` on the tab actor.
+ *
+ * @param {String} name
+ * @param {SourceActor} source
+ */
+ onSourceEvent: function (name, source) {
+ this.conn.send({
+ from: this._parent.actorID,
+ type: name,
+ source: source.form()
+ });
+
+ // For compatibility and debugger still using `newSource` on the thread client,
+ // still emit this event here. Clean up in bug 1247084
+ if (name === "newSource") {
+ this.conn.send({
+ from: this.actorID,
+ type: name,
+ source: source.form()
+ });
+ }
+ },
+
+ /**
+ * Add the provided source to the server cache.
+ *
+ * @param aSource Debugger.Source
+ * The source that will be stored.
+ * @returns true, if the source was added; false otherwise.
+ */
+ _addSource: function (aSource) {
+ if (!this.sources.allowSource(aSource) || this._debuggerSourcesSeen.has(aSource)) {
+ return false;
+ }
+
+ let sourceActor = this.sources.createNonSourceMappedActor(aSource);
+ let bpActors = [...this.breakpointActorMap.findActors()];
+
+ if (this._options.useSourceMaps) {
+ let promises = [];
+
+ // Go ahead and establish the source actors for this script, which
+ // fetches sourcemaps if available and sends onNewSource
+ // notifications.
+ let sourceActorsCreated = this.sources._createSourceMappedActors(aSource);
+
+ if (bpActors.length) {
+ // We need to use unsafeSynchronize here because if the page is being reloaded,
+ // this call will replace the previous set of source actors for this source
+ // with a new one. If the source actors have not been replaced by the time
+ // we try to reset the breakpoints below, their location objects will still
+ // point to the old set of source actors, which point to different
+ // scripts.
+ this.unsafeSynchronize(sourceActorsCreated);
+ }
+
+ for (let _actor of bpActors) {
+ // XXX bug 1142115: We do async work in here, so we need to create a fresh
+ // binding because for/of does not yet do that in SpiderMonkey.
+ let actor = _actor;
+
+ if (actor.isPending) {
+ promises.push(actor.originalLocation.originalSourceActor._setBreakpoint(actor));
+ } else {
+ promises.push(this.sources.getAllGeneratedLocations(actor.originalLocation)
+ .then((generatedLocations) => {
+ if (generatedLocations.length > 0 &&
+ generatedLocations[0].generatedSourceActor.actorID === sourceActor.actorID) {
+ sourceActor._setBreakpointAtAllGeneratedLocations(actor, generatedLocations);
+ }
+ }));
+ }
+ }
+
+ if (promises.length > 0) {
+ this.unsafeSynchronize(promise.all(promises));
+ }
+ } else {
+ // Bug 1225160: If addSource is called in response to a new script
+ // notification, and this notification was triggered by loading a JSM from
+ // chrome code, calling unsafeSynchronize could cause a debuggee timer to
+ // fire. If this causes the JSM to be loaded a second time, the browser
+ // will crash, because loading JSMS is not reentrant, and the first load
+ // has not completed yet.
+ //
+ // The root of the problem is that unsafeSynchronize can cause debuggee
+ // code to run. Unfortunately, fixing that is prohibitively difficult. The
+ // best we can do at the moment is disable source maps for the browser
+ // debugger, and carefully avoid the use of unsafeSynchronize in this
+ // function when source maps are disabled.
+ for (let actor of bpActors) {
+ if (actor.isPending) {
+ actor.originalLocation.originalSourceActor._setBreakpoint(actor);
+ } else {
+ actor.originalLocation.originalSourceActor._setBreakpointAtGeneratedLocation(
+ actor, GeneratedLocation.fromOriginalLocation(actor.originalLocation)
+ );
+ }
+ }
+ }
+
+ this._debuggerSourcesSeen.add(aSource);
+ return true;
+ },
+
+
+ /**
+ * Get prototypes and properties of multiple objects.
+ */
+ onPrototypesAndProperties: function (aRequest) {
+ let result = {};
+ for (let actorID of aRequest.actors) {
+ // This code assumes that there are no lazily loaded actors returned
+ // by this call.
+ let actor = this.conn.getActor(actorID);
+ if (!actor) {
+ return { from: this.actorID,
+ error: "noSuchActor" };
+ }
+ let handler = actor.onPrototypeAndProperties;
+ if (!handler) {
+ return { from: this.actorID,
+ error: "unrecognizedPacketType",
+ message: ('Actor "' + actorID +
+ '" does not recognize the packet type ' +
+ '"prototypeAndProperties"') };
+ }
+ result[actorID] = handler.call(actor, {});
+ }
+ return { from: this.actorID,
+ actors: result };
+ }
+});
+
+ThreadActor.prototype.requestTypes = object.merge(ThreadActor.prototype.requestTypes, {
+ "attach": ThreadActor.prototype.onAttach,
+ "detach": ThreadActor.prototype.onDetach,
+ "reconfigure": ThreadActor.prototype.onReconfigure,
+ "resume": ThreadActor.prototype.onResume,
+ "clientEvaluate": ThreadActor.prototype.onClientEvaluate,
+ "frames": ThreadActor.prototype.onFrames,
+ "interrupt": ThreadActor.prototype.onInterrupt,
+ "eventListeners": ThreadActor.prototype.onEventListeners,
+ "releaseMany": ThreadActor.prototype.onReleaseMany,
+ "sources": ThreadActor.prototype.onSources,
+ "threadGrips": ThreadActor.prototype.onThreadGrips,
+ "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
+});
+
+exports.ThreadActor = ThreadActor;
+
+/**
+ * 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"
+};
+
+
+/**
+ * A base actor for any actors that should only respond receive messages in the
+ * paused state. Subclasses may expose a `threadActor` which is used to help
+ * determine when we are in a paused state. Subclasses should set their own
+ * "constructor" property if they want better error messages. You should never
+ * instantiate a PauseScopedActor directly, only through subclasses.
+ */
+function PauseScopedActor()
+{
+}
+
+/**
+ * A function decorator for creating methods to handle protocol messages that
+ * should only be received while in the paused state.
+ *
+ * @param aMethod Function
+ * The function we are decorating.
+ */
+PauseScopedActor.withPaused = function (aMethod) {
+ return function () {
+ if (this.isPaused()) {
+ return aMethod.apply(this, arguments);
+ } else {
+ return this._wrongState();
+ }
+ };
+};
+
+PauseScopedActor.prototype = {
+
+ /**
+ * Returns true if we are in the paused state.
+ */
+ isPaused: function () {
+ // When there is not a ThreadActor available (like in the webconsole) we
+ // have to be optimistic and assume that we are paused so that we can
+ // respond to requests.
+ return this.threadActor ? this.threadActor.state === "paused" : true;
+ },
+
+ /**
+ * Returns the wrongState response packet for this actor.
+ */
+ _wrongState: function () {
+ return {
+ error: "wrongState",
+ message: this.constructor.name +
+ " actors can only be accessed while the thread is paused."
+ };
+ }
+};
+
+/**
+ * Creates a pause-scoped actor for the specified object.
+ * @see ObjectActor
+ */
+function PauseScopedObjectActor(obj, hooks) {
+ ObjectActor.call(this, obj, hooks);
+ this.hooks.promote = hooks.promote;
+ this.hooks.isThreadLifetimePool = hooks.isThreadLifetimePool;
+}
+
+PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype);
+
+Object.assign(PauseScopedObjectActor.prototype, ObjectActor.prototype);
+
+Object.assign(PauseScopedObjectActor.prototype, {
+ constructor: PauseScopedObjectActor,
+ actorPrefix: "pausedobj",
+
+ onOwnPropertyNames:
+ PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames),
+
+ onPrototypeAndProperties:
+ PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties),
+
+ onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype),
+ onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty),
+ onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile),
+
+ onDisplayString:
+ PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString),
+
+ onParameterNames:
+ PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames),
+
+ /**
+ * Handle a protocol request to promote a pause-lifetime grip to a
+ * thread-lifetime grip.
+ *
+ * @param aRequest object
+ * The protocol request object.
+ */
+ onThreadGrip: PauseScopedActor.withPaused(function (aRequest) {
+ this.hooks.promote();
+ return {};
+ }),
+
+ /**
+ * Handle a protocol request to release a thread-lifetime grip.
+ *
+ * @param aRequest object
+ * The protocol request object.
+ */
+ onRelease: PauseScopedActor.withPaused(function (aRequest) {
+ if (this.hooks.isThreadLifetimePool()) {
+ return { error: "notReleasable",
+ message: "Only thread-lifetime actors can be released." };
+ }
+
+ this.release();
+ return {};
+ }),
+});
+
+Object.assign(PauseScopedObjectActor.prototype.requestTypes, {
+ "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip,
+});
+
+function hackDebugger(Debugger) {
+ // TODO: Improve native code instead of hacking on top of it
+
+ /**
+ * Override the toString method in order to get more meaningful script output
+ * for debugging the debugger.
+ */
+ Debugger.Script.prototype.toString = function () {
+ let output = "";
+ if (this.url) {
+ output += this.url;
+ }
+ if (typeof this.staticLevel != "undefined") {
+ output += ":L" + this.staticLevel;
+ }
+ if (typeof this.startLine != "undefined") {
+ output += ":" + this.startLine;
+ if (this.lineCount && this.lineCount > 1) {
+ output += "-" + (this.startLine + this.lineCount - 1);
+ }
+ }
+ if (typeof this.startLine != "undefined") {
+ output += ":" + this.startLine;
+ if (this.lineCount && this.lineCount > 1) {
+ output += "-" + (this.startLine + this.lineCount - 1);
+ }
+ }
+ if (this.strictMode) {
+ output += ":strict";
+ }
+ return output;
+ };
+
+ /**
+ * Helper property for quickly getting to the line number a stack frame is
+ * currently paused at.
+ */
+ Object.defineProperty(Debugger.Frame.prototype, "line", {
+ configurable: true,
+ get: function () {
+ if (this.script) {
+ return this.script.getOffsetLocation(this.offset).lineNumber;
+ } else {
+ return null;
+ }
+ }
+ });
+}
+
+
+/**
+ * Creates an actor for handling chrome debugging. ChromeDebuggerActor is a
+ * thin wrapper over ThreadActor, slightly changing some of its behavior.
+ *
+ * @param aConnection object
+ * The DebuggerServerConnection with which this ChromeDebuggerActor
+ * is associated. (Currently unused, but required to make this
+ * constructor usable with addGlobalActor.)
+ *
+ * @param aParent object
+ * This actor's parent actor. See ThreadActor for a list of expected
+ * properties.
+ */
+function ChromeDebuggerActor(aConnection, aParent)
+{
+ ThreadActor.prototype.initialize.call(this, aParent);
+}
+
+ChromeDebuggerActor.prototype = Object.create(ThreadActor.prototype);
+
+Object.assign(ChromeDebuggerActor.prototype, {
+ constructor: ChromeDebuggerActor,
+
+ // A constant prefix that will be used to form the actor ID by the server.
+ actorPrefix: "chromeDebugger"
+});
+
+exports.ChromeDebuggerActor = ChromeDebuggerActor;
+
+/**
+ * Creates an actor for handling add-on debugging. AddonThreadActor is
+ * a thin wrapper over ThreadActor.
+ *
+ * @param aConnection object
+ * The DebuggerServerConnection with which this AddonThreadActor
+ * is associated. (Currently unused, but required to make this
+ * constructor usable with addGlobalActor.)
+ *
+ * @param aParent object
+ * This actor's parent actor. See ThreadActor for a list of expected
+ * properties.
+ */
+function AddonThreadActor(aConnect, aParent) {
+ ThreadActor.prototype.initialize.call(this, aParent);
+}
+
+AddonThreadActor.prototype = Object.create(ThreadActor.prototype);
+
+Object.assign(AddonThreadActor.prototype, {
+ constructor: AddonThreadActor,
+
+ // A constant prefix that will be used to form the actor ID by the server.
+ actorPrefix: "addonThread"
+});
+
+exports.AddonThreadActor = AddonThreadActor;
+
+// Utility functions.
+
+/**
+ * Report the given error in the error console and to stdout.
+ *
+ * @param Error aError
+ * The error object you wish to report.
+ * @param String aPrefix
+ * An optional prefix for the reported error message.
+ */
+var oldReportError = reportError;
+reportError = function (aError, aPrefix = "") {
+ assert(aError instanceof Error, "Must pass Error objects to reportError");
+ let msg = aPrefix + aError.message + ":\n" + aError.stack;
+ oldReportError(msg);
+ dumpn(msg);
+};
+
+/**
+ * Find the scripts which contain offsets that are an entry point to the given
+ * line.
+ *
+ * @param Array scripts
+ * The set of Debugger.Scripts to consider.
+ * @param Number line
+ * The line we are searching for entry points into.
+ * @returns Array of objects of the form { script, offsets } where:
+ * - script is a Debugger.Script
+ * - offsets is an array of offsets that are entry points into the
+ * given line.
+ */
+function findEntryPointsForLine(scripts, line) {
+ const entryPoints = [];
+ for (let script of scripts) {
+ const offsets = script.getLineOffsets(line);
+ if (offsets.length) {
+ entryPoints.push({ script, offsets });
+ }
+ }
+ return entryPoints;
+}
+
+/**
+ * Unwrap a global that is wrapped in a |Debugger.Object|, or if the global has
+ * become a dead object, return |undefined|.
+ *
+ * @param Debugger.Object wrappedGlobal
+ * The |Debugger.Object| which wraps a global.
+ *
+ * @returns {Object|undefined}
+ * Returns the unwrapped global object or |undefined| if unwrapping
+ * failed.
+ */
+exports.unwrapDebuggerObjectGlobal = wrappedGlobal => {
+ try {
+ // Because of bug 991399 we sometimes get nuked window references here. We
+ // just bail out in that case.
+ //
+ // Note that addon sandboxes have a DOMWindow as their prototype. So make
+ // sure that we can touch the prototype too (whatever it is), in case _it_
+ // is it a nuked window reference. We force stringification to make sure
+ // that any dead object proxies make themselves known.
+ let global = wrappedGlobal.unsafeDereference();
+ Object.getPrototypeOf(global) + "";
+ return global;
+ }
+ catch (e) {
+ return undefined;
+ }
+};