Bug 1498293 - Ensure the webextension fallback window has a TabChild actor in content. r=ochameau
authorLuca Greco <lgreco@mozilla.com>
Thu, 15 Nov 2018 19:00:44 +0000
changeset 503086 3c9736576736aba5810577e6b89ae00e5e9a6b98
parent 503085 06be7f6e26ec99e904c705ceb8a84fe2964aab7e
child 503087 4187b44086624b243b7163c4346d86db44164565
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1498293
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1498293 - Ensure the webextension fallback window has a TabChild actor in content. r=ochameau This patch remove the usage of `Services.appShell.createWindowlessBrowser` from the webextension target actor (that runs in a child process when the extension is in oop-mode). As a fallback window (needed when an extension doesn't have an extension page yet, e.g. while the extension is being reloaded, or when the extension doesn't have a background page), the actor is going to search for the window related to the XUL browser element created to connect into the extension process. If the extension runs in the child process (e.g. as it currently happens on all platforms supported by Firefox Desktop), the TabParent/TabChild's tabId is used to identify the fallback window. On the contrary, when the extension runs in the parent process (e.g. as it currently happens on Firefox for Android), the XUL browser's ownerGlobal innerWindowID is used to identify the fallback window. Differential Revision: https://phabricator.services.mozilla.com/D8573
devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
devtools/server/actors/targets/webextension.js
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
@@ -40,30 +40,38 @@ add_task(async function testWebExtension
   } = await setupTestAboutDebuggingWebExtension(ADDON_NOBG_NAME, addonFile);
 
   // Be careful, this JS function is going to be executed in the addon toolbox,
   // which lives in another process. So do not try to use any scope variable!
   const env = Cc["@mozilla.org/process/environment;1"]
         .getService(Ci.nsIEnvironment);
   const testScript = function() {
     /* eslint-disable no-undef */
-    toolbox.selectTool("inspector").then(async inspector => {
-      const nodeActor = await inspector.walker.querySelector(
-        inspector.walker.rootNode, "body");
+    // This is webextension toolbox process. So we can't access mochitest framework.
+    const waitUntil = async function(predicate, interval = 10) {
+      if (await predicate()) {
+        return true;
+      }
+      return new Promise(resolve => {
+        toolbox.win.setTimeout(function() {
+          waitUntil(predicate, interval).then(() => resolve(true));
+        }, interval);
+      });
+    };
 
-      if (!nodeActor) {
-        throw new Error("nodeActor not found");
-      }
+    toolbox.selectTool("inspector").then(async inspector => {
+      let nodeActor;
 
-      if (!(nodeActor.inlineTextChild)) {
-        throw new Error("inlineTextChild not found");
-      }
+      dump(`Wait the fallback window to be fully loaded\n`);
+      await waitUntil(async () => {
+        nodeActor = await inspector.walker.querySelector(inspector.walker.rootNode, "h1");
+        return nodeActor && nodeActor.inlineTextChild;
+      });
 
       dump("Got a nodeActor with an inline text child\n");
-
       const expectedValue = "Your addon does not have any document opened yet.";
       const actualValue = nodeActor.inlineTextChild._form.nodeValue;
 
       if (actualValue !== expectedValue) {
         throw new Error(
           `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
         );
       }
--- a/devtools/server/actors/targets/webextension.js
+++ b/devtools/server/actors/targets/webextension.js
@@ -71,24 +71,21 @@ const webExtensionTargetPrototype = exte
  *        DebuggerServer.connectToFrame method.
  * @param {string} prefix
  *        the custom RDP prefix to use.
  * @param {string} addonId
  *        the addonId of the target WebExtension.
  */
 webExtensionTargetPrototype.initialize = function(conn, chromeGlobal, prefix, addonId) {
   this.addonId = addonId;
+  this.chromeGlobal = chromeGlobal;
 
   // Try to discovery an existent extension page to attach (which will provide the initial
   // URL shown in the window tittle when the addon debugger is opened).
-  let extensionWindow = this._searchForExtensionWindow();
-  if (!extensionWindow) {
-    this._createFallbackWindow();
-    extensionWindow = this.fallbackWindow;
-  }
+  const extensionWindow = this._searchForExtensionWindow();
 
   parentProcessTargetPrototype.initialize.call(this, conn, extensionWindow);
   this._chromeGlobal = chromeGlobal;
   this._prefix = prefix;
 
   // Redefine the messageManager getter to return the chromeGlobal
   // as the messageManager for this actor (which is the browser XUL
   // element used by the parent actor running in the main process to
@@ -149,59 +146,50 @@ webExtensionTargetPrototype.exit = funct
   this.addon = null;
   this.addonId = null;
 
   return ParentProcessTargetActor.prototype.exit.apply(this);
 };
 
 // Private helpers.
 
-webExtensionTargetPrototype._createFallbackWindow = function() {
+webExtensionTargetPrototype._searchFallbackWindow = function() {
   if (this.fallbackWindow) {
     // Skip if there is already an existent fallback window.
-    return;
+    return this.fallbackWindow;
   }
 
-  // Create an empty hidden window as a fallback (e.g. the background page could be
-  // not defined for the target add-on or not yet when the actor instance has been
-  // created).
-  this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true);
+  // Set and initialized the fallbackWindow (which initially is a empty
+  // about:blank browser), this window is related to a XUL browser element
+  // specifically created for the devtools server and it is never used
+  // or navigated anywhere else.
+  this.fallbackWindow = this.chromeGlobal.content;
+  this.fallbackWindow.location = "data:text/html,<h1>" + FALLBACK_DOC_MESSAGE;
 
-  // Save the reference to the fallback DOMWindow.
-  this.fallbackWindow = this.fallbackWebNav.document.defaultView;
-
-  // Insert the fallback doc message.
-  this.fallbackWindow.document.body.innerText = FALLBACK_DOC_MESSAGE;
+  return this.fallbackWindow;
 };
 
 webExtensionTargetPrototype._destroyFallbackWindow = function() {
-  if (this.fallbackWebNav) {
-    const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
-    // Explicitly close the fallback windowless browser to prevent it to leak
-    // (and to prevent it to freeze devtools xpcshell tests).
-    this.fallbackWebNav.loadURI("about:blank", 0, null, null, null, systemPrincipal);
-    this.fallbackWebNav.close();
-
-    this.fallbackWebNav = null;
+  if (this.fallbackWindow) {
     this.fallbackWindow = null;
   }
 };
 
 // Discovery an extension page to use as a default target window.
 // NOTE: This currently fail to discovery an extension page running in a
 // windowless browser when running in non-oop mode, and the background page
 // is set later using _onNewExtensionWindow.
 webExtensionTargetPrototype._searchForExtensionWindow = function() {
   for (const window of Services.ww.getWindowEnumerator(null)) {
     if (window.document.nodePrincipal.addonId == this.addonId) {
       return window;
     }
   }
 
-  return undefined;
+  return this._searchFallbackWindow();
 };
 
 // Customized ParentProcessTargetActor/BrowsingContextTargetActor hooks.
 
 webExtensionTargetPrototype._onDocShellDestroy = function(docShell) {
   // Stop watching this docshell (the unwatch() method will check if we
   // started watching it before).
   this._unwatchDocShell(docShell);
@@ -209,43 +197,34 @@ webExtensionTargetPrototype._onDocShellD
   // Let the _onDocShellDestroy notify that the docShell has been destroyed.
   const webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIWebProgress);
   this._notifyDocShellDestroy(webProgress);
 
   // If the destroyed docShell was the current docShell and the actor is
   // currently attached, switch to the fallback window
   if (this.attached && docShell == this.docShell) {
-    // Creates a fallback window if it doesn't exist yet.
-    this._createFallbackWindow();
-    this._changeTopLevelDocument(this.fallbackWindow);
+    this._changeTopLevelDocument(this._searchForExtensionWindow());
   }
 };
 
 webExtensionTargetPrototype._onNewExtensionWindow = function(window) {
   if (!this.window || this.window === this.fallbackWindow) {
     this._changeTopLevelDocument(window);
   }
 };
 
 webExtensionTargetPrototype._attach = function() {
   // NOTE: we need to be sure that `this.window` can return a window before calling the
   // ParentProcessTargetActor.onAttach, or the BrowsingContextTargetActor will not be
   // subscribed to the child doc shell updates.
 
   if (!this.window || this.window.document.nodePrincipal.addonId !== this.addonId) {
-    // Discovery an existent extension page to attach.
-    const extensionWindow = this._searchForExtensionWindow();
-
-    if (!extensionWindow) {
-      this._createFallbackWindow();
-      this._setWindow(this.fallbackWindow);
-    } else {
-      this._setWindow(extensionWindow);
-    }
+    // Discovery an existent extension page (or fallback window) to attach.
+    this._setWindow(this._searchForExtensionWindow());
   }
 
   // Call ParentProcessTargetActor's _attach to listen for any new/destroyed chrome
   // docshell.
   ParentProcessTargetActor.prototype._attach.apply(this);
 };
 
 webExtensionTargetPrototype._detach = function() {