michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: let {Ci,Cu} = require("chrome"); michael@0: let {createExtraActors, appendExtraActors} = require("devtools/server/actors/common"); michael@0: let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); michael@0: michael@0: let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); michael@0: michael@0: // Assumptions on events module: michael@0: // events needs to be dispatched synchronously, michael@0: // by calling the listeners in the order or registration. michael@0: XPCOMUtils.defineLazyGetter(this, "events", () => { michael@0: return devtools.require("sdk/event/core"); michael@0: }); michael@0: michael@0: // Also depends on following symbols, shared by common scope with main.js: michael@0: // DebuggerServer, CommonCreateExtraActors, CommonAppendExtraActors, ActorPool, michael@0: // ThreadActor michael@0: michael@0: /** michael@0: * Browser-specific actors. michael@0: */ michael@0: michael@0: /** michael@0: * Yield all windows of type |aWindowType|, from the oldest window to the michael@0: * youngest, using nsIWindowMediator::getEnumerator. We're usually michael@0: * interested in "navigator:browser" windows. michael@0: */ michael@0: function allAppShellDOMWindows(aWindowType) michael@0: { michael@0: let e = Services.wm.getEnumerator(aWindowType); michael@0: while (e.hasMoreElements()) { michael@0: yield e.getNext(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Retrieve the window type of the top-level window |aWindow|. michael@0: */ michael@0: function appShellDOMWindowType(aWindow) { michael@0: /* This is what nsIWindowMediator's enumerator checks. */ michael@0: return aWindow.document.documentElement.getAttribute('windowtype'); michael@0: } michael@0: michael@0: /** michael@0: * Send Debugger:Shutdown events to all "navigator:browser" windows. michael@0: */ michael@0: function sendShutdownEvent() { michael@0: for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) { michael@0: let evt = win.document.createEvent("Event"); michael@0: evt.initEvent("Debugger:Shutdown", true, false); michael@0: win.document.documentElement.dispatchEvent(evt); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Construct a root actor appropriate for use in a server running in a michael@0: * browser. The returned root actor: michael@0: * - respects the factories registered with DebuggerServer.addGlobalActor, michael@0: * - uses a BrowserTabList to supply tab actors, michael@0: * - sends all navigator:browser window documents a Debugger:Shutdown event michael@0: * when it exits. michael@0: * michael@0: * * @param aConnection DebuggerServerConnection michael@0: * The conection to the client. michael@0: */ michael@0: function createRootActor(aConnection) michael@0: { michael@0: return new RootActor(aConnection, michael@0: { michael@0: tabList: new BrowserTabList(aConnection), michael@0: addonList: new BrowserAddonList(aConnection), michael@0: globalActorFactories: DebuggerServer.globalActorFactories, michael@0: onShutdown: sendShutdownEvent michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * A live list of BrowserTabActors representing the current browser tabs, michael@0: * to be provided to the root actor to answer 'listTabs' requests. michael@0: * michael@0: * This object also takes care of listening for TabClose events and michael@0: * onCloseWindow notifications, and exiting the BrowserTabActors concerned. michael@0: * michael@0: * (See the documentation for RootActor for the definition of the "live michael@0: * list" interface.) michael@0: * michael@0: * @param aConnection DebuggerServerConnection michael@0: * The connection in which this list's tab actors may participate. michael@0: * michael@0: * Some notes: michael@0: * michael@0: * This constructor is specific to the desktop browser environment; it michael@0: * maintains the tab list by tracking XUL windows and their XUL documents' michael@0: * "tabbrowser", "tab", and "browser" elements. What's entailed in maintaining michael@0: * an accurate list of open tabs in this context? michael@0: * michael@0: * - Opening and closing XUL windows: michael@0: * michael@0: * An nsIWindowMediatorListener is notified when new XUL windows (i.e., desktop michael@0: * windows) are opened and closed. It is not notified of individual content michael@0: * browser tabs coming and going within such a XUL window. That seems michael@0: * reasonable enough; it's concerned with XUL windows, not tab elements in the michael@0: * window's XUL document. michael@0: * michael@0: * However, even if we attach TabOpen and TabClose event listeners to each XUL michael@0: * window as soon as it is created: michael@0: * michael@0: * - we do not receive a TabOpen event for the initial empty tab of a new XUL michael@0: * window; and michael@0: * michael@0: * - we do not receive TabClose events for the tabs of a XUL window that has michael@0: * been closed. michael@0: * michael@0: * This means that TabOpen and TabClose events alone are not sufficient to michael@0: * maintain an accurate list of live tabs and mark tab actors as closed michael@0: * promptly. Our nsIWindowMediatorListener onCloseWindow handler must find and michael@0: * exit all actors for tabs that were in the closing window. michael@0: * michael@0: * Since this is a bit hairy, we don't make each individual attached tab actor michael@0: * responsible for noticing when it has been closed; we watch for that, and michael@0: * promise to call each actor's 'exit' method when it's closed, regardless of michael@0: * how we learn the news. michael@0: * michael@0: * - nsIWindowMediator locks michael@0: * michael@0: * nsIWindowMediator holds a lock protecting its list of top-level windows michael@0: * while it calls nsIWindowMediatorListener methods. nsIWindowMediator's michael@0: * GetEnumerator method also tries to acquire that lock. Thus, enumerating michael@0: * windows from within a listener method deadlocks (bug 873589). Rah. One michael@0: * can sometimes work around this by leaving the enumeration for a later michael@0: * tick. michael@0: * michael@0: * - Dragging tabs between windows: michael@0: * michael@0: * When a tab is dragged from one desktop window to another, we receive a michael@0: * TabOpen event for the new tab, and a TabClose event for the old tab; tab XUL michael@0: * elements do not really move from one document to the other (although their michael@0: * linked browser's content window objects do). michael@0: * michael@0: * However, while we could thus assume that each tab stays with the XUL window michael@0: * it belonged to when it was created, I'm not sure this is behavior one should michael@0: * rely upon. When a XUL window is closed, we take the less efficient, more michael@0: * conservative approach of simply searching the entire table for actors that michael@0: * belong to the closing XUL window, rather than trying to somehow track which michael@0: * XUL window each tab belongs to. michael@0: */ michael@0: function BrowserTabList(aConnection) michael@0: { michael@0: this._connection = aConnection; michael@0: michael@0: /* michael@0: * The XUL document of a tabbed browser window has "tab" elements, whose michael@0: * 'linkedBrowser' JavaScript properties are "browser" elements; those michael@0: * browsers' 'contentWindow' properties are wrappers on the tabs' content michael@0: * window objects. michael@0: * michael@0: * This map's keys are "browser" XUL elements; it maps each browser element michael@0: * to the tab actor we've created for its content window, if we've created michael@0: * one. This map serves several roles: michael@0: * michael@0: * - During iteration, we use it to find actors we've created previously. michael@0: * michael@0: * - On a TabClose event, we use it to find the tab's actor and exit it. michael@0: * michael@0: * - When the onCloseWindow handler is called, we iterate over it to find all michael@0: * tabs belonging to the closing XUL window, and exit them. michael@0: * michael@0: * - When it's empty, and the onListChanged hook is null, we know we can michael@0: * stop listening for events and notifications. michael@0: * michael@0: * We listen for TabClose events and onCloseWindow notifications in order to michael@0: * send onListChanged notifications, but also to tell actors when their michael@0: * referent has gone away and remove entries for dead browsers from this map. michael@0: * If that code is working properly, neither this map nor the actors in it michael@0: * should ever hold dead tabs alive. michael@0: */ michael@0: this._actorByBrowser = new Map(); michael@0: michael@0: /* The current onListChanged handler, or null. */ michael@0: this._onListChanged = null; michael@0: michael@0: /* michael@0: * True if we've been iterated over since we last called our onListChanged michael@0: * hook. michael@0: */ michael@0: this._mustNotify = false; michael@0: michael@0: /* True if we're testing, and should throw if consistency checks fail. */ michael@0: this._testing = false; michael@0: } michael@0: michael@0: BrowserTabList.prototype.constructor = BrowserTabList; michael@0: michael@0: michael@0: /** michael@0: * Get the selected browser for the given navigator:browser window. michael@0: * @private michael@0: * @param aWindow nsIChromeWindow michael@0: * The navigator:browser window for which you want the selected browser. michael@0: * @return nsIDOMElement|null michael@0: * The currently selected xul:browser element, if any. Note that the michael@0: * browser window might not be loaded yet - the function will return michael@0: * |null| in such cases. michael@0: */ michael@0: BrowserTabList.prototype._getSelectedBrowser = function(aWindow) { michael@0: return aWindow.gBrowser ? aWindow.gBrowser.selectedBrowser : null; michael@0: }; michael@0: michael@0: BrowserTabList.prototype._getChildren = function(aWindow) { michael@0: return aWindow.gBrowser.browsers; michael@0: }; michael@0: michael@0: BrowserTabList.prototype.getList = function() { michael@0: let topXULWindow = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType); michael@0: michael@0: // As a sanity check, make sure all the actors presently in our map get michael@0: // picked up when we iterate over all windows' tabs. michael@0: let initialMapSize = this._actorByBrowser.size; michael@0: let foundCount = 0; michael@0: michael@0: // To avoid mysterious behavior if tabs are closed or opened mid-iteration, michael@0: // we update the map first, and then make a second pass over it to yield michael@0: // the actors. Thus, the sequence yielded is always a snapshot of the michael@0: // actors that were live when we began the iteration. michael@0: michael@0: let actorPromises = []; michael@0: michael@0: // Iterate over all navigator:browser XUL windows. michael@0: for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) { michael@0: let selectedBrowser = this._getSelectedBrowser(win); michael@0: if (!selectedBrowser) { michael@0: continue; michael@0: } michael@0: michael@0: // For each tab in this XUL window, ensure that we have an actor for michael@0: // it, reusing existing actors where possible. We actually iterate michael@0: // over 'browser' XUL elements, and BrowserTabActor uses michael@0: // browser.contentWindow as the debuggee global. michael@0: for (let browser of this._getChildren(win)) { michael@0: // Do we have an existing actor for this browser? If not, create one. michael@0: let actor = this._actorByBrowser.get(browser); michael@0: if (actor) { michael@0: actorPromises.push(promise.resolve(actor)); michael@0: foundCount++; michael@0: } else if (browser.isRemoteBrowser) { michael@0: actor = new RemoteBrowserTabActor(this._connection, browser); michael@0: this._actorByBrowser.set(browser, actor); michael@0: let promise = actor.connect().then((form) => { michael@0: actor._form = form; michael@0: return actor; michael@0: }); michael@0: actorPromises.push(promise); michael@0: } else { michael@0: actor = new BrowserTabActor(this._connection, browser, win.gBrowser); michael@0: this._actorByBrowser.set(browser, actor); michael@0: actorPromises.push(promise.resolve(actor)); michael@0: } michael@0: michael@0: // Set the 'selected' properties on all actors correctly. michael@0: actor.selected = (win === topXULWindow && browser === selectedBrowser); michael@0: } michael@0: } michael@0: michael@0: if (this._testing && initialMapSize !== foundCount) michael@0: throw Error("_actorByBrowser map contained actors for dead tabs"); michael@0: michael@0: this._mustNotify = true; michael@0: this._checkListening(); michael@0: michael@0: return promise.all(actorPromises); michael@0: }; michael@0: michael@0: Object.defineProperty(BrowserTabList.prototype, 'onListChanged', { michael@0: enumerable: true, configurable:true, michael@0: get: function() { return this._onListChanged; }, michael@0: set: function(v) { michael@0: if (v !== null && typeof v !== 'function') { michael@0: throw Error("onListChanged property may only be set to 'null' or a function"); michael@0: } michael@0: this._onListChanged = v; michael@0: this._checkListening(); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * The set of tabs has changed somehow. Call our onListChanged handler, if michael@0: * one is set, and if we haven't already called it since the last iteration. michael@0: */ michael@0: BrowserTabList.prototype._notifyListChanged = function() { michael@0: if (!this._onListChanged) michael@0: return; michael@0: if (this._mustNotify) { michael@0: this._onListChanged(); michael@0: this._mustNotify = false; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Exit |aActor|, belonging to |aBrowser|, and notify the onListChanged michael@0: * handle if needed. michael@0: */ michael@0: BrowserTabList.prototype._handleActorClose = function(aActor, aBrowser) { michael@0: if (this._testing) { michael@0: if (this._actorByBrowser.get(aBrowser) !== aActor) { michael@0: throw Error("BrowserTabActor not stored in map under given browser"); michael@0: } michael@0: if (aActor.browser !== aBrowser) { michael@0: throw Error("actor's browser and map key don't match"); michael@0: } michael@0: } michael@0: michael@0: this._actorByBrowser.delete(aBrowser); michael@0: aActor.exit(); michael@0: michael@0: this._notifyListChanged(); michael@0: this._checkListening(); michael@0: }; michael@0: michael@0: /** michael@0: * Make sure we are listening or not listening for activity elsewhere in michael@0: * the browser, as appropriate. Other than setting up newly created XUL michael@0: * windows, all listener / observer connection and disconnection should michael@0: * happen here. michael@0: */ michael@0: BrowserTabList.prototype._checkListening = function() { michael@0: /* michael@0: * If we have an onListChanged handler that we haven't sent an announcement michael@0: * to since the last iteration, we need to watch for tab creation. michael@0: * michael@0: * Oddly, we don't need to watch for 'close' events here. If our actor list michael@0: * is empty, then either it was empty the last time we iterated, and no michael@0: * close events are possible, or it was not empty the last time we michael@0: * iterated, but all the actors have since been closed, and we must have michael@0: * sent a notification already when they closed. michael@0: */ michael@0: this._listenForEventsIf(this._onListChanged && this._mustNotify, michael@0: "_listeningForTabOpen", ["TabOpen", "TabSelect"]); michael@0: michael@0: /* If we have live actors, we need to be ready to mark them dead. */ michael@0: this._listenForEventsIf(this._actorByBrowser.size > 0, michael@0: "_listeningForTabClose", ["TabClose"]); michael@0: michael@0: /* michael@0: * We must listen to the window mediator in either case, since that's the michael@0: * only way to find out about tabs that come and go when top-level windows michael@0: * are opened and closed. michael@0: */ michael@0: this._listenToMediatorIf((this._onListChanged && this._mustNotify) || michael@0: (this._actorByBrowser.size > 0)); michael@0: }; michael@0: michael@0: /* michael@0: * Add or remove event listeners for all XUL windows. michael@0: * michael@0: * @param aShouldListen boolean michael@0: * True if we should add event handlers; false if we should remove them. michael@0: * @param aGuard string michael@0: * The name of a guard property of 'this', indicating whether we're michael@0: * already listening for those events. michael@0: * @param aEventNames array of strings michael@0: * An array of event names. michael@0: */ michael@0: BrowserTabList.prototype._listenForEventsIf = function(aShouldListen, aGuard, aEventNames) { michael@0: if (!aShouldListen !== !this[aGuard]) { michael@0: let op = aShouldListen ? "addEventListener" : "removeEventListener"; michael@0: for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) { michael@0: for (let name of aEventNames) { michael@0: win[op](name, this, false); michael@0: } michael@0: } michael@0: this[aGuard] = aShouldListen; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Implement nsIDOMEventListener. michael@0: */ michael@0: BrowserTabList.prototype.handleEvent = DevToolsUtils.makeInfallible(function(aEvent) { michael@0: switch (aEvent.type) { michael@0: case "TabOpen": michael@0: case "TabSelect": michael@0: /* Don't create a new actor; iterate will take care of that. Just notify. */ michael@0: this._notifyListChanged(); michael@0: this._checkListening(); michael@0: break; michael@0: case "TabClose": michael@0: let browser = aEvent.target.linkedBrowser; michael@0: let actor = this._actorByBrowser.get(browser); michael@0: if (actor) { michael@0: this._handleActorClose(actor, browser); michael@0: } michael@0: break; michael@0: } michael@0: }, "BrowserTabList.prototype.handleEvent"); michael@0: michael@0: /* michael@0: * If |aShouldListen| is true, ensure we've registered a listener with the michael@0: * window mediator. Otherwise, ensure we haven't registered a listener. michael@0: */ michael@0: BrowserTabList.prototype._listenToMediatorIf = function(aShouldListen) { michael@0: if (!aShouldListen !== !this._listeningToMediator) { michael@0: let op = aShouldListen ? "addListener" : "removeListener"; michael@0: Services.wm[op](this); michael@0: this._listeningToMediator = aShouldListen; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * nsIWindowMediatorListener implementation. michael@0: * michael@0: * See _onTabClosed for explanation of why we needn't actually tweak any michael@0: * actors or tables here. michael@0: * michael@0: * An nsIWindowMediatorListener's methods get passed all sorts of windows; we michael@0: * only care about the tab containers. Those have 'getBrowser' methods. michael@0: */ michael@0: BrowserTabList.prototype.onWindowTitleChange = () => { }; michael@0: michael@0: BrowserTabList.prototype.onOpenWindow = DevToolsUtils.makeInfallible(function(aWindow) { michael@0: let handleLoad = DevToolsUtils.makeInfallible(() => { michael@0: /* We don't want any further load events from this window. */ michael@0: aWindow.removeEventListener("load", handleLoad, false); michael@0: michael@0: if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType) michael@0: return; michael@0: michael@0: // Listen for future tab activity. michael@0: if (this._listeningForTabOpen) { michael@0: aWindow.addEventListener("TabOpen", this, false); michael@0: aWindow.addEventListener("TabSelect", this, false); michael@0: } michael@0: if (this._listeningForTabClose) { michael@0: aWindow.addEventListener("TabClose", this, false); michael@0: } michael@0: michael@0: // As explained above, we will not receive a TabOpen event for this michael@0: // document's initial tab, so we must notify our client of the new tab michael@0: // this will have. michael@0: this._notifyListChanged(); michael@0: }); michael@0: michael@0: /* michael@0: * You can hardly do anything at all with a XUL window at this point; it michael@0: * doesn't even have its document yet. Wait until its document has michael@0: * loaded, and then see what we've got. This also avoids michael@0: * nsIWindowMediator enumeration from within listeners (bug 873589). michael@0: */ michael@0: aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindow); michael@0: michael@0: aWindow.addEventListener("load", handleLoad, false); michael@0: }, "BrowserTabList.prototype.onOpenWindow"); michael@0: michael@0: BrowserTabList.prototype.onCloseWindow = DevToolsUtils.makeInfallible(function(aWindow) { michael@0: aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindow); michael@0: michael@0: if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType) michael@0: return; michael@0: michael@0: /* michael@0: * nsIWindowMediator deadlocks if you call its GetEnumerator method from michael@0: * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so michael@0: * handle the close in a different tick. michael@0: */ michael@0: Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => { michael@0: /* michael@0: * Scan the entire map for actors representing tabs that were in this michael@0: * top-level window, and exit them. michael@0: */ michael@0: for (let [browser, actor] of this._actorByBrowser) { michael@0: /* The browser document of a closed window has no default view. */ michael@0: if (!browser.ownerDocument.defaultView) { michael@0: this._handleActorClose(actor, browser); michael@0: } michael@0: } michael@0: }, "BrowserTabList.prototype.onCloseWindow's delayed body"), 0); michael@0: }, "BrowserTabList.prototype.onCloseWindow"); michael@0: michael@0: /** michael@0: * Creates a tab actor for handling requests to a browser tab, like michael@0: * attaching and detaching. TabActor respects the actor factories michael@0: * registered with DebuggerServer.addTabActor. michael@0: * michael@0: * This class is subclassed by BrowserTabActor and michael@0: * ContentActor. Subclasses are expected to implement a getter michael@0: * the docShell properties. michael@0: * michael@0: * @param aConnection DebuggerServerConnection michael@0: * The conection to the client. michael@0: * @param aChromeEventHandler michael@0: * An object on which listen for DOMWindowCreated and pageshow events. michael@0: */ michael@0: function TabActor(aConnection) michael@0: { michael@0: this.conn = aConnection; michael@0: this._tabActorPool = null; michael@0: // A map of actor names to actor instances provided by extensions. michael@0: this._extraActors = {}; michael@0: this._exited = false; michael@0: michael@0: this.traits = { reconfigure: true }; michael@0: } michael@0: michael@0: // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a michael@0: // *complete* mess, needs to be rethought asap. michael@0: michael@0: TabActor.prototype = { michael@0: traits: null, michael@0: michael@0: get exited() { return this._exited; }, michael@0: get attached() { return !!this._attached; }, michael@0: michael@0: _tabPool: null, michael@0: get tabActorPool() { return this._tabPool; }, michael@0: michael@0: _contextPool: null, michael@0: get contextActorPool() { return this._contextPool; }, michael@0: michael@0: _pendingNavigation: null, michael@0: michael@0: // A constant prefix that will be used to form the actor ID by the server. michael@0: actorPrefix: "tab", michael@0: michael@0: /** michael@0: * An object on which listen for DOMWindowCreated and pageshow events. michael@0: */ michael@0: get chromeEventHandler() { michael@0: // TODO: bug 992778, fix docShell.chromeEventHandler in child processes michael@0: return this.docShell.chromeEventHandler || michael@0: this.docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIContentFrameMessageManager); michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the nsIMessageManager associated to the tab. michael@0: */ michael@0: get messageManager() { michael@0: return this.docShell michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIContentFrameMessageManager); michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the tab's doc shell. michael@0: */ michael@0: get docShell() { michael@0: throw "The docShell getter should be implemented by a subclass of TabActor"; michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the tab content's DOM window. michael@0: */ michael@0: get window() { michael@0: return this.docShell michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindow); michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the nsIWebProgress for watching this window. michael@0: */ michael@0: get webProgress() { michael@0: return this.docShell michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebProgress); michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the nsIWebNavigation for the tab. michael@0: */ michael@0: get webNavigation() { michael@0: return this.docShell michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation); michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the tab's document. michael@0: */ michael@0: get contentDocument() { michael@0: return this.webNavigation.document; michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the tab title. michael@0: * @return string michael@0: * Tab title. michael@0: */ michael@0: get title() { michael@0: return this.contentDocument.contentTitle; michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the tab URL. michael@0: * @return string michael@0: * Tab URL. michael@0: */ michael@0: get url() { michael@0: if (this.webNavigation.currentURI) { michael@0: return this.webNavigation.currentURI.spec; michael@0: } michael@0: // Abrupt closing of the browser window may leave callbacks without a michael@0: // currentURI. michael@0: return null; michael@0: }, michael@0: michael@0: form: function BTA_form() { michael@0: dbg_assert(!this.exited, michael@0: "grip() shouldn't be called on exited browser actor."); michael@0: dbg_assert(this.actorID, michael@0: "tab should have an actorID."); michael@0: michael@0: let windowUtils = this.window michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: michael@0: let response = { michael@0: actor: this.actorID, michael@0: title: this.title, michael@0: url: this.url, michael@0: outerWindowID: windowUtils.outerWindowID michael@0: }; michael@0: michael@0: // Walk over tab actors added by extensions and add them to a new ActorPool. michael@0: let actorPool = new ActorPool(this.conn); michael@0: this._createExtraActors(DebuggerServer.tabActorFactories, actorPool); michael@0: if (!actorPool.isEmpty()) { michael@0: this._tabActorPool = actorPool; michael@0: this.conn.addActorPool(this._tabActorPool); michael@0: } michael@0: michael@0: this._appendExtraActors(response); michael@0: return response; michael@0: }, michael@0: michael@0: /** michael@0: * Called when the actor is removed from the connection. michael@0: */ michael@0: disconnect: function BTA_disconnect() { michael@0: this._detach(); michael@0: this._extraActors = null; michael@0: this._exited = true; michael@0: }, michael@0: michael@0: /** michael@0: * Called by the root actor when the underlying tab is closed. michael@0: */ michael@0: exit: function BTA_exit() { michael@0: if (this.exited) { michael@0: return; michael@0: } michael@0: michael@0: if (this._detach()) { michael@0: this.conn.send({ from: this.actorID, michael@0: type: "tabDetached" }); michael@0: } michael@0: michael@0: this._exited = true; michael@0: }, michael@0: michael@0: /* Support for DebuggerServer.addTabActor. */ michael@0: _createExtraActors: createExtraActors, michael@0: _appendExtraActors: appendExtraActors, michael@0: michael@0: /** michael@0: * Does the actual work of attching to a tab. michael@0: */ michael@0: _attach: function BTA_attach() { michael@0: if (this._attached) { michael@0: return; michael@0: } michael@0: michael@0: // Create a pool for tab-lifetime actors. michael@0: dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached."); michael@0: this._tabPool = new ActorPool(this.conn); michael@0: this.conn.addActorPool(this._tabPool); michael@0: michael@0: // ... and a pool for context-lifetime actors. michael@0: this._pushContext(); michael@0: michael@0: this._progressListener = new DebuggerProgressListener(this); michael@0: this._progressListener.watch(this.docShell); michael@0: michael@0: this._attached = true; michael@0: }, michael@0: michael@0: /** michael@0: * Creates a thread actor and a pool for context-lifetime actors. It then sets michael@0: * up the content window for debugging. michael@0: */ michael@0: _pushContext: function BTA_pushContext() { michael@0: dbg_assert(!this._contextPool, "Can't push multiple contexts"); michael@0: michael@0: this._contextPool = new ActorPool(this.conn); michael@0: this.conn.addActorPool(this._contextPool); michael@0: michael@0: this.threadActor = new ThreadActor(this, this.window); michael@0: this._contextPool.addActor(this.threadActor); michael@0: }, michael@0: michael@0: /** michael@0: * Exits the current thread actor and removes the context-lifetime actor pool. michael@0: * The content window is no longer being debugged after this call. michael@0: */ michael@0: _popContext: function BTA_popContext() { michael@0: dbg_assert(!!this._contextPool, "No context to pop."); michael@0: michael@0: this.conn.removeActorPool(this._contextPool); michael@0: this._contextPool = null; michael@0: this.threadActor.exit(); michael@0: this.threadActor = null; michael@0: }, michael@0: michael@0: /** michael@0: * Does the actual work of detaching from a tab. michael@0: * michael@0: * @returns false if the tab wasn't attached or true of detahing succeeds. michael@0: */ michael@0: _detach: function BTA_detach() { michael@0: if (!this.attached) { michael@0: return false; michael@0: } michael@0: michael@0: // Check for docShell availability, as it can be already gone michael@0: // during Firefox shutdown. michael@0: if (this.docShell) { michael@0: this._progressListener.unwatch(this.docShell); michael@0: } michael@0: this._progressListener = null; michael@0: michael@0: this._popContext(); michael@0: michael@0: // Shut down actors that belong to this tab's pool. michael@0: this.conn.removeActorPool(this._tabPool); michael@0: this._tabPool = null; michael@0: if (this._tabActorPool) { michael@0: this.conn.removeActorPool(this._tabActorPool); michael@0: this._tabActorPool = null; michael@0: } michael@0: michael@0: this._attached = false; michael@0: return true; michael@0: }, michael@0: michael@0: // Protocol Request Handlers michael@0: michael@0: onAttach: function BTA_onAttach(aRequest) { michael@0: if (this.exited) { michael@0: return { type: "exited" }; michael@0: } michael@0: michael@0: this._attach(); michael@0: michael@0: return { michael@0: type: "tabAttached", michael@0: threadActor: this.threadActor.actorID, michael@0: cacheEnabled: this._getCacheEnabled(), michael@0: javascriptEnabled: this._getJavascriptEnabled(), michael@0: traits: this.traits, michael@0: }; michael@0: }, michael@0: michael@0: onDetach: function BTA_onDetach(aRequest) { michael@0: if (!this._detach()) { michael@0: return { error: "wrongState" }; michael@0: } michael@0: michael@0: return { type: "detached" }; michael@0: }, michael@0: michael@0: /** michael@0: * Reload the page in this tab. michael@0: */ michael@0: onReload: function(aRequest) { michael@0: // Wait a tick so that the response packet can be dispatched before the michael@0: // subsequent navigation event packet. michael@0: Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => { michael@0: this.window.location.reload(); michael@0: }, "TabActor.prototype.onReload's delayed body"), 0); michael@0: return {}; michael@0: }, michael@0: michael@0: /** michael@0: * Navigate this tab to a new location michael@0: */ michael@0: onNavigateTo: function(aRequest) { michael@0: // Wait a tick so that the response packet can be dispatched before the michael@0: // subsequent navigation event packet. michael@0: Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => { michael@0: this.window.location = aRequest.url; michael@0: }, "TabActor.prototype.onNavigateTo's delayed body"), 0); michael@0: return {}; michael@0: }, michael@0: michael@0: /** michael@0: * Reconfigure options. michael@0: */ michael@0: onReconfigure: function (aRequest) { michael@0: let options = aRequest.options || {}; michael@0: michael@0: this._toggleJsOrCache(options); michael@0: return {}; michael@0: }, michael@0: michael@0: /** michael@0: * Handle logic to enable/disable JS/cache. michael@0: */ michael@0: _toggleJsOrCache: function(options) { michael@0: // Wait a tick so that the response packet can be dispatched before the michael@0: // subsequent navigation event packet. michael@0: let reload = false; michael@0: michael@0: if (typeof options.javascriptEnabled !== "undefined" && michael@0: options.javascriptEnabled !== this._getJavascriptEnabled()) { michael@0: this._setJavascriptEnabled(options.javascriptEnabled); michael@0: reload = true; michael@0: } michael@0: if (typeof options.cacheEnabled !== "undefined" && michael@0: options.cacheEnabled !== this._getCacheEnabled()) { michael@0: this._setCacheEnabled(options.cacheEnabled); michael@0: reload = true; michael@0: } michael@0: michael@0: // Reload if: michael@0: // - there's an explicit `performReload` flag and it's true michael@0: // - there's no `performReload` flag, but it makes sense to do so michael@0: let hasExplicitReloadFlag = "performReload" in options; michael@0: if ((hasExplicitReloadFlag && options.performReload) || michael@0: (!hasExplicitReloadFlag && reload)) { michael@0: this.onReload(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Disable or enable the cache via docShell. michael@0: */ michael@0: _setCacheEnabled: function(allow) { michael@0: let enable = Ci.nsIRequest.LOAD_NORMAL; michael@0: let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE | michael@0: Ci.nsIRequest.INHIBIT_CACHING; michael@0: if (this.docShell) { michael@0: this.docShell.defaultLoadFlags = allow ? enable : disable; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Disable or enable JS via docShell. michael@0: */ michael@0: _setJavascriptEnabled: function(allow) { michael@0: if (this.docShell) { michael@0: this.docShell.allowJavascript = allow; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Return cache allowed status. michael@0: */ michael@0: _getCacheEnabled: function() { michael@0: if (!this.docShell) { michael@0: // The tab is already closed. michael@0: return null; michael@0: } michael@0: michael@0: let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE | michael@0: Ci.nsIRequest.INHIBIT_CACHING; michael@0: return this.docShell.defaultLoadFlags !== disable; michael@0: }, michael@0: michael@0: /** michael@0: * Return JS allowed status. michael@0: */ michael@0: _getJavascriptEnabled: function() { michael@0: if (!this.docShell) { michael@0: // The tab is already closed. michael@0: return null; michael@0: } michael@0: michael@0: return this.docShell.allowJavascript; michael@0: }, michael@0: michael@0: /** michael@0: * Prepare to enter a nested event loop by disabling debuggee events. michael@0: */ michael@0: preNest: function BTA_preNest() { michael@0: if (!this.window) { michael@0: // The tab is already closed. michael@0: return; michael@0: } michael@0: let windowUtils = this.window michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: windowUtils.suppressEventHandling(true); michael@0: windowUtils.suspendTimeouts(); michael@0: }, michael@0: michael@0: /** michael@0: * Prepare to exit a nested event loop by enabling debuggee events. michael@0: */ michael@0: postNest: function BTA_postNest(aNestData) { michael@0: if (!this.window) { michael@0: // The tab is already closed. michael@0: return; michael@0: } michael@0: let windowUtils = this.window michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: windowUtils.resumeTimeouts(); michael@0: windowUtils.suppressEventHandling(false); michael@0: if (this._pendingNavigation) { michael@0: this._pendingNavigation.resume(); michael@0: this._pendingNavigation = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle location changes, by clearing the previous debuggees and enabling michael@0: * debugging, which may have been disabled temporarily by the michael@0: * DebuggerProgressListener. michael@0: */ michael@0: _windowReady: function (window) { michael@0: let isTopLevel = window == this.window; michael@0: dumpn("window-ready: " + window.location + " isTopLevel:" + isTopLevel); michael@0: michael@0: events.emit(this, "window-ready", { michael@0: window: window, michael@0: isTopLevel: isTopLevel michael@0: }); michael@0: michael@0: // TODO bug 997119: move that code to ThreadActor by listening to window-ready michael@0: let threadActor = this.threadActor; michael@0: if (isTopLevel) { michael@0: threadActor.clearDebuggees(); michael@0: if (threadActor.dbg) { michael@0: threadActor.dbg.enabled = true; michael@0: threadActor.global = window; michael@0: threadActor.maybePauseOnExceptions(); michael@0: } michael@0: } michael@0: michael@0: // Refresh the debuggee list when a new window object appears (top window or michael@0: // iframe). michael@0: if (threadActor.attached) { michael@0: threadActor.findGlobals(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Start notifying server codebase and client about a new document michael@0: * being loaded in the currently targeted context. michael@0: */ michael@0: _willNavigate: function (window, newURI, request) { michael@0: let isTopLevel = window == this.window; michael@0: michael@0: // will-navigate event needs to be dispatched synchronously, michael@0: // by calling the listeners in the order or registration. michael@0: // This event fires once navigation starts, michael@0: // (all pending user prompts are dealt with), michael@0: // but before the first request starts. michael@0: events.emit(this, "will-navigate", { michael@0: window: window, michael@0: isTopLevel: isTopLevel, michael@0: newURI: newURI, michael@0: request: request michael@0: }); michael@0: michael@0: michael@0: // We don't do anything for inner frames in TabActor. michael@0: // (we will only update thread actor on window-ready) michael@0: if (!isTopLevel) { michael@0: return; michael@0: } michael@0: michael@0: // Proceed normally only if the debuggee is not paused. michael@0: // TODO bug 997119: move that code to ThreadActor by listening to will-navigate michael@0: let threadActor = this.threadActor; michael@0: if (request && threadActor.state == "paused") { michael@0: request.suspend(); michael@0: threadActor.onResume(); michael@0: threadActor.dbg.enabled = false; michael@0: this._pendingNavigation = request; michael@0: } michael@0: threadActor.disableAllBreakpoints(); michael@0: michael@0: this.conn.send({ michael@0: from: this.actorID, michael@0: type: "tabNavigated", michael@0: url: newURI, michael@0: nativeConsoleAPI: true, michael@0: state: "start" michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Notify server and client about a new document done loading in the current michael@0: * targeted context. michael@0: */ michael@0: _navigate: function (window) { michael@0: let isTopLevel = window == this.window; michael@0: michael@0: // navigate event needs to be dispatched synchronously, michael@0: // by calling the listeners in the order or registration. michael@0: // This event is fired once the document is loaded, michael@0: // after the load event, it's document ready-state is 'complete'. michael@0: events.emit(this, "navigate", { michael@0: window: window, michael@0: isTopLevel: isTopLevel michael@0: }); michael@0: michael@0: // We don't do anything for inner frames in TabActor. michael@0: // (we will only update thread actor on window-ready) michael@0: if (!isTopLevel) { michael@0: return; michael@0: } michael@0: michael@0: // TODO bug 997119: move that code to ThreadActor by listening to navigate michael@0: let threadActor = this.threadActor; michael@0: if (threadActor.state == "running") { michael@0: threadActor.dbg.enabled = true; michael@0: } michael@0: michael@0: this.conn.send({ michael@0: from: this.actorID, michael@0: type: "tabNavigated", michael@0: url: this.url, michael@0: title: this.title, michael@0: nativeConsoleAPI: this.hasNativeConsoleAPI(this.window), michael@0: state: "stop" michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Tells if the window.console object is native or overwritten by script in michael@0: * the page. michael@0: * michael@0: * @param nsIDOMWindow aWindow michael@0: * The window object you want to check. michael@0: * @return boolean michael@0: * True if the window.console object is native, or false otherwise. michael@0: */ michael@0: hasNativeConsoleAPI: function BTA_hasNativeConsoleAPI(aWindow) { michael@0: let isNative = false; michael@0: try { michael@0: // We are very explicitly examining the "console" property of michael@0: // the non-Xrayed object here. michael@0: let console = aWindow.wrappedJSObject.console; michael@0: isNative = console instanceof aWindow.Console; michael@0: } michael@0: catch (ex) { } michael@0: return isNative; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The request types this actor can handle. michael@0: */ michael@0: TabActor.prototype.requestTypes = { michael@0: "attach": TabActor.prototype.onAttach, michael@0: "detach": TabActor.prototype.onDetach, michael@0: "reload": TabActor.prototype.onReload, michael@0: "navigateTo": TabActor.prototype.onNavigateTo, michael@0: "reconfigure": TabActor.prototype.onReconfigure michael@0: }; michael@0: michael@0: /** michael@0: * Creates a tab actor for handling requests to a single in-process michael@0: * tab. Most of the implementation comes from TabActor. michael@0: * michael@0: * @param aConnection DebuggerServerConnection michael@0: * The conection to the client. michael@0: * @param aBrowser browser michael@0: * The browser instance that contains this tab. michael@0: * @param aTabBrowser tabbrowser michael@0: * The tabbrowser that can receive nsIWebProgressListener events. michael@0: */ michael@0: function BrowserTabActor(aConnection, aBrowser, aTabBrowser) michael@0: { michael@0: TabActor.call(this, aConnection, aBrowser); michael@0: this._browser = aBrowser; michael@0: this._tabbrowser = aTabBrowser; michael@0: } michael@0: michael@0: BrowserTabActor.prototype = Object.create(TabActor.prototype); michael@0: michael@0: BrowserTabActor.prototype.constructor = BrowserTabActor; michael@0: michael@0: Object.defineProperty(BrowserTabActor.prototype, "docShell", { michael@0: get: function() { michael@0: return this._browser.docShell; michael@0: }, michael@0: enumerable: true, michael@0: configurable: false michael@0: }); michael@0: michael@0: Object.defineProperty(BrowserTabActor.prototype, "title", { michael@0: get: function() { michael@0: let title = this.contentDocument.title || this._browser.contentTitle; michael@0: // If contentTitle is empty (e.g. on a not-yet-restored tab), but there is a michael@0: // tabbrowser (i.e. desktop Firefox, but not Fennec), we can use the label michael@0: // as the title. michael@0: if (!title && this._tabbrowser) { michael@0: let tab = this._tabbrowser._getTabForContentWindow(this.window); michael@0: if (tab) { michael@0: title = tab.label; michael@0: } michael@0: } michael@0: return title; michael@0: }, michael@0: enumerable: true, michael@0: configurable: false michael@0: }); michael@0: michael@0: Object.defineProperty(BrowserTabActor.prototype, "browser", { michael@0: get: function() { michael@0: return this._browser; michael@0: }, michael@0: enumerable: true, michael@0: configurable: false michael@0: }); michael@0: michael@0: BrowserTabActor.prototype.disconnect = function() { michael@0: TabActor.prototype.disconnect.call(this); michael@0: this._browser = null; michael@0: this._tabbrowser = null; michael@0: }; michael@0: michael@0: BrowserTabActor.prototype.exit = function() { michael@0: TabActor.prototype.exit.call(this); michael@0: this._browser = null; michael@0: this._tabbrowser = null; michael@0: }; michael@0: michael@0: /** michael@0: * This actor is a shim that connects to a ContentActor in a remote michael@0: * browser process. All RDP packets get forwarded using the message michael@0: * manager. michael@0: * michael@0: * @param aConnection The main RDP connection. michael@0: * @param aBrowser XUL element to connect to. michael@0: */ michael@0: function RemoteBrowserTabActor(aConnection, aBrowser) michael@0: { michael@0: this._conn = aConnection; michael@0: this._browser = aBrowser; michael@0: this._form = null; michael@0: } michael@0: michael@0: RemoteBrowserTabActor.prototype = { michael@0: connect: function() { michael@0: return DebuggerServer.connectToChild(this._conn, this._browser); michael@0: }, michael@0: michael@0: form: function() { michael@0: return this._form; michael@0: }, michael@0: michael@0: exit: function() { michael@0: this._browser = null; michael@0: }, michael@0: }; michael@0: michael@0: function BrowserAddonList(aConnection) michael@0: { michael@0: this._connection = aConnection; michael@0: this._actorByAddonId = new Map(); michael@0: this._onListChanged = null; michael@0: } michael@0: michael@0: BrowserAddonList.prototype.getList = function() { michael@0: var deferred = promise.defer(); michael@0: AddonManager.getAllAddons((addons) => { michael@0: for (let addon of addons) { michael@0: let actor = this._actorByAddonId.get(addon.id); michael@0: if (!actor) { michael@0: actor = new BrowserAddonActor(this._connection, addon); michael@0: this._actorByAddonId.set(addon.id, actor); michael@0: } michael@0: } michael@0: deferred.resolve([actor for ([_, actor] of this._actorByAddonId)]); michael@0: }); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: Object.defineProperty(BrowserAddonList.prototype, "onListChanged", { michael@0: enumerable: true, configurable: true, michael@0: get: function() { return this._onListChanged; }, michael@0: set: function(v) { michael@0: if (v !== null && typeof v != "function") { michael@0: throw Error("onListChanged property may only be set to 'null' or a function"); michael@0: } michael@0: this._onListChanged = v; michael@0: if (this._onListChanged) { michael@0: AddonManager.addAddonListener(this); michael@0: } else { michael@0: AddonManager.removeAddonListener(this); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: BrowserAddonList.prototype.onInstalled = function (aAddon) { michael@0: this._onListChanged(); michael@0: }; michael@0: michael@0: BrowserAddonList.prototype.onUninstalled = function (aAddon) { michael@0: this._actorByAddonId.delete(aAddon.id); michael@0: this._onListChanged(); michael@0: }; michael@0: michael@0: function BrowserAddonActor(aConnection, aAddon) { michael@0: this.conn = aConnection; michael@0: this._addon = aAddon; michael@0: this._contextPool = null; michael@0: this._threadActor = null; michael@0: this._global = null; michael@0: AddonManager.addAddonListener(this); michael@0: } michael@0: michael@0: BrowserAddonActor.prototype = { michael@0: actorPrefix: "addon", michael@0: michael@0: get exited() { michael@0: return !this._addon; michael@0: }, michael@0: michael@0: get id() { michael@0: return this._addon.id; michael@0: }, michael@0: michael@0: get url() { michael@0: return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined; michael@0: }, michael@0: michael@0: get attached() { michael@0: return this._threadActor; michael@0: }, michael@0: michael@0: get global() { michael@0: return this._global; michael@0: }, michael@0: michael@0: form: function BAA_form() { michael@0: dbg_assert(this.actorID, "addon should have an actorID."); michael@0: michael@0: return { michael@0: actor: this.actorID, michael@0: id: this.id, michael@0: name: this._addon.name, michael@0: url: this.url, michael@0: debuggable: this._addon.isDebuggable, michael@0: }; michael@0: }, michael@0: michael@0: disconnect: function BAA_disconnect() { michael@0: this._addon = null; michael@0: this._global = null; michael@0: AddonManager.removeAddonListener(this); michael@0: }, michael@0: michael@0: setOptions: function BAA_setOptions(aOptions) { michael@0: if ("global" in aOptions) { michael@0: this._global = aOptions.global; michael@0: } michael@0: }, michael@0: michael@0: onDisabled: function BAA_onDisabled(aAddon) { michael@0: if (aAddon != this._addon) { michael@0: return; michael@0: } michael@0: michael@0: this._global = null; michael@0: }, michael@0: michael@0: onUninstalled: function BAA_onUninstalled(aAddon) { michael@0: if (aAddon != this._addon) { michael@0: return; michael@0: } michael@0: michael@0: if (this.attached) { michael@0: this.onDetach(); michael@0: this.conn.send({ from: this.actorID, type: "tabDetached" }); michael@0: } michael@0: michael@0: this.disconnect(); michael@0: }, michael@0: michael@0: onAttach: function BAA_onAttach() { michael@0: if (this.exited) { michael@0: return { type: "exited" }; michael@0: } michael@0: michael@0: if (!this.attached) { michael@0: this._contextPool = new ActorPool(this.conn); michael@0: this.conn.addActorPool(this._contextPool); michael@0: michael@0: this._threadActor = new AddonThreadActor(this.conn, this, michael@0: this._addon.id); michael@0: this._contextPool.addActor(this._threadActor); michael@0: } michael@0: michael@0: return { type: "tabAttached", threadActor: this._threadActor.actorID }; michael@0: }, michael@0: michael@0: onDetach: function BAA_onDetach() { michael@0: if (!this.attached) { michael@0: return { error: "wrongState" }; michael@0: } michael@0: michael@0: this.conn.removeActorPool(this._contextPool); michael@0: this._contextPool = null; michael@0: michael@0: this._threadActor = null; michael@0: michael@0: return { type: "detached" }; michael@0: }, michael@0: michael@0: preNest: function() { michael@0: let e = Services.wm.getEnumerator(null); michael@0: while (e.hasMoreElements()) { michael@0: let win = e.getNext(); michael@0: let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: windowUtils.suppressEventHandling(true); michael@0: windowUtils.suspendTimeouts(); michael@0: } michael@0: }, michael@0: michael@0: postNest: function() { michael@0: let e = Services.wm.getEnumerator(null); michael@0: while (e.hasMoreElements()) { michael@0: let win = e.getNext(); michael@0: let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: windowUtils.resumeTimeouts(); michael@0: windowUtils.suppressEventHandling(false); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: BrowserAddonActor.prototype.requestTypes = { michael@0: "attach": BrowserAddonActor.prototype.onAttach, michael@0: "detach": BrowserAddonActor.prototype.onDetach michael@0: }; michael@0: michael@0: /** michael@0: * The DebuggerProgressListener object is an nsIWebProgressListener which michael@0: * handles onStateChange events for the inspected browser. If the user tries to michael@0: * navigate away from a paused page, the listener makes sure that the debuggee michael@0: * is resumed before the navigation begins. michael@0: * michael@0: * @param TabActor aTabActor michael@0: * The tab actor associated with this listener. michael@0: */ michael@0: function DebuggerProgressListener(aTabActor) { michael@0: this._tabActor = aTabActor; michael@0: this._onWindowCreated = this.onWindowCreated.bind(this); michael@0: } michael@0: michael@0: DebuggerProgressListener.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIWebProgressListener, michael@0: Ci.nsISupportsWeakReference, michael@0: Ci.nsISupports, michael@0: ]), michael@0: michael@0: watch: function DPL_watch(docShell) { michael@0: let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebProgress); michael@0: webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATUS | michael@0: Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | michael@0: Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); michael@0: michael@0: // TODO: fix docShell.chromeEventHandler in child processes! michael@0: let chromeEventHandler = docShell.chromeEventHandler || michael@0: docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIContentFrameMessageManager); michael@0: michael@0: // Watch for globals being created in this docshell tree. michael@0: chromeEventHandler.addEventListener("DOMWindowCreated", michael@0: this._onWindowCreated, true); michael@0: chromeEventHandler.addEventListener("pageshow", michael@0: this._onWindowCreated, true); michael@0: }, michael@0: michael@0: unwatch: function DPL_unwatch(docShell) { michael@0: let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebProgress); michael@0: webProgress.removeProgressListener(this); michael@0: michael@0: // TODO: fix docShell.chromeEventHandler in child processes! michael@0: let chromeEventHandler = docShell.chromeEventHandler || michael@0: docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIContentFrameMessageManager); michael@0: chromeEventHandler.removeEventListener("DOMWindowCreated", michael@0: this._onWindowCreated, true); michael@0: chromeEventHandler.removeEventListener("pageshow", michael@0: this._onWindowCreated, true); michael@0: }, michael@0: michael@0: onWindowCreated: michael@0: DevToolsUtils.makeInfallible(function DPL_onWindowCreated(evt) { michael@0: // Ignore any event if the tab actor isn't attached. michael@0: if (!this._tabActor.attached) { michael@0: return; michael@0: } michael@0: michael@0: // pageshow events for non-persisted pages have already been handled by a michael@0: // prior DOMWindowCreated event. michael@0: if (evt.type == "pageshow" && !evt.persisted) { michael@0: return; michael@0: } michael@0: michael@0: let window = evt.target.defaultView; michael@0: this._tabActor._windowReady(window); michael@0: }, "DebuggerProgressListener.prototype.onWindowCreated"), michael@0: michael@0: onStateChange: michael@0: DevToolsUtils.makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) { michael@0: // Ignore any event if the tab actor isn't attached. michael@0: if (!this._tabActor.attached) { michael@0: return; michael@0: } michael@0: michael@0: let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START; michael@0: let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP; michael@0: let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; michael@0: let isWindow = aFlag & Ci.nsIWebProgressListener.STATE_IS_WINDOW; michael@0: michael@0: let window = aProgress.DOMWindow; michael@0: if (isDocument && isStart) { michael@0: let newURI = aRequest instanceof Ci.nsIChannel ? aRequest.URI.spec : null; michael@0: this._tabActor._willNavigate(window, newURI, aRequest); michael@0: } michael@0: if (isWindow && isStop) { michael@0: this._tabActor._navigate(window); michael@0: } michael@0: }, "DebuggerProgressListener.prototype.onStateChange") michael@0: };