Bug 1317101 - Part 7b: Run remote extension background pages in a visible window for testing. r?billm draft
authorKris Maglione <maglione.k@gmail.com>
Sat, 12 Nov 2016 17:09:39 -0800
changeset 438074 bbe0af7c3c5e12f08314d309ac51b089c0201e68
parent 438073 14201484d276bef8b0a741218b5ed47dd9963320
child 438075 e4a12bca96f9f67e8d38739af41b9b23465bc14f
push id35614
push usermaglione.k@gmail.com
push dateSun, 13 Nov 2016 03:28:59 +0000
reviewersbillm
bugs1317101
milestone52.0a1
Bug 1317101 - Part 7b: Run remote extension background pages in a visible window for testing. r?billm MozReview-Commit-ID: DsgpoYAFKmC
toolkit/components/extensions/ext-backgroundPage.js
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -6,130 +6,220 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 const {
   promiseDocumentLoaded,
+  promiseEvent,
   promiseObserved,
 } = ExtensionUtils;
 
 const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI(
   `<?xml version="1.0"?>
   <window id="documentElement"/>`);
 
 // WeakMap[Extension -> BackgroundPage]
 var backgroundPagesMap = new WeakMap();
 
 // Responsible for the background_page section of the manifest.
-function BackgroundPage(options, extension) {
-  this.extension = extension;
-  this.page = options.page || null;
-  this.isGenerated = !!options.scripts;
-  this.windowlessBrowser = null;
-  this.webNav = null;
-}
+class BackgroundPageBase {
+  constructor(options, extension) {
+    this.extension = extension;
+    this.page = options.page || null;
+    this.isGenerated = !!options.scripts;
+    this.webNav = null;
+  }
+
+  build() {
+    return Task.spawn(function* () {
+      let url;
+      if (this.page) {
+        url = this.extension.baseURI.resolve(this.page);
+      } else if (this.isGenerated) {
+        url = this.extension.baseURI.resolve("_generated_background_page.html");
+      }
 
-BackgroundPage.prototype = {
-  build: Task.async(function* () {
-    let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
-    this.windowlessBrowser = windowlessBrowser;
+      if (!this.extension.isExtensionURL(url)) {
+        this.extension.manifestError("Background page must be a file within the extension");
+        url = this.extension.baseURI.resolve("_blank.html");
+      }
+
+      let chromeDoc = yield this.getParentDocument();
 
-    let url;
-    if (this.page) {
-      url = this.extension.baseURI.resolve(this.page);
-    } else if (this.isGenerated) {
-      url = this.extension.baseURI.resolve("_generated_background_page.html");
-    }
+      let browser = chromeDoc.createElement("browser");
+      browser.setAttribute("type", "content");
+      browser.setAttribute("disableglobalhistory", "true");
+      browser.setAttribute("webextension-view-type", "background");
+
+      let awaitFrameLoader;
+      if (this.extension.remote) {
+        browser.setAttribute("remote", "true");
+        awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
+      }
 
-    if (!this.extension.isExtensionURL(url)) {
-      this.extension.manifestError("Background page must be a file within the extension");
-      url = this.extension.baseURI.resolve("_blank.html");
-    }
+      chromeDoc.documentElement.appendChild(browser);
+      yield awaitFrameLoader;
+
+      this.browser = browser;
+
+      extensions.emit("extension-browser-inserted", browser);
 
-    let system = Services.scriptSecurityManager.getSystemPrincipal();
+      browser.loadURI(url);
+
+      yield new Promise(resolve => {
+        browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad() {
+          browser.messageManager.removeMessageListener("Extension:ExtensionViewLoaded", onLoad);
+          resolve();
+        });
+      });
 
-    // The windowless browser is a thin wrapper around a docShell that keeps
-    // its related resources alive. It implements nsIWebNavigation and
-    // forwards its methods to the underlying docShell, but cannot act as a
-    // docShell itself. Calling `getInterface(nsIDocShell)` gives us the
-    // underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us
-    // access to the webNav methods that are already available on the
-    // windowless browser, but contrary to appearances, they are not the same
-    // object.
-    let chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
-                                       .getInterface(Ci.nsIDocShell)
-                                       .QueryInterface(Ci.nsIWebNavigation);
+      if (browser.docShell) {
+        this.webNav = browser.docShell.QueryInterface(Ci.nsIWebNavigation);
+        let window = this.webNav.document.defaultView;
+
 
+        // Set the add-on's main debugger global, for use in the debugger
+        // console.
+        if (this.extension.addonData.instanceID) {
+          AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
+                      .then(addon => addon.setDebugGlobal(window));
+        }
+      }
+
+      this.extension.emit("startup");
+    }.bind(this));
+  }
+
+  initParentWindow(chromeShell) {
+    let system = Services.scriptSecurityManager.getSystemPrincipal();
+    chromeShell.createAboutBlankContentViewer(system);
     chromeShell.useGlobalHistory = false;
-    chromeShell.createAboutBlankContentViewer(system);
     chromeShell.loadURI(XUL_URL, 0, null, null, null);
 
-
-    yield promiseObserved("chrome-document-global-created",
-                          win => win.document == chromeShell.document);
-
-    let chromeDoc = yield promiseDocumentLoaded(chromeShell.document);
-
-    let browser = chromeDoc.createElement("browser");
-    browser.setAttribute("type", "content");
-    browser.setAttribute("disableglobalhistory", "true");
-    browser.setAttribute("webextension-view-type", "background");
-    chromeDoc.documentElement.appendChild(browser);
-
-    extensions.emit("extension-browser-inserted", browser);
-
-    browser.loadURI(url);
-
-    yield new Promise(resolve => {
-      browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad() {
-        browser.messageManager.removeMessageListener("Extension:ExtensionViewLoaded", onLoad);
-        resolve();
-      });
-    });
-
-    // TODO(robwu): This is not webext-oop compatible.
-    this.webNav = browser.docShell.QueryInterface(Ci.nsIWebNavigation);
-    let window = this.webNav.document.defaultView;
-
-
-    // Set the add-on's main debugger global, for use in the debugger
-    // console.
-    if (this.extension.addonData.instanceID) {
-      AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
-                  .then(addon => addon.setDebugGlobal(window));
-    }
-
-    this.extension.emit("startup");
-  }),
+    return promiseObserved("chrome-document-global-created",
+                           win => win.document == chromeShell.document);
+  }
 
   shutdown() {
     if (this.extension.addonData.instanceID) {
       AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
                   .then(addon => addon.setDebugGlobal(null));
     }
 
+    if (this.browser) {
+      this.browser.remove();
+      this.browser = null;
+    }
+
     // Navigate away from the background page to invalidate any
     // setTimeouts or other callbacks.
     if (this.webNav) {
       this.webNav.loadURI("about:blank", 0, null, null, null);
       this.webNav = null;
     }
+  }
+}
+
+/**
+ * A background page loaded into a windowless browser, with no on-screen
+ * representation or graphical display abilities.
+ *
+ * This currently does not support remote browsers, and therefore cannot
+ * be used with out-of-process extensions.
+ */
+class WindowlessBackgroundPage extends BackgroundPageBase {
+  constructor(options, extension) {
+    super(options, extension);
+    this.windowlessBrowser = null;
+  }
+
+  getParentDocument() {
+    return Task.spawn(function* () {
+      let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
+      this.windowlessBrowser = windowlessBrowser;
+
+      // The windowless browser is a thin wrapper around a docShell that keeps
+      // its related resources alive. It implements nsIWebNavigation and
+      // forwards its methods to the underlying docShell, but cannot act as a
+      // docShell itself. Calling `getInterface(nsIDocShell)` gives us the
+      // underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us
+      // access to the webNav methods that are already available on the
+      // windowless browser, but contrary to appearances, they are not the same
+      // object.
+      let chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
+                                         .getInterface(Ci.nsIDocShell)
+                                         .QueryInterface(Ci.nsIWebNavigation);
+
+      yield this.initParentWindow(chromeShell);
+
+      return promiseDocumentLoaded(windowlessBrowser.document);
+    }.bind(this));
+  }
+
+  shutdown() {
+    super.shutdown();
 
     this.windowlessBrowser.loadURI("about:blank", 0, null, null, null);
     this.windowlessBrowser.close();
     this.windowlessBrowser = null;
-  },
-};
+  }
+}
+
+/**
+ * A background page loaded into a visible dialog window. Only to be
+ * used for debugging, and in temporary, test-only use for
+ * out-of-process extensions.
+ */
+class WindowedBackgroundPage extends BackgroundPageBase {
+  constructor(options, extension) {
+    super(options, extension);
+    this.parentWindow = null;
+  }
+
+  getParentDocument() {
+    return Task.spawn(function* () {
+      let window = Services.ww.openWindow(null, "about:blank", "_blank",
+                                          "chrome,alwaysLowered,dialog", null);
+
+      this.parentWindow = window;
+
+      let chromeShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIDocShell)
+                              .QueryInterface(Ci.nsIWebNavigation);
+
+      yield this.initParentWindow(chromeShell);
+
+      window.minimize();
+
+      return promiseDocumentLoaded(window.document);
+    }.bind(this));
+  }
+
+  shutdown() {
+    super.shutdown();
+
+    if (this.parentWindow) {
+      this.parentWindow.close();
+      this.parentWindow = null;
+    }
+  }
+}
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_background", (type, directive, extension, manifest) => {
-  let bgPage = new BackgroundPage(manifest.background, extension);
+  let bgPage;
+  if (extension.remote) {
+    bgPage = new WindowedBackgroundPage(manifest.background, extension);
+  } else {
+    bgPage = new WindowlessBackgroundPage(manifest.background, extension);
+  }
+
   backgroundPagesMap.set(extension, bgPage);
   return bgPage.build();
 });
 
 extensions.on("shutdown", (type, extension) => {
   if (backgroundPagesMap.has(extension)) {
     backgroundPagesMap.get(extension).shutdown();
     backgroundPagesMap.delete(extension);