diff options
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js')
-rw-r--r-- | toolkit/components/extensions/test/xpcshell/test_ext_background_early_shutdown.js | 905 |
1 files changed, 905 insertions, 0 deletions
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 `<script defer>`. For more detail about this + // trick, see test_ext_background_iframe.js. + let allowStylesheetToLoad; + let stylesheetBlockerPromise = new Promise(resolve => { + allowStylesheetToLoad = resolve; + }); + let resolveFirstLoad; + let firstLoadPromise = new Promise(resolve => { + resolveFirstLoad = resolve; + }); + let requestCount = 0; + server.registerPathHandler("/slow.css", (request, response) => { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/css", false); + response.processAsync(); + ++requestCount; + resolveFirstLoad(); + stylesheetBlockerPromise.then(() => { + response.write("body { color: rgb(1, 2, 3); }"); + response.finish(); + }); + }); + const getRequestCount = () => requestCount; + return { allowStylesheetToLoad, getRequestCount, firstLoadPromise }; +} + +// We can only crash the extension process when they are running out-of-process. +// Otherwise we would be killing the test runner itself... +const CAN_CRASH_EXTENSIONS = WebExtensionPolicy.useRemoteWebExtensions; + +add_setup(function () { + // Set a high threshold because this test crashes a few times on purpose and + // we don't want to disable process spawning. + Services.prefs.setIntPref("extensions.webextensions.crash.threshold", 100); + + // Need a profile to init Glean. + do_get_profile(); + Services.fog.initializeFOG(); + + Assert.equal( + ExtensionProcessCrashObserver.appInForeground, + AppConstants.platform !== "android", + "Expect appInForeground to be initially true on desktop and false on android builds" + ); + + // For Android build we mock the app moving in the foreground for the first time + // (which, in a real Fenix instance, happens when the application receives the first + // call to the onPause lifecycle callback and the geckoview-initial-foreground + // topic is being notified to Gecko as a side-effect of that). + // + // We have to mock the app moving in the foreground before any of the test extension + // startup, so that both Desktop and Mobile builds are in the same initial foreground + // state for the rest of the test file. + if (AppConstants.platform === "android") { + info("Mock geckoview-initial-foreground observer service topic"); + ExtensionProcessCrashObserver.observe(null, "geckoview-initial-foreground"); + Assert.equal( + ExtensionProcessCrashObserver.appInForeground, + true, + "Expect appInForeground to be true after geckoview-initial-foreground topic" + ); + } +}); + +add_setup( + // Crash dumps are only generated when MOZ_CRASHREPORTER is set. + // Crashes are only generated if tests can crash the extension process. + { skip_if: () => !AppConstants.MOZ_CRASHREPORTER || !CAN_CRASH_EXTENSIONS }, + setup_crash_reporter_override_and_cleaner +); + +// Verifies that a delayed background page is not loaded when an extension is +// shut down during startup. +add_task(async function test_unload_extension_before_background_page_startup() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background() { + browser.test.sendMessage("background_startup_observed"); + }, + }); + + // Delayed startup are only enabled for browser (re)starts, so we need to + // install the extension first, and then unload it. + // ^ Note: an alternative is to use APP_STARTUP, see elsewhere in this file. + + await extension.startup(); + await extension.awaitMessage("background_startup_observed"); + + // Now the actual test: Unloading an extension before the startup has + // finished should interrupt the start-up and abort pending delayed loads. + info("Starting extension whose startup will be interrupted"); + await promiseRestartManager({ earlyStartup: false }); + await extension.awaitStartup(); + + let extensionBrowserInsertions = 0; + let onExtensionBrowserInserted = () => ++extensionBrowserInsertions; + Management.on("extension-browser-inserted", onExtensionBrowserInserted); + + info("Unloading extension before the delayed background page starts loading"); + await extension.addon.disable(); + + // Re-enable the add-on to let enough time pass to load a whole background + // page. If at the end of this the original background page hasn't loaded, + // we can consider the test successful. + await extension.addon.enable(); + + // Trigger the notification that would load a background page. + info("Forcing pending delayed background page to load"); + AddonTestUtils.notifyLateStartup(); + + // This is the expected message from the re-enabled add-on. + await extension.awaitMessage("background_startup_observed"); + await extension.unload(); + + await promiseShutdownManager(); + + Management.off("extension-browser-inserted", onExtensionBrowserInserted); + Assert.equal( + extensionBrowserInsertions, + 1, + "Extension browser should have been inserted only once" + ); +}); + +// Verifies that the "build" method of BackgroundPage in ext-backgroundPage.js +// does not deadlock when startup is interrupted by extension shutdown. +add_task(async function test_unload_extension_during_background_page_startup() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background() { + browser.test.sendMessage("background_starting"); + }, + }); + + // Delayed startup are only enabled for browser (re)starts, so we need to + // install the extension first, and then reload it. + // ^ Note: an alternative is to use APP_STARTUP, see elsewhere in this file. + await extension.startup(); + await extension.awaitMessage("background_starting"); + + await promiseRestartManager({ lateStartup: false }); + await extension.awaitStartup(); + + let bgStartupPromise = new Promise(resolve => { + function onBackgroundPageDone(eventName) { + extension.extension.off( + "background-script-started", + onBackgroundPageDone + ); + extension.extension.off( + "background-script-aborted", + onBackgroundPageDone + ); + + if (eventName === "background-script-aborted") { + info("Background script startup was interrupted"); + resolve("bg_aborted"); + } else { + info("Background script startup finished normally"); + resolve("bg_fully_loaded"); + } + } + extension.extension.on("background-script-started", onBackgroundPageDone); + extension.extension.on("background-script-aborted", onBackgroundPageDone); + }); + + let bgStartingPromise = new Promise(resolve => { + let backgroundLoadCount = 0; + let backgroundPageUrl = extension.extension.baseURI.resolve( + "_generated_background_page.html" + ); + + // Prevent the background page from actually loading. + Management.once("extension-browser-inserted", (eventName, browser) => { + // Intercept background page load. + let browserFixupAndLoadURIString = browser.fixupAndLoadURIString; + browser.fixupAndLoadURIString = function () { + Assert.equal(++backgroundLoadCount, 1, "loadURI should be called once"); + Assert.equal( + arguments[0], + backgroundPageUrl, + "Expected background page" + ); + // Reset to "about:blank" to not load the actual background page. + arguments[0] = "about:blank"; + browserFixupAndLoadURIString.apply(this, arguments); + + // And force the extension process to crash. + if (CAN_CRASH_EXTENSIONS) { + crashFrame(browser); + } else { + // If extensions are not running in out-of-process mode, then the + // non-remote process should not be killed (or the test runner dies). + // Remove <browser> instead, to simulate the immediate disconnection + // of the message manager (that would happen if the process crashed). + browser.remove(); + } + resolve(); + }; + }); + }); + + // Force background page to initialize. + AddonTestUtils.notifyLateStartup(); + await bgStartingPromise; + + await extension.unload(); + await promiseShutdownManager(); + + // This part is the regression test for bug 1501375. It verifies that the + // background building completes eventually. + // If it does not, then the next line will cause a timeout. + info("Waiting for background builder to finish"); + let bgLoadState = await bgStartupPromise; + Assert.equal(bgLoadState, "bg_aborted", "Startup should be interrupted"); +}); + +// Verifies correct state when background crashes while starting. +// The difference with test_unload_extension_during_background_page_startup is +// that in this version, the background has progressed far enough for the +// extension context to have been initialized, but before the background is +// considered loaded. +// withContext: Whether to trigger initialization of ProxyContextParent. +async function do_test_crash_while_starting_background({ + // Whether to trigger initialization of ProxyContextParent during startup. + withContext = false, + // Whether to use an event page instead of a persistent background page. + isEventPage = false, +}) { + let extension = ExtensionTestUtils.loadExtension({ + // Delay startup, so that we can get an extension reference before the + // background page starts. + startupReason: "APP_STARTUP", + // APP_STARTUP is not enough, delayedStartup is needed (bug 1756225). + delayedStartup: true, + manifest: { + background: { + page: "background.html", + persistent: !isEventPage, + }, + }, + files: { + "background.html": `<!DOCTYPE html> + <meta charset="utf-8"> + <script src="background-immediate.js"></script> + + <!-- Delays DOMContentLoaded - see registerSlowStyleSheet --> + <link rel="stylesheet" href="http://example.com/slow.css"> + <script src="background-deferred.js" defer></script> + `, + "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 <browser> 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(); + } +); |