/* -*- 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(); } );