Bug 975084 - Part 2: B2G tab list actors. r=ochameau
authorJ. Ryan Stinnett <jryans@gmail.com>
Mon, 13 Oct 2014 16:47:00 +0200
changeset 210304 4b9af1d6b1a8317c82ac7e1e0ef20d9817dcd6b2
parent 210303 7d7d64553c684fb913c0d53d7e49813da15d0c0b
child 210305 b648f59fb34ec9f4dd15ef3ac50c6e31945bd8f6
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersochameau
bugs975084
milestone36.0a1
Bug 975084 - Part 2: B2G tab list actors. r=ochameau
b2g/chrome/content/devtools/debugger.js
b2g/components/DebuggerActors.js
b2g/components/moz.build
toolkit/devtools/server/actors/childtab.js
toolkit/devtools/server/actors/webbrowser.js
toolkit/devtools/server/child.js
--- 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