From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../xpcshell/test_ext_background_early_shutdown.js | 905 +++++++++++++++++++++ 1 file changed, 905 insertions(+) create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js') diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js new file mode 100644 index 0000000000..706f6d0a67 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js @@ -0,0 +1,905 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { ExtensionTestCommon } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionTestCommon.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "43" +); + +let { promiseRestartManager, promiseShutdownManager, promiseStartupManager } = + AddonTestUtils; + +const { ExtensionProcessCrashObserver, Management } = + ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs"); + +const server = AddonTestUtils.createHttpServer({ + hosts: ["example.com"], +}); +function registerSlowStyleSheet() { + // We can delay DOMContentLoaded of a background page by loading a slow + // stylesheet and using ` + + + + + `, + "background-immediate.js": String.raw` + dump("background-immediate.js is executing as expected.\n"); + if (${!!withContext}) { + // Accessing the browser API triggers context creation. + browser.test.sendMessage("background_started_to_load"); + } + `, + "background-deferred.js": () => { + dump("background-deferred.js is UNEXPECTEDLY executing.\n"); + browser.test.fail("Background startup should have been interrupted"); + }, + }, + }); + let slowStyleSheet = registerSlowStyleSheet(); + await ExtensionTestCommon.resetStartupPromises(); + await extension.startup(); + function assertBackgroundState(expected, message) { + Assert.equal(extension.extension.backgroundState, expected, message); + } + + assertBackgroundState("stopped", "Background should not have started yet"); + + let bgBrowserPromise = new Promise(resolve => { + Management.once("extension-browser-inserted", (eventName, browser) => { + assertBackgroundState("starting", "State when bg is inserted"); + resolve(browser); + }); + }); + + info("Triggering background creation..."); + await ExtensionTestCommon.notifyEarlyStartup(); + await ExtensionTestCommon.notifyLateStartup(); + + let bgBrowser = await bgBrowserPromise; + + if (withContext) { + info("Waiting for background-immediate.js to notify us..."); + await extension.awaitMessage("background_started_to_load"); + Assert.ok( + extension.extension.backgroundContext, + "Context exists when an extension API was called" + ); + // Probably resolved by now, but wait explicitly in case it hasn't, so we + // know that the stylesheet has started to load. + await slowStyleSheet.firstLoadPromise; + } else { + // Wait for the stylesheet request to infer that the background content has + // started to be loaded. + await slowStyleSheet.firstLoadPromise; + Assert.ok( + !extension.extension.backgroundContext, + "Context should not be set while loading" + ); + } + + // Still starting because registerSlowStyleSheet postponed startup completion. + assertBackgroundState("starting", "Background should still be loading"); + + await crashExtensionBackground(extension, bgBrowser); + + assertBackgroundState("stopped", "Background state after crash"); + + // Now that the background is gone, the server can respond without the + // possibility of triggering the execution of background-deferred.js + slowStyleSheet.allowStylesheetToLoad(); + await extension.unload(); + + // Can't be 0 because the background has started to load. + // Can't be 2 because we are loading the background only once. + Assert.equal( + slowStyleSheet.getRequestCount(), + 1, + "Expected exactly one request for slow.css from background page" + ); +} + +add_task( + { + // TODO: consider adding explicit coverage for auto-restart behavior + // when a crash is hit while there is not background context yet. + pref_set: [ + ["extensions.background.disableRestartPersistentAfterCrash", true], + ], + }, + async function test_crash_while_starting_background_without_context() { + await do_test_crash_while_starting_background({ withContext: false }); + } +); + +add_task( + { + // Expected auto-restart behavior is tested in the test task named + // test_persistent_restarted_after_crash. + pref_set: [ + ["extensions.background.disableRestartPersistentAfterCrash", true], + ], + }, + async function test_crash_while_starting_background_with_context() { + await do_test_crash_while_starting_background({ withContext: true }); + } +); + +add_task(async function test_crash_while_starting_event_page_without_context() { + await do_test_crash_while_starting_background({ + withContext: false, + isEventPage: true, + }); +}); + +add_task(async function test_crash_while_starting_event_page_with_context() { + await do_test_crash_while_starting_background({ + withContext: true, + isEventPage: true, + }); +}); + +async function do_test_crash_while_running_background({ isEventPage = false }) { + // wakeupBackground() only wakes up after the early startup notification. + // Trigger explicitly to be independent of other tests. + await ExtensionTestCommon.resetStartupPromises(); + await AddonTestUtils.notifyEarlyStartup(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + background: { persistent: !isEventPage }, + }, + background() { + window.onload = () => { + browser.test.sendMessage("background_has_fully_loaded"); + }; + }, + }); + await extension.startup(); + function assertBackgroundState(expected, message) { + Assert.equal(extension.extension.backgroundState, expected, message); + } + + await extension.awaitMessage("background_has_fully_loaded"); + assertBackgroundState("running", "Background should have started"); + + await crashExtensionBackground(extension); + assertBackgroundState("stopped", "Background state after crash"); + + await extension.wakeupBackground(); + await extension.awaitMessage("background_has_fully_loaded"); + assertBackgroundState("running", "Background resumed after crash recovery"); + await extension.terminateBackground(); + assertBackgroundState("stopped", "Background can sleep after crash recovery"); + + await extension.unload(); +} + +add_task( + { + // Disable auto-restart persistent background pages after a crash, this test + // case is checking that the backgroundState is set to stopped when an + // extension process crash is it but if the background page is restarted + // automatically then the background state will be already set to "starting". + pref_set: [ + ["extensions.background.disableRestartPersistentAfterCrash", true], + ], + }, + async function test_crash_after_background_startup() { + await do_test_crash_while_running_background({ isEventPage: false }); + } +); + +add_task(async function test_crash_after_event_page_startup() { + await do_test_crash_while_running_background({ isEventPage: true }); +}); + +add_task(async function test_crash_and_wakeup_via_persistent_listeners() { + // Ensure that the background can start in response to a primed listener. + await ExtensionTestCommon.resetStartupPromises(); + await AddonTestUtils.notifyEarlyStartup(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + background: { persistent: false }, + optional_permissions: ["tabs"], + }, + background() { + const domreadyPromise = new Promise(resolve => { + window.addEventListener("load", resolve, { once: true }); + }); + browser.permissions.onAdded.addListener(() => { + browser.test.log("permissions.onAdded has fired"); + // Wait for DOMContentLoaded to have fired before notifying the test. + // This guarantees that backgroundState is "running" instead of + // potentially "starting". + domreadyPromise.then(() => { + browser.test.sendMessage("event_fired"); + }); + }); + }, + }); + await extension.startup(); + function assertBackgroundState(expected, message) { + Assert.equal(extension.extension.backgroundState, expected, message); + } + function triggerEventInEventPage() { + // Trigger an event, with the expectation that the event page will wake up. + // As long as we are the only one to trigger the extension API event in this + // test, the exact event is not significant. Trigger permissions.onAdded: + Management.emit("change-permissions", { + extensionId: extension.id, + added: { + origins: [], + permissions: ["tabs"], + }, + }); + } + + assertBackgroundState("running", "Background after extension.startup()"); + + // Sanity check: triggerEventInEventPage does actually trigger event_fired. + triggerEventInEventPage(); + await extension.awaitMessage("event_fired"); + + // Restart a few times to verify that the behavior is consistent over time. + const TEST_RESTART_ATTEMPTS = 5; + for (let i = 1; i <= TEST_RESTART_ATTEMPTS; ++i) { + info(`Testing that a crashed background wakes via event, attempt ${i}/5`); + + await crashExtensionBackground(extension); + + assertBackgroundState("stopped", "Background state after crash"); + + triggerEventInEventPage(); + await extension.awaitMessage("event_fired"); + + assertBackgroundState("running", "Persistent event can wake up event page"); + } + + await extension.unload(); +}); + +add_task( + { + skip_if: () => !CAN_CRASH_EXTENSIONS, + pref_set: [ + ["extensions.webextensions.crash.threshold", 3], + // Set a long timeframe to make sure the few crashes we produce in this + // test will all be counted within the same timeframe. + ["extensions.webextensions.crash.timeframe", 60 * 1000], + ], + }, + async function test_process_spawning_disabled_because_of_too_many_crashes() { + // Force-enable process spawning because that will reset the internals of + // the crash observer. + ExtensionProcessCrashObserver.enableProcessSpawning(); + Services.fog.testResetFOG(); + + function assertCrashThresholdTelemetry({ expectToBeSet }) { + // Desktop builds are only expected to record crashed_over_threshold_fg, + // on Android builds xpcshell tests are detected as being in foreground + // unless we explicitly mock the app being moved in the background as + // the test tasks test_background_restarted_after_crash already does + // (and crashed_over_threshold_bg is covered in that test task). + + equal( + undefined, + Glean.extensions.processEvent.crashed_over_threshold_bg.testGetValue(), + `Initial value of crashed_over_threshold_bg.` + ); + + if (expectToBeSet) { + Assert.greater( + Glean.extensions.processEvent.crashed_over_threshold_fg.testGetValue(), + 0, + "Expect crashed_over_threshold_fg count to be set." + ); + return; + } + + equal( + undefined, + Glean.extensions.processEvent.crashed_over_threshold_fg.testGetValue(), + `Initial value of crashed_over_threshold_fg.` + ); + } + + assertCrashThresholdTelemetry({ expectToBeSet: false }); + + Assert.equal( + ExtensionProcessCrashObserver.processSpawningDisabled, + false, + "Expect process spawning to be enabled" + ); + + // Ensure that the background can start in response to a primed listener. + await ExtensionTestCommon.resetStartupPromises(); + await AddonTestUtils.notifyEarlyStartup(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + background: { persistent: false }, + optional_permissions: ["tabs"], + }, + background() { + const domreadyPromise = new Promise(resolve => { + window.addEventListener("load", resolve, { once: true }); + }); + browser.permissions.onAdded.addListener(() => { + browser.test.log("permissions.onAdded has fired"); + // Wait for DOMContentLoaded to have fired before notifying the test. + // This guarantees that backgroundState is "running" instead of + // potentially "starting". + domreadyPromise.then(() => { + browser.test.sendMessage("event_fired"); + }); + }); + }, + }); + await extension.startup(); + function assertBackgroundState(expected, message) { + Assert.equal(extension.extension.backgroundState, expected, message); + } + function triggerEventInEventPage() { + // Trigger an event, with the expectation that the event page will wake up. + // As long as we are the only one to trigger the extension API event in this + // test, the exact event is not significant. Trigger permissions.onAdded: + Management.emit("change-permissions", { + extensionId: extension.id, + added: { + origins: [], + permissions: ["tabs"], + }, + }); + } + + assertBackgroundState("running", "Background after extension.startup()"); + + // Sanity check: triggerEventInEventPage does actually trigger event_fired. + triggerEventInEventPage(); + await extension.awaitMessage("event_fired"); + + // Crash/restart a few times to force the crash observer to disable process + // spawning on the crash _after_ the loop. Note that the value below should + // match the "threshold" pref set above. + const TEST_RESTART_ATTEMPTS = 3; + for (let i = 1; i <= TEST_RESTART_ATTEMPTS; ++i) { + info( + `Crash/restart extension background, attempt ${i}/${TEST_RESTART_ATTEMPTS}` + ); + + await crashExtensionBackground(extension); + assertBackgroundState("stopped", "Background state after crash"); + + triggerEventInEventPage(); + await extension.awaitMessage("event_fired"); + assertBackgroundState( + "running", + "Persistent event can wake up event page" + ); + } + + assertCrashThresholdTelemetry({ expectToBeSet: false }); + + info("Crash one more time"); + await crashExtensionBackground(extension); + + assertCrashThresholdTelemetry({ expectToBeSet: true }); + + Assert.ok( + ExtensionProcessCrashObserver.processSpawningDisabled, + "Expect process spawning to be disabled" + ); + + info("Trigger an event, which shouldn't wake up the event page"); + triggerEventInEventPage(); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 100)); + assertBackgroundState("stopped", "Background should not have started yet"); + + info("Enable process spawning"); + ExtensionProcessCrashObserver.enableProcessSpawning(); + Assert.equal( + ExtensionProcessCrashObserver.processSpawningDisabled, + false, + "Expect process spawning to be enabled" + ); + assertBackgroundState("stopped", "Background should still be suspended"); + + info("Trigger an event, which should wake up the event page"); + triggerEventInEventPage(); + await extension.awaitMessage("event_fired"); + assertBackgroundState("running", "Persistent event can wake up event page"); + + info("Crash again"); + await crashExtensionBackground(extension); + assertBackgroundState("stopped", "Background state after crash"); + Assert.equal( + ExtensionProcessCrashObserver.processSpawningDisabled, + false, + "Expect process spawning to be enabled" + ); + + info("Trigger an event, which should wake up the event page again"); + triggerEventInEventPage(); + await extension.awaitMessage("event_fired"); + assertBackgroundState("running", "Persistent event can wake up event page"); + + await extension.unload(); + } +); + +add_task( + { + skip_if: () => !CAN_CRASH_EXTENSIONS, + pref_set: [ + ["extensions.webextensions.crash.threshold", 2], + // Set a long timeframe to make sure the few crashes we produce in this + // test will all be counted within the same timeframe. + ["extensions.webextensions.crash.timeframe", 60 * 1000], + ], + }, + async function test_background_restarted_after_crash() { + // Force-enable process spawning because that will reset the internals of + // the crash observer. + ExtensionProcessCrashObserver.enableProcessSpawning(); + Assert.equal( + ExtensionProcessCrashObserver.processSpawningDisabled, + false, + "Expect process spawning to be enabled" + ); + + function assertCrashThresholdTelemetry({ fg, bg }) { + if (fg) { + Assert.greater( + Glean.extensions.processEvent.crashed_over_threshold_fg.testGetValue(), + 0, + "Expect crashed_over_threshold_fg count to be set." + ); + } else { + equal( + undefined, + Glean.extensions.processEvent.crashed_over_threshold_fg.testGetValue(), + `Initial value of crashed_over_threshold_fg.` + ); + } + if (bg) { + Assert.greater( + Glean.extensions.processEvent.crashed_over_threshold_bg.testGetValue(), + 0, + "Expect crashed_over_threshold_bg count to be set." + ); + } else { + equal( + undefined, + Glean.extensions.processEvent.crashed_over_threshold_bg.testGetValue(), + `Initial value of crashed_over_threshold_bg.` + ); + } + } + + // Setup test environment to match a fully started browser instance + await ExtensionTestCommon.resetStartupPromises(); + await AddonTestUtils.notifyEarlyStartup(); + Services.fog.testResetFOG(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + background: { persistent: true }, + }, + background() { + window.addEventListener( + "load", + () => { + browser.test.sendMessage("persistentbg_started"); + }, + { once: true } + ); + }, + }); + + await extension.startup(); + await extension.awaitMessage("persistentbg_started"); + + function assertBackgroundState(expected, message) { + Assert.equal(extension.extension.backgroundState, expected, message); + } + + async function assertStillStoppedAfterTimeout(timeout = 100) { + // Confirm that the state is still stopped and the background page + // was not actually in the process of being restarted. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, timeout)); + assertBackgroundState("stopped", "Background should still be stopped"); + } + + async function mockCrashOnAndroidAppInBackground() { + info("Mock application-background observer service topic"); + ExtensionProcessCrashObserver.observe(null, "application-background"); + + Assert.equal( + ExtensionProcessCrashObserver.appInForeground, + false, + "Got expected value set on ExtensionProcessCrashObserver.appInForeground" + ); + await crashExtensionBackground(extension); + assertBackgroundState( + "stopped", + "Persistent Background state after crash while in the background" + ); + + await assertStillStoppedAfterTimeout(); + + info("Mock application-foreground observer service topic"); + ExtensionProcessCrashObserver.observe(null, "application-foreground"); + + Assert.equal( + ExtensionProcessCrashObserver.appInForeground, + true, + "Ensure the application is detected as being in foreground" + ); + } + + assertBackgroundState("running", "Background after extension.startup()"); + + // Restart a few times to verify that the behavior is consistent over time. + info( + "Testing that a crashed persistent background is restarted after a crash" + ); + + Assert.equal( + ExtensionProcessCrashObserver.appInForeground, + true, + "Ensure the application is detected as being in foreground" + ); + await crashExtensionBackground(extension); + + await extension.awaitMessage("persistentbg_started"); + assertBackgroundState("running", "Persistent Background state after crash"); + + // Mock application moved into the background and background page + // auto-restart to be deferred to the application being moved + // back in the foreground. + if (ExtensionProcessCrashObserver._isAndroid) { + await mockCrashOnAndroidAppInBackground(); + } else { + await crashExtensionBackground(extension); + } + + info("Wait for the persistent background context to be started"); + await extension.awaitMessage("persistentbg_started"); + assertBackgroundState("running", "Persistent Background state after crash"); + + info("Mock another crash to be exceeding enforced crash threshold"); + + assertCrashThresholdTelemetry({ fg: false, bg: false }); + + // Mock application moved into the background and background page + // auto-restart to be deferred to the application being moved + // back in the foreground. + if (ExtensionProcessCrashObserver._isAndroid) { + await mockCrashOnAndroidAppInBackground(); + assertCrashThresholdTelemetry({ fg: false, bg: true }); + } else { + await crashExtensionBackground(extension); + assertCrashThresholdTelemetry({ fg: true, bg: false }); + } + + Assert.ok( + ExtensionProcessCrashObserver.processSpawningDisabled, + "Expect process spawning to be disabled" + ); + + assertBackgroundState( + "stopped", + "Persistent Background state after crash exceeding threshold" + ); + + await assertStillStoppedAfterTimeout(); + + info("Enable process spawning"); + ExtensionProcessCrashObserver.enableProcessSpawning(); + Assert.equal( + ExtensionProcessCrashObserver.processSpawningDisabled, + false, + "Expect process spawning to be enabled" + ); + + info("Wait for Persistent Background Context to be started"); + await extension.awaitMessage("persistentbg_started"); + assertBackgroundState("running", "Persistent Background to be running"); + + // Crash again to confirm the threshold has been reset. + await crashExtensionBackground(extension); + info("Wait for Persistent Background Context to be started"); + await extension.awaitMessage("persistentbg_started"); + assertBackgroundState("running", "Persistent Background to be running"); + + // Crash again to cover explicitly exceeding the crash threshold + // while the application is in foreground. + if (ExtensionProcessCrashObserver._isAndroid) { + Assert.equal( + ExtensionProcessCrashObserver.appInForeground, + true, + "Ensure the application is detected as being in foreground" + ); + + await crashExtensionBackground(extension); + + info("Wait for Persistent Background Context to be started"); + await extension.awaitMessage("persistentbg_started"); + assertBackgroundState("running", "Persistent Background to be running"); + + // Crash one more time to exceed the threshold. + await crashExtensionBackground(extension); + + assertCrashThresholdTelemetry({ fg: true, bg: true }); + + Assert.ok( + ExtensionProcessCrashObserver.processSpawningDisabled, + "Expect process spawning to be disabled" + ); + await assertStillStoppedAfterTimeout(); + } + + await extension.unload(); + } +); -- cgit v1.2.3