/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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"; let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise; /** * Browser-specific actors. */ /** * Yield all windows of type |aWindowType|, from the oldest window to the * youngest, using nsIWindowMediator::getEnumerator. We're usually * interested in "navigator:browser" windows. */ function allAppShellDOMWindows(aWindowType) { let e = windowMediator.getEnumerator(aWindowType); while (e.hasMoreElements()) { yield e.getNext(); } } /** * Retrieve the window type of the top-level window |aWindow|. */ function appShellDOMWindowType(aWindow) { /* This is what nsIWindowMediator's enumerator checks. */ return aWindow.document.documentElement.getAttribute('windowtype'); } /** * Send Debugger:Shutdown events to all "navigator:browser" windows. */ function sendShutdownEvent() { for (let win of allAppShellDOMWindows("navigator:browser")) { let evt = win.document.createEvent("Event"); evt.initEvent("Debugger:Shutdown", true, false); win.document.documentElement.dispatchEvent(evt); } } /** * 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 aConnection DebuggerServerConnection * The conection to the client. */ function createRootActor(aConnection) { return new RootActor(aConnection, { tabList: new BrowserTabList(aConnection), globalActorFactories: DebuggerServer.globalActorFactories, onShutdown: sendShutdownEvent }); } var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"] .getService(Ci.nsIWindowMediator); /** * 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 aConnection 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. */ function BrowserTabList(aConnection) { this._connection = aConnection; /* * 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; BrowserTabList.prototype.getList = function() { let topXULWindow = windowMediator.getMostRecentWindow("navigator:browser"); // 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; let 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. // Iterate over all navigator:browser XUL windows. for (let win of allAppShellDOMWindows("navigator:browser")) { let selectedTab = win.gBrowser.selectedBrowser; // 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.wrappedJSObject as the debuggee global. for (let browser of win.gBrowser.browsers) { // Do we have an existing actor for this browser? If not, create one. let actor = this._actorByBrowser.get(browser); if (actor) { foundCount++; } else { actor = new BrowserTabActor(this._connection, browser, win.gBrowser); this._actorByBrowser.set(browser, actor); } // Set the 'selected' properties on all actors correctly. actor.selected = (win === topXULWindow && browser === selectedTab); } } if (this._testing && initialMapSize !== foundCount) throw Error("_actorByBrowser map contained actors for dead tabs"); this._mustNotify = true; this._checkListening(); return promise.resolve([actor for ([_, actor] of this._actorByBrowser)]); }; Object.defineProperty(BrowserTabList.prototype, 'onListChanged', { enumerable: true, configurable:true, get: function() { return this._onListChanged; }, set: function(v) { if (v !== null && typeof v !== 'function') { throw 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 |aActor|, belonging to |aBrowser|, and notify the onListChanged * handle if needed. */ BrowserTabList.prototype._handleActorClose = function(aActor, aBrowser) { if (this._testing) { if (this._actorByBrowser.get(aBrowser) !== aActor) { throw Error("BrowserTabActor not stored in map under given browser"); } if (aActor.browser !== aBrowser) { throw Error("actor's browser and map key don't match"); } } this._actorByBrowser.delete(aBrowser); aActor.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. * * 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"]); /* If we have live actors, we need to be ready to mark them dead. */ this._listenForEventsIf(this._actorByBrowser.size > 0, "_listeningForTabClose", ["TabClose"]); /* * 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)); }; /* * Add or remove event listeners for all XUL windows. * * @param aShouldListen boolean * True if we should add event handlers; 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 events. * @param aEventNames array of strings * An array of event names. */ BrowserTabList.prototype._listenForEventsIf = function(aShouldListen, aGuard, aEventNames) { if (!aShouldListen !== !this[aGuard]) { let op = aShouldListen ? "addEventListener" : "removeEventListener"; for (let win of allAppShellDOMWindows("navigator:browser")) { for (let name of aEventNames) { win[op](name, this, false); } } this[aGuard] = aShouldListen; } }; /** * Implement nsIDOMEventListener. */ BrowserTabList.prototype.handleEvent = makeInfallible(function(aEvent) { switch (aEvent.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 browser = aEvent.target.linkedBrowser; let actor = this._actorByBrowser.get(browser); if (actor) { this._handleActorClose(actor, browser); } break; } }, "BrowserTabList.prototype.handleEvent"); /* * If |aShouldListen| is true, ensure we've registered a listener with the * window mediator. Otherwise, ensure we haven't registered a listener. */ BrowserTabList.prototype._listenToMediatorIf = function(aShouldListen) { if (!aShouldListen !== !this._listeningToMediator) { let op = aShouldListen ? "addListener" : "removeListener"; windowMediator[op](this); this._listeningToMediator = aShouldListen; } }; /** * 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 = makeInfallible(function(aWindow) { let handleLoad = makeInfallible(() => { /* We don't want any further load events from this window. */ aWindow.removeEventListener("load", handleLoad, false); if (appShellDOMWindowType(aWindow) !== "navigator:browser") return; // Listen for future tab activity. if (this._listeningForTabOpen) { aWindow.addEventListener("TabOpen", this, false); aWindow.addEventListener("TabSelect", this, false); } if (this._listeningForTabClose) { aWindow.addEventListener("TabClose", this, false); } // 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). */ aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow); aWindow.addEventListener("load", handleLoad, false); }, "BrowserTabList.prototype.onOpenWindow"); BrowserTabList.prototype.onCloseWindow = makeInfallible(function(aWindow) { aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow); if (appShellDOMWindowType(aWindow) !== "navigator:browser") 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(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"); /** * Creates a tab actor for handling requests to a browser tab, like * attaching and detaching. BrowserTabActor respects the actor factories * registered with DebuggerServer.addTabActor. * * @param aConnection DebuggerServerConnection * The conection to the client. * @param aBrowser browser * The browser instance that contains this tab. * @param aTabBrowser tabbrowser * The tabbrowser that can receive nsIWebProgressListener events. */ function BrowserTabActor(aConnection, aBrowser, aTabBrowser) { this.conn = aConnection; this._browser = aBrowser; this._tabbrowser = aTabBrowser; this._tabActorPool = null; // A map of actor names to actor instances provided by extensions. this._extraActors = {}; this._onWindowCreated = this.onWindowCreated.bind(this); } // XXX (bug 710213): BrowserTabActor attach/detach/exit/disconnect is a // *complete* mess, needs to be rethought asap. BrowserTabActor.prototype = { get browser() { return this._browser; }, get exited() { return !this.browser; }, get attached() { return !!this._attached; }, _tabPool: null, get tabActorPool() { return this._tabPool; }, _contextPool: null, get contextActorPool() { return this._contextPool; }, _pendingNavigation: null, /** * Add the specified actor to the default actor pool connection, in order to * keep it alive as long as the server is. This is used by breakpoints in the * thread actor. * * @param actor aActor * The actor object. */ addToParentPool: function BTA_addToParentPool(aActor) { this.conn.addActor(aActor); }, /** * Remove the specified actor from the default actor pool. * * @param BreakpointActor aActor * The actor object. */ removeFromParentPool: function BTA_removeFromParentPool(aActor) { this.conn.removeActor(aActor); }, // A constant prefix that will be used to form the actor ID by the server. actorPrefix: "tab", /** * Getter for the tab title. * @return string * Tab title. */ get title() { let title = this.browser.contentTitle; // 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 (!title && this._tabbrowser) { title = this._tabbrowser ._getTabForContentWindow(this.window).label; } return title; }, /** * Getter for the tab URL. * @return string * Tab URL. */ get url() { return this.browser.currentURI.spec; }, /** * Getter for the tab content window, will be used by child actors to target * the right window. * @return nsIDOMWindow * Tab content window. */ get window() { if (this.browser instanceof Ci.nsIDOMWindow) { return this.browser; } else if (this.browser instanceof Ci.nsIDOMElement) { return this.browser.contentWindow; } else { return null; } }, grip: function BTA_grip() { dbg_assert(!this.exited, "grip() shouldn't be called on exited browser actor."); dbg_assert(this.actorID, "tab should have an actorID."); let response = { actor: this.actorID, title: this.title, url: this.url }; // Walk over tab actors added by extensions and add them to a new ActorPool. let actorPool = new ActorPool(this.conn); this._createExtraActors(DebuggerServer.tabActorFactories, actorPool); if (!actorPool.isEmpty()) { this._tabActorPool = actorPool; this.conn.addActorPool(this._tabActorPool); } this._appendExtraActors(response); return response; }, /** * Called when the actor is removed from the connection. */ disconnect: function BTA_disconnect() { this._detach(); this._extraActors = null; }, /** * Called by the root actor when the underlying tab is closed. */ exit: function BTA_exit() { if (this.exited) { return; } if (this.attached) { this._detach(); this.conn.send({ from: this.actorID, type: "tabDetached" }); } this._browser = null; this._tabbrowser = null; }, /* Support for DebuggerServer.addTabActor. */ _createExtraActors: CommonCreateExtraActors, _appendExtraActors: CommonAppendExtraActors, /** * Does the actual work of attching to a tab. */ _attach: function BTA_attach() { if (this._attached) { return; } // Create a pool for tab-lifetime actors. dbg_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(); // Watch for globals being created in this tab. this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true); this.browser.addEventListener("pageshow", this._onWindowCreated, true); if (this._tabbrowser) { this._progressListener = new DebuggerProgressListener(this); } this._attached = true; }, /** * Creates a thread actor and a pool for context-lifetime actors. It then sets * up the content window for debugging. */ _pushContext: function BTA_pushContext() { dbg_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.wrappedJSObject); 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: function BTA_popContext() { dbg_assert(!!this._contextPool, "No context to pop."); this.conn.removeActorPool(this._contextPool); this._contextPool = null; this.threadActor.exit(); this.threadActor = null; }, /** * Does the actual work of detaching from a tab. */ _detach: function BTA_detach() { if (!this.attached) { return; } if (this._progressListener) { this._progressListener.destroy(); } this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true); this.browser.removeEventListener("pageshow", this._onWindowCreated, true); this._popContext(); // Shut down actors that belong to this tab's pool. this.conn.removeActorPool(this._tabPool); this._tabPool = null; if (this._tabActorPool) { this.conn.removeActorPool(this._tabActorPool, true); this._tabActorPool = null; } this._attached = false; }, // Protocol Request Handlers onAttach: function BTA_onAttach(aRequest) { if (this.exited) { return { type: "exited" }; } this._attach(); return { type: "tabAttached", threadActor: this.threadActor.actorID }; }, onDetach: function BTA_onDetach(aRequest) { if (!this.attached) { return { error: "wrongState" }; } this._detach(); return { type: "detached" }; }, /** * Reload the page in this tab. */ onReload: function(aRequest) { // Wait a tick so that the response packet can be dispatched before the // subsequent navigation event packet. Services.tm.currentThread.dispatch(makeInfallible(() => { this.window.location.reload(); }, "BrowserTabActor.prototype.onReload's delayed body"), 0); return {}; }, /** * Navigate this tab to a new location */ onNavigateTo: function(aRequest) { // Wait a tick so that the response packet can be dispatched before the // subsequent navigation event packet. Services.tm.currentThread.dispatch(makeInfallible(() => { this.window.location = aRequest.url; }, "BrowserTabActor.prototype.onNavigateTo's delayed body"), 0); return {}; }, /** * Prepare to enter a nested event loop by disabling debuggee events. */ preNest: function BTA_preNest() { if (!this.browser) { // 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: function BTA_postNest(aNestData) { if (!this.browser) { // The tab is already closed. return; } let windowUtils = this.window .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); windowUtils.resumeTimeouts(); windowUtils.suppressEventHandling(false); if (this._pendingNavigation) { this._pendingNavigation.resume(); this._pendingNavigation = null; } }, /** * Handle location changes, by clearing the previous debuggees and enabling * debugging, which may have been disabled temporarily by the * DebuggerProgressListener. */ onWindowCreated: makeInfallible(function BTA_onWindowCreated(evt) { if (evt.target === this.browser.contentDocument) { // pageshow events for non-persisted pages have already been handled by a // prior DOMWindowCreated event. if (evt.type == "pageshow" && !evt.persisted) { return; } if (this._attached) { this.threadActor.clearDebuggees(); if (this.threadActor.dbg) { this.threadActor.dbg.enabled = true; this.threadActor.maybePauseOnExceptions(); } } } if (this._attached) { this.threadActor.global = evt.target.defaultView.wrappedJSObject; if (this.threadActor.attached) { this.threadActor.findGlobals(); } } }, "BrowserTabActor.prototype.onWindowCreated"), /** * Tells if the window.console object is native or overwritten by script in * the page. * * @param nsIDOMWindow aWindow * The window object you want to check. * @return boolean * True if the window.console object is native, or false otherwise. */ hasNativeConsoleAPI: function BTA_hasNativeConsoleAPI(aWindow) { let isNative = false; try { let console = aWindow.wrappedJSObject.console; isNative = "__mozillaConsole__" in console; } catch (ex) { } return isNative; } }; /** * The request types this actor can handle. */ BrowserTabActor.prototype.requestTypes = { "attach": BrowserTabActor.prototype.onAttach, "detach": BrowserTabActor.prototype.onDetach, "reload": BrowserTabActor.prototype.onReload, "navigateTo": BrowserTabActor.prototype.onNavigateTo }; /** * 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 BrowserTabActor aBrowserTabActor * The tab actor associated with this listener. */ function DebuggerProgressListener(aBrowserTabActor) { this._tabActor = aBrowserTabActor; this._tabActor._tabbrowser.addProgressListener(this); } DebuggerProgressListener.prototype = { onStateChange: makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) { let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START; let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP; let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; let isNetwork = aFlag & Ci.nsIWebProgressListener.STATE_IS_NETWORK; let isRequest = aFlag & Ci.nsIWebProgressListener.STATE_IS_REQUEST; let isWindow = aFlag & Ci.nsIWebProgressListener.STATE_IS_WINDOW; // Skip non-interesting states. if (!isWindow || !isNetwork || aProgress.DOMWindow != this._tabActor.window) { return; } if (isStart && aRequest instanceof Ci.nsIChannel) { // Proceed normally only if the debuggee is not paused. if (this._tabActor.threadActor.state == "paused") { aRequest.suspend(); this._tabActor.threadActor.onResume(); this._tabActor.threadActor.dbg.enabled = false; this._tabActor._pendingNavigation = aRequest; } this._tabActor.threadActor.disableAllBreakpoints(); this._tabActor.conn.send({ from: this._tabActor.actorID, type: "tabNavigated", url: aRequest.URI.spec, nativeConsoleAPI: true, state: "start" }); } else if (isStop) { if (this._tabActor.threadActor.state == "running") { this._tabActor.threadActor.dbg.enabled = true; } let window = this._tabActor.window; this._tabActor.conn.send({ from: this._tabActor.actorID, type: "tabNavigated", url: this._tabActor.url, title: this._tabActor.title, nativeConsoleAPI: this._tabActor.hasNativeConsoleAPI(window), state: "stop" }); } }, "DebuggerProgressListener.prototype.onStateChange"), /** * Destroy the progress listener instance. */ destroy: function DPL_destroy() { if (this._tabActor._tabbrowser.removeProgressListener) { try { this._tabActor._tabbrowser.removeProgressListener(this); } catch (ex) { // This can throw during browser shutdown. } } this._tabActor._progressListener = null; this._tabActor = null; } };