Bug 1748524 sleep/waken functionality for background, restartable via persistent events r=rpl

Differential Revision: https://phabricator.services.mozilla.com/D135812
This commit is contained in:
Shane Caraveo 2022-01-25 20:49:15 +00:00
parent d860937b39
commit 9d410ec1bb
17 changed files with 372 additions and 44 deletions

View File

@ -4,7 +4,12 @@
add_task(async function testTabSwitchActionContext() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.manifestV3.enabled", true]],
set: [
["extensions.manifestV3.enabled", true],
// Since we're not using AOM, and MV3 forces event pages, bypass
// delayed-startup for MV3 test. These tests do not rely on startup events.
["extensions.webextensions.background-delayed-startup", false],
],
});
});

View File

@ -19,10 +19,20 @@ async function grantOptionalPermission(extension, permissions) {
return ExtensionPermissions.add(extension.id, permissions, ext);
}
var someOtherTab, testTab;
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.manifestV3.enabled", true]],
});
// To help diagnose an intermittent later.
SimpleTest.requestCompleteLog();
// Setup the test tab now, rather than for each test
someOtherTab = gBrowser.selectedTab;
testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
registerCleanupFunction(() => BrowserTestUtils.removeTab(testTab));
});
// Registers a context menu using menus.create(menuCreateParams) and checks
@ -38,7 +48,7 @@ async function testShowHideEvent({
forceTabToBackground = false,
manifest_version = 2,
}) {
async function background() {
async function background(menu_create_params) {
function awaitMessage(expectedId) {
return new Promise(resolve => {
browser.test.log(`Waiting for message: ${expectedId}`);
@ -56,7 +66,6 @@ async function testShowHideEvent({
});
}
let menuCreateParams = await awaitMessage("create-params");
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
@ -72,7 +81,7 @@ async function testShowHideEvent({
args[0].targetElementId = 13337; // = EXPECT_TARGET_ELEMENT
}
shownEvents.push(args[0]);
if (menuCreateParams.title.includes("TEST_EXPECT_NO_TAB")) {
if (menu_create_params.title.includes("TEST_EXPECT_NO_TAB")) {
browser.test.assertEq(undefined, args[1], "expect no tab");
} else {
browser.test.assertEq(tab.id, args[1].id, "expected tab");
@ -85,7 +94,7 @@ async function testShowHideEvent({
let menuId;
await new Promise(resolve => {
menuId = browser.menus.create(menuCreateParams, resolve);
menuId = browser.menus.create(menu_create_params, resolve);
});
browser.test.assertEq(0, shownEvents.length, "no onShown before menu");
browser.test.assertEq(0, hiddenEvents.length, "no onHidden before menu");
@ -106,14 +115,13 @@ async function testShowHideEvent({
browser.test.sendMessage("onShown-event-data2", shownEvents[1]);
}
const someOtherTab = gBrowser.selectedTab;
// Tab must initially open as a foreground tab, because the test extension
// looks for the active tab.
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
gBrowser.selectedTab = testTab;
const action = manifest_version < 3 ? "browser_action" : "action";
let extension = ExtensionTestUtils.loadExtension({
background,
background: `(${background})(${JSON.stringify(menuCreateParams)})`,
manifest: {
manifest_version,
page_action: {},
@ -128,14 +136,13 @@ async function testShowHideEvent({
},
});
await extension.startup();
extension.sendMessage("create-params", menuCreateParams);
let menuId = await extension.awaitMessage("menu-registered");
if (forceTabToBackground) {
gBrowser.selectedTab = someOtherTab;
}
await doOpenMenu(extension, tab);
await doOpenMenu(extension, testTab);
extension.sendMessage("assert-menu-shown");
let shownEvent = await extension.awaitMessage("onShown-event-data");
@ -154,7 +161,7 @@ async function testShowHideEvent({
permissions: [],
origins: [PAGE_HOST_PATTERN],
});
await doOpenMenu(extension, tab);
await doOpenMenu(extension, testTab);
extension.sendMessage("optional-menu-shown-with-permissions");
let shownEvent2 = await extension.awaitMessage("onShown-event-data2");
Assert.deepEqual(
@ -166,7 +173,6 @@ async function testShowHideEvent({
}
await extension.unload();
BrowserTestUtils.removeTab(tab);
}
// Make sure that we won't trigger onShown when extensions cannot add menus.

View File

@ -5,6 +5,12 @@
/* globals chrome */
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
// Since we're not using AOM, and MV3 forces event pages, bypass
// delayed-startup for MV3 test. These tests do not rely on startup events.
Services.prefs.setBoolPref(
"extensions.webextensions.background-delayed-startup",
false
);
async function testPermission(options) {
function background(bgOptions) {

View File

@ -117,6 +117,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
"[]"
);
// This pref modifies behavior for MV2. MV3 is enabled regardless.
XPCOMUtils.defineLazyPreferenceGetter(
this,
"eventPagesEnabled",
"extensions.eventPages.enabled"
);
var {
GlobalManager,
ParentAPIManager,
@ -543,6 +550,7 @@ class ExtensionData {
this.errors = [];
this.warnings = [];
this.eventPagesEnabled = eventPagesEnabled;
}
get builtinMessages() {
@ -942,6 +950,24 @@ class ExtensionData {
return this.manifest.manifest_version;
}
get persistentBackground() {
let { manifest } = this;
if (
!manifest.background ||
manifest.background.service_worker ||
this.manifestVersion > 2
) {
return false;
}
// V2 addons can only use event pages if the pref is also flipped and
// persistent is explicilty set to false.
let { persistent } = manifest.background;
if (!this.eventPagesEnabled && !persistent) {
this.logWarning("Event pages are not currently supported.");
}
return !this.eventPagesEnabled || persistent;
}
async getExtensionVersionWithoutValidation() {
return (await this.readJSON("manifest.json")).version;
}

View File

@ -2216,7 +2216,7 @@ class EventManager {
*/
static _initPersistentListeners(extension) {
if (extension.persistentListeners) {
return false;
return !!extension.persistentListeners.size;
}
let listeners = new DefaultMap(() => new DefaultMap(() => new Map()));
@ -2266,7 +2266,7 @@ class EventManager {
// in an extension's startup data.
// This function is only called during browser startup, it stores details
// about all primed listeners in the extension's persistentListeners Map.
static primeListeners(extension) {
static primeListeners(extension, isInStartup = false) {
if (!EventManager._initPersistentListeners(extension)) {
return;
}
@ -2305,7 +2305,8 @@ class EventManager {
extension,
event,
fire,
listener.params
listener.params,
isInStartup
);
if (handler) {
listener.primed = primed;
@ -2322,6 +2323,10 @@ class EventManager {
// `clearPersistent` is false. If false, the listeners are cleared from
// memory, but not removed from the extension's startup data.
static clearPrimedListeners(extension, clearPersistent = true) {
if (!extension.persistentListeners) {
return;
}
for (let [module, moduleEntry] of extension.persistentListeners) {
for (let [event, listeners] of moduleEntry) {
for (let [key, listener] of listeners) {

View File

@ -325,11 +325,14 @@ ExtensionTestCommon = class ExtensionTestCommon {
if (data.background) {
let bgScript = Services.uuid.generateUUID().number + ".js";
// If persistent is set keep the flag.
let persistent = manifest.background?.persistent;
let scriptKey = data.useServiceWorker
? ["background", "service_worker"]
: ["background", "scripts"];
let scriptVal = data.useServiceWorker ? bgScript : [bgScript];
provide(manifest, scriptKey, scriptVal, true);
provide(manifest, ["background", "persistent"], persistent);
files[bgScript] = data.background;
}

View File

@ -258,6 +258,14 @@ class ExtensionWrapper {
this.state = "unloaded";
}
/**
* This method sends the message to force-sleep the background scripts.
* @returns {Promise} resolves after the background is asleep and listeners primed.
*/
terminateBackground() {
return this.extension.terminateBackground();
}
/*
* This method marks the extension unloading without actually calling
* shutdown, since shutting down a MockExtension causes it to be uninstalled.

View File

@ -266,9 +266,12 @@ this.backgroundPage = class extends ExtensionAPI {
return this.bgInstance.build();
}
onManifestEntry(entryName) {
async primeBackground(isInStartup = true) {
let { extension } = this;
if (this.bgInstance) {
Cu.reportError(`background script exists before priming ${extension.id}`);
}
this.bgInstance = null;
// When in PPB background pages all run in a private context. This check
@ -300,11 +303,36 @@ this.backgroundPage = class extends ExtensionAPI {
return bgStartupPromise;
};
if (extension.startupReason !== "APP_STARTUP" || !DELAYED_STARTUP) {
extension.terminateBackground = async () => {
await bgStartupPromise;
this.onShutdown(false);
EventManager.clearPrimedListeners(this.extension, false);
// Setup background startup listeners for next primed event.
return this.primeBackground(false);
};
extension.once("terminate-background-script", async () => {
if (!this.extension) {
// Extension was already shut down.
return;
}
this.extension.terminateBackground();
});
// Persistent backgrounds are started immediately except during APP_STARTUP.
// Non-persistent backgrounds must be started immediately for new install or enable
// to initialize the addon and create the persisted listeners.
if (
isInStartup &&
(!DELAYED_STARTUP ||
(extension.persistentBackground &&
extension.startupReason !== "APP_STARTUP") ||
["ADDON_INSTALL", "ADDON_ENABLE"].includes(extension.startupReason))
) {
return this.build();
}
EventManager.primeListeners(extension);
EventManager.primeListeners(extension, isInStartup);
extension.once("start-background-script", async () => {
if (!this.extension) {
@ -321,23 +349,50 @@ this.backgroundPage = class extends ExtensionAPI {
// to touch browserPaintedPromise here to initialize the listener
// or else we can miss it if the event occurs after the first
// window is painted but before #2
// 2. After all windows have been restored.
// 2. After all windows have been restored on startup (see onManifestEntry).
extension.once("background-script-event", async () => {
await ExtensionParent.browserPaintedPromise;
extension.emit("start-background-script");
});
ExtensionParent.browserStartupPromise.then(() => {
extension.emit("start-background-script");
});
}
onShutdown(isAppShutdown) {
if (this.bgInstance) {
this.bgInstance.shutdown(isAppShutdown);
this.bgInstance = null;
this.extension.emit("shutdown-background-script");
} else {
EventManager.clearPrimedListeners(this.extension, false);
}
}
async onManifestEntry(entryName) {
let { extension } = this;
await this.primeBackground();
ExtensionParent.browserStartupPromise.then(() => {
// If the background has been created earlier than session restore,
// we do not want to continue with creating it here.
if (this.bgInstance) {
return;
}
// If there are no listeners for the extension that were persisted, we need to
// start the event page so they can be registered.
if (
extension.persistentBackground ||
!extension.persistentListeners?.size
) {
extension.emit("start-background-script");
} else {
// During startup we only prime startup blocking listeners. At
// this stage we need to prime all listeners for event pages.
EventManager.clearPrimedListeners(extension, false);
// Allow re-priming by deleting existing listeners.
extension.persistentListeners = null;
EventManager.primeListeners(extension, false);
}
});
}
};

View File

@ -126,9 +126,9 @@ function makeWebRequestEvent(context, name) {
}
this.webRequest = class extends ExtensionAPI {
primeListener(extension, event, fire, params) {
primeListener(extension, event, fire, params, isInStartup) {
// During early startup if the listener does not use blocking we do not prime it.
if (params[1]?.includes("blocking")) {
if (!isInStartup || params[1]?.includes("blocking")) {
return registerEvent(extension, event, fire, ...params);
}
}

View File

@ -132,7 +132,9 @@
"page": { "$ref": "ExtensionURL" },
"persistent": {
"optional": true,
"$ref": "PersistentBackgroundProperty"
"type": "boolean",
"max_manifest_version": 2,
"default": true
}
},
"additionalProperties": { "$ref": "UnrecognizedProperty" }
@ -146,7 +148,9 @@
},
"persistent": {
"optional": true,
"$ref": "PersistentBackgroundProperty"
"type": "boolean",
"max_manifest_version": 2,
"default": true
}
},
"additionalProperties": { "$ref": "UnrecognizedProperty" }
@ -722,20 +726,6 @@
"id": "UnrecognizedProperty",
"type": "any",
"deprecated": "An unexpected property was found in the WebExtension manifest."
},
{
"id": "PersistentBackgroundProperty",
"choices": [
{
"type": "boolean",
"enum": [true]
},
{
"type": "boolean",
"enum": [false],
"deprecated": "Event pages are not currently supported. This will run as a persistent background page."
}
]
}
]
}

View File

@ -9,6 +9,12 @@ Services.scriptloader.loadSubScript(
this
);
// Bug 1748665 remove when events will start serviceworker
Services.prefs.setBoolPref(
"extensions.webextensions.background-delayed-startup",
false
);
add_task(assert_background_serviceworker_pref_enabled);
add_task(async function test_serviceWorker_register_guarded_by_pref() {

View File

@ -1,6 +1,12 @@
"use strict";
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
// Since we're not using AOM, and MV3 forces event pages, bypass
// delayed-startup for MV3 test. These tests do not rely on startup events.
Services.prefs.setBoolPref(
"extensions.webextensions.background-delayed-startup",
false
);
const server = createHttpServer({ hosts: ["example.com"] });

View File

@ -7,6 +7,12 @@ const { TestUtils } = ChromeUtils.import(
);
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
// Since we're not using AOM, and MV3 forces event pages, bypass
// delayed-startup for MV3 test. These tests do not rely on startup events.
Services.prefs.setBoolPref(
"extensions.webextensions.background-delayed-startup",
false
);
const server = createHttpServer({
hosts: ["example.com", "csplog.example.net"],

View File

@ -153,6 +153,15 @@ async function promiseObservable(topic, count, fn = null) {
return results;
}
function trackEvents(wrapper) {
let events = new Map();
for (let event of ["background-script-event", "start-background-script"]) {
events.set(event, false);
wrapper.extension.once(event, () => events.set(event, true));
}
return events;
}
add_task(async function setup() {
Services.prefs.setBoolPref(
"extensions.webextensions.background-delayed-startup",
@ -526,3 +535,119 @@ add_task(async function test_shutdown_before_background_loaded() {
await AddonTestUtils.promiseShutdownManager();
});
// This test checks whether primed listeners are correctly primed to
// restart the background once the background has been shutdown or
// put to sleep.
add_task(async function test_background_restarted() {
await AddonTestUtils.promiseStartupManager();
// ensure normal delayed startup notification had already happened at some point
Services.obs.notifyObservers(null, "browser-delayed-startup-finished");
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
background() {
let listener = arg => browser.test.sendMessage("triggered", arg);
browser.eventtest.onEvent1.addListener(listener, "triggered");
browser.test.sendMessage("bg_started");
},
});
await Promise.all([
promiseObservable("register-event-listener", 1),
extension.startup(),
]);
await extension.awaitMessage("bg_started");
assertPersistentListeners(extension, "eventtest", "onEvent1", {
primed: false,
});
// Shutdown the background page
await Promise.all([
promiseObservable("unregister-event-listener", 1),
extension.terminateBackground(),
]);
// When sleeping the background, its events should become persisted
assertPersistentListeners(extension, "eventtest", "onEvent1", {
primed: true,
});
info("Triggering persistent event to force the background page to start");
Services.obs.notifyObservers({ listenerArgs: 123 }, "fire-onEvent1");
await extension.awaitMessage("bg_started");
equal(await extension.awaitMessage("triggered"), 123, "triggered event");
await extension.unload();
await AddonTestUtils.promiseShutdownManager();
});
// This test checks whether primed listeners are correctly primed to
// restart the background once the background has been shutdown or
// put to sleep.
add_task(async function test_eventpage_startup() {
Services.prefs.setBoolPref("extensions.eventPages.enabled", true);
await AddonTestUtils.promiseStartupManager();
// ensure normal delayed startup notification had already happened at some point
Services.obs.notifyObservers(null, "browser-delayed-startup-finished");
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
applications: { gecko: { id: "eventpage@test" } },
background: { persistent: false },
},
background() {
let listener = arg => browser.test.sendMessage("triggered", arg);
browser.eventtest.onEvent1.addListener(listener, "triggered");
browser.test.sendMessage("bg_started");
},
});
await Promise.all([
promiseObservable("register-event-listener", 1),
extension.startup(),
]);
await extension.awaitMessage("bg_started");
async function testAfterRestart() {
assertPersistentListeners(extension, "eventtest", "onEvent1", {
primed: true,
});
let events = trackEvents(extension);
ok(
!events.get("background-script-event"),
"Should not have received a background script event"
);
ok(
!events.get("start-background-script"),
"Background script should not be started"
);
info("Triggering persistent event to force the background page to start");
Services.obs.notifyObservers({ listenerArgs: 123 }, "fire-onEvent1");
await extension.awaitMessage("bg_started");
equal(await extension.awaitMessage("triggered"), 123, "triggered event");
ok(
events.get("background-script-event"),
"Should have received a background script event"
);
ok(
events.get("start-background-script"),
"Background script should be started"
);
}
// Shutdown the background page
await AddonTestUtils.promiseRestartManager();
await extension.awaitStartup();
await testAfterRestart();
// We sleep twice to ensure startup and shutdown work correctly
await extension.terminateBackground();
await testAfterRestart();
await extension.terminateBackground();
await testAfterRestart();
await extension.unload();
await AddonTestUtils.promiseShutdownManager();
Services.prefs.setBoolPref("extensions.eventPages.enabled", false);
});

View File

@ -52,10 +52,10 @@ function trackEvents(wrapper) {
* expect the starting event
* @param {boolean} expect.request wait for the request event
*/
async function testPersistentRequestStartup(extension, events, expect) {
async function testPersistentRequestStartup(extension, events, expect = {}) {
equal(
events.get("background-script-event"),
expect.background,
!!expect.background,
"Should have gotten a background script event"
);
equal(
@ -70,7 +70,7 @@ async function testPersistentRequestStartup(extension, events, expect) {
equal(
events.get("start-background-script"),
expect.delayedStart,
!!expect.delayedStart,
"Should have gotten start-background-script event"
);
}
@ -162,6 +162,80 @@ add_task(async function test_nonblocking() {
await promiseShutdownManager();
});
// Test that a non-blocking listener does not start the background on
// startup, but that it does work after startup.
add_task(async function test_eventpage_nonblocking() {
Services.prefs.setBoolPref("extensions.eventPages.enabled", true);
await promiseStartupManager();
let id = "event-nonblocking@test";
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
applications: { gecko: { id } },
permissions: ["webRequest", "http://example.com/"],
background: { persistent: false },
},
background() {
browser.webRequest.onBeforeRequest.addListener(
details => {
browser.test.sendMessage("got-request");
},
{ urls: ["http://example.com/data/file_sample.html"] }
);
},
});
// First install runs background immediately, this sets persistent listeners
await extension.startup();
// Restart to get APP_STARTUP, the background should not start
await promiseRestartManager();
await extension.awaitStartup();
assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
primed: false,
});
// Test an early startup event
let events = trackEvents(extension);
await ExtensionTestUtils.fetch(
"http://example.com/",
"http://example.com/data/file_sample.html"
);
await testPersistentRequestStartup(extension, events);
Services.obs.notifyObservers(null, "sessionstore-windows-restored");
await ExtensionParent.browserStartupPromise;
// After late startup, event page listeners should be primed.
assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
primed: true,
});
// We should not have seen any events yet.
await testPersistentRequestStartup(extension, events);
// Test an event after startup
await ExtensionTestUtils.fetch(
"http://example.com/",
"http://example.com/data/file_sample.html"
);
// Now the event page should be started and we'll see the request.
await testPersistentRequestStartup(extension, events, {
background: true,
started: true,
request: true,
});
await extension.unload();
await promiseShutdownManager();
Services.prefs.setBoolPref("extensions.eventPages.enabled", false);
});
// Tests that filters are handled properly: if we have a blocking listener
// with a filter, a request that does not match the filter does not get
// suspended and does not start the background page.

View File

@ -3,6 +3,12 @@
const HOSTS = new Set(["example.com"]);
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
// Since we're not using AOM, and MV3 forces event pages, bypass
// delayed-startup for MV3 test. These tests do not rely on startup events.
Services.prefs.setBoolPref(
"extensions.webextensions.background-delayed-startup",
false
);
const server = createHttpServer({ hosts: HOSTS });

View File

@ -12,6 +12,7 @@ prefs =
extensions.backgroundServiceWorker.enabled=true
extensions.backgroundServiceWorker.forceInTestExtension=true
extensions.webextensions.remote=true
extensions.webextensions.background-delayed-startup=false
[test_ext_background_service_worker.js]
[test_ext_alarms.js]