Bug 1416163 - Implement EveryWindow.jsm to run arbitrary per-window code. r=johannh
authorNihanth Subramanya <nhnt11@gmail.com>
Tue, 16 Apr 2019 16:17:25 +0000
changeset 469701 c640010582e2
parent 469700 fbd1effe6fe4
child 469702 a8acb6aa4a52
push id35879
push usernerli@mozilla.com
push dateTue, 16 Apr 2019 22:01:48 +0000
treeherdermozilla-central@12a60898fdc1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjohannh
bugs1416163
milestone68.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 1416163 - Implement EveryWindow.jsm to run arbitrary per-window code. r=johannh Differential Revision: https://phabricator.services.mozilla.com/D26947
browser/base/content/test/static/browser_all_files_referenced.js
browser/extensions/fxmonitor/moz.build
browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm
browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm
browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm
browser/modules/EveryWindow.jsm
browser/modules/moz.build
browser/modules/test/browser/browser.ini
browser/modules/test/browser/browser_EveryWindow.js
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -121,16 +121,20 @@ var whitelist = [
   // browser/extensions/pdfjs/content/web/viewer.js#7450
   {file: "resource://pdf.js/web/debugger.js"},
 
   // resource://app/modules/translation/TranslationContentHandler.jsm
   {file: "resource://app/modules/translation/BingTranslator.jsm"},
   {file: "resource://app/modules/translation/GoogleTranslator.jsm"},
   {file: "resource://app/modules/translation/YandexTranslator.jsm"},
 
+  // Used in Firefox Monitor, which is an extension - we don't check
+  // files inside the XPI.
+  {file: "resource://app/modules/EveryWindow.jsm"},
+
   // Starting from here, files in the whitelist are bugs that need fixing.
   // Bug 1339424 (wontfix?)
   {file: "chrome://browser/locale/taskbar.properties",
    platforms: ["linux", "macosx"]},
   // Bug 1356031 (only used by devtools)
   {file: "chrome://global/skin/icons/error-16.png"},
   // Bug 1344267
   {file: "chrome://marionette/content/test_anonymous_content.xul"},
--- a/browser/extensions/fxmonitor/moz.build
+++ b/browser/extensions/fxmonitor/moz.build
@@ -22,14 +22,13 @@ FINAL_TARGET_FILES.features['fxmonitor@m
 FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['privileged'] += [
   'privileged/api.js',
   'privileged/FirefoxMonitor.css',
   'privileged/FirefoxMonitor.jsm',
   'privileged/schema.json'
 ]
 
 FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['privileged']['subscripts'] += [
-  'privileged/subscripts/EveryWindow.jsm',
   'privileged/subscripts/Globals.jsm'
 ]
 
 with Files('**'):
   BUG_COMPONENT = ('Firefox', 'Firefox Monitor')
--- a/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm
+++ b/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm
@@ -93,23 +93,20 @@ this.FirefoxMonitor = {
   _delayedInited: false,
   async delayedInit() {
     if (this._delayedInited) {
       return;
     }
 
     this._delayedInited = true;
 
-    /* globals Preferences, RemoteSettings, fetch, btoa, XUL_NS */
+    /* globals EveryWindow, Preferences, RemoteSettings, fetch, btoa, XUL_NS */
     Services.scriptloader.loadSubScript(
       this.getURL("privileged/subscripts/Globals.jsm"));
 
-    /* globals EveryWindow */
-    Services.scriptloader.loadSubScript(
-      this.getURL("privileged/subscripts/EveryWindow.jsm"));
 
     // Expire our telemetry on November 1, at which time
     // we should redo data-review.
     let telemetryExpiryDate = new Date(2019, 10, 1); // Month is zero-index
     let today = new Date();
     let expired = today.getTime() > telemetryExpiryDate.getTime();
 
     Services.telemetry.registerEvents("fxmonitor", {
deleted file mode 100644
--- a/browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm
+++ /dev/null
@@ -1,62 +0,0 @@
-/* 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/. */
-
-/* globals Services */
-
-this.EveryWindow = {
-  _callbacks: new Map(),
-  _initialized: false,
-
-  registerCallback: function EW_registerCallback(id, init, uninit) {
-    if (this._callbacks.has(id)) {
-      return;
-    }
-
-    this._callForEveryWindow(init);
-    this._callbacks.set(id, {id, init, uninit});
-
-    if (!this._initialized) {
-      Services.obs.addObserver(this._onOpenWindow.bind(this),
-                               "browser-delayed-startup-finished");
-      this._initialized = true;
-    }
-  },
-
-  unregisterCallback: function EW_unregisterCallback(aId, aCallUninit = true) {
-    if (!this._callbacks.has(aId)) {
-      return;
-    }
-
-    if (aCallUninit) {
-      this._callForEveryWindow(this._callbacks.get(aId).uninit);
-    }
-
-    this._callbacks.delete(aId);
-  },
-
-  _callForEveryWindow(aFunction) {
-    let windowList = Services.wm.getEnumerator("navigator:browser");
-    while (windowList.hasMoreElements()) {
-      let win = windowList.getNext();
-      win.delayedStartupPromise.then(() => { aFunction(win); });
-    }
-  },
-
-  _onOpenWindow(aWindow) {
-    for (let c of this._callbacks.values()) {
-      c.init(aWindow);
-    }
-
-    aWindow.addEventListener("unload",
-                             this._onWindowClosing.bind(this),
-                             { once: true });
-  },
-
-  _onWindowClosing(aEvent) {
-    let win = aEvent.target;
-    for (let c of this._callbacks.values()) {
-      c.uninit(win, true);
-    }
-  },
-};
--- a/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm
+++ b/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm
@@ -5,12 +5,14 @@
 /* eslint-disable no-unused-vars */
 
 ChromeUtils.defineModuleGetter(this, "Preferences",
                                "resource://gre/modules/Preferences.jsm");
 ChromeUtils.defineModuleGetter(this, "PluralForm",
                                "resource://gre/modules/PluralForm.jsm");
 ChromeUtils.defineModuleGetter(this, "RemoteSettings",
                                "resource://services-settings/remote-settings.js");
+ChromeUtils.defineModuleGetter(this, "EveryWindow",
+                               "resource:///modules/EveryWindow.jsm");
 const {setTimeout, clearTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm", {});
 Cu.importGlobalProperties(["fetch", "btoa"]);
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
new file mode 100644
--- /dev/null
+++ b/browser/modules/EveryWindow.jsm
@@ -0,0 +1,109 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["EveryWindow"];
+
+/*
+ * This module enables consumers to register callbacks on every
+ * current and future browser window.
+ *
+ * Usage: EveryWindow.registerCallback(id, init, uninit);
+ *        EveryWindow.unregisterCallback(id);
+ *
+ * id is expected to be a unique value that identifies the
+ * consumer, to be used for unregistration. If the id is already
+ * in use, registerCallback returns false without doing anything.
+ *
+ * Each callback will receive the window for which it is presently
+ * being called as the first argument.
+ *
+ * init is called on every existing window at the time of registration,
+ * and on all future windows at browser-delayed-startup-finished.
+ *
+ * uninit is called on every existing window if requested at the time
+ * of unregistration, and at the time of domwindowclosed.
+ * If the window is closing, a second argument is passed with value `true`.
+ */
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var initialized = false;
+var callbacks = new Map();
+
+function callForEveryWindow(callback) {
+  let windowList = Services.wm.getEnumerator("navigator:browser");
+  for (let win of windowList) {
+    win.delayedStartupPromise.then(() => { callback(win); });
+  }
+}
+
+this.EveryWindow = {
+  /**
+   * Registers init and uninit functions to be called on every window.
+   *
+   * @param {string} id A unique identifier for the consumer, to be
+   *   used for unregistration.
+   * @param {function} init The function to be called on every currently
+   *   existing window and every future window after delayed startup.
+   * @param {function} uninit The function to be called on every window
+   *   at the time of callback unregistration or after domwindowclosed.
+   * @returns {boolean} Returns false if the id was taken, else true.
+   */
+  registerCallback: function EW_registerCallback(id, init, uninit) {
+    if (callbacks.has(id)) {
+      return false;
+    }
+
+    if (!initialized) {
+      let addUnloadListener = (win) => {
+        function observer(subject, topic, data) {
+          if (topic == "domwindowclosed" && subject === win) {
+            Services.ww.unregisterNotification(observer);
+            for (let c of callbacks.values()) {
+              c.uninit(win, true);
+            }
+          }
+        }
+        Services.ww.registerNotification(observer);
+      };
+
+      Services.obs.addObserver(win => {
+        for (let c of callbacks.values()) {
+          c.init(win);
+        }
+        addUnloadListener(win);
+      }, "browser-delayed-startup-finished");
+
+      callForEveryWindow(addUnloadListener);
+
+      initialized = true;
+    }
+
+    callForEveryWindow(init);
+    callbacks.set(id, {id, init, uninit});
+
+    return true;
+  },
+
+  /**
+   * Unregisters a previously registered consumer.
+   *
+   * @param {string} id The id to unregister.
+   * @param {boolean} [callUninit=true] Whether to call the registered uninit
+   *   function on every window.
+   */
+  unregisterCallback: function EW_unregisterCallback(id, callUninit = true) {
+    if (!callbacks.has(id)) {
+      return;
+    }
+
+    if (callUninit) {
+      callForEveryWindow(callbacks.get(id).uninit);
+    }
+
+    callbacks.delete(id);
+  },
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -56,16 +56,19 @@ with Files("*Telemetry.jsm"):
     BUG_COMPONENT = ("Toolkit", "Telemetry")
 
 with Files("ContentCrashHandlers.jsm"):
     BUG_COMPONENT = ("Toolkit", "Crash Reporting")
 
 with Files("ContentSearch.jsm"):
     BUG_COMPONENT = ("Firefox", "Search")
 
+with Files("EveryWindow.jsm"):
+    BUG_COMPONENT = ("Firefox", "General")
+
 with Files("ExtensionsUI.jsm"):
     BUG_COMPONENT = ("WebExtensions", "General")
 
 with Files("LaterRun.jsm"):
     BUG_COMPONENT = ("Firefox", "Tours")
 
 with Files("LiveBookmarkMigrator.jsm"):
     BUG_COMPONENT = ("Firefox", "General")
@@ -131,16 +134,17 @@ EXTRA_JS_MODULES += [
     'BrowserUsageTelemetry.jsm',
     'BrowserWindowTracker.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentMetaHandler.jsm',
     'ContentObservers.js',
     'ContentSearch.jsm',
     'Discovery.jsm',
+    'EveryWindow.jsm',
     'ExtensionsUI.jsm',
     'FaviconLoader.jsm',
     'FormValidationHandler.jsm',
     'HomePage.jsm',
     'LaterRun.jsm',
     'LiveBookmarkMigrator.jsm',
     'NewTabPagePreloading.jsm',
     'OpenInTabsUtils.jsm',
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -9,16 +9,17 @@ prefs =
 support-files =
   contentSearch.js
   contentSearchBadImage.xml
   contentSearchSuggestions.sjs
   contentSearchSuggestions.xml
   !/browser/components/search/test/browser/head.js
   !/browser/components/search/test/browser/testEngine.xml
   testEngine_chromeicon.xml
+[browser_EveryWindow.js]
 [browser_LiveBookmarkMigrator.js]
 [browser_PageActions.js]
 [browser_PermissionUI.js]
 [browser_PermissionUI_prompts.js]
 [browser_preloading_tab_moving.js]
 [browser_ProcessHangNotifications.js]
 skip-if = !e10s
 [browser_SitePermissions.js]
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/browser_EveryWindow.js
@@ -0,0 +1,129 @@
+/* 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";
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const {EveryWindow} = ChromeUtils.import("resource:///modules/EveryWindow.jsm");
+
+async function windowInited(aId, aWin) {
+  // TestUtils.topicObserved returns [subject, data]. We return the
+  // subject, which in this case is the window.
+  return (await TestUtils.topicObserved(`${aId}:init`, (win) => {
+    return aWin ? win == aWin : true;
+  }))[0];
+}
+
+function windowUninited(aId, aWin, aClosing) {
+  return TestUtils.topicObserved(`${aId}:uninit`, (win, closing) => {
+    if (aWin && aWin != win) {
+      return false;
+    }
+    if (!aWin) {
+      return true;
+    }
+    if (!!aClosing != !!closing) {
+      return false;
+    }
+    return true;
+  });
+}
+
+function registerEWCallback(id) {
+  EveryWindow.registerCallback(
+    id,
+    (win) => {
+      Services.obs.notifyObservers(win, `${id}:init`);
+    },
+    (win, closing) => {
+      Services.obs.notifyObservers(win, `${id}:uninit`, closing);
+    },
+  );
+}
+
+function unregisterEWCallback(id, aCallUninit) {
+  EveryWindow.unregisterCallback(id, aCallUninit);
+}
+
+add_task(async function test_stuff() {
+  let win2 = await BrowserTestUtils.openNewBrowserWindow();
+  let win3 = await BrowserTestUtils.openNewBrowserWindow();
+
+  let callbackId1 = "EveryWindow:test:1";
+  let callbackId2 = "EveryWindow:test:2";
+
+  let initPromise = Promise.all([windowInited(callbackId1, window),
+                                windowInited(callbackId1, win2),
+                                windowInited(callbackId1, win3),
+                                windowInited(callbackId2, window),
+                                windowInited(callbackId2, win2),
+                                windowInited(callbackId2, win3)]);
+
+  registerEWCallback(callbackId1);
+  registerEWCallback(callbackId2);
+
+  await initPromise;
+  ok(true, "Init called for all existing windows for all registered consumers");
+
+  let uninitPromise = Promise.all([windowUninited(callbackId1, window, false),
+                                  windowUninited(callbackId1, win2, false),
+                                  windowUninited(callbackId1, win3, false),
+                                  windowUninited(callbackId2, window, false),
+                                  windowUninited(callbackId2, win2, false),
+                                  windowUninited(callbackId2, win3, false)]);
+
+  unregisterEWCallback(callbackId1);
+  unregisterEWCallback(callbackId2);
+  await uninitPromise;
+  ok(true, "Uninit called for all existing windows");
+
+  initPromise = Promise.all([windowInited(callbackId1, window),
+                            windowInited(callbackId1, win2),
+                            windowInited(callbackId1, win3),
+                            windowInited(callbackId2, window),
+                            windowInited(callbackId2, win2),
+                            windowInited(callbackId2, win3)]);
+
+  registerEWCallback(callbackId1);
+  registerEWCallback(callbackId2);
+
+  await initPromise;
+  ok(true, "Init called for all existing windows for all registered consumers");
+
+  uninitPromise = Promise.all([windowUninited(callbackId1, win2, true),
+                              windowUninited(callbackId2, win2, true)]);
+  await BrowserTestUtils.closeWindow(win2);
+  await uninitPromise;
+  ok(true, "Uninit called with closing=true for win2 for all registered consumers");
+
+  uninitPromise = Promise.all([windowUninited(callbackId1, win3, true),
+                              windowUninited(callbackId2, win3, true)]);
+  await BrowserTestUtils.closeWindow(win3);
+  await uninitPromise;
+  ok(true, "Uninit called with closing=true for win3 for all registered consumers");
+
+  initPromise = windowInited(callbackId1);
+  let initPromise2 = windowInited(callbackId2);
+  win2 = await BrowserTestUtils.openNewBrowserWindow();
+  is(await initPromise, win2, "Init called for new window for callback 1");
+  is(await initPromise2, win2, "Init called for new window for callback 2");
+
+  uninitPromise = Promise.all([windowUninited(callbackId1, win2, true),
+                              windowUninited(callbackId2, win2, true)]);
+  await BrowserTestUtils.closeWindow(win2);
+  await uninitPromise;
+  ok(true, "Uninit called with closing=true for win2 for all registered consumers");
+
+  uninitPromise = windowUninited(callbackId1, window, false);
+  unregisterEWCallback(callbackId1);
+  await uninitPromise;
+  ok(true, "Uninit called for main window without closing flag for the unregistered consumer");
+
+  uninitPromise = windowUninited(callbackId2, window, false);
+  let timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
+  unregisterEWCallback(callbackId2, false);
+  let result = await Promise.race([uninitPromise, timeoutPromise]);
+  is(result, undefined, "Uninit not called when unregistering a consumer with aCallUninit=false");
+});