Bug 1416163 - Implement EveryWindow.jsm to run arbitrary per-window code. r=johannh

Differential Revision: https://phabricator.services.mozilla.com/D26947

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Nihanth Subramanya 2019-04-16 16:17:25 +00:00
Родитель 7f4b636f4f
Коммит 0f3e51207d
9 изменённых файлов: 250 добавлений и 67 удалений

Просмотреть файл

@ -126,6 +126,10 @@ var whitelist = [
{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",

Просмотреть файл

@ -27,7 +27,6 @@ FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['privileged'] += [
]
FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['privileged']['subscripts'] += [
'privileged/subscripts/EveryWindow.jsm',
'privileged/subscripts/Globals.jsm'
]

Просмотреть файл

@ -98,13 +98,10 @@ this.FirefoxMonitor = {
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.

Просмотреть файл

@ -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);
}
},
};

Просмотреть файл

@ -10,6 +10,8 @@ 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"]);

Просмотреть файл

@ -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);
},
};

Просмотреть файл

@ -61,6 +61,9 @@ with Files("ContentCrashHandlers.jsm"):
with Files("ContentSearch.jsm"):
BUG_COMPONENT = ("Firefox", "Search")
with Files("EveryWindow.jsm"):
BUG_COMPONENT = ("Firefox", "General")
with Files("ExtensionsUI.jsm"):
BUG_COMPONENT = ("WebExtensions", "General")
@ -136,6 +139,7 @@ EXTRA_JS_MODULES += [
'ContentObservers.js',
'ContentSearch.jsm',
'Discovery.jsm',
'EveryWindow.jsm',
'ExtensionsUI.jsm',
'FaviconLoader.jsm',
'FormValidationHandler.jsm',

Просмотреть файл

@ -14,6 +14,7 @@ support-files =
!/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]

Просмотреть файл

@ -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");
});