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