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 };