Bug 1477956 - Change overlay loader so it can run before document load; stop requiring restart to start legacy extension; r=Fallen
authorGeoff Lankow <geoff@darktrojan.net>
Mon, 03 Sep 2018 21:57:32 +1200
changeset 24636 4dfbf61d868bf20c254b843674696b17f64efff9
parent 24635 7a59c0ecec8e892b7175910be9cfdb46dff8e60c
child 24637 b67c87c8e4fe8712e75d85e050b107a21198967a
push id14823
push usergeoff@darktrojan.net
push dateMon, 03 Sep 2018 09:58:36 +0000
treeherdercomm-central@4da4607269c1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersFallen
bugs1477956
Bug 1477956 - Change overlay loader so it can run before document load; stop requiring restart to start legacy extension; r=Fallen
calendar/base/content/calendar-common-sets.js
common/src/Overlays.jsm
mail/base/content/extensions.xml
mail/components/extensions/parent/ext-legacy.js
--- a/calendar/base/content/calendar-common-sets.js
+++ b/calendar/base/content/calendar-common-sets.js
@@ -782,17 +782,17 @@ var calendarController2 = {
 function injectCalendarCommandController() {
     // We need to put our new command controller *before* the one that
     // gets installed by thunderbird. Since we get called pretty early
     // during startup we need to install the function below as a callback
     // that periodically checks when the original thunderbird controller
     // gets alive. Please note that setTimeout with a value of 0 means that
     // we leave the current thread in order to re-enter the message loop.
 
-    let tbController = top.controllers.getControllerForCommand("cmd_runJunkControls");
+    let tbController = top.DefaultController;
     if (tbController) {
         calendarController.defaultController = tbController;
         top.controllers.insertControllerAt(0, calendarController);
         document.commandDispatcher.updateCommands("calendar_commands");
     } else {
         setTimeout(injectCalendarCommandController, 0);
     }
 }
--- a/common/src/Overlays.jsm
+++ b/common/src/Overlays.jsm
@@ -81,16 +81,20 @@ class Overlays {
     let xulStore = Services.xulStore;
     this.persistedIDs = new Set();
 
     // Load css styles from the registry
     for (let sheet of this.overlayProvider.style.get(this.location, false)) {
       unloadedSheets.push(sheet);
     }
 
+    if (!unloadedOverlays.length && !unloadedSheets.length) {
+      return;
+    }
+
     while (unloadedOverlays.length) {
       let url = unloadedOverlays.shift();
       let xhr = this.fetchOverlay(url);
       let doc = xhr.responseXML;
 
       oconsole.debug(`Applying ${url} to ${this.location}`);
 
       // clean the document a bit
@@ -165,71 +169,85 @@ class Overlays {
       oconsole.warn(`Could not resolve ${forwardReferences.length} references`, forwardReferences);
     }
 
     // Loading the sheets now to avoid race conditions with xbl bindings
     for (let sheet of unloadedSheets) {
       this.loadCSS(sheet);
     }
 
-    for (let bar of this._toolbarsToResolve) {
-      let currentset = xulStore.getValue(this.location, bar.id, "currentset");
-      if (currentset) {
-        bar.currentSet = currentset;
-      } else if (bar.getAttribute("defaultset")) {
-        bar.currentSet = bar.getAttribute("defaultset");
+    this._decksToResolve = new Map();
+    for (let id of this.persistedIDs.values()) {
+      let element = this.document.getElementById(id);
+      if (element) {
+        let attrNames = xulStore.getAttributeEnumerator(this.location, id);
+        while (attrNames.hasMore()) {
+          let attrName = attrNames.getNext();
+          let attrValue = xulStore.getValue(this.location, id, attrName);
+          if (attrName == "selectedIndex" && element.localName == "deck") {
+            this._decksToResolve.set(element, attrValue);
+          } else {
+            element.setAttribute(attrName, attrValue);
+          }
+        }
       }
     }
 
     // We've resolved all the forward references we can, we can now go ahead and load the scripts
     let deferredLoad = [];
     for (let script of unloadedScripts) {
       deferredLoad.push(...this.loadScript(script));
     }
 
-    let sheet;
-    let overlayTrigger = this.document.createElement("overlayTrigger");
-    overlayTrigger.addEventListener("bindingattached", () => {
-      oconsole.debug("XBL binding attached, continuing with load");
-      sheet.remove();
-      overlayTrigger.remove();
+    if (this.document.readyState == "complete") {
+      let sheet;
+      let overlayTrigger = this.document.createElement("overlayTrigger");
+      overlayTrigger.addEventListener("bindingattached", () => {
+        oconsole.debug("XBL binding attached, continuing with load");
+        sheet.remove();
+        overlayTrigger.remove();
 
-      setTimeout(() => {
-        for (let id of this.persistedIDs.values()) {
-          let element = this.document.getElementById(id);
-          if (element) {
-            let attrNames = xulStore.getAttributeEnumerator(this.location, id);
-            while (attrNames.hasMore()) {
-              let attrName = attrNames.getNext();
-              let attrValue = xulStore.getValue(this.location, id, attrName);
-              element.setAttribute(attrName, attrValue);
-              if (attrName == "currentset") {
-                element.currentSet = attrValue;
-              }
+        setTimeout(() => {
+          this._finish();
+
+          // Now execute load handlers since we are done loading scripts
+          let bubbles = [];
+          for (let { listener, useCapture } of deferredLoad) {
+            if (useCapture) {
+              this._fireEventListener(listener);
+            } else {
+              bubbles.push(listener);
             }
           }
-        }
 
-        // Now execute load handlers since we are done loading scripts
-        let bubbles = [];
-        for (let { listener, useCapture } of deferredLoad) {
-          if (useCapture) {
+          for (let listener of bubbles) {
             this._fireEventListener(listener);
-          } else {
-            bubbles.push(listener);
           }
-        }
+        }, 0);
+      }, { once: true });
+      this.document.documentElement.appendChild(overlayTrigger);
+      sheet = this.loadCSS("chrome://messenger/content/overlayBindings.css");
+    } else {
+      this.document.defaultView.addEventListener("load", this._finish.bind(this), { once: true });
+    }
+  }
 
-        for (let listener of bubbles) {
-          this._fireEventListener(listener);
-        }
-      }, 0);
-    }, { once: true });
-    this.document.documentElement.appendChild(overlayTrigger);
-    sheet = this.loadCSS("chrome://messenger/content/overlayBindings.css");
+  _finish() {
+    for (let [deck, selectedIndex] of this._decksToResolve.entries()) {
+      deck.setAttribute("selectedIndex", selectedIndex);
+    }
+
+    for (let bar of this._toolbarsToResolve) {
+      let currentset = Services.xulStore.getValue(this.location, bar.id, "currentset");
+      if (currentset) {
+        bar.currentSet = currentset;
+      } else if (bar.getAttribute("defaultset")) {
+        bar.currentSet = bar.getAttribute("defaultset");
+      }
+    }
   }
 
   /**
    * Gets the overlays referenced by processing instruction on a document.
    *
    * @param {DOMDocument} document  The document to read instuctions from
    * @return {String[]}             URLs of the overlays from the document
    */
@@ -266,32 +284,38 @@ class Overlays {
    * Resolves forward references for the given node. If the node exists in the target document, it
    * is merged in with the target node. If the node has no id it is inserted at documentElement
    * level.
    *
    * @param {Element} node          The DOM Element to resolve in the target document.
    * @return {Boolean}              True, if the node was merged/inserted, false otherwise
    */
   _resolveForwardReference(node) {
-    if (node.id && node.localName == "toolbarpalette") {
-      // These vanish from the document but still exist via the palette property
-      let boxes = [...this.document.getElementsByTagName("toolbox")];
-      let box = boxes.find(box => box.palette && box.palette.id == node.id);
-      let palette = box ? box.palette : null;
+    if (node.id) {
+      let target = this.document.getElementById(node.id);
+      if (node.localName == "toolbarpalette") {
+        let box;
+        if (target) {
+          box = target.closest("toolbox");
+        } else {
+          // These vanish from the document but still exist via the palette property
+          let boxes = [...this.document.getElementsByTagName("toolbox")];
+          box = boxes.find(box => box.palette && box.palette.id == node.id);
+          let palette = box ? box.palette : null;
 
-      if (!palette) {
-        oconsole.debug(`The palette for ${node.id} could not be found, deferring to later`);
-        return false;
-      }
+          if (!palette) {
+            oconsole.debug(`The palette for ${node.id} could not be found, deferring to later`);
+            return false;
+          }
 
-      this._mergeElement(palette, node);
-      this._toolbarsToResolve.push(...box.getElementsByTagName("toolbar"));
-    } else if (node.id) {
-      let target = this.document.getElementById(node.id);
-      if (!target) {
+          target = palette;
+        }
+
+        this._toolbarsToResolve.push(...box.querySelectorAll("toolbar:not([type=\"menubar\"])"));
+      } else if (!target) {
         oconsole.debug(`The node ${node.id} could not be found, deferring to later`);
         return false;
       }
 
       this._mergeElement(target, node);
     } else {
        this._insertElement(this.document.documentElement, node);
     }
@@ -422,52 +446,55 @@ class Overlays {
    * @return {Object[]}                             An object with listener and useCapture,
    *                                                  describing load handlers the script creates
    *                                                  when first run.
    */
   loadScript(node) {
     let deferredLoad = [];
 
     let oldAddEventListener = this.window.addEventListener;
-    this.window.addEventListener = function(type, listener, useCapture, ...args) {
-      if (type == "load") {
-        if (typeof useCapture == "object") {
-          useCapture = useCapture.capture;
-        }
+    if (this.document.readyState == "complete") {
+      this.window.addEventListener = function(type, listener, useCapture, ...args) {
+        if (type == "load") {
+          if (typeof useCapture == "object") {
+            useCapture = useCapture.capture;
+          }
 
-        if (typeof useCapture == "undefined") {
-          useCapture = true;
+          if (typeof useCapture == "undefined") {
+            useCapture = true;
+          }
+          deferredLoad.push({ listener, useCapture });
+          return null;
         }
-        deferredLoad.push({ listener, useCapture });
-        return null;
-      }
-      return oldAddEventListener.call(this, type, listener, useCapture, ...args);
-    };
+        return oldAddEventListener.call(this, type, listener, useCapture, ...args);
+      };
+    }
 
     if (node.hasAttribute("src")) {
       let url = node.getAttribute("src");
       oconsole.debug(`Loading script ${url} into ${this.window.location}`);
       try {
         Services.scriptloader.loadSubScript(url, this.window);
       } catch (ex) {
-        oconsole.error(`Error loading script ${url} into ${this.window.location}`, ex.message);
+        Cu.reportError(ex);
       }
     } else if (node.textContent) {
       oconsole.debug(`Loading eval'd script into ${this.window.location}`);
       try {
         // It would be great if we could have script errors show the right url, but for now
         // window.eval will have to do.
         this.window.eval(node.textContent);
       } catch (ex) {
-        oconsole.error(`Error loading eval script from ${node.baseURI} into ` +
-                       this.window.location, ex.message);
+        Cu.reportError(ex);
       }
     }
 
-    this.window.addEventListener = oldAddEventListener;
+    if (this.document.readyState == "complete") {
+      this.window.addEventListener = oldAddEventListener;
+    }
 
     // This works because we only care about immediately executed addEventListener calls and
     // loadSubScript is synchronous. Everyone else should be checking readyState anyway.
     return deferredLoad;
   }
 
   /**
    * Load the CSS stylesheet from the given url
--- a/mail/base/content/extensions.xml
+++ b/mail/base/content/extensions.xml
@@ -31,20 +31,17 @@
         ]]></getter>
       </property>
       <method name="_updateState">
         <body><![CDATA[
           this.__proto__.__proto__._updateState.call(this);
           let id = this.mAddon.id;
           let webex = this.webextension;
 
-          if (webex && webex.manifest.legacy && (
-                (webex.startupReason != "APP_STARTUP" && !webex.legacyLoaded) ||
-                (this.mAddon.userDisabled && webex.legacyLoaded)
-              )) {
+          if (webex && webex.manifest.legacy && this.mAddon.userDisabled && webex.legacyLoaded) {
             this.setAttribute("notification", "warning");
             this._warning.textContent = this._bundle.GetStringFromName("warnLegacyRestart");
             this._warningBtn.label = this._bundle.GetStringFromName("warnLegacyRestartButton");
             this._warningBtn.setAttribute("oncommand", "BrowserUtils.restartApplication()");
             this._warningBtn.hidden = false;
             this._warningLink.hidden = true;
           }
         ]]></body>
--- a/mail/components/extensions/parent/ext-legacy.js
+++ b/mail/components/extensions/parent/ext-legacy.js
@@ -15,23 +15,16 @@ var loadedOnce = new Set();
 this.legacy = class extends ExtensionAPI {
   async onManifestEntry(entryName) {
     if (this.extension.manifest.legacy) {
       await this.register();
     }
   }
 
   async register() {
-    let enumerator = Services.wm.getEnumerator("mail:3pane");
-    if (enumerator.hasMoreElements() && enumerator.getNext().document.readyState == "complete") {
-      // It's too late!
-      console.log(`Legacy WebExtension ${this.extension.id} loading after app startup, refusing to load immediately.`);
-      return;
-    }
-
     this.extension.legacyLoaded = true;
 
     if (loadedOnce.has(this.extension.id)) {
       console.log(`Legacy WebExtension ${this.extension.id} has already been loaded in this run, refusing to do so again. Please restart`);
       return;
     }
     loadedOnce.add(this.extension.id);
 
@@ -89,24 +82,55 @@ this.legacy = class extends ExtensionAPI
         }
 
         instance.observe(null, "profile-after-change", null);
       } catch (e) {
         console.error("Error firing profile-after-change listener for", contractid);
       }
     }
 
+    // Add overlays to all existing windows.
+    let enumerator = Services.wm.getEnumerator("mail:3pane");
+    if (enumerator.hasMoreElements() && enumerator.getNext().document.readyState == "complete") {
+      getAllWindows().forEach(w => Overlays.load(chromeManifest, w));
+    }
+
+    // Listen for new windows to overlay.
     let documentObserver = {
       observe(document) {
         if (ExtensionCommon.instanceOf(document, "XULDocument")) {
           Overlays.load(chromeManifest, document.defaultView);
         }
       },
     };
-    Services.obs.addObserver(documentObserver, "chrome-document-loaded");
+    Services.obs.addObserver(documentObserver, "chrome-document-interactive");
 
     this.extension.callOnClose({
       close: () => {
-        Services.obs.removeObserver(documentObserver, "chrome-document-loaded");
+        Services.obs.removeObserver(documentObserver, "chrome-document-interactive");
       },
     });
   }
 };
+
+function getAllWindows() {
+  function getChildDocShells(parentDocShell) {
+    let docShellEnum = parentDocShell.getDocShellEnumerator(
+      Ci.nsIDocShellTreeItem.typeAll,
+      Ci.nsIDocShell.ENUMERATE_FORWARDS
+    );
+
+    for (let docShell of docShellEnum) {
+      docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+              .getInterface(Ci.nsIWebProgress);
+      domWindows.push(docShell.domWindow);
+    }
+  }
+
+  let domWindows = [];
+  for (let win of Services.ww.getWindowEnumerator()) {
+    let parentDocShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIWebNavigation)
+                            .QueryInterface(Ci.nsIDocShell);
+    getChildDocShells(parentDocShell);
+  }
+  return domWindows;
+}