summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/webbrowser.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/webbrowser.js')
-rw-r--r--devtools/server/actors/webbrowser.js2529
1 files changed, 2529 insertions, 0 deletions
diff --git a/devtools/server/actors/webbrowser.js b/devtools/server/actors/webbrowser.js
new file mode 100644
index 000000000..0edcdc187
--- /dev/null
+++ b/devtools/server/actors/webbrowser.js
@@ -0,0 +1,2529 @@
+/* -*- indent-tabs-mode: nil; 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";
+
+/* global XPCNativeWrapper */
+
+var { Ci, Cu, Cr } = require("chrome");
+var Services = require("Services");
+var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+var promise = require("promise");
+var {
+ ActorPool, createExtraActors, appendExtraActors, GeneratedLocation
+} = require("devtools/server/actors/common");
+var { DebuggerServer } = require("devtools/server/main");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { assert } = DevToolsUtils;
+var { TabSources } = require("./utils/TabSources");
+var makeDebugger = require("./utils/make-debugger");
+
+loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true);
+loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true);
+loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
+loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true);
+loader.lazyRequireGetter(this, "WebExtensionActor", "devtools/server/actors/webextension", true);
+loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker", true);
+loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "devtools/server/actors/worker", true);
+loader.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true);
+loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+loader.lazyImporter(this, "ExtensionContent", "resource://gre/modules/ExtensionContent.jsm");
+
+// Assumptions on events module:
+// events needs to be dispatched synchronously,
+// by calling the listeners in the order or registration.
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+
+loader.lazyRequireGetter(this, "StyleSheetActor", "devtools/server/actors/stylesheets", true);
+
+function getWindowID(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+}
+
+function getDocShellChromeEventHandler(docShell) {
+ let handler = docShell.chromeEventHandler;
+ if (!handler) {
+ try {
+ // Toplevel xul window's docshell doesn't have chromeEventHandler
+ // attribute. The chrome event handler is just the global window object.
+ handler = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ } catch (e) {
+ // ignore
+ }
+ }
+ return handler;
+}
+
+function getChildDocShells(parentDocShell) {
+ let docShellsEnum = parentDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeAll,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS
+ );
+
+ let docShells = [];
+ while (docShellsEnum.hasMoreElements()) {
+ let docShell = docShellsEnum.getNext();
+ docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ docShells.push(docShell);
+ }
+ return docShells;
+}
+
+exports.getChildDocShells = getChildDocShells;
+
+/**
+ * Browser-specific actors.
+ */
+
+function getInnerId(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+}
+
+/**
+ * Yield all windows of type |windowType|, from the oldest window to the
+ * youngest, using nsIWindowMediator::getEnumerator. We're usually
+ * interested in "navigator:browser" windows.
+ */
+function* allAppShellDOMWindows(windowType) {
+ let e = Services.wm.getEnumerator(windowType);
+ while (e.hasMoreElements()) {
+ yield e.getNext();
+ }
+}
+
+exports.allAppShellDOMWindows = allAppShellDOMWindows;
+
+/**
+ * Retrieve the window type of the top-level window |window|.
+ */
+function appShellDOMWindowType(window) {
+ /* This is what nsIWindowMediator's enumerator checks. */
+ return window.document.documentElement.getAttribute("windowtype");
+}
+
+/**
+ * Send Debugger:Shutdown events to all "navigator:browser" windows.
+ */
+function sendShutdownEvent() {
+ for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
+ let evt = win.document.createEvent("Event");
+ evt.initEvent("Debugger:Shutdown", true, false);
+ win.document.documentElement.dispatchEvent(evt);
+ }
+}
+
+exports.sendShutdownEvent = sendShutdownEvent;
+
+/**
+ * Construct a root actor appropriate for use in a server running in a
+ * browser. The returned root actor:
+ * - respects the factories registered with DebuggerServer.addGlobalActor,
+ * - uses a BrowserTabList to supply tab actors,
+ * - sends all navigator:browser window documents a Debugger:Shutdown event
+ * when it exits.
+ *
+ * * @param connection DebuggerServerConnection
+ * The conection to the client.
+ */
+function createRootActor(connection) {
+ return new RootActor(connection, {
+ tabList: new BrowserTabList(connection),
+ addonList: new BrowserAddonList(connection),
+ workerList: new WorkerActorList(connection, {}),
+ serviceWorkerRegistrationList:
+ new ServiceWorkerRegistrationActorList(connection),
+ processList: new ProcessActorList(),
+ globalActorFactories: DebuggerServer.globalActorFactories,
+ onShutdown: sendShutdownEvent
+ });
+}
+
+/**
+ * A live list of BrowserTabActors representing the current browser tabs,
+ * to be provided to the root actor to answer 'listTabs' requests.
+ *
+ * This object also takes care of listening for TabClose events and
+ * onCloseWindow notifications, and exiting the BrowserTabActors concerned.
+ *
+ * (See the documentation for RootActor for the definition of the "live
+ * list" interface.)
+ *
+ * @param connection DebuggerServerConnection
+ * The connection in which this list's tab actors may participate.
+ *
+ * Some notes:
+ *
+ * This constructor is specific to the desktop browser environment; it
+ * maintains the tab list by tracking XUL windows and their XUL documents'
+ * "tabbrowser", "tab", and "browser" elements. What's entailed in maintaining
+ * an accurate list of open tabs in this context?
+ *
+ * - Opening and closing XUL windows:
+ *
+ * An nsIWindowMediatorListener is notified when new XUL windows (i.e., desktop
+ * windows) are opened and closed. It is not notified of individual content
+ * browser tabs coming and going within such a XUL window. That seems
+ * reasonable enough; it's concerned with XUL windows, not tab elements in the
+ * window's XUL document.
+ *
+ * However, even if we attach TabOpen and TabClose event listeners to each XUL
+ * window as soon as it is created:
+ *
+ * - we do not receive a TabOpen event for the initial empty tab of a new XUL
+ * window; and
+ *
+ * - we do not receive TabClose events for the tabs of a XUL window that has
+ * been closed.
+ *
+ * This means that TabOpen and TabClose events alone are not sufficient to
+ * maintain an accurate list of live tabs and mark tab actors as closed
+ * promptly. Our nsIWindowMediatorListener onCloseWindow handler must find and
+ * exit all actors for tabs that were in the closing window.
+ *
+ * Since this is a bit hairy, we don't make each individual attached tab actor
+ * responsible for noticing when it has been closed; we watch for that, and
+ * promise to call each actor's 'exit' method when it's closed, regardless of
+ * how we learn the news.
+ *
+ * - nsIWindowMediator locks
+ *
+ * nsIWindowMediator holds a lock protecting its list of top-level windows
+ * while it calls nsIWindowMediatorListener methods. nsIWindowMediator's
+ * GetEnumerator method also tries to acquire that lock. Thus, enumerating
+ * windows from within a listener method deadlocks (bug 873589). Rah. One
+ * can sometimes work around this by leaving the enumeration for a later
+ * tick.
+ *
+ * - Dragging tabs between windows:
+ *
+ * When a tab is dragged from one desktop window to another, we receive a
+ * TabOpen event for the new tab, and a TabClose event for the old tab; tab XUL
+ * elements do not really move from one document to the other (although their
+ * linked browser's content window objects do).
+ *
+ * However, while we could thus assume that each tab stays with the XUL window
+ * it belonged to when it was created, I'm not sure this is behavior one should
+ * rely upon. When a XUL window is closed, we take the less efficient, more
+ * conservative approach of simply searching the entire table for actors that
+ * belong to the closing XUL window, rather than trying to somehow track which
+ * XUL window each tab belongs to.
+ *
+ * - Title changes:
+ *
+ * For tabs living in the child process, we listen for DOMTitleChange message
+ * via the top-level window's message manager. Doing this also allows listening
+ * for title changes on Fennec.
+ * But as these messages aren't sent for tabs loaded in the parent process,
+ * we also listen for TabAttrModified event, which is fired only on Firefox
+ * desktop.
+ */
+function BrowserTabList(connection) {
+ this._connection = connection;
+
+ /*
+ * The XUL document of a tabbed browser window has "tab" elements, whose
+ * 'linkedBrowser' JavaScript properties are "browser" elements; those
+ * browsers' 'contentWindow' properties are wrappers on the tabs' content
+ * window objects.
+ *
+ * This map's keys are "browser" XUL elements; it maps each browser element
+ * to the tab actor we've created for its content window, if we've created
+ * one. This map serves several roles:
+ *
+ * - During iteration, we use it to find actors we've created previously.
+ *
+ * - On a TabClose event, we use it to find the tab's actor and exit it.
+ *
+ * - When the onCloseWindow handler is called, we iterate over it to find all
+ * tabs belonging to the closing XUL window, and exit them.
+ *
+ * - When it's empty, and the onListChanged hook is null, we know we can
+ * stop listening for events and notifications.
+ *
+ * We listen for TabClose events and onCloseWindow notifications in order to
+ * send onListChanged notifications, but also to tell actors when their
+ * referent has gone away and remove entries for dead browsers from this map.
+ * If that code is working properly, neither this map nor the actors in it
+ * should ever hold dead tabs alive.
+ */
+ this._actorByBrowser = new Map();
+
+ /* The current onListChanged handler, or null. */
+ this._onListChanged = null;
+
+ /*
+ * True if we've been iterated over since we last called our onListChanged
+ * hook.
+ */
+ this._mustNotify = false;
+
+ /* True if we're testing, and should throw if consistency checks fail. */
+ this._testing = false;
+}
+
+BrowserTabList.prototype.constructor = BrowserTabList;
+
+/**
+ * Get the selected browser for the given navigator:browser window.
+ * @private
+ * @param window nsIChromeWindow
+ * The navigator:browser window for which you want the selected browser.
+ * @return nsIDOMElement|null
+ * The currently selected xul:browser element, if any. Note that the
+ * browser window might not be loaded yet - the function will return
+ * |null| in such cases.
+ */
+BrowserTabList.prototype._getSelectedBrowser = function (window) {
+ return window.gBrowser ? window.gBrowser.selectedBrowser : null;
+};
+
+/**
+ * Produces an iterable (in this case a generator) to enumerate all available
+ * browser tabs.
+ */
+BrowserTabList.prototype._getBrowsers = function* () {
+ // Iterate over all navigator:browser XUL windows.
+ for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
+ // For each tab in this XUL window, ensure that we have an actor for
+ // it, reusing existing actors where possible. We actually iterate
+ // over 'browser' XUL elements, and BrowserTabActor uses
+ // browser.contentWindow as the debuggee global.
+ for (let browser of this._getChildren(win)) {
+ yield browser;
+ }
+ }
+};
+
+BrowserTabList.prototype._getChildren = function (window) {
+ if (!window.gBrowser) {
+ return [];
+ }
+ let { gBrowser } = window;
+ if (!gBrowser.browsers) {
+ return [];
+ }
+ return gBrowser.browsers.filter(browser => {
+ // Filter tabs that are closing. listTabs calls made right after TabClose
+ // events still list tabs in process of being closed.
+ let tab = gBrowser.getTabForBrowser(browser);
+ return !tab.closing;
+ });
+};
+
+BrowserTabList.prototype.getList = function () {
+ let topXULWindow = Services.wm.getMostRecentWindow(
+ DebuggerServer.chromeWindowType);
+ let selectedBrowser = null;
+ if (topXULWindow) {
+ selectedBrowser = this._getSelectedBrowser(topXULWindow);
+ }
+
+ // As a sanity check, make sure all the actors presently in our map get
+ // picked up when we iterate over all windows' tabs.
+ let initialMapSize = this._actorByBrowser.size;
+ this._foundCount = 0;
+
+ // To avoid mysterious behavior if tabs are closed or opened mid-iteration,
+ // we update the map first, and then make a second pass over it to yield
+ // the actors. Thus, the sequence yielded is always a snapshot of the
+ // actors that were live when we began the iteration.
+
+ let actorPromises = [];
+
+ for (let browser of this._getBrowsers()) {
+ let selected = browser === selectedBrowser;
+ actorPromises.push(
+ this._getActorForBrowser(browser)
+ .then(actor => {
+ // Set the 'selected' properties on all actors correctly.
+ actor.selected = selected;
+ return actor;
+ })
+ );
+ }
+
+ if (this._testing && initialMapSize !== this._foundCount) {
+ throw new Error("_actorByBrowser map contained actors for dead tabs");
+ }
+
+ this._mustNotify = true;
+ this._checkListening();
+
+ return promise.all(actorPromises);
+};
+
+BrowserTabList.prototype._getActorForBrowser = function (browser) {
+ // Do we have an existing actor for this browser? If not, create one.
+ let actor = this._actorByBrowser.get(browser);
+ if (actor) {
+ this._foundCount++;
+ return actor.update();
+ }
+
+ actor = new BrowserTabActor(this._connection, browser);
+ this._actorByBrowser.set(browser, actor);
+ this._checkListening();
+ return actor.connect();
+};
+
+BrowserTabList.prototype.getTab = function ({ outerWindowID, tabId }) {
+ if (typeof outerWindowID == "number") {
+ // First look for in-process frames with this ID
+ let window = Services.wm.getOuterWindowWithId(outerWindowID);
+ // Safety check to prevent debugging top level window via getTab
+ if (window instanceof Ci.nsIDOMChromeWindow) {
+ return promise.reject({
+ error: "forbidden",
+ message: "Window with outerWindowID '" + outerWindowID + "' is chrome"
+ });
+ }
+ if (window) {
+ let iframe = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .containerElement;
+ if (iframe) {
+ return this._getActorForBrowser(iframe);
+ }
+ }
+ // Then also look on registered <xul:browsers> when using outerWindowID for
+ // OOP tabs
+ for (let browser of this._getBrowsers()) {
+ if (browser.outerWindowID == outerWindowID) {
+ return this._getActorForBrowser(browser);
+ }
+ }
+ return promise.reject({
+ error: "noTab",
+ message: "Unable to find tab with outerWindowID '" + outerWindowID + "'"
+ });
+ } else if (typeof tabId == "number") {
+ // Tabs OOP
+ for (let browser of this._getBrowsers()) {
+ if (browser.frameLoader.tabParent &&
+ browser.frameLoader.tabParent.tabId === tabId) {
+ return this._getActorForBrowser(browser);
+ }
+ }
+ return promise.reject({
+ error: "noTab",
+ message: "Unable to find tab with tabId '" + tabId + "'"
+ });
+ }
+
+ let topXULWindow = Services.wm.getMostRecentWindow(
+ DebuggerServer.chromeWindowType);
+ if (topXULWindow) {
+ let selectedBrowser = this._getSelectedBrowser(topXULWindow);
+ return this._getActorForBrowser(selectedBrowser);
+ }
+ return promise.reject({
+ error: "noTab",
+ message: "Unable to find any selected browser"
+ });
+};
+
+Object.defineProperty(BrowserTabList.prototype, "onListChanged", {
+ enumerable: true,
+ configurable: true,
+ get() {
+ return this._onListChanged;
+ },
+ set(v) {
+ if (v !== null && typeof v !== "function") {
+ throw new Error(
+ "onListChanged property may only be set to 'null' or a function");
+ }
+ this._onListChanged = v;
+ this._checkListening();
+ }
+});
+
+/**
+ * The set of tabs has changed somehow. Call our onListChanged handler, if
+ * one is set, and if we haven't already called it since the last iteration.
+ */
+BrowserTabList.prototype._notifyListChanged = function () {
+ if (!this._onListChanged) {
+ return;
+ }
+ if (this._mustNotify) {
+ this._onListChanged();
+ this._mustNotify = false;
+ }
+};
+
+/**
+ * Exit |actor|, belonging to |browser|, and notify the onListChanged
+ * handle if needed.
+ */
+BrowserTabList.prototype._handleActorClose = function (actor, browser) {
+ if (this._testing) {
+ if (this._actorByBrowser.get(browser) !== actor) {
+ throw new Error("BrowserTabActor not stored in map under given browser");
+ }
+ if (actor.browser !== browser) {
+ throw new Error("actor's browser and map key don't match");
+ }
+ }
+
+ this._actorByBrowser.delete(browser);
+ actor.exit();
+
+ this._notifyListChanged();
+ this._checkListening();
+};
+
+/**
+ * Make sure we are listening or not listening for activity elsewhere in
+ * the browser, as appropriate. Other than setting up newly created XUL
+ * windows, all listener / observer connection and disconnection should
+ * happen here.
+ */
+BrowserTabList.prototype._checkListening = function () {
+ /*
+ * If we have an onListChanged handler that we haven't sent an announcement
+ * to since the last iteration, we need to watch for tab creation as well as
+ * change of the currently selected tab and tab title changes of tabs in
+ * parent process via TabAttrModified (tabs oop uses DOMTitleChanges).
+ *
+ * Oddly, we don't need to watch for 'close' events here. If our actor list
+ * is empty, then either it was empty the last time we iterated, and no
+ * close events are possible, or it was not empty the last time we
+ * iterated, but all the actors have since been closed, and we must have
+ * sent a notification already when they closed.
+ */
+ this._listenForEventsIf(this._onListChanged && this._mustNotify,
+ "_listeningForTabOpen",
+ ["TabOpen", "TabSelect", "TabAttrModified"]);
+
+ /* If we have live actors, we need to be ready to mark them dead. */
+ this._listenForEventsIf(this._actorByBrowser.size > 0,
+ "_listeningForTabClose",
+ ["TabClose", "TabRemotenessChange"]);
+
+ /*
+ * We must listen to the window mediator in either case, since that's the
+ * only way to find out about tabs that come and go when top-level windows
+ * are opened and closed.
+ */
+ this._listenToMediatorIf((this._onListChanged && this._mustNotify) ||
+ (this._actorByBrowser.size > 0));
+
+ /*
+ * We also listen for title changed from the child process.
+ * This allows listening for title changes from Fennec and OOP tabs in Fx.
+ */
+ this._listenForMessagesIf(this._onListChanged && this._mustNotify,
+ "_listeningForTitleChange",
+ ["DOMTitleChanged"]);
+};
+
+/*
+ * Add or remove event listeners for all XUL windows.
+ *
+ * @param shouldListen boolean
+ * True if we should add event handlers; false if we should remove them.
+ * @param guard string
+ * The name of a guard property of 'this', indicating whether we're
+ * already listening for those events.
+ * @param eventNames array of strings
+ * An array of event names.
+ */
+BrowserTabList.prototype._listenForEventsIf =
+ function (shouldListen, guard, eventNames) {
+ if (!shouldListen !== !this[guard]) {
+ let op = shouldListen ? "addEventListener" : "removeEventListener";
+ for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
+ for (let name of eventNames) {
+ win[op](name, this, false);
+ }
+ }
+ this[guard] = shouldListen;
+ }
+ };
+
+/*
+ * Add or remove message listeners for all XUL windows.
+ *
+ * @param aShouldListen boolean
+ * True if we should add message listeners; false if we should remove them.
+ * @param aGuard string
+ * The name of a guard property of 'this', indicating whether we're
+ * already listening for those messages.
+ * @param aMessageNames array of strings
+ * An array of message names.
+ */
+BrowserTabList.prototype._listenForMessagesIf =
+ function (shouldListen, guard, messageNames) {
+ if (!shouldListen !== !this[guard]) {
+ let op = shouldListen ? "addMessageListener" : "removeMessageListener";
+ for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
+ for (let name of messageNames) {
+ win.messageManager[op](name, this);
+ }
+ }
+ this[guard] = shouldListen;
+ }
+ };
+
+/**
+ * Implement nsIMessageListener.
+ */
+BrowserTabList.prototype.receiveMessage = DevToolsUtils.makeInfallible(
+ function (message) {
+ let browser = message.target;
+ switch (message.name) {
+ case "DOMTitleChanged": {
+ let actor = this._actorByBrowser.get(browser);
+ if (actor) {
+ this._notifyListChanged();
+ this._checkListening();
+ }
+ break;
+ }
+ }
+ });
+
+/**
+ * Implement nsIDOMEventListener.
+ */
+BrowserTabList.prototype.handleEvent =
+DevToolsUtils.makeInfallible(function (event) {
+ let browser = event.target.linkedBrowser;
+ switch (event.type) {
+ case "TabOpen":
+ case "TabSelect": {
+ /* Don't create a new actor; iterate will take care of that. Just notify. */
+ this._notifyListChanged();
+ this._checkListening();
+ break;
+ }
+ case "TabClose": {
+ let actor = this._actorByBrowser.get(browser);
+ if (actor) {
+ this._handleActorClose(actor, browser);
+ }
+ break;
+ }
+ case "TabRemotenessChange": {
+ // We have to remove the cached actor as we have to create a new instance.
+ let actor = this._actorByBrowser.get(browser);
+ if (actor) {
+ this._actorByBrowser.delete(browser);
+ // Don't create a new actor; iterate will take care of that. Just notify.
+ this._notifyListChanged();
+ this._checkListening();
+ }
+ break;
+ }
+ case "TabAttrModified": {
+ // Remote <browser> title changes are handled via DOMTitleChange message
+ // TabAttrModified is only here for browsers in parent process which
+ // don't send this message.
+ if (browser.isRemoteBrowser) {
+ break;
+ }
+ let actor = this._actorByBrowser.get(browser);
+ if (actor) {
+ // TabAttrModified is fired in various cases, here only care about title
+ // changes
+ if (event.detail.changed.includes("label")) {
+ this._notifyListChanged();
+ this._checkListening();
+ }
+ }
+ break;
+ }
+ }
+}, "BrowserTabList.prototype.handleEvent");
+
+/*
+ * If |shouldListen| is true, ensure we've registered a listener with the
+ * window mediator. Otherwise, ensure we haven't registered a listener.
+ */
+BrowserTabList.prototype._listenToMediatorIf = function (shouldListen) {
+ if (!shouldListen !== !this._listeningToMediator) {
+ let op = shouldListen ? "addListener" : "removeListener";
+ Services.wm[op](this);
+ this._listeningToMediator = shouldListen;
+ }
+};
+
+/**
+ * nsIWindowMediatorListener implementation.
+ *
+ * See _onTabClosed for explanation of why we needn't actually tweak any
+ * actors or tables here.
+ *
+ * An nsIWindowMediatorListener's methods get passed all sorts of windows; we
+ * only care about the tab containers. Those have 'getBrowser' methods.
+ */
+BrowserTabList.prototype.onWindowTitleChange = () => { };
+
+BrowserTabList.prototype.onOpenWindow =
+DevToolsUtils.makeInfallible(function (window) {
+ let handleLoad = DevToolsUtils.makeInfallible(() => {
+ /* We don't want any further load events from this window. */
+ window.removeEventListener("load", handleLoad, false);
+
+ if (appShellDOMWindowType(window) !== DebuggerServer.chromeWindowType) {
+ return;
+ }
+
+ // Listen for future tab activity.
+ if (this._listeningForTabOpen) {
+ window.addEventListener("TabOpen", this, false);
+ window.addEventListener("TabSelect", this, false);
+ window.addEventListener("TabAttrModified", this, false);
+ }
+ if (this._listeningForTabClose) {
+ window.addEventListener("TabClose", this, false);
+ window.addEventListener("TabRemotenessChange", this, false);
+ }
+ if (this._listeningForTitleChange) {
+ window.messageManager.addMessageListener("DOMTitleChanged", this);
+ }
+
+ // As explained above, we will not receive a TabOpen event for this
+ // document's initial tab, so we must notify our client of the new tab
+ // this will have.
+ this._notifyListChanged();
+ });
+
+ /*
+ * You can hardly do anything at all with a XUL window at this point; it
+ * doesn't even have its document yet. Wait until its document has
+ * loaded, and then see what we've got. This also avoids
+ * nsIWindowMediator enumeration from within listeners (bug 873589).
+ */
+ window = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+ window.addEventListener("load", handleLoad, false);
+}, "BrowserTabList.prototype.onOpenWindow");
+
+BrowserTabList.prototype.onCloseWindow =
+DevToolsUtils.makeInfallible(function (window) {
+ window = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+ if (appShellDOMWindowType(window) !== DebuggerServer.chromeWindowType) {
+ return;
+ }
+
+ /*
+ * nsIWindowMediator deadlocks if you call its GetEnumerator method from
+ * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so
+ * handle the close in a different tick.
+ */
+ Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
+ /*
+ * Scan the entire map for actors representing tabs that were in this
+ * top-level window, and exit them.
+ */
+ for (let [browser, actor] of this._actorByBrowser) {
+ /* The browser document of a closed window has no default view. */
+ if (!browser.ownerDocument.defaultView) {
+ this._handleActorClose(actor, browser);
+ }
+ }
+ }, "BrowserTabList.prototype.onCloseWindow's delayed body"), 0);
+}, "BrowserTabList.prototype.onCloseWindow");
+
+exports.BrowserTabList = BrowserTabList;
+
+/**
+ * Creates a TabActor whose main goal is to manage lifetime and
+ * expose the tab actors being registered via DebuggerServer.registerModule.
+ * But also track the lifetime of the document being tracked.
+ *
+ * ### Main requests:
+ *
+ * `attach`/`detach` requests:
+ * - start/stop document watching:
+ * Starts watching for new documents and emits `tabNavigated` and
+ * `frameUpdate` over RDP.
+ * - retrieve the thread actor:
+ * Instantiates a ThreadActor that can be later attached to in order to
+ * debug JS sources in the document.
+ * `switchToFrame`:
+ * Change the targeted document of the whole TabActor, and its child tab actors
+ * to an iframe or back to its original document.
+ *
+ * Most of the TabActor properties (like `chromeEventHandler` or `docShells`)
+ * are meant to be used by the various child tab actors.
+ *
+ * ### RDP events:
+ *
+ * - `tabNavigated`:
+ * Sent when the tab is about to navigate or has just navigated to
+ * a different document.
+ * This event contains the following attributes:
+ * * url (string) The new URI being loaded.
+ * * nativeConsoleAPI (boolean) `false` if the console API of the page has
+ * been overridden (e.g. by Firebug),
+ * `true` if the Gecko implementation is used.
+ * * state (string) `start` if we just start requesting the new URL,
+ * `stop` if the new URL is done loading.
+ * * isFrameSwitching (boolean) Indicates the event is dispatched when
+ * switching the TabActor context to
+ * a different frame. When we switch to
+ * an iframe, there is no document load.
+ * The targeted document is most likely
+ * going to be already done loading.
+ * * title (string) The document title being loaded.
+ * (sent only on state=stop)
+ *
+ * - `frameUpdate`:
+ * Sent when there was a change in the child frames contained in the document
+ * or when the tab's context was switched to another frame.
+ * This event can have four different forms depending on the type of change:
+ * * One or many frames are updated:
+ * { frames: [{ id, url, title, parentID }, ...] }
+ * * One frame got destroyed:
+ * { frames: [{ id, destroy: true }]}
+ * * All frames got destroyed:
+ * { destroyAll: true }
+ * * We switched the context of the TabActor to a specific frame:
+ * { selected: #id }
+ *
+ * ### Internal, non-rdp events:
+ * Various events are also dispatched on the TabActor itself that are not
+ * related to RDP, so, not sent to the client. They all relate to the documents
+ * tracked by the TabActor (its main targeted document, but also any of its
+ * iframes).
+ * - will-navigate
+ * This event fires once navigation starts.
+ * All pending user prompts are dealt with,
+ * but it is fired before the first request starts.
+ * - navigate
+ * This event is fired once the document's readyState is "complete".
+ * - window-ready
+ * This event is fired on three distinct scenarios:
+ * * When a new Window object is crafted, equivalent of `DOMWindowCreated`.
+ * It is dispatched before any page script is executed.
+ * * We will have already received a window-ready event for this window
+ * when it was created, but we received a window-destroyed event when
+ * it was frozen into the bfcache, and now the user navigated back to
+ * this page, so it's now live again and we should resume handling it.
+ * * For each existing document, when an `attach` request is received.
+ * At this point scripts in the page will be already loaded.
+ * - window-destroyed
+ * This event is fired in two cases:
+ * * When the window object is destroyed, i.e. when the related document
+ * is garbage collected. This can happen when the tab is closed or the
+ * iframe is removed from the DOM.
+ * It is equivalent of `inner-window-destroyed` event.
+ * * When the page goes into the bfcache and gets frozen.
+ * The equivalent of `pagehide`.
+ * - changed-toplevel-document
+ * This event fires when we switch the TabActor targeted document
+ * to one of its iframes, or back to its original top document.
+ * It is dispatched between window-destroyed and window-ready.
+ * - stylesheet-added
+ * This event is fired when a StyleSheetActor is created.
+ * It contains the following attribute :
+ * * actor (StyleSheetActor) The created actor.
+ *
+ * Note that *all* these events are dispatched in the following order
+ * when we switch the context of the TabActor to a given iframe:
+ * - will-navigate
+ * - window-destroyed
+ * - changed-toplevel-document
+ * - window-ready
+ * - navigate
+ *
+ * This class is subclassed by ContentActor and others.
+ * Subclasses are expected to implement a getter for the docShell property.
+ *
+ * @param connection DebuggerServerConnection
+ * The conection to the client.
+ */
+function TabActor(connection) {
+ this.conn = connection;
+ this._tabActorPool = null;
+ // A map of actor names to actor instances provided by extensions.
+ this._extraActors = {};
+ this._exited = false;
+ this._sources = null;
+
+ // Map of DOM stylesheets to StyleSheetActors
+ this._styleSheetActors = new Map();
+
+ this._shouldAddNewGlobalAsDebuggee =
+ this._shouldAddNewGlobalAsDebuggee.bind(this);
+
+ this.makeDebugger = makeDebugger.bind(null, {
+ findDebuggees: () => {
+ return this.windows.concat(this.webextensionsContentScriptGlobals);
+ },
+ shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee
+ });
+
+ // Flag eventually overloaded by sub classes in order to watch new docshells
+ // Used by the ChromeActor to list all frames in the Browser Toolbox
+ this.listenForNewDocShells = false;
+
+ this.traits = {
+ reconfigure: true,
+ // Supports frame listing via `listFrames` request and `frameUpdate` events
+ // as well as frame switching via `switchToFrame` request
+ frames: true,
+ // Do not require to send reconfigure request to reset the document state
+ // to what it was before using the TabActor
+ noTabReconfigureOnClose: true
+ };
+
+ this._workerActorList = null;
+ this._workerActorPool = null;
+ this._onWorkerActorListChanged = this._onWorkerActorListChanged.bind(this);
+}
+
+// XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
+// *complete* mess, needs to be rethought asap.
+
+TabActor.prototype = {
+ traits: null,
+
+ // Optional console API listener options (e.g. used by the WebExtensionActor to
+ // filter console messages by addonID), set to an empty (no options) object by default.
+ consoleAPIListenerOptions: {},
+
+ // Optional TabSources filter function (e.g. used by the WebExtensionActor to filter
+ // sources by addonID), allow all sources by default.
+ _allowSource() {
+ return true;
+ },
+
+ get exited() {
+ return this._exited;
+ },
+
+ get attached() {
+ return !!this._attached;
+ },
+
+ _tabPool: null,
+ get tabActorPool() {
+ return this._tabPool;
+ },
+
+ _contextPool: null,
+ get contextActorPool() {
+ return this._contextPool;
+ },
+
+ // A constant prefix that will be used to form the actor ID by the server.
+ actorPrefix: "tab",
+
+ /**
+ * An object on which listen for DOMWindowCreated and pageshow events.
+ */
+ get chromeEventHandler() {
+ return getDocShellChromeEventHandler(this.docShell);
+ },
+
+ /**
+ * Getter for the nsIMessageManager associated to the tab.
+ */
+ get messageManager() {
+ try {
+ return this.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ } catch (e) {
+ return null;
+ }
+ },
+
+ /**
+ * Getter for the tab's doc shell.
+ */
+ get docShell() {
+ throw new Error(
+ "The docShell getter should be implemented by a subclass of TabActor");
+ },
+
+ /**
+ * Getter for the list of all docshell in this tabActor
+ * @return {Array}
+ */
+ get docShells() {
+ return getChildDocShells(this.docShell);
+ },
+
+ /**
+ * Getter for the tab content's DOM window.
+ */
+ get window() {
+ // On xpcshell, there is no document
+ if (this.docShell) {
+ return this.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ }
+ return null;
+ },
+
+ get outerWindowID() {
+ if (this.window) {
+ return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+ }
+ return null;
+ },
+
+ /**
+ * Getter for the WebExtensions ContentScript globals related to the
+ * current tab content's DOM window.
+ */
+ get webextensionsContentScriptGlobals() {
+ // Ignore xpcshell runtime which spawn TabActors without a window.
+ if (this.window) {
+ return ExtensionContent.getContentScriptGlobalsForWindow(this.window);
+ }
+
+ return [];
+ },
+
+ /**
+ * Getter for the list of all content DOM windows in this tabActor
+ * @return {Array}
+ */
+ get windows() {
+ return this.docShells.map(docShell => {
+ return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ });
+ },
+
+ /**
+ * Getter for the original docShell the tabActor got attached to in the first
+ * place.
+ * Note that your actor should normally *not* rely on this top level docShell
+ * if you want it to show information relative to the iframe that's currently
+ * being inspected in the toolbox.
+ */
+ get originalDocShell() {
+ if (!this._originalWindow) {
+ return this.docShell;
+ }
+
+ return this._originalWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ },
+
+ /**
+ * Getter for the original window the tabActor got attached to in the first
+ * place.
+ * Note that your actor should normally *not* rely on this top level window if
+ * you want it to show information relative to the iframe that's currently
+ * being inspected in the toolbox.
+ */
+ get originalWindow() {
+ return this._originalWindow || this.window;
+ },
+
+ /**
+ * Getter for the nsIWebProgress for watching this window.
+ */
+ get webProgress() {
+ return this.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ },
+
+ /**
+ * Getter for the nsIWebNavigation for the tab.
+ */
+ get webNavigation() {
+ return this.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation);
+ },
+
+ /**
+ * Getter for the tab's document.
+ */
+ get contentDocument() {
+ return this.webNavigation.document;
+ },
+
+ /**
+ * Getter for the tab title.
+ * @return string
+ * Tab title.
+ */
+ get title() {
+ return this.contentDocument.contentTitle;
+ },
+
+ /**
+ * Getter for the tab URL.
+ * @return string
+ * Tab URL.
+ */
+ get url() {
+ if (this.webNavigation.currentURI) {
+ return this.webNavigation.currentURI.spec;
+ }
+ // Abrupt closing of the browser window may leave callbacks without a
+ // currentURI.
+ return null;
+ },
+
+ get sources() {
+ if (!this._sources) {
+ this._sources = new TabSources(this.threadActor, this._allowSource);
+ }
+ return this._sources;
+ },
+
+ /**
+ * This is called by BrowserTabList.getList for existing tab actors prior to
+ * calling |form| below. It can be used to do any async work that may be
+ * needed to assemble the form.
+ */
+ update() {
+ return promise.resolve(this);
+ },
+
+ form() {
+ assert(!this.exited,
+ "form() shouldn't be called on exited browser actor.");
+ assert(this.actorID,
+ "tab should have an actorID.");
+
+ let response = {
+ actor: this.actorID
+ };
+
+ // We may try to access window while the document is closing, then
+ // accessing window throws. Also on xpcshell we are using tabactor even if
+ // there is no valid document.
+ if (this.docShell && !this.docShell.isBeingDestroyed()) {
+ response.title = this.title;
+ response.url = this.url;
+ response.outerWindowID = this.outerWindowID;
+ }
+
+ // Always use the same ActorPool, so existing actor instances
+ // (created in createExtraActors) are not lost.
+ if (!this._tabActorPool) {
+ this._tabActorPool = new ActorPool(this.conn);
+ this.conn.addActorPool(this._tabActorPool);
+ }
+
+ // Walk over tab actor factories and make sure they are all
+ // instantiated and added into the ActorPool. Note that some
+ // factories can be added dynamically by extensions.
+ this._createExtraActors(DebuggerServer.tabActorFactories,
+ this._tabActorPool);
+
+ this._appendExtraActors(response);
+ return response;
+ },
+
+ /**
+ * Called when the actor is removed from the connection.
+ */
+ disconnect() {
+ this.exit();
+ },
+
+ /**
+ * Called by the root actor when the underlying tab is closed.
+ */
+ exit() {
+ if (this.exited) {
+ return;
+ }
+
+ // Tell the thread actor that the tab is closed, so that it may terminate
+ // instead of resuming the debuggee script.
+ if (this._attached) {
+ this.threadActor._tabClosed = true;
+ }
+
+ this._detach();
+
+ Object.defineProperty(this, "docShell", {
+ value: null,
+ configurable: true
+ });
+
+ this._extraActors = null;
+
+ this._exited = true;
+ },
+
+ /**
+ * Return true if the given global is associated with this tab and should be
+ * added as a debuggee, false otherwise.
+ */
+ _shouldAddNewGlobalAsDebuggee(wrappedGlobal) {
+ if (wrappedGlobal.hostAnnotations &&
+ wrappedGlobal.hostAnnotations.type == "document" &&
+ wrappedGlobal.hostAnnotations.element === this.window) {
+ return true;
+ }
+
+ let global = unwrapDebuggerObjectGlobal(wrappedGlobal);
+ if (!global) {
+ return false;
+ }
+
+ // Check if the global is a sdk page-mod sandbox.
+ let metadata = {};
+ let id = "";
+ try {
+ id = getInnerId(this.window);
+ metadata = Cu.getSandboxMetadata(global);
+ } catch (e) {
+ // ignore
+ }
+ if (metadata
+ && metadata["inner-window-id"]
+ && metadata["inner-window-id"] == id) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /* Support for DebuggerServer.addTabActor. */
+ _createExtraActors: createExtraActors,
+ _appendExtraActors: appendExtraActors,
+
+ /**
+ * Does the actual work of attaching to a tab.
+ */
+ _attach() {
+ if (this._attached) {
+ return;
+ }
+
+ // Create a pool for tab-lifetime actors.
+ assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
+ this._tabPool = new ActorPool(this.conn);
+ this.conn.addActorPool(this._tabPool);
+
+ // ... and a pool for context-lifetime actors.
+ this._pushContext();
+
+ // on xpcshell, there is no document
+ if (this.window) {
+ this._progressListener = new DebuggerProgressListener(this);
+
+ // Save references to the original document we attached to
+ this._originalWindow = this.window;
+
+ // Ensure replying to attach() request first
+ // before notifying about new docshells.
+ DevToolsUtils.executeSoon(() => this._watchDocshells());
+ }
+
+ this._attached = true;
+ },
+
+ _watchDocshells() {
+ // In child processes, we watch all docshells living in the process.
+ if (this.listenForNewDocShells) {
+ Services.obs.addObserver(this, "webnavigation-create", false);
+ }
+ Services.obs.addObserver(this, "webnavigation-destroy", false);
+
+ // We watch for all child docshells under the current document,
+ this._progressListener.watch(this.docShell);
+
+ // And list all already existing ones.
+ this._updateChildDocShells();
+ },
+
+ onSwitchToFrame(request) {
+ let windowId = request.windowId;
+ let win;
+
+ try {
+ win = Services.wm.getOuterWindowWithId(windowId);
+ } catch (e) {
+ // ignore
+ }
+ if (!win) {
+ return { error: "noWindow",
+ message: "The related docshell is destroyed or not found" };
+ } else if (win == this.window) {
+ return {};
+ }
+
+ // Reply first before changing the document
+ DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));
+
+ return {};
+ },
+
+ onListFrames(request) {
+ let windows = this._docShellsToWindows(this.docShells);
+ return { frames: windows };
+ },
+
+ onListWorkers(request) {
+ if (!this.attached) {
+ return { error: "wrongState" };
+ }
+
+ if (this._workerActorList === null) {
+ this._workerActorList = new WorkerActorList(this.conn, {
+ type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
+ window: this.window
+ });
+ }
+
+ return this._workerActorList.getList().then((actors) => {
+ let pool = new ActorPool(this.conn);
+ for (let actor of actors) {
+ pool.addActor(actor);
+ }
+
+ this.conn.removeActorPool(this._workerActorPool);
+ this._workerActorPool = pool;
+ this.conn.addActorPool(this._workerActorPool);
+
+ this._workerActorList.onListChanged = this._onWorkerActorListChanged;
+
+ return {
+ "from": this.actorID,
+ "workers": actors.map((actor) => actor.form())
+ };
+ });
+ },
+
+ _onWorkerActorListChanged() {
+ this._workerActorList.onListChanged = null;
+ this.conn.sendActorEvent(this.actorID, "workerListChanged");
+ },
+
+ observe(subject, topic, data) {
+ // Ignore any event that comes before/after the tab actor is attached
+ // That typically happens during firefox shutdown.
+ if (!this.attached) {
+ return;
+ }
+ if (topic == "webnavigation-create") {
+ subject.QueryInterface(Ci.nsIDocShell);
+ this._onDocShellCreated(subject);
+ } else if (topic == "webnavigation-destroy") {
+ this._onDocShellDestroy(subject);
+ }
+ },
+
+ _onDocShellCreated(docShell) {
+ // (chrome-)webnavigation-create is fired very early during docshell
+ // construction. In new root docshells within child processes, involving
+ // TabChild, this event is from within this call:
+ // http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
+ // whereas the chromeEventHandler (and most likely other stuff) is set
+ // later:
+ // http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944
+ // So wait a tick before watching it:
+ DevToolsUtils.executeSoon(() => {
+ // Bug 1142752: sometimes, the docshell appears to be immediately
+ // destroyed, bailout early to prevent random exceptions.
+ if (docShell.isBeingDestroyed()) {
+ return;
+ }
+
+ // In child processes, we have new root docshells,
+ // let's watch them and all their child docshells.
+ if (this._isRootDocShell(docShell)) {
+ this._progressListener.watch(docShell);
+ }
+ this._notifyDocShellsUpdate([docShell]);
+ });
+ },
+
+ _onDocShellDestroy(docShell) {
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ this._notifyDocShellDestroy(webProgress);
+ },
+
+ _isRootDocShell(docShell) {
+ // Should report as root docshell:
+ // - New top level window's docshells, when using ChromeActor against a
+ // process. It allows tracking iframes of the newly opened windows
+ // like Browser console or new browser windows.
+ // - MozActivities or window.open frames on B2G, where a new root docshell
+ // is spawn in the child process of the app.
+ return !docShell.parent;
+ },
+
+ // Convert docShell list to windows objects list being sent to the client
+ _docShellsToWindows(docshells) {
+ return docshells.map(docShell => {
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ let window = webProgress.DOMWindow;
+ let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+ let parentID = undefined;
+ // Ignore the parent of the original document on non-e10s firefox,
+ // as we get the xul window as parent and don't care about it.
+ if (window.parent && window != this._originalWindow) {
+ parentID = window.parent
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+ }
+
+ // Collect the addonID from the document origin attributes.
+ let addonID = window.document.nodePrincipal.originAttributes.addonId;
+
+ return {
+ id,
+ parentID,
+ addonID,
+ url: window.location.href,
+ title: window.document.title,
+ };
+ });
+ },
+
+ _notifyDocShellsUpdate(docshells) {
+ let windows = this._docShellsToWindows(docshells);
+
+ // Do not send the `frameUpdate` event if the windows array is empty.
+ if (windows.length == 0) {
+ return;
+ }
+
+ this.conn.send({
+ from: this.actorID,
+ type: "frameUpdate",
+ frames: windows
+ });
+ },
+
+ _updateChildDocShells() {
+ this._notifyDocShellsUpdate(this.docShells);
+ },
+
+ _notifyDocShellDestroy(webProgress) {
+ webProgress = webProgress.QueryInterface(Ci.nsIWebProgress);
+ let id = webProgress.DOMWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+ this.conn.send({
+ from: this.actorID,
+ type: "frameUpdate",
+ frames: [{
+ id,
+ destroy: true
+ }]
+ });
+
+ // Stop watching this docshell (the unwatch() method will check if we
+ // started watching it before).
+ webProgress.QueryInterface(Ci.nsIDocShell);
+ this._progressListener.unwatch(webProgress);
+
+ if (webProgress.DOMWindow == this._originalWindow) {
+ // If the original top level document we connected to is removed,
+ // we try to switch to any other top level document
+ let rootDocShells = this.docShells
+ .filter(d => {
+ return d != this.docShell &&
+ this._isRootDocShell(d);
+ });
+ if (rootDocShells.length > 0) {
+ let newRoot = rootDocShells[0];
+ this._originalWindow = newRoot.DOMWindow;
+ this._changeTopLevelDocument(this._originalWindow);
+ } else {
+ // If for some reason (typically during Firefox shutdown), the original
+ // document is destroyed, and there is no other top level docshell,
+ // we detach the tab actor to unregister all listeners and prevent any
+ // exception
+ this.exit();
+ }
+ return;
+ }
+
+ // If the currently targeted context is destroyed,
+ // and we aren't on the top-level document,
+ // we have to switch to the top-level one.
+ if (webProgress.DOMWindow == this.window &&
+ this.window != this._originalWindow) {
+ this._changeTopLevelDocument(this._originalWindow);
+ }
+ },
+
+ _notifyDocShellDestroyAll() {
+ this.conn.send({
+ from: this.actorID,
+ type: "frameUpdate",
+ destroyAll: true
+ });
+ },
+
+ /**
+ * Creates a thread actor and a pool for context-lifetime actors. It then sets
+ * up the content window for debugging.
+ */
+ _pushContext() {
+ assert(!this._contextPool, "Can't push multiple contexts");
+
+ this._contextPool = new ActorPool(this.conn);
+ this.conn.addActorPool(this._contextPool);
+
+ this.threadActor = new ThreadActor(this, this.window);
+ this._contextPool.addActor(this.threadActor);
+ },
+
+ /**
+ * Exits the current thread actor and removes the context-lifetime actor pool.
+ * The content window is no longer being debugged after this call.
+ */
+ _popContext() {
+ assert(!!this._contextPool, "No context to pop.");
+
+ this.conn.removeActorPool(this._contextPool);
+ this._contextPool = null;
+ this.threadActor.exit();
+ this.threadActor = null;
+ this._sources = null;
+ },
+
+ /**
+ * Does the actual work of detaching from a tab.
+ *
+ * @returns false if the tab wasn't attached or true of detaching succeeds.
+ */
+ _detach() {
+ if (!this.attached) {
+ return false;
+ }
+
+ // Check for docShell availability, as it can be already gone
+ // during Firefox shutdown.
+ if (this.docShell) {
+ this._progressListener.unwatch(this.docShell);
+ this._restoreDocumentSettings();
+ }
+ if (this._progressListener) {
+ this._progressListener.destroy();
+ this._progressListener = null;
+ this._originalWindow = null;
+
+ // Removes the observers being set in _watchDocShells
+ if (this.listenForNewDocShells) {
+ Services.obs.removeObserver(this, "webnavigation-create");
+ }
+ Services.obs.removeObserver(this, "webnavigation-destroy");
+ }
+
+ this._popContext();
+
+ // Shut down actors that belong to this tab's pool.
+ for (let sheetActor of this._styleSheetActors.values()) {
+ this._tabPool.removeActor(sheetActor);
+ }
+ this._styleSheetActors.clear();
+ this.conn.removeActorPool(this._tabPool);
+ this._tabPool = null;
+ if (this._tabActorPool) {
+ this.conn.removeActorPool(this._tabActorPool);
+ this._tabActorPool = null;
+ }
+
+ // Make sure that no more workerListChanged notifications are sent.
+ if (this._workerActorList !== null) {
+ this._workerActorList.onListChanged = null;
+ this._workerActorList = null;
+ }
+
+ if (this._workerActorPool !== null) {
+ this.conn.removeActorPool(this._workerActorPool);
+ this._workerActorPool = null;
+ }
+
+ this._attached = false;
+
+ this.conn.send({ from: this.actorID,
+ type: "tabDetached" });
+
+ return true;
+ },
+
+ // Protocol Request Handlers
+
+ onAttach(request) {
+ if (this.exited) {
+ return { type: "exited" };
+ }
+
+ this._attach();
+
+ return {
+ type: "tabAttached",
+ threadActor: this.threadActor.actorID,
+ cacheDisabled: this._getCacheDisabled(),
+ javascriptEnabled: this._getJavascriptEnabled(),
+ traits: this.traits,
+ };
+ },
+
+ onDetach(request) {
+ if (!this._detach()) {
+ return { error: "wrongState" };
+ }
+
+ return { type: "detached" };
+ },
+
+ /**
+ * Bring the tab's window to front.
+ */
+ onFocus() {
+ if (this.window) {
+ this.window.focus();
+ }
+ return {};
+ },
+
+ /**
+ * Reload the page in this tab.
+ */
+ onReload(request) {
+ let force = request && request.options && request.options.force;
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
+ // This won't work while the browser is shutting down and we don't really
+ // care.
+ if (Services.startup.shuttingDown) {
+ return;
+ }
+ this.webNavigation.reload(force ?
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE :
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
+ }, "TabActor.prototype.onReload's delayed body"), 0);
+ return {};
+ },
+
+ /**
+ * Navigate this tab to a new location
+ */
+ onNavigateTo(request) {
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
+ this.window.location = request.url;
+ }, "TabActor.prototype.onNavigateTo's delayed body"), 0);
+ return {};
+ },
+
+ /**
+ * Reconfigure options.
+ */
+ onReconfigure(request) {
+ let options = request.options || {};
+
+ if (!this.docShell) {
+ // The tab is already closed.
+ return {};
+ }
+ this._toggleDevToolsSettings(options);
+
+ return {};
+ },
+
+ /**
+ * Handle logic to enable/disable JS/cache/Service Worker testing.
+ */
+ _toggleDevToolsSettings(options) {
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ let reload = false;
+
+ if (typeof options.javascriptEnabled !== "undefined" &&
+ options.javascriptEnabled !== this._getJavascriptEnabled()) {
+ this._setJavascriptEnabled(options.javascriptEnabled);
+ reload = true;
+ }
+ if (typeof options.cacheDisabled !== "undefined" &&
+ options.cacheDisabled !== this._getCacheDisabled()) {
+ this._setCacheDisabled(options.cacheDisabled);
+ }
+ if ((typeof options.serviceWorkersTestingEnabled !== "undefined") &&
+ (options.serviceWorkersTestingEnabled !==
+ this._getServiceWorkersTestingEnabled())) {
+ this._setServiceWorkersTestingEnabled(
+ options.serviceWorkersTestingEnabled
+ );
+ }
+
+ // Reload if:
+ // - there's an explicit `performReload` flag and it's true
+ // - there's no `performReload` flag, but it makes sense to do so
+ let hasExplicitReloadFlag = "performReload" in options;
+ if ((hasExplicitReloadFlag && options.performReload) ||
+ (!hasExplicitReloadFlag && reload)) {
+ this.onReload();
+ }
+ },
+
+ /**
+ * Opposite of the _toggleDevToolsSettings method, that reset document state
+ * when closing the toolbox.
+ */
+ _restoreDocumentSettings() {
+ this._restoreJavascript();
+ this._setCacheDisabled(false);
+ this._setServiceWorkersTestingEnabled(false);
+ },
+
+ /**
+ * Disable or enable the cache via docShell.
+ */
+ _setCacheDisabled(disabled) {
+ let enable = Ci.nsIRequest.LOAD_NORMAL;
+ let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
+ Ci.nsIRequest.INHIBIT_CACHING;
+
+ this.docShell.defaultLoadFlags = disabled ? disable : enable;
+ },
+
+ /**
+ * Disable or enable JS via docShell.
+ */
+ _wasJavascriptEnabled: null,
+ _setJavascriptEnabled(allow) {
+ if (this._wasJavascriptEnabled === null) {
+ this._wasJavascriptEnabled = this.docShell.allowJavascript;
+ }
+ this.docShell.allowJavascript = allow;
+ },
+
+ /**
+ * Restore JS state, before the actor modified it.
+ */
+ _restoreJavascript() {
+ if (this._wasJavascriptEnabled !== null) {
+ this._setJavascriptEnabled(this._wasJavascriptEnabled);
+ this._wasJavascriptEnabled = null;
+ }
+ },
+
+ /**
+ * Return JS allowed status.
+ */
+ _getJavascriptEnabled() {
+ if (!this.docShell) {
+ // The tab is already closed.
+ return null;
+ }
+
+ return this.docShell.allowJavascript;
+ },
+
+ /**
+ * Disable or enable the service workers testing features.
+ */
+ _setServiceWorkersTestingEnabled(enabled) {
+ let windowUtils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.serviceWorkersTestingEnabled = enabled;
+ },
+
+ /**
+ * Return cache allowed status.
+ */
+ _getCacheDisabled() {
+ if (!this.docShell) {
+ // The tab is already closed.
+ return null;
+ }
+
+ let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
+ Ci.nsIRequest.INHIBIT_CACHING;
+ return this.docShell.defaultLoadFlags === disable;
+ },
+
+ /**
+ * Return service workers testing allowed status.
+ */
+ _getServiceWorkersTestingEnabled() {
+ if (!this.docShell) {
+ // The tab is already closed.
+ return null;
+ }
+
+ let windowUtils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return windowUtils.serviceWorkersTestingEnabled;
+ },
+
+ /**
+ * Prepare to enter a nested event loop by disabling debuggee events.
+ */
+ preNest() {
+ if (!this.window) {
+ // The tab is already closed.
+ return;
+ }
+ let windowUtils = this.window
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.suppressEventHandling(true);
+ windowUtils.suspendTimeouts();
+ },
+
+ /**
+ * Prepare to exit a nested event loop by enabling debuggee events.
+ */
+ postNest(nestData) {
+ if (!this.window) {
+ // The tab is already closed.
+ return;
+ }
+ let windowUtils = this.window
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.resumeTimeouts();
+ windowUtils.suppressEventHandling(false);
+ },
+
+ _changeTopLevelDocument(window) {
+ // Fake a will-navigate on the previous document
+ // to let a chance to unregister it
+ this._willNavigate(this.window, window.location.href, null, true);
+
+ this._windowDestroyed(this.window, null, true);
+
+ // Immediately change the window as this window, if in process of unload
+ // may already be non working on the next cycle and start throwing
+ this._setWindow(window);
+
+ DevToolsUtils.executeSoon(() => {
+ // Then fake window-ready and navigate on the given document
+ this._windowReady(window, true);
+ DevToolsUtils.executeSoon(() => {
+ this._navigate(window, true);
+ });
+ });
+ },
+
+ _setWindow(window) {
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ // Here is the very important call where we switch the currently
+ // targeted context (it will indirectly update this.window and
+ // many other attributes defined from docShell).
+ Object.defineProperty(this, "docShell", {
+ value: docShell,
+ enumerable: true,
+ configurable: true
+ });
+ events.emit(this, "changed-toplevel-document");
+ this.conn.send({
+ from: this.actorID,
+ type: "frameUpdate",
+ selected: this.outerWindowID
+ });
+ },
+
+ /**
+ * Handle location changes, by clearing the previous debuggees and enabling
+ * debugging, which may have been disabled temporarily by the
+ * DebuggerProgressListener.
+ */
+ _windowReady(window, isFrameSwitching = false) {
+ let isTopLevel = window == this.window;
+
+ // We just reset iframe list on WillNavigate, so we now list all existing
+ // frames when we load a new document in the original window
+ if (window == this._originalWindow && !isFrameSwitching) {
+ this._updateChildDocShells();
+ }
+
+ events.emit(this, "window-ready", {
+ window: window,
+ isTopLevel: isTopLevel,
+ id: getWindowID(window)
+ });
+
+ // TODO bug 997119: move that code to ThreadActor by listening to
+ // window-ready
+ let threadActor = this.threadActor;
+ if (isTopLevel && threadActor.state != "detached") {
+ this.sources.reset({ sourceMaps: true });
+ threadActor.clearDebuggees();
+ threadActor.dbg.enabled = true;
+ threadActor.maybePauseOnExceptions();
+ // Update the global no matter if the debugger is on or off,
+ // otherwise the global will be wrong when enabled later.
+ threadActor.global = window;
+ }
+
+ // Refresh the debuggee list when a new window object appears (top window or
+ // iframe).
+ if (threadActor.attached) {
+ threadActor.dbg.addDebuggees();
+ }
+ },
+
+ _windowDestroyed(window, id = null, isFrozen = false) {
+ events.emit(this, "window-destroyed", {
+ window: window,
+ isTopLevel: window == this.window,
+ id: id || getWindowID(window),
+ isFrozen: isFrozen
+ });
+ },
+
+ /**
+ * Start notifying server and client about a new document
+ * being loaded in the currently targeted context.
+ */
+ _willNavigate(window, newURI, request, isFrameSwitching = false) {
+ let isTopLevel = window == this.window;
+ let reset = false;
+
+ if (window == this._originalWindow && !isFrameSwitching) {
+ // Clear the iframe list if the original top-level document changes.
+ this._notifyDocShellDestroyAll();
+
+ // If the top level document changes and we are targeting
+ // an iframe, we need to reset to the upcoming new top level document.
+ // But for this will-navigate event, we will dispatch on the old window.
+ // (The inspector codebase expect to receive will-navigate for the
+ // currently displayed document in order to cleanup the markup view)
+ if (this.window != this._originalWindow) {
+ reset = true;
+ window = this.window;
+ isTopLevel = true;
+ }
+ }
+
+ // will-navigate event needs to be dispatched synchronously,
+ // by calling the listeners in the order or registration.
+ // This event fires once navigation starts,
+ // (all pending user prompts are dealt with),
+ // but before the first request starts.
+ events.emit(this, "will-navigate", {
+ window: window,
+ isTopLevel: isTopLevel,
+ newURI: newURI,
+ request: request
+ });
+
+ // We don't do anything for inner frames in TabActor.
+ // (we will only update thread actor on window-ready)
+ if (!isTopLevel) {
+ return;
+ }
+
+ // Proceed normally only if the debuggee is not paused.
+ // TODO bug 997119: move that code to ThreadActor by listening to
+ // will-navigate
+ let threadActor = this.threadActor;
+ if (threadActor.state == "paused") {
+ this.conn.send(
+ threadActor.unsafeSynchronize(Promise.resolve(threadActor.onResume())));
+ threadActor.dbg.enabled = false;
+ }
+ threadActor.disableAllBreakpoints();
+
+ this.conn.send({
+ from: this.actorID,
+ type: "tabNavigated",
+ url: newURI,
+ nativeConsoleAPI: true,
+ state: "start",
+ isFrameSwitching: isFrameSwitching
+ });
+
+ if (reset) {
+ this._setWindow(this._originalWindow);
+ }
+ },
+
+ /**
+ * Notify server and client about a new document done loading in the current
+ * targeted context.
+ */
+ _navigate(window, isFrameSwitching = false) {
+ let isTopLevel = window == this.window;
+
+ // navigate event needs to be dispatched synchronously,
+ // by calling the listeners in the order or registration.
+ // This event is fired once the document is loaded,
+ // after the load event, it's document ready-state is 'complete'.
+ events.emit(this, "navigate", {
+ window: window,
+ isTopLevel: isTopLevel
+ });
+
+ // We don't do anything for inner frames in TabActor.
+ // (we will only update thread actor on window-ready)
+ if (!isTopLevel) {
+ return;
+ }
+
+ // TODO bug 997119: move that code to ThreadActor by listening to navigate
+ let threadActor = this.threadActor;
+ if (threadActor.state == "running") {
+ threadActor.dbg.enabled = true;
+ }
+
+ this.conn.send({
+ from: this.actorID,
+ type: "tabNavigated",
+ url: this.url,
+ title: this.title,
+ nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
+ state: "stop",
+ isFrameSwitching: isFrameSwitching
+ });
+ },
+
+ /**
+ * Tells if the window.console object is native or overwritten by script in
+ * the page.
+ *
+ * @param nsIDOMWindow window
+ * The window object you want to check.
+ * @return boolean
+ * True if the window.console object is native, or false otherwise.
+ */
+ hasNativeConsoleAPI(window) {
+ let isNative = false;
+ try {
+ // We are very explicitly examining the "console" property of
+ // the non-Xrayed object here.
+ let console = window.wrappedJSObject.console;
+ isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE;
+ } catch (ex) {
+ // ignore
+ }
+ return isNative;
+ },
+
+ /**
+ * Create or return the StyleSheetActor for a style sheet. This method
+ * is here because the Style Editor and Inspector share style sheet actors.
+ *
+ * @param DOMStyleSheet styleSheet
+ * The style sheet to create an actor for.
+ * @return StyleSheetActor actor
+ * The actor for this style sheet.
+ *
+ */
+ createStyleSheetActor(styleSheet) {
+ if (this._styleSheetActors.has(styleSheet)) {
+ return this._styleSheetActors.get(styleSheet);
+ }
+ let actor = new StyleSheetActor(styleSheet, this);
+ this._styleSheetActors.set(styleSheet, actor);
+
+ this._tabPool.addActor(actor);
+ events.emit(this, "stylesheet-added", actor);
+
+ return actor;
+ },
+
+ removeActorByName(name) {
+ if (name in this._extraActors) {
+ const actor = this._extraActors[name];
+ if (this._tabActorPool.has(actor)) {
+ this._tabActorPool.removeActor(actor);
+ }
+ delete this._extraActors[name];
+ }
+ },
+
+ /**
+ * Takes a packet containing a url, line and column and returns
+ * the updated url, line and column based on the current source mapping
+ * (source mapped files, pretty prints).
+ *
+ * @param {String} request.url
+ * @param {Number} request.line
+ * @param {Number?} request.column
+ * @return {Promise<Object>}
+ */
+ onResolveLocation(request) {
+ let { url, line } = request;
+ let column = request.column || 0;
+ const scripts = this.threadActor.dbg.findScripts({ url });
+
+ if (!scripts[0] || !scripts[0].source) {
+ return promise.resolve({
+ from: this.actorID,
+ type: "resolveLocation",
+ error: "SOURCE_NOT_FOUND"
+ });
+ }
+ const source = scripts[0].source;
+ const generatedActor = this.sources.createNonSourceMappedActor(source);
+ let generatedLocation = new GeneratedLocation(
+ generatedActor, line, column);
+ return this.sources.getOriginalLocation(generatedLocation).then(loc => {
+ // If no map found, return this packet
+ if (loc.originalLine == null) {
+ return {
+ type: "resolveLocation",
+ error: "MAP_NOT_FOUND"
+ };
+ }
+
+ loc = loc.toJSON();
+ return {
+ from: this.actorID,
+ url: loc.source.url,
+ column: loc.column,
+ line: loc.line
+ };
+ });
+ },
+};
+
+/**
+ * The request types this actor can handle.
+ */
+TabActor.prototype.requestTypes = {
+ "attach": TabActor.prototype.onAttach,
+ "detach": TabActor.prototype.onDetach,
+ "focus": TabActor.prototype.onFocus,
+ "reload": TabActor.prototype.onReload,
+ "navigateTo": TabActor.prototype.onNavigateTo,
+ "reconfigure": TabActor.prototype.onReconfigure,
+ "switchToFrame": TabActor.prototype.onSwitchToFrame,
+ "listFrames": TabActor.prototype.onListFrames,
+ "listWorkers": TabActor.prototype.onListWorkers,
+ "resolveLocation": TabActor.prototype.onResolveLocation
+};
+
+exports.TabActor = TabActor;
+
+/**
+ * Creates a tab actor for handling requests to a single browser frame.
+ * Both <xul:browser> and <iframe mozbrowser> are supported.
+ * This actor is a shim that connects to a ContentActor in a remote browser process.
+ * All RDP packets get forwarded using the message manager.
+ *
+ * @param connection The main RDP connection.
+ * @param browser <xul:browser> or <iframe mozbrowser> element to connect to.
+ */
+function BrowserTabActor(connection, browser) {
+ this._conn = connection;
+ this._browser = browser;
+ this._form = null;
+}
+
+BrowserTabActor.prototype = {
+ connect() {
+ let onDestroy = () => {
+ this._form = null;
+ };
+ let connect = DebuggerServer.connectToChild(this._conn, this._browser, onDestroy);
+ return connect.then(form => {
+ this._form = form;
+ return this;
+ });
+ },
+
+ get _tabbrowser() {
+ if (typeof this._browser.getTabBrowser == "function") {
+ return this._browser.getTabBrowser();
+ }
+ return null;
+ },
+
+ get _mm() {
+ // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
+ // or else fallback to asking the frameLoader itself.
+ return this._browser.messageManager ||
+ this._browser.frameLoader.messageManager;
+ },
+
+ update() {
+ // If the child happens to be crashed/close/detach, it won't have _form set,
+ // so only request form update if some code is still listening on the other
+ // side.
+ if (this._form) {
+ let deferred = promise.defer();
+ let onFormUpdate = msg => {
+ // There may be more than just one childtab.js up and running
+ if (this._form.actor != msg.json.actor) {
+ return;
+ }
+ this._mm.removeMessageListener("debug:form", onFormUpdate);
+ this._form = msg.json;
+ deferred.resolve(this);
+ };
+ this._mm.addMessageListener("debug:form", onFormUpdate);
+ this._mm.sendAsyncMessage("debug:form");
+ return deferred.promise;
+ }
+
+ return this.connect();
+ },
+
+ /**
+ * If we don't have a title from the content side because it's a zombie tab, try to find
+ * it on the chrome side.
+ */
+ get title() {
+ // On Fennec, we can check the session store data for zombie tabs
+ if (this._browser.__SS_restore) {
+ let sessionStore = this._browser.__SS_data;
+ // Get the last selected entry
+ let entry = sessionStore.entries[sessionStore.index - 1];
+ return entry.title;
+ }
+ // If contentTitle is empty (e.g. on a not-yet-restored tab), but there is a
+ // tabbrowser (i.e. desktop Firefox, but not Fennec), we can use the label
+ // as the title.
+ if (this._tabbrowser) {
+ let tab = this._tabbrowser.getTabForBrowser(this._browser);
+ if (tab) {
+ return tab.label;
+ }
+ }
+ return "";
+ },
+
+ /**
+ * If we don't have a url from the content side because it's a zombie tab, try to find
+ * it on the chrome side.
+ */
+ get url() {
+ // On Fennec, we can check the session store data for zombie tabs
+ if (this._browser.__SS_restore) {
+ let sessionStore = this._browser.__SS_data;
+ // Get the last selected entry
+ let entry = sessionStore.entries[sessionStore.index - 1];
+ return entry.url;
+ }
+ return null;
+ },
+
+ form() {
+ let form = Object.assign({}, this._form);
+ // In some cases, the title and url fields might be empty. Zombie tabs (not yet
+ // restored) are a good example. In such cases, try to look up values for these
+ // fields using other data in the parent process.
+ if (!form.title) {
+ form.title = this.title;
+ }
+ if (!form.url) {
+ form.url = this.url;
+ }
+ return form;
+ },
+
+ exit() {
+ this._browser = null;
+ },
+};
+
+exports.BrowserTabActor = BrowserTabActor;
+
+function BrowserAddonList(connection) {
+ this._connection = connection;
+ this._actorByAddonId = new Map();
+ this._onListChanged = null;
+}
+
+BrowserAddonList.prototype.getList = function () {
+ let deferred = promise.defer();
+ AddonManager.getAllAddons((addons) => {
+ for (let addon of addons) {
+ let actor = this._actorByAddonId.get(addon.id);
+ if (!actor) {
+ if (addon.isWebExtension) {
+ actor = new WebExtensionActor(this._connection, addon);
+ } else {
+ actor = new BrowserAddonActor(this._connection, addon);
+ }
+
+ this._actorByAddonId.set(addon.id, actor);
+ }
+ }
+ deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor));
+ });
+ return deferred.promise;
+};
+
+Object.defineProperty(BrowserAddonList.prototype, "onListChanged", {
+ enumerable: true,
+ configurable: true,
+ get() {
+ return this._onListChanged;
+ },
+ set(v) {
+ if (v !== null && typeof v != "function") {
+ throw new Error(
+ "onListChanged property may only be set to 'null' or a function");
+ }
+ this._onListChanged = v;
+ this._adjustListener();
+ }
+});
+
+BrowserAddonList.prototype.onInstalled = function (addon) {
+ this._notifyListChanged();
+ this._adjustListener();
+};
+
+BrowserAddonList.prototype.onUninstalled = function (addon) {
+ this._actorByAddonId.delete(addon.id);
+ this._notifyListChanged();
+ this._adjustListener();
+};
+
+BrowserAddonList.prototype._notifyListChanged = function () {
+ if (this._onListChanged) {
+ this._onListChanged();
+ }
+};
+
+BrowserAddonList.prototype._adjustListener = function () {
+ if (this._onListChanged) {
+ // As long as the callback exists, we need to listen for changes
+ // so we can notify about add-on changes.
+ AddonManager.addAddonListener(this);
+ } else if (this._actorByAddonId.size === 0) {
+ // When the callback does not exist, we only need to keep listening
+ // if the actor cache will need adjusting when add-ons change.
+ AddonManager.removeAddonListener(this);
+ }
+};
+
+exports.BrowserAddonList = BrowserAddonList;
+
+/**
+ * The DebuggerProgressListener object is an nsIWebProgressListener which
+ * handles onStateChange events for the inspected browser. If the user tries to
+ * navigate away from a paused page, the listener makes sure that the debuggee
+ * is resumed before the navigation begins.
+ *
+ * @param TabActor aTabActor
+ * The tab actor associated with this listener.
+ */
+function DebuggerProgressListener(tabActor) {
+ this._tabActor = tabActor;
+ this._onWindowCreated = this.onWindowCreated.bind(this);
+ this._onWindowHidden = this.onWindowHidden.bind(this);
+
+ // Watch for windows destroyed (global observer that will need filtering)
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+
+ // XXX: for now we maintain the list of windows we know about in this instance
+ // so that we can discriminate windows we care about when observing
+ // inner-window-destroyed events. Bug 1016952 would remove the need for this.
+ this._knownWindowIDs = new Map();
+
+ this._watchedDocShells = new WeakSet();
+}
+
+DebuggerProgressListener.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports,
+ ]),
+
+ destroy() {
+ Services.obs.removeObserver(this, "inner-window-destroyed", false);
+ this._knownWindowIDs.clear();
+ this._knownWindowIDs = null;
+ },
+
+ watch(docShell) {
+ // Add the docshell to the watched set. We're actually adding the window,
+ // because docShell objects are not wrappercached and would be rejected
+ // by the WeakSet.
+ let docShellWindow = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ this._watchedDocShells.add(docShellWindow);
+
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this,
+ Ci.nsIWebProgress.NOTIFY_STATUS |
+ Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+
+ let handler = getDocShellChromeEventHandler(docShell);
+ handler.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
+ handler.addEventListener("pageshow", this._onWindowCreated, true);
+ handler.addEventListener("pagehide", this._onWindowHidden, true);
+
+ // Dispatch the _windowReady event on the tabActor for pre-existing windows
+ for (let win of this._getWindowsInDocShell(docShell)) {
+ this._tabActor._windowReady(win);
+ this._knownWindowIDs.set(getWindowID(win), win);
+ }
+ },
+
+ unwatch(docShell) {
+ let docShellWindow = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ if (!this._watchedDocShells.has(docShellWindow)) {
+ return;
+ }
+
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ // During process shutdown, the docshell may already be cleaned up and throw
+ try {
+ webProgress.removeProgressListener(this);
+ } catch (e) {
+ // ignore
+ }
+
+ let handler = getDocShellChromeEventHandler(docShell);
+ handler.removeEventListener("DOMWindowCreated",
+ this._onWindowCreated, true);
+ handler.removeEventListener("pageshow", this._onWindowCreated, true);
+ handler.removeEventListener("pagehide", this._onWindowHidden, true);
+
+ for (let win of this._getWindowsInDocShell(docShell)) {
+ this._knownWindowIDs.delete(getWindowID(win));
+ }
+ },
+
+ _getWindowsInDocShell(docShell) {
+ return getChildDocShells(docShell).map(d => {
+ return d.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ });
+ },
+
+ onWindowCreated: DevToolsUtils.makeInfallible(function (evt) {
+ if (!this._tabActor.attached) {
+ return;
+ }
+
+ // pageshow events for non-persisted pages have already been handled by a
+ // prior DOMWindowCreated event. For persisted pages, act as if the window
+ // had just been created since it's been unfrozen from bfcache.
+ if (evt.type == "pageshow" && !evt.persisted) {
+ return;
+ }
+
+ let window = evt.target.defaultView;
+ this._tabActor._windowReady(window);
+
+ if (evt.type !== "pageshow") {
+ this._knownWindowIDs.set(getWindowID(window), window);
+ }
+ }, "DebuggerProgressListener.prototype.onWindowCreated"),
+
+ onWindowHidden: DevToolsUtils.makeInfallible(function (evt) {
+ if (!this._tabActor.attached) {
+ return;
+ }
+
+ // Only act as if the window has been destroyed if the 'pagehide' event
+ // was sent for a persisted window (persisted is set when the page is put
+ // and frozen in the bfcache). If the page isn't persisted, the observer's
+ // inner-window-destroyed event will handle it.
+ if (!evt.persisted) {
+ return;
+ }
+
+ let window = evt.target.defaultView;
+ this._tabActor._windowDestroyed(window, null, true);
+ }, "DebuggerProgressListener.prototype.onWindowHidden"),
+
+ observe: DevToolsUtils.makeInfallible(function (subject, topic) {
+ if (!this._tabActor.attached) {
+ return;
+ }
+
+ // Because this observer will be called for all inner-window-destroyed in
+ // the application, we need to filter out events for windows we are not
+ // watching
+ let innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ let window = this._knownWindowIDs.get(innerID);
+ if (window) {
+ this._knownWindowIDs.delete(innerID);
+ this._tabActor._windowDestroyed(window, innerID);
+ }
+ }, "DebuggerProgressListener.prototype.observe"),
+
+ onStateChange:
+ DevToolsUtils.makeInfallible(function (progress, request, flag, status) {
+ if (!this._tabActor.attached) {
+ return;
+ }
+
+ let isStart = flag & Ci.nsIWebProgressListener.STATE_START;
+ let isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
+ let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ let isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
+
+ // Catch any iframe location change
+ if (isDocument && isStop) {
+ // Watch document stop to ensure having the new iframe url.
+ progress.QueryInterface(Ci.nsIDocShell);
+ this._tabActor._notifyDocShellsUpdate([progress]);
+ }
+
+ let window = progress.DOMWindow;
+ if (isDocument && isStart) {
+ // One of the earliest events that tells us a new URI
+ // is being loaded in this window.
+ let newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
+ this._tabActor._willNavigate(window, newURI, request);
+ }
+ if (isWindow && isStop) {
+ // Don't dispatch "navigate" event just yet when there is a redirect to
+ // about:neterror page.
+ if (request.status != Cr.NS_OK) {
+ // Instead, listen for DOMContentLoaded as about:neterror is loaded
+ // with LOAD_BACKGROUND flags and never dispatches load event.
+ // That may be the same reason why there is no onStateChange event
+ // for about:neterror loads.
+ let handler = getDocShellChromeEventHandler(progress);
+ let onLoad = evt => {
+ // Ignore events from iframes
+ if (evt.target == window.document) {
+ handler.removeEventListener("DOMContentLoaded", onLoad, true);
+ this._tabActor._navigate(window);
+ }
+ };
+ handler.addEventListener("DOMContentLoaded", onLoad, true);
+ } else {
+ // Somewhat equivalent of load event.
+ // (window.document.readyState == complete)
+ this._tabActor._navigate(window);
+ }
+ }
+ }, "DebuggerProgressListener.prototype.onStateChange")
+};
+
+exports.register = function (handle) {
+ handle.setRootActor(createRootActor);
+};
+
+exports.unregister = function (handle) {
+ handle.setRootActor(null);
+};