Bug 975084 - Part 2: B2G tab list actors. r=ochameau
--- a/b2g/chrome/content/devtools/debugger.js
+++ b/b2g/chrome/content/devtools/debugger.js
@@ -16,16 +16,22 @@ XPCOMUtils.defineLazyGetter(this, "devto
Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
return devtools;
});
XPCOMUtils.defineLazyGetter(this, "discovery", function() {
return devtools.require("devtools/toolkit/discovery/discovery");
});
+XPCOMUtils.defineLazyGetter(this, "B2GTabList", function() {
+ const { B2GTabList } =
+ devtools.require("resource://gre/modules/DebuggerActors.js");
+ return B2GTabList;
+});
+
let RemoteDebugger = {
_promptDone: false,
_promptAnswer: false,
_listening: false,
prompt: function() {
this._listen();
@@ -86,25 +92,17 @@ let RemoteDebugger = {
*
* * @param connection DebuggerServerConnection
* The conection to the client.
*/
DebuggerServer.createRootActor = function createRootActor(connection)
{
let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let parameters = {
- // We do not expose browser tab actors yet,
- // but we still have to define tabList.getList(),
- // otherwise, client won't be able to fetch global actors
- // from listTabs request!
- tabList: {
- getList: function() {
- return promise.resolve([]);
- }
- },
+ tabList: new B2GTabList(connection),
// Use an explicit global actor list to prevent exposing
// unexpected actors
globalActorFactories: restrictPrivileges ? {
webappsActor: DebuggerServer.globalActorFactories.webappsActor,
deviceActor: DebuggerServer.globalActorFactories.deviceActor,
} : DebuggerServer.globalActorFactories
};
let { RootActor } = devtools.require("devtools/server/actors/root");
new file mode 100644
--- /dev/null
+++ b/b2g/components/DebuggerActors.js
@@ -0,0 +1,83 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* 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";
+
+const { Cu } = require("chrome");
+const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
+const promise = require("promise");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const { BrowserTabList } = require("devtools/server/actors/webbrowser");
+
+XPCOMUtils.defineLazyGetter(this, "Frames", function() {
+ const { Frames } =
+ Cu.import("resource://gre/modules/Frames.jsm", {});
+ return Frames;
+});
+
+/**
+ * Unlike the original BrowserTabList which iterates over XUL windows, we
+ * override many portions to refer to Frames for the info needed here.
+ */
+function B2GTabList(connection) {
+ BrowserTabList.call(this, connection);
+ this._listening = false;
+}
+
+B2GTabList.prototype = Object.create(BrowserTabList.prototype);
+
+B2GTabList.prototype._getBrowsers = function() {
+ return Frames.list().filter(frame => {
+ // Ignore app frames
+ return !frame.getAttribute("mozapp");
+ });
+};
+
+B2GTabList.prototype._getSelectedBrowser = function() {
+ return this._getBrowsers().find(frame => {
+ // Find the one visible browser (if any)
+ return !frame.classList.contains("hidden");
+ });
+};
+
+B2GTabList.prototype._checkListening = function() {
+ // The conditions from BrowserTabList are merged here, since we must listen to
+ // all events with our observer design.
+ this._listenForEventsIf(this._onListChanged && this._mustNotify ||
+ this._actorByBrowser.size > 0);
+};
+
+B2GTabList.prototype._listenForEventsIf = function(shouldListen) {
+ if (this._listening != shouldListen) {
+ let op = shouldListen ? "addObserver" : "removeObserver";
+ Frames[op](this);
+ this._listening = shouldListen;
+ }
+};
+
+B2GTabList.prototype.onFrameCreated = function(frame) {
+ let mozapp = frame.getAttribute("mozapp");
+ if (mozapp) {
+ // Ignore app frames
+ return;
+ }
+ this._notifyListChanged();
+ this._checkListening();
+};
+
+B2GTabList.prototype.onFrameDestroyed = function(frame) {
+ let mozapp = frame.getAttribute("mozapp");
+ if (mozapp) {
+ // Ignore app frames
+ return;
+ }
+ let actor = this._actorByBrowser.get(frame);
+ if (actor) {
+ this._handleActorClose(actor, frame);
+ }
+};
+
+exports.B2GTabList = B2GTabList;
--- a/b2g/components/moz.build
+++ b/b2g/components/moz.build
@@ -44,16 +44,17 @@ EXTRA_PP_COMPONENTS += [
if CONFIG['MOZ_UPDATER']:
EXTRA_PP_COMPONENTS += [
'UpdatePrompt.js',
]
EXTRA_JS_MODULES += [
'AlertsHelper.jsm',
'ContentRequestHelper.jsm',
+ 'DebuggerActors.js',
'ErrorPage.jsm',
'Frames.jsm',
'FxAccountsMgmtService.jsm',
'LogCapture.jsm',
'LogParser.jsm',
'LogShake.jsm',
'SignInToWebsite.jsm',
'SystemAppProxy.jsm',
--- a/toolkit/devtools/server/actors/childtab.js
+++ b/toolkit/devtools/server/actors/childtab.js
@@ -22,49 +22,68 @@ let { TabActor } = require("devtools/ser
* @param chromeGlobal
* The content script global holding |content| and |docShell| properties for a tab.
*/
function ContentActor(connection, chromeGlobal)
{
this._chromeGlobal = chromeGlobal;
TabActor.call(this, connection, chromeGlobal);
this.traits.reconfigure = false;
+ this._sendForm = this._sendForm.bind(this);
+ this._chromeGlobal.addMessageListener("debug:form", this._sendForm);
}
ContentActor.prototype = Object.create(TabActor.prototype);
ContentActor.prototype.constructor = ContentActor;
Object.defineProperty(ContentActor.prototype, "docShell", {
get: function() {
return this._chromeGlobal.docShell;
},
enumerable: true,
configurable: true
});
+Object.defineProperty(ContentActor.prototype, "title", {
+ get: function() {
+ return this.window.document.title;
+ },
+ enumerable: true,
+ configurable: true
+});
+
ContentActor.prototype.exit = function() {
+ this._chromeGlobal.removeMessageListener("debug:form", this._sendForm);
+ this._sendForm = null;
TabActor.prototype.exit.call(this);
};
-// Override grip just to rename this._tabActorPool to this._tabActorPool2
+// Override form just to rename this._tabActorPool to this._tabActorPool2
// in order to prevent it to be cleaned on detach.
// We have to keep tab actors alive as we keep the ContentActor
// alive after detach and reuse it for multiple debug sessions.
-ContentActor.prototype.grip = function () {
+ContentActor.prototype.form = function () {
let response = {
- 'actor': this.actorID,
- 'title': this.title,
- 'url': this.url
+ "actor": this.actorID,
+ "title": this.title,
+ "url": this.url
};
// Walk over tab actors added by extensions and add them to a new ActorPool.
let actorPool = new ActorPool(this.conn);
this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
if (!actorPool.isEmpty()) {
this._tabActorPool2 = actorPool;
this.conn.addActorPool(this._tabActorPool2);
}
this._appendExtraActors(response);
return response;
};
+/**
+ * On navigation events, our URL and/or title may change, so we update our
+ * counterpart in the parent process that participates in the tab list.
+ */
+ContentActor.prototype._sendForm = function() {
+ this._chromeGlobal.sendAsyncMessage("debug:form", this.form());
+};
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -262,69 +262,79 @@ BrowserTabList.prototype.constructor = B
* The currently selected xul:browser element, if any. Note that the
* browser window might not be loaded yet - the function will return
* |null| in such cases.
*/
BrowserTabList.prototype._getSelectedBrowser = function(aWindow) {
return aWindow.gBrowser ? aWindow.gBrowser.selectedBrowser : null;
};
+/**
+ * Produces an iterable (in this case a generator) to enumerate all available
+ * browser tabs.
+ */
+BrowserTabList.prototype._getBrowsers = function*() {
+ // Iterate over all navigator:browser XUL windows.
+ for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
+ // For each tab in this XUL window, ensure that we have an actor for
+ // it, reusing existing actors where possible. We actually iterate
+ // over 'browser' XUL elements, and BrowserTabActor uses
+ // browser.contentWindow as the debuggee global.
+ for (let browser of this._getChildren(win)) {
+ yield browser;
+ }
+ }
+};
+
BrowserTabList.prototype._getChildren = function(aWindow) {
return aWindow.gBrowser.browsers;
};
+BrowserTabList.prototype._isRemoteBrowser = function(browser) {
+ return browser.getAttribute("remote");
+};
+
BrowserTabList.prototype.getList = function() {
let topXULWindow = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
+ let selectedBrowser = null;
+ if (topXULWindow) {
+ selectedBrowser = this._getSelectedBrowser(topXULWindow);
+ }
// As a sanity check, make sure all the actors presently in our map get
// picked up when we iterate over all windows' tabs.
let initialMapSize = this._actorByBrowser.size;
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.
let actorPromises = [];
- // Iterate over all navigator:browser XUL windows.
- for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
- let selectedBrowser = this._getSelectedBrowser(win);
- if (!selectedBrowser) {
- continue;
+ for (let browser of this._getBrowsers()) {
+ // Do we have an existing actor for this browser? If not, create one.
+ let actor = this._actorByBrowser.get(browser);
+ if (actor) {
+ actorPromises.push(actor.update());
+ foundCount++;
+ } else if (this._isRemoteBrowser(browser)) {
+ actor = new RemoteBrowserTabActor(this._connection, browser);
+ this._actorByBrowser.set(browser, actor);
+ actorPromises.push(actor.connect());
+ } else {
+ actor = new BrowserTabActor(this._connection, browser,
+ browser.getTabBrowser());
+ this._actorByBrowser.set(browser, actor);
+ actorPromises.push(promise.resolve(actor));
}
- // For each tab in this XUL window, ensure that we have an actor for
- // it, reusing existing actors where possible. We actually iterate
- // over 'browser' XUL elements, and BrowserTabActor uses
- // browser.contentWindow as the debuggee global.
- for (let browser of this._getChildren(win)) {
- // Do we have an existing actor for this browser? If not, create one.
- let actor = this._actorByBrowser.get(browser);
- if (actor) {
- actorPromises.push(promise.resolve(actor));
- foundCount++;
- } else if (browser.isRemoteBrowser) {
- actor = new RemoteBrowserTabActor(this._connection, browser);
- this._actorByBrowser.set(browser, actor);
- let promise = actor.connect().then((form) => {
- actor._form = form;
- return actor;
- });
- actorPromises.push(promise);
- } else {
- actor = new BrowserTabActor(this._connection, browser, win.gBrowser);
- this._actorByBrowser.set(browser, actor);
- actorPromises.push(promise.resolve(actor));
- }
-
- // Set the 'selected' properties on all actors correctly.
- actor.selected = (win === topXULWindow && browser === selectedBrowser);
- }
+ // Set the 'selected' properties on all actors correctly.
+ actor.selected = browser === selectedBrowser;
}
if (this._testing && initialMapSize !== foundCount)
throw Error("_actorByBrowser map contained actors for dead tabs");
this._mustNotify = true;
this._checkListening();
@@ -733,19 +743,28 @@ TabActor.prototype = {
if (this.webNavigation.currentURI) {
return this.webNavigation.currentURI.spec;
}
// Abrupt closing of the browser window may leave callbacks without a
// currentURI.
return null;
},
+ /**
+ * This is called by BrowserTabList.getList for existing tab actors prior to
+ * calling |form| below. It can be used to do any async work that may be
+ * needed to assemble the form.
+ */
+ update: function() {
+ return promise.resolve(this);
+ },
+
form: function BTA_form() {
dbg_assert(!this.exited,
- "grip() shouldn't be called on exited browser actor.");
+ "form() shouldn't be called on exited browser actor.");
dbg_assert(this.actorID,
"tab should have an actorID.");
let windowUtils = this.window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let response = {
@@ -956,17 +975,17 @@ TabActor.prototype = {
parentID = window.parent
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
}
return {
id: id,
url: window.location.href,
- title: window.title,
+ title: window.document.title,
parentID: parentID
};
});
},
_notifyDocShellsUpdate: function (docshells) {
let windows = this._docShellsToWindows(docshells);
this.conn.send({ from: this.actorID,
@@ -1612,28 +1631,51 @@ function RemoteBrowserTabActor(aConnecti
{
this._conn = aConnection;
this._browser = aBrowser;
this._form = null;
}
RemoteBrowserTabActor.prototype = {
connect: function() {
- return DebuggerServer.connectToChild(this._conn, this._browser);
+ let connect = DebuggerServer.connectToChild(this._conn, this._browser);
+ return connect.then(form => {
+ this._form = form;
+ return this;
+ });
+ },
+
+ get _mm() {
+ return this._browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader
+ .messageManager;
+ },
+
+ update: function() {
+ let deferred = promise.defer();
+ let onFormUpdate = msg => {
+ this._mm.removeMessageListener("debug:form", onFormUpdate);
+ this._form = msg.json;
+ deferred.resolve(this);
+ };
+ this._mm.addMessageListener("debug:form", onFormUpdate);
+ this._mm.sendAsyncMessage("debug:form");
+ return deferred.promise;
},
form: function() {
return this._form;
},
exit: function() {
this._browser = null;
},
};
+exports.RemoteBrowserTabActor = RemoteBrowserTabActor;
+
function BrowserAddonList(aConnection)
{
this._connection = aConnection;
this._actorByAddonId = new Map();
this._onListChanged = null;
}
BrowserAddonList.prototype.getList = function() {
--- a/toolkit/devtools/server/child.js
+++ b/toolkit/devtools/server/child.js
@@ -41,17 +41,17 @@ let chromeGlobal = this;
let conn = DebuggerServer.connectToParent(prefix, mm);
connections.set(id, conn);
let actor = new DebuggerServer.ContentActor(conn, chromeGlobal);
let actorPool = new ActorPool(conn);
actorPool.addActor(actor);
conn.addActorPool(actorPool);
- sendAsyncMessage("debug:actor", {actor: actor.grip(), childID: id});
+ sendAsyncMessage("debug:actor", {actor: actor.form(), childID: id});
});
addMessageListener("debug:connect", onConnect);
let onDisconnect = DevToolsUtils.makeInfallible(function (msg) {
removeMessageListener("debug:disconnect", onDisconnect);
// Call DebuggerServerConnection.close to destroy all child actors