diff --git a/b2g/chrome/content/dbg-browser-actors.js b/b2g/chrome/content/dbg-browser-actors.js index c90d5785b454..685aa37a7a31 100644 --- a/b2g/chrome/content/dbg-browser-actors.js +++ b/b2g/chrome/content/dbg-browser-actors.js @@ -6,110 +6,106 @@ 'use strict'; /** - * B2G-specific actors that extend BrowserRootActor and BrowserTabActor, - * overriding some of their methods. + * B2G-specific actors. */ /** - * The function that creates the root actor. DebuggerServer expects to find this - * function in the loaded actors in order to initialize properly. + * Construct a root actor appropriate for use in a server running in B2G. The + * returned root actor: + * - respects the factories registered with DebuggerServer.addGlobalActor, + * - uses a ContentTabList 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 DeviceRootActor(connection); +function createRootActor(connection) +{ + let parameters = { +#ifndef MOZ_WIDGET_GONK + tabList: new ContentTabList(connection), +#else + tabList: [], +#endif + globalActorFactories: DebuggerServer.globalActorFactories, + onShutdown: sendShutdownEvent + }; + let root = new RootActor(connection, parameters); + root.applicationType = "operating-system"; + return root; } /** - * Creates the root actor that client-server communications always start with. - * The root actor is responsible for the initial 'hello' packet and for - * responding to a 'listTabs' request that produces the list of currently open - * tabs. + * A live list of BrowserTabActors representing the current browser tabs, + * to be provided to the root actor to answer 'listTabs' requests. In B2G, + * only a single tab is ever present. + * + * @param connection DebuggerServerConnection + * The connection in which this list's tab actors may participate. + * + * @see BrowserTabList for more a extensive description of how tab list objects + * work. + */ +function ContentTabList(connection) +{ + BrowserTabList.call(this, connection); +} + +ContentTabList.prototype = Object.create(BrowserTabList.prototype); + +ContentTabList.prototype.constructor = ContentTabList; + +ContentTabList.prototype.iterator = function() { + let browser = Services.wm.getMostRecentWindow('navigator:browser'); + // Do we have an existing actor for this browser? If not, create one. + let actor = this._actorByBrowser.get(browser); + if (!actor) { + actor = new ContentTabActor(this._connection, browser); + this._actorByBrowser.set(browser, actor); + actor.selected = true; + } + + yield actor; +}; + +ContentTabList.prototype.onCloseWindow = makeInfallible(function(aWindow) { + /* + * 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) { + this._handleActorClose(actor, browser); + } + }, "ContentTabList.prototype.onCloseWindow's delayed body"), 0); +}, "ContentTabList.prototype.onCloseWindow"); + +/** + * Creates a tab actor for handling requests to the single tab, like + * attaching and detaching. ContentTabActor respects the actor factories + * registered with DebuggerServer.addTabActor. * * @param connection DebuggerServerConnection * The conection to the client. - */ -function DeviceRootActor(connection) { - BrowserRootActor.call(this, connection); - this.browser = Services.wm.getMostRecentWindow('navigator:browser'); -} - -DeviceRootActor.prototype = new BrowserRootActor(); - -/** - * Disconnects the actor from the browser window. - */ -DeviceRootActor.prototype.disconnect = function DRA_disconnect() { - this._extraActors = null; - let actor = this._tabActors.get(this.browser); - if (actor) { - actor.exit(); - } -}; - -/** - * Handles the listTabs request. Builds a list of actors for the single - * tab (window) running in the process. The actors will survive - * until at least the next listTabs request. - */ -DeviceRootActor.prototype.onListTabs = function DRA_onListTabs() { - let actorPool = new ActorPool(this.conn); - -#ifndef MOZ_WIDGET_GONK - let actor = this._tabActors.get(this.browser); - if (!actor) { - actor = new DeviceTabActor(this.conn, this.browser); - // this.actorID is set by ActorPool when an actor is put into one. - actor.parentID = this.actorID; - this._tabActors.set(this.browser, actor); - } - actorPool.addActor(actor); -#endif - - this._createExtraActors(DebuggerServer.globalActorFactories, actorPool); - - // Now drop the old actorID -> actor map. Actors that still mattered were - // added to the new map, others will go away. - if (this._tabActorPool) { - this.conn.removeActorPool(this._tabActorPool); - } - this._tabActorPool = actorPool; - this.conn.addActorPool(this._tabActorPool); - - let response = { - 'from': 'root', - 'selected': 0, -#ifndef MOZ_WIDGET_GONK - 'tabs': [actor.grip()] -#else - 'tabs': [] -#endif - }; - this._appendExtraActors(response); - return response; -}; - -/** - * The request types this actor can handle. - */ -DeviceRootActor.prototype.requestTypes = { - 'listTabs': DeviceRootActor.prototype.onListTabs -}; - -/** - * Creates a tab actor for handling requests to the single tab, like attaching - * and detaching. - * - * @param connection DebuggerServerConnection - * The connection to the client. * @param browser browser * The browser instance that contains this tab. */ -function DeviceTabActor(connection, browser) { +function ContentTabActor(connection, browser) +{ BrowserTabActor.call(this, connection, browser); } -DeviceTabActor.prototype = new BrowserTabActor(); +ContentTabActor.prototype.constructor = ContentTabActor; -Object.defineProperty(DeviceTabActor.prototype, "title", { +ContentTabActor.prototype = Object.create(BrowserTabActor.prototype); + +Object.defineProperty(ContentTabActor.prototype, "title", { get: function() { return this.browser.title; }, @@ -117,7 +113,7 @@ Object.defineProperty(DeviceTabActor.prototype, "title", { configurable: false }); -Object.defineProperty(DeviceTabActor.prototype, "url", { +Object.defineProperty(ContentTabActor.prototype, "url", { get: function() { return this.browser.document.documentURI; }, @@ -125,7 +121,7 @@ Object.defineProperty(DeviceTabActor.prototype, "url", { configurable: false }); -Object.defineProperty(DeviceTabActor.prototype, "contentWindow", { +Object.defineProperty(ContentTabActor.prototype, "contentWindow", { get: function() { return this.browser; }, diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index 8c51050b6559..ad081e093de4 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -1001,6 +1001,7 @@ let RemoteDebugger = { DebuggerServer.init(this.prompt.bind(this)); DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js"); #ifndef MOZ_WIDGET_GONK + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js"); DebuggerServer.addGlobalActor(DebuggerServer.ChromeDebuggerActor, "chromeDebugger"); DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webconsole.js"); DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/gcli.js"); diff --git a/browser/devtools/debugger/test/Makefile.in b/browser/devtools/debugger/test/Makefile.in index 0e8cc91068d9..3e15dd818f61 100644 --- a/browser/devtools/debugger/test/Makefile.in +++ b/browser/devtools/debugger/test/Makefile.in @@ -17,7 +17,8 @@ MOCHITEST_BROWSER_TESTS = \ browser_dbg_cmd_break.js \ $(browser_dbg_createRemote.js disabled for intermittent failures, bug 753225) \ browser_dbg_debuggerstatement.js \ - browser_dbg_listtabs.js \ + browser_dbg_listtabs-01.js \ + browser_dbg_listtabs-02.js \ browser_dbg_tabactor-01.js \ browser_dbg_tabactor-02.js \ browser_dbg_globalactor-01.js \ diff --git a/browser/devtools/debugger/test/browser_dbg_listtabs.js b/browser/devtools/debugger/test/browser_dbg_listtabs-01.js similarity index 97% rename from browser/devtools/debugger/test/browser_dbg_listtabs.js rename to browser/devtools/debugger/test/browser_dbg_listtabs-01.js index 8cca2953590c..12b0ce77cfc8 100644 --- a/browser/devtools/debugger/test/browser_dbg_listtabs.js +++ b/browser/devtools/debugger/test/browser_dbg_listtabs-01.js @@ -1,6 +1,8 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ +// Make sure the listTabs request works as specified. + var gTab1 = null; var gTab1Actor = null; diff --git a/browser/devtools/debugger/test/browser_dbg_listtabs-02.js b/browser/devtools/debugger/test/browser_dbg_listtabs-02.js new file mode 100644 index 000000000000..c48d374caf56 --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_listtabs-02.js @@ -0,0 +1,150 @@ +/* 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/. */ + +// Make sure the root actor's live tab list implementation works as specified. + +let testPage = ("data:text/html;charset=utf-8," + + encodeURIComponent("JS Debugger BrowserTabList test page" + + "Yo.")); +// The tablist object whose behavior we observe. +let tabList; +let firstActor, actorA; +let tabA, tabB, tabC; +let newWin; +// Stock onListChanged handler. +let onListChangedCount = 0; +function onListChangedHandler() { + onListChangedCount++; +} + +function test() { + tabList = new DebuggerServer.BrowserTabList("fake DebuggerServerConnection"); + tabList._testing = true; + tabList.onListChanged = onListChangedHandler; + + checkSingleTab(); + // Open a new tab. We should be notified. + is(onListChangedCount, 0, "onListChanged handler call count"); + tabA = addTab(testPage, onTabA); +} + +function checkSingleTab() { + var tabActors = [t for (t of tabList)]; + is(tabActors.length, 1, "initial tab list: contains initial tab"); + firstActor = tabActors[0]; + is(firstActor.url, "about:blank", "initial tab list: initial tab URL is 'about:blank'"); + is(firstActor.title, "New Tab", "initial tab list: initial tab title is 'New Tab'"); +} + +function onTabA() { + is(onListChangedCount, 1, "onListChanged handler call count"); + + var tabActors = new Set([t for (t of tabList)]); + is(tabActors.size, 2, "tabA opened: two tabs in list"); + ok(tabActors.has(firstActor), "tabA opened: initial tab present"); + + info("actors: " + [a.url for (a of tabActors)]); + actorA = [a for (a of tabActors) if (a !== firstActor)][0]; + ok(actorA.url.match(/^data:text\/html;/), "tabA opened: new tab URL"); + is(actorA.title, "JS Debugger BrowserTabList test page", "tabA opened: new tab title"); + + tabB = addTab(testPage, onTabB); +} + +function onTabB() { + is(onListChangedCount, 2, "onListChanged handler call count"); + + var tabActors = new Set([t for (t of tabList)]); + is(tabActors.size, 3, "tabB opened: three tabs in list"); + + // Test normal close. + gBrowser.tabContainer.addEventListener("TabClose", function onClose(aEvent) { + gBrowser.tabContainer.removeEventListener("TabClose", onClose, false); + ok(!aEvent.detail, "This was a normal tab close"); + // Let the actor's TabClose handler finish first. + executeSoon(testTabClose); + }, false); + gBrowser.removeTab(tabA); +} + +function testTabClose() { + is(onListChangedCount, 3, "onListChanged handler call count"); + + var tabActors = new Set([t for (t of tabList)]); + is(tabActors.size, 2, "tabA closed: two tabs in list"); + ok(tabActors.has(firstActor), "tabA closed: initial tab present"); + + info("actors: " + [a.url for (a of tabActors)]); + actorA = [a for (a of tabActors) if (a !== firstActor)][0]; + ok(actorA.url.match(/^data:text\/html;/), "tabA closed: new tab URL"); + is(actorA.title, "JS Debugger BrowserTabList test page", "tabA closed: new tab title"); + + // Test tab close by moving tab to a window. + tabC = addTab(testPage, onTabC); +} + +function onTabC() { + is(onListChangedCount, 4, "onListChanged handler call count"); + + var tabActors = new Set([t for (t of tabList)]); + is(tabActors.size, 3, "tabC opened: three tabs in list"); + + gBrowser.tabContainer.addEventListener("TabClose", function onClose2(aEvent) { + gBrowser.tabContainer.removeEventListener("TabClose", onClose2, false); + ok(aEvent.detail, "This was a tab closed by moving"); + // Let the actor's TabClose handler finish first. + executeSoon(testWindowClose); + }, false); + newWin = gBrowser.replaceTabWithWindow(tabC); +} + +function testWindowClose() { + is(onListChangedCount, 5, "onListChanged handler call count"); + + var tabActors = new Set([t for (t of tabList)]); + is(tabActors.size, 3, "tabC closed: three tabs in list"); + ok(tabActors.has(firstActor), "tabC closed: initial tab present"); + + info("actors: " + [a.url for (a of tabActors)]); + actorA = [a for (a of tabActors) if (a !== firstActor)][0]; + ok(actorA.url.match(/^data:text\/html;/), "tabC closed: new tab URL"); + is(actorA.title, "JS Debugger BrowserTabList test page", "tabC closed: new tab title"); + + // Cleanup. + newWin.addEventListener("unload", function onUnload(aEvent) { + newWin.removeEventListener("unload", onUnload, false); + ok(!aEvent.detail, "This was a normal window close"); + // Let the actor's TabClose handler finish first. + executeSoon(checkWindowClose); + }, false); + newWin.close(); +} + +function checkWindowClose() { + is(onListChangedCount, 6, "onListChanged handler call count"); + + // Check that closing a XUL window leaves the other actors intact. + var tabActors = new Set([t for (t of tabList)]); + is(tabActors.size, 2, "newWin closed: two tabs in list"); + ok(tabActors.has(firstActor), "newWin closed: initial tab present"); + + info("actors: " + [a.url for (a of tabActors)]); + actorA = [a for (a of tabActors) if (a !== firstActor)][0]; + ok(actorA.url.match(/^data:text\/html;/), "newWin closed: new tab URL"); + is(actorA.title, "JS Debugger BrowserTabList test page", "newWin closed: new tab title"); + + // Test normal close. + gBrowser.tabContainer.addEventListener("TabClose", function onClose(aEvent) { + gBrowser.tabContainer.removeEventListener("TabClose", onClose, false); + ok(!aEvent.detail, "This was a normal tab close"); + // Let the actor's TabClose handler finish first. + executeSoon(finishTest); + }, false); + gBrowser.removeTab(tabB); +} + +function finishTest() { + checkSingleTab(); + finish(); +} diff --git a/mobile/android/chrome/content/dbg-browser-actors.js b/mobile/android/chrome/content/dbg-browser-actors.js index 071464796aa4..f0b9be0566bd 100644 --- a/mobile/android/chrome/content/dbg-browser-actors.js +++ b/mobile/android/chrome/content/dbg-browser-actors.js @@ -5,120 +5,98 @@ "use strict"; /** - * Fennec-specific root actor that extends BrowserRootActor and overrides some - * of its methods. + * Fennec-specific actors. */ /** - * The function that creates the root actor. DebuggerServer expects to find this - * function in the loaded actors in order to initialize properly. - */ -function createRootActor(aConnection) { - return new DeviceRootActor(aConnection); -} - -/** - * Creates the root actor that client-server communications always start with. - * The root actor is responsible for the initial 'hello' packet and for - * responding to a 'listTabs' request that produces the list of currently open - * tabs. + * Construct a root actor appropriate for use in a server running in a + * browser on Android. The returned root actor: + * - respects the factories registered with DebuggerServer.addGlobalActor, + * - uses a MobileTabList to supply tab actors, + * - sends all navigator:browser window documents a Debugger:Shutdown event + * when it exits. * - * @param aConnection DebuggerServerConnection + * * @param aConnection DebuggerServerConnection * The conection to the client. */ -function DeviceRootActor(aConnection) { - BrowserRootActor.call(this, aConnection); +function createRootActor(aConnection) +{ + let parameters = { + tabList: new MobileTabList(aConnection), + globalActorFactories: DebuggerServer.globalActorFactories, + onShutdown: sendShutdownEvent + }; + return new RootActor(aConnection, parameters); } -DeviceRootActor.prototype = new BrowserRootActor(); - /** - * Handles the listTabs request. Builds a list of actors - * for the tabs running in the process. The actors will survive - * until at least the next listTabs request. + * 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. + * + * @see BrowserTabList for more a extensive description of how tab list objects + * work. */ -DeviceRootActor.prototype.onListTabs = function DRA_onListTabs() { - // Get actors for all the currently-running tabs (reusing - // existing actors where applicable), and store them in - // an ActorPool. +function MobileTabList(aConnection) +{ + BrowserTabList.call(this, aConnection); +} - let actorPool = new ActorPool(this.conn); - let tabActorList = []; +MobileTabList.prototype = Object.create(BrowserTabList.prototype); - let win = windowMediator.getMostRecentWindow("navigator:browser"); - this.browser = win.BrowserApp.selectedBrowser; +MobileTabList.prototype.constructor = MobileTabList; - // Watch the window for tab closes so we can invalidate - // actors as needed. - this.watchWindow(win); +MobileTabList.prototype.iterator = function() { + // 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; - let tabs = win.BrowserApp.tabs; - let selected; + // 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. - for each (let tab in tabs) { - let browser = tab.browser; + // Iterate over all navigator:browser XUL windows. + for (let win of allAppShellDOMWindows("navigator:browser")) { + let selectedTab = win.BrowserApp.selectedBrowser; - if (browser == this.browser) { - selected = tabActorList.length; + // 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 tab of win.BrowserApp.tabs) { + let browser = tab.browser; + // 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); + this._actorByBrowser.set(browser, actor); + } + + // Set the 'selected' properties on all actors correctly. + actor.selected = (browser === selectedTab); } - - let actor = this._tabActors.get(browser); - if (!actor) { - actor = new BrowserTabActor(this.conn, browser); - actor.parentID = this.actorID; - this._tabActors.set(browser, actor); - } - - actorPool.addActor(actor); - tabActorList.push(actor); } - this._createExtraActors(DebuggerServer.globalActorFactories, actorPool); + if (this._testing && initialMapSize !== foundCount) + throw Error("_actorByBrowser map contained actors for dead tabs"); - // Now drop the old actorID -> actor map. Actors that still - // mattered were added to the new map, others will go - // away. - if (this._tabActorPool) { - this.conn.removeActorPool(this._tabActorPool); - } + this._mustNotify = true; + this._checkListening(); - this._tabActorPool = actorPool; - this.conn.addActorPool(this._tabActorPool); - - let response = { - "from": "root", - "selected": selected, - "tabs": [actor.grip() for (actor of tabActorList)] - }; - this._appendExtraActors(response); - return response; -}; - -/** - * Return the tab container for the specified window. - */ -DeviceRootActor.prototype.getTabContainer = function DRA_getTabContainer(aWindow) { - return aWindow.document.getElementById("browsers"); -}; - -/** - * When a tab is closed, exit its tab actor. The actor - * will be dropped at the next listTabs request. - */ -DeviceRootActor.prototype.onTabClosed = function DRA_onTabClosed(aEvent) { - this.exitTabActor(aEvent.target.browser); -}; - -// nsIWindowMediatorListener -DeviceRootActor.prototype.onCloseWindow = function DRA_onCloseWindow(aWindow) { - if (aWindow.BrowserApp) { - this.unwatchWindow(aWindow); + /* Yield the values. */ + for (let [browser, actor] of this._actorByBrowser) { + yield actor; } }; - -/** - * The request types this actor can handle. - */ -DeviceRootActor.prototype.requestTypes = { - "listTabs": DeviceRootActor.prototype.onListTabs -}; diff --git a/toolkit/devtools/client/dbg-client.jsm b/toolkit/devtools/client/dbg-client.jsm index f0362b7a5f81..b1c1f010fab4 100644 --- a/toolkit/devtools/client/dbg-client.jsm +++ b/toolkit/devtools/client/dbg-client.jsm @@ -182,6 +182,7 @@ const UnsolicitedNotifications = { "newScript": "newScript", "newSource": "newSource", "tabDetached": "tabDetached", + "tabListChanged": "tabListChanged", "tabNavigated": "tabNavigated", "pageError": "pageError", "webappsEvent": "webappsEvent", diff --git a/toolkit/devtools/server/actors/root.js b/toolkit/devtools/server/actors/root.js new file mode 100644 index 000000000000..f620ae8139d7 --- /dev/null +++ b/toolkit/devtools/server/actors/root.js @@ -0,0 +1,328 @@ +/* -*- 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"; + +/* Root actor for the remote debugging protocol. */ + +/** + * Methods shared between RootActor and BrowserTabActor. + */ + +/** + * Populate |this._extraActors| as specified by |aFactories|, reusing whatever + * actors are already there. Add all actors in the final extra actors table to + * |aPool|. + * + * The root actor and the tab actor use this to instantiate actors that other + * parts of the browser have specified with DebuggerServer.addTabActor antd + * DebuggerServer.addGlobalActor. + * + * @param aFactories + * An object whose own property names are the names of properties to add to + * some reply packet (say, a tab actor grip or the "listTabs" response + * form), and whose own property values are actor constructor functions, as + * documented for addTabActor and addGlobalActor. + * + * @param this + * The BrowserRootActor or BrowserTabActor with which the new actors will + * be associated. It should support whatever API the |aFactories| + * constructor functions might be interested in, as it is passed to them. + * For the sake of CommonCreateExtraActors itself, it should have at least + * the following properties: + * + * - _extraActors + * An object whose own property names are factory table (and packet) + * property names, and whose values are no-argument actor constructors, + * of the sort that one can add to an ActorPool. + * + * - conn + * The DebuggerServerConnection in which the new actors will participate. + * + * - actorID + * The actor's name, for use as the new actors' parentID. + */ +function CommonCreateExtraActors(aFactories, aPool) { + // Walk over global actors added by extensions. + for (let name in aFactories) { + let actor = this._extraActors[name]; + if (!actor) { + actor = aFactories[name].bind(null, this.conn, this); + actor.prototype = aFactories[name].prototype; + actor.parentID = this.actorID; + this._extraActors[name] = actor; + } + aPool.addActor(actor); + } +} + +/** + * Append the extra actors in |this._extraActors|, constructed by a prior call + * to CommonCreateExtraActors, to |aObject|. + * + * @param aObject + * The object to which the extra actors should be added, under the + * property names given in the |aFactories| table passed to + * CommonCreateExtraActors. + * + * @param this + * The BrowserRootActor or BrowserTabActor whose |_extraActors| table we + * should use; see above. + */ +function CommonAppendExtraActors(aObject) { + for (let name in this._extraActors) { + let actor = this._extraActors[name]; + aObject[name] = actor.actorID; + } +} + +/** + * Create a remote debugging protocol root actor. + * + * @param aConnection + * The DebuggerServerConnection whose root actor we are constructing. + * + * @param aParameters + * The properties of |aParameters| provide backing objects for the root + * actor's requests; if a given property is omitted from |aParameters|, the + * root actor won't implement the corresponding requests or notifications. + * Supported properties: + * + * - tabList: a live list (see below) of tab actors. If present, the + * new root actor supports the 'listTabs' request, providing the live + * list's elements as its tab actors, and sending 'tabListChanged' + * notifications when the live list's contents change. One actor in + * this list must have a true '.selected' property. + * + * - globalActorFactories: an object |A| describing further actors to + * attach to the 'listTabs' reply. This is the type accumulated by + * DebuggerServer.addGlobalActor. For each own property |P| of |A|, + * the root actor adds a property named |P| to the 'listTabs' + * reply whose value is the name of an actor constructed by + * |A[P]|. + * + * - onShutdown: a function to call when the root actor is disconnected. + * + * Instance properties: + * + * - applicationType: the string the root actor will include as the + * "applicationType" property in the greeting packet. By default, this + * is "browser". + * + * Live lists: + * + * A "live list", as used for the |tabList|, is an object that presents a + * list of actors, and also notifies its clients of changes to the list. A + * live list's interface is two properties: + * + * - iterator: a method that returns an iterator. A for-of loop will call + * this method to obtain an iterator for the loop, so if LL is + * a live list, one can simply write 'for (i of LL) ...'. + * + * - onListChanged: a handler called, with no arguments, when the set of + * values the iterator would produce has changed since the last + * time 'iterator' was called. This may only be set to null or a + * callable value (one for which the typeof operator returns + * 'function'). (Note that the live list will not call the + * onListChanged handler until the list has been iterated over + * once; if nobody's seen the list in the first place, nobody + * should care if its contents have changed!) + * + * When the list changes, the list implementation should ensure that any + * actors yielded in previous iterations whose referents (tabs) still exist + * get yielded again in subsequent iterations. If the underlying referent + * is the same, the same actor should be presented for it. + * + * The root actor registers an 'onListChanged' handler on the appropriate + * list when it may need to send the client 'tabListChanged' notifications, + * and is careful to remove the handler whenever it does not need to send + * such notifications (including when it is disconnected). This means that + * live list implementations can use the state of the handler property (set + * or null) to install and remove observers and event listeners. + * + * Note that, as the only way for the root actor to see the members of the + * live list is to begin an iteration over the list, the live list need not + * actually produce any actors until they are reached in the course of + * iteration: alliterative lazy live lists. + */ +function RootActor(aConnection, aParameters) { + this.conn = aConnection; + this._parameters = aParameters; + this._onTabListChanged = this.onTabListChanged.bind(this); + this._extraActors = {}; +} + +RootActor.prototype = { + constructor: RootActor, + applicationType: "browser", + + /** + * Return a 'hello' packet as specified by the Remote Debugging Protocol. + */ + sayHello: function() { + return { + from: "root", + applicationType: this.applicationType, + /* This is not in the spec, but it's used by tests. */ + testConnectionPrefix: this.conn.prefix, + traits: { + sources: true + } + }; + }, + + /** + * Disconnects the actor from the browser window. + */ + disconnect: function() { + /* Tell the live lists we aren't watching any more. */ + if (this._parameters.tabList) { + this._parameters.tabList.onListChanged = null; + } + if (typeof this._parameters.onShutdown === 'function') { + this._parameters.onShutdown(); + } + this._extraActors = null; + }, + + /* The 'listTabs' request and the 'tabListChanged' notification. */ + + /** + * Handles the listTabs request. The actors will survive until at least + * the next listTabs request. + */ + onListTabs: function() { + let tabList = this._parameters.tabList; + if (!tabList) { + return { from: "root", error: "noTabs", + message: "This root actor has no browser tabs." }; + } + + /* + * Walk the tab list, accumulating the array of tab actors for the + * reply, and moving all the actors to a new ActorPool. We'll + * replace the old tab actor pool with the one we build here, thus + * retiring any actors that didn't get listed again, and preparing any + * new actors to receive packets. + */ + let newActorPool = new ActorPool(this.conn); + let tabActorList = []; + let selected; + for (let tabActor of tabList) { + if (tabActor.selected) { + selected = tabActorList.length; + } + tabActor.parentID = this.actorID; + newActorPool.addActor(tabActor); + tabActorList.push(tabActor); + } + + /* DebuggerServer.addGlobalActor support: create actors. */ + this._createExtraActors(this._parameters.globalActorFactories, newActorPool); + + /* + * Drop the old actorID -> actor map. Actors that still mattered were + * added to the new map; others will go away. + */ + if (this._tabActorPool) { + this.conn.removeActorPool(this._tabActorPool); + } + this._tabActorPool = newActorPool; + this.conn.addActorPool(this._tabActorPool); + + let reply = { + "from": "root", + "selected": selected || 0, + "tabs": [actor.grip() for (actor of tabActorList)], + }; + + /* DebuggerServer.addGlobalActor support: name actors in 'listTabs' reply. */ + this._appendExtraActors(reply); + + /* + * Now that we're actually going to report the contents of tabList to + * the client, we're responsible for letting the client know if it + * changes. + */ + tabList.onListChanged = this._onTabListChanged; + + return reply; + }, + + onTabListChanged: function () { + this.conn.send({ from:"root", type:"tabListChanged" }); + /* It's a one-shot notification; no need to watch any more. */ + this._parameters.tabList.onListChanged = null; + }, + + /* This is not in the spec, but it's used by tests. */ + onEcho: (aRequest) => aRequest, + + /* Support for DebuggerServer.addGlobalActor. */ + _createExtraActors: CommonCreateExtraActors, + _appendExtraActors: CommonAppendExtraActors, + + /* ThreadActor hooks. */ + + /** + * Prepare to enter a nested event loop by disabling debuggee events. + */ + preNest: function() { + // Disable events in all open windows. + let e = windowMediator.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.suppressEventHandling(true); + windowUtils.suspendTimeouts(); + } + }, + + /** + * Prepare to exit a nested event loop by enabling debuggee events. + */ + postNest: function(aNestData) { + // Enable events in all open windows. + let e = windowMediator.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.resumeTimeouts(); + windowUtils.suppressEventHandling(false); + } + }, + + /* ChromeDebuggerActor hooks. */ + + /** + * 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 and chrome debugger actors. + * + * @param actor aActor + * The actor object. + */ + addToParentPool: function(aActor) { + this.conn.addActor(aActor); + }, + + /** + * Remove the specified actor from the default actor pool. + * + * @param BreakpointActor aActor + * The actor object. + */ + removeFromParentPool: function(aActor) { + this.conn.removeActor(aActor); + } +} + +RootActor.prototype.requestTypes = { + "listTabs": RootActor.prototype.onListTabs, + "echo": RootActor.prototype.onEcho +}; diff --git a/toolkit/devtools/server/actors/webbrowser.js b/toolkit/devtools/server/actors/webbrowser.js index f346633483b5..fdd48f6115ff 100644 --- a/toolkit/devtools/server/actors/webbrowser.js +++ b/toolkit/devtools/server/actors/webbrowser.js @@ -10,329 +10,437 @@ */ /** - * Methods shared between BrowserRootActor and BrowserTabActor. + * Yield all windows of type |aWindowType|, from the oldest window to the + * youngest, using nsIWindowMediator::getEnumerator. We're usually + * interested in "navigator:browser" windows. */ - -/** - * Populate |this._extraActors| as specified by |aFactories|, reusing whatever - * actors are already there. Add all actors in the final extra actors table to - * |aPool|. - * - * The root actor and the tab actor use this to instantiate actors that other - * parts of the browser have specified with DebuggerServer.addTabActor antd - * DebuggerServer.addGlobalActor. - * - * @param aFactories - * An object whose own property names are the names of properties to add to - * some reply packet (say, a tab actor grip or the "listTabs" response - * form), and whose own property values are actor constructor functions, as - * documented for addTabActor and addGlobalActor. - * - * @param this - * The BrowserRootActor or BrowserTabActor with which the new actors will - * be associated. It should support whatever API the |aFactories| - * constructor functions might be interested in, as it is passed to them. - * For the sake of CommonCreateExtraActors itself, it should have at least - * the following properties: - * - * - _extraActors - * An object whose own property names are factory table (and packet) - * property names, and whose values are no-argument actor constructors, - * of the sort that one can add to an ActorPool. - * - * - conn - * The DebuggerServerConnection in which the new actors will participate. - * - * - actorID - * The actor's name, for use as the new actors' parentID. - */ -function CommonCreateExtraActors(aFactories, aPool) { - // Walk over global actors added by extensions. - for (let name in aFactories) { - let actor = this._extraActors[name]; - if (!actor) { - actor = aFactories[name].bind(null, this.conn, this); - actor.prototype = aFactories[name].prototype; - actor.parentID = this.actorID; - this._extraActors[name] = actor; - } - aPool.addActor(actor); +function allAppShellDOMWindows(aWindowType) +{ + let e = windowMediator.getEnumerator(aWindowType); + while (e.hasMoreElements()) { + yield e.getNext(); } } /** - * Append the extra actors in |this._extraActors|, constructed by a prior call - * to CommonCreateExtraActors, to |aObject|. - * - * @param aObject - * The object to which the extra actors should be added, under the - * property names given in the |aFactories| table passed to - * CommonCreateExtraActors. - * - * @param this - * The BrowserRootActor or BrowserTabActor whose |_extraActors| table we - * should use; see above. + * Return true if the top-level window |aWindow| is a "navigator:browser" + * window. */ -function CommonAppendExtraActors(aObject) { - for (let name in this._extraActors) { - let actor = this._extraActors[name]; - aObject[name] = actor.actorID; +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); - -function createRootActor(aConnection) -{ - return new BrowserRootActor(aConnection); -} + .getService(Ci.nsIWindowMediator); /** - * Creates the root actor that client-server communications always start with. - * The root actor is responsible for the initial 'hello' packet and for - * responding to a 'listTabs' request that produces the list of currently open - * tabs. + * 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 conection to the client. + * 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 BrowserRootActor(aConnection) +function BrowserTabList(aConnection) { - this.conn = aConnection; - this._tabActors = new WeakMap(); - this._tabActorPool = null; - // A map of actor names to actor instances provided by extensions. - this._extraActors = {}; + this._connection = aConnection; - this.onTabClosed = this.onTabClosed.bind(this); - windowMediator.addListener(this); + /* + * 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; } -BrowserRootActor.prototype = { +BrowserTabList.prototype.constructor = BrowserTabList; - /** - * Return a 'hello' packet as specified by the Remote Debugging Protocol. - */ - sayHello: function BRA_sayHello() { - return { - from: "root", - applicationType: "browser", - traits: { - sources: true +BrowserTabList.prototype.iterator = 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); } - }; - }, - /** - * Disconnects the actor from the browser window. - */ - disconnect: function BRA_disconnect() { - windowMediator.removeListener(this); - this._extraActors = null; - - // We may have registered event listeners on browser windows to - // watch for tab closes, remove those. - let e = windowMediator.getEnumerator("navigator:browser"); - while (e.hasMoreElements()) { - let win = e.getNext(); - this.unwatchWindow(win); - // Signal our imminent shutdown. - let evt = win.document.createEvent("Event"); - evt.initEvent("Debugger:Shutdown", true, false); - win.document.documentElement.dispatchEvent(evt); + // Set the 'selected' properties on all actors correctly. + actor.selected = (win === topXULWindow && browser === selectedTab); } - }, + } - /** - * Handles the listTabs request. Builds a list of actors for the tabs running - * in the process. The actors will survive until at least the next listTabs - * request. - */ - onListTabs: function BRA_onListTabs() { - // Get actors for all the currently-running tabs (reusing existing actors - // where applicable), and store them in an ActorPool. + if (this._testing && initialMapSize !== foundCount) + throw Error("_actorByBrowser map contained actors for dead tabs"); - let actorPool = new ActorPool(this.conn); - let tabActorList = []; + this._mustNotify = true; + this._checkListening(); - // Walk over open browser windows. - let e = windowMediator.getEnumerator("navigator:browser"); - let top = windowMediator.getMostRecentWindow("navigator:browser"); - let selected; - while (e.hasMoreElements()) { - let win = e.getNext(); - - // Watch the window for tab closes so we can invalidate actors as needed. - this.watchWindow(win); - - // List the tabs in this browser. - let selectedBrowser = win.getBrowser().selectedBrowser; - - let browsers = win.getBrowser().browsers; - for each (let browser in browsers) { - if (browser == selectedBrowser && win == top) { - selected = tabActorList.length; - } - let actor = this._tabActors.get(browser); - if (!actor) { - actor = new BrowserTabActor(this.conn, browser, win.gBrowser); - actor.parentID = this.actorID; - this._tabActors.set(browser, actor); - } - actorPool.addActor(actor); - tabActorList.push(actor); - } - } - - this._createExtraActors(DebuggerServer.globalActorFactories, actorPool); - - // Now drop the old actorID -> actor map. Actors that still mattered were - // added to the new map, others will go away. - if (this._tabActorPool) { - this.conn.removeActorPool(this._tabActorPool); - } - this._tabActorPool = actorPool; - this.conn.addActorPool(this._tabActorPool); - - let response = { - "from": "root", - "selected": selected, - "tabs": [actor.grip() for (actor of tabActorList)] - }; - this._appendExtraActors(response); - return response; - }, - - /* Support for DebuggerServer.addGlobalActor. */ - _createExtraActors: CommonCreateExtraActors, - _appendExtraActors: CommonAppendExtraActors, - - /** - * Watch a window that was visited during onListTabs for - * tab closures. - */ - watchWindow: function BRA_watchWindow(aWindow) { - this.getTabContainer(aWindow).addEventListener("TabClose", - this.onTabClosed, - false); - }, - - /** - * Stop watching a window for tab closes. - */ - unwatchWindow: function BRA_unwatchWindow(aWindow) { - this.getTabContainer(aWindow).removeEventListener("TabClose", - this.onTabClosed); - this.exitTabActor(aWindow); - }, - - /** - * Return the tab container for the specified window. - */ - getTabContainer: function BRA_getTabContainer(aWindow) { - return aWindow.getBrowser().tabContainer; - }, - - /** - * When a tab is closed, exit its tab actor. The actor - * will be dropped at the next listTabs request. - */ - onTabClosed: - makeInfallible(function BRA_onTabClosed(aEvent) { - this.exitTabActor(aEvent.target.linkedBrowser); - }, "BrowserRootActor.prototype.onTabClosed"), - - /** - * Exit the tab actor of the specified tab. - */ - exitTabActor: function BRA_exitTabActor(aWindow) { - let actor = this._tabActors.get(aWindow); - if (actor) { - this._tabActors.delete(actor.browser); - actor.exit(); - } - }, - - // ChromeDebuggerActor hooks. - - /** - * 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 and chrome debugger actors. - * - * @param actor aActor - * The actor object. - */ - addToParentPool: function BRA_addToParentPool(aActor) { - this.conn.addActor(aActor); - }, - - /** - * Remove the specified actor from the default actor pool. - * - * @param BreakpointActor aActor - * The actor object. - */ - removeFromParentPool: function BRA_removeFromParentPool(aActor) { - this.conn.removeActor(aActor); - }, - - /** - * Prepare to enter a nested event loop by disabling debuggee events. - */ - preNest: function BRA_preNest() { - // Disable events in all open windows. - let e = windowMediator.getEnumerator(null); - while (e.hasMoreElements()) { - let win = e.getNext(); - let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - windowUtils.suppressEventHandling(true); - windowUtils.suspendTimeouts(); - } - }, - - /** - * Prepare to exit a nested event loop by enabling debuggee events. - */ - postNest: function BRA_postNest(aNestData) { - // Enable events in all open windows. - let e = windowMediator.getEnumerator(null); - while (e.hasMoreElements()) { - let win = e.getNext(); - let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - windowUtils.resumeTimeouts(); - windowUtils.suppressEventHandling(false); - } - }, - - // nsIWindowMediatorListener. - - onWindowTitleChange: function BRA_onWindowTitleChange(aWindow, aTitle) { }, - onOpenWindow: function BRA_onOpenWindow(aWindow) { }, - onCloseWindow: - makeInfallible(function BRA_onCloseWindow(aWindow) { - // An nsIWindowMediatorListener's onCloseWindow method gets passed all - // sorts of windows; we only care about the tab containers. Those have - // 'getBrowser' methods. - if (aWindow.getBrowser) { - this.unwatchWindow(aWindow); - } - }, "BrowserRootActor.prototype.onCloseWindow"), + /* Yield the values. */ + for (let [browser, actor] of this._actorByBrowser) { + yield actor; + } }; +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 request types this actor can handle. + * 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. */ -BrowserRootActor.prototype.requestTypes = { - "listTabs": BrowserRootActor.prototype.onListTabs +BrowserTabList.prototype._notifyListChanged = function() { + if (!this._onListChanged) + return; + if (this._mustNotify) { + this._onListChanged(); + this._mustNotify = false; + } }; /** - * Creates a tab actor for handling requests to a browser tab, like attaching - * and detaching. + * 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) { + /* + * 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", makeInfallible(handleLoad.bind(this)), false); + + function handleLoad(aEvent) { + /* 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(); + } +}, "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. @@ -360,7 +468,7 @@ BrowserTabActor.prototype = { get browser() { return this._browser; }, get exited() { return !this.browser; }, - get attached() { return !!this._attached }, + get attached() { return !!this._attached; }, _tabPool: null, get tabActorPool() { return this._tabPool; }, @@ -439,7 +547,7 @@ BrowserTabActor.prototype = { let response = { actor: this.actorID, title: this.title, - url: this.url, + url: this.url }; // Walk over tab actors added by extensions and add them to a new ActorPool. @@ -666,7 +774,7 @@ BrowserTabActor.prototype = { } catch (ex) { } return isNative; - }, + } }; /** @@ -722,7 +830,7 @@ DebuggerProgressListener.prototype = { type: "tabNavigated", url: aRequest.URI.spec, nativeConsoleAPI: true, - state: "start", + state: "start" }); } else if (isStop) { if (this._tabActor.threadActor.state == "running") { @@ -736,7 +844,7 @@ DebuggerProgressListener.prototype = { url: this._tabActor.url, title: this._tabActor.title, nativeConsoleAPI: this._tabActor.hasNativeConsoleAPI(window), - state: "stop", + state: "stop" }); } }, "DebuggerProgressListener.prototype.onStateChange"), diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js index 71638ee5e19d..ecb0cff1b51e 100644 --- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -114,7 +114,7 @@ var DebuggerServer = { this.xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); this.initTransport(aAllowConnectionCallback); - this.addActors("resource://gre/modules/devtools/server/actors/script.js"); + this.addActors("resource://gre/modules/devtools/server/actors/root.js"); this._initialized = true; }, @@ -183,6 +183,7 @@ var DebuggerServer = { */ addBrowserActors: function DS_addBrowserActors() { this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js"); + this.addActors("resource://gre/modules/devtools/server/actors/script.js"); this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger"); this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js"); this.addActors("resource://gre/modules/devtools/server/actors/gcli.js"); @@ -271,7 +272,6 @@ var DebuggerServer = { return clientTransport; }, - // nsIServerSocketListener implementation onSocketAccepted: @@ -441,7 +441,7 @@ ActorPool.prototype = { * * @param aActor object * The actor implementation. If the object has a - * 'disconnected' property, it will be called when the actor + * 'disconnect' property, it will be called when the actor * pool is cleaned up. */ addActor: function AP_addActor(aActor) { diff --git a/toolkit/devtools/server/tests/mochitest/Makefile.in b/toolkit/devtools/server/tests/mochitest/Makefile.in index 7e54d7d838fb..1ac54173731b 100644 --- a/toolkit/devtools/server/tests/mochitest/Makefile.in +++ b/toolkit/devtools/server/tests/mochitest/Makefile.in @@ -11,9 +11,9 @@ relativesrcdir = @relativesrcdir@ include $(DEPTH)/config/autoconf.mk -MOCHITEST_CHROME_FILES = \ - test_unsafeDereference.html \ - nonchrome_unsafeDereference.html \ +MOCHITEST_CHROME_FILES = \ + test_unsafeDereference.html \ + nonchrome_unsafeDereference.html \ $(NULL) include $(topsrcdir)/config/rules.mk diff --git a/toolkit/devtools/server/tests/unit/head_dbg.js b/toolkit/devtools/server/tests/unit/head_dbg.js index 4530d6f45e4d..d6f106c3612b 100644 --- a/toolkit/devtools/server/tests/unit/head_dbg.js +++ b/toolkit/devtools/server/tests/unit/head_dbg.js @@ -156,6 +156,7 @@ function attachTestTabAndResume(aClient, aTitle, aCallback) { */ function initTestDebuggerServer() { + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js"); DebuggerServer.addActors("resource://test/testactors.js"); // Allow incoming connections. DebuggerServer.init(function () { return true; }); @@ -163,7 +164,9 @@ function initTestDebuggerServer() function initSourcesBackwardsCompatDebuggerServer() { + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js"); DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js"); + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js"); DebuggerServer.addActors("resource://test/testcompatactors.js"); DebuggerServer.init(function () { return true; }); } diff --git a/toolkit/devtools/server/tests/unit/testactors.js b/toolkit/devtools/server/tests/unit/testactors.js index 980d4a729696..de4ea7f7e25f 100644 --- a/toolkit/devtools/server/tests/unit/testactors.js +++ b/toolkit/devtools/server/tests/unit/testactors.js @@ -6,15 +6,15 @@ DebuggerServer.addTestGlobal = function(aGlobal) { gTestGlobals.push(aGlobal); }; -function createRootActor(aConnection) -{ - return new TestRootActor(aConnection); -} - -function TestRootActor(aConnection) -{ +// A mock tab list, for use by tests. This simply presents each global in +// gTestGlobals as a tab, and the list is fixed: it never calls its +// onListChanged handler. +// +// As implemented now, we consult gTestGlobals when we're constructed, not +// when we're iterated over, so tests have to add their globals before the +// root actor is created. +function TestTabList(aConnection) { this.conn = aConnection; - this.actorID = "root"; // An array of actors for each global added with // DebuggerServer.addTestGlobal. @@ -25,37 +25,33 @@ function TestRootActor(aConnection) for (let global of gTestGlobals) { let actor = new TestTabActor(aConnection, global); + actor.selected = false; this._tabActors.push(actor); this._tabActorPool.addActor(actor); } + if (this._tabActors.length > 0) { + this._tabActors[0].selected = true; + } aConnection.addActorPool(this._tabActorPool); } -TestRootActor.prototype = { - constructor: TestRootActor, - - sayHello: function () { - return { from: "root", - applicationType: "xpcshell-tests", - testConnectionPrefix: this.conn.prefix, - traits: { - sources: true - } - }; - }, - - onListTabs: function(aRequest) { - return { tabs:[actor.grip() for (actor of this._tabActors)], selected:0 }; - }, - - onEcho: function(aRequest) { return aRequest; }, +TestTabList.prototype = { + constructor: TestTabList, + iterator: function() { + for (let actor of this._tabActors) { + yield actor; + } + } }; -TestRootActor.prototype.requestTypes = { - "listTabs": TestRootActor.prototype.onListTabs, - "echo": TestRootActor.prototype.onEcho -}; +function createRootActor(aConnection) +{ + let root = new RootActor(aConnection, + { tabList: new TestTabList(aConnection) }); + root.applicationType = "xpcshell-tests"; + return root; +} function TestTabActor(aConnection, aGlobal) { @@ -68,7 +64,7 @@ function TestTabActor(aConnection, aGlobal) TestTabActor.prototype = { constructor: TestTabActor, - actorPrefix:"TestTabActor", + actorPrefix: "TestTabActor", grip: function() { return { actor: this.actorID, title: this._global.__name };