Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
// Helper to observe process shutdowns. Used to detect when extension processes
// have shut down. For simplicity, this helper does not filter by extension
// processes because the callers knowingly pass extension process childIDs only.
class ProcessWatcher {
constructor() {
// Map of childID to boolean (whether process ended abnormally)
this.seenChildIDs = new Map();
this.onShutdownCallbacks = new Set();
Services.obs.addObserver(this, "ipc:content-shutdown");
// See setExtProcessTerminationDeadline and waitAndCheckIsProcessAlive.
// We measure the duration of an earlier test to determine the reasonable
// duration during which a terminated extension process should stay alive.
// Use a high default in case that task was skipped, e.g. by .only().
this.deadDeadline = 5000;
}
unregister() {
Services.obs.removeObserver(this, "ipc:content-shutdown");
}
observe(subject, topic, data) {
const childID = parseInt(data, 10);
const abnormal = subject.QueryInterface(Ci.nsIPropertyBag2).get("abnormal");
info(`Observed content shutdown, childID=${childID}, abnormal=${abnormal}`);
this.seenChildIDs.set(childID, !!abnormal);
for (let onShutdownCallback of this.onShutdownCallbacks) {
onShutdownCallback(childID);
}
}
isProcessAlive(childID) {
return !this.seenChildIDs.has(childID);
}
async waitForTermination(childID, expectAbnormal = false) {
// We only expect content processes, so childID should never be zero.
Assert.ok(childID, `waitForTermination: ${childID}`);
if (!this.isProcessAlive(childID)) {
info(`Process has already shut down: ${childID}`);
} else {
info(`Waiting for process to shut down: ${childID}`);
await new Promise(resolve => {
const onShutdownCallback = _childID => {
if (_childID === childID) {
info(`Process has shut down: ${childID}`);
this.onShutdownCallbacks.delete(onShutdownCallback);
resolve();
}
};
this.onShutdownCallbacks.add(onShutdownCallback);
});
}
// When we get here, !isProcessAlive or onShutdownCallback was called,
// which implies that childID is a key in the seenChildIDs Map.
const abnormal = this.seenChildIDs.get(childID);
if (expectAbnormal) {
Assert.ok(abnormal, "Process should have ended abnormally.");
} else if (AppConstants.platform === "android" && abnormal) {
// On Android, the implementation sometimes triggers abnormal shutdowns
// when we expect normal shutdown. This is undesired, but as it happens
// intermittently, pretend that everything is OK and log a message.
Assert.ok(true, "Process should have ended normally, but did not.");
} else {
Assert.ok(!abnormal, "Process should have ended normally.");
}
}
// Set the deadline as used by "waitAndCheckIsProcessAlive". The deadline is
// the time by which an unexpected process termination should happen to catch
// unexpected process termination.
setExtProcessTerminationDeadline(deadline) {
// Have some reasonably small minimum deadline, in case the caller
// experiences a drifted timer that results in negative value.
const MIN_DEADLINE = 1000;
// Tests time out after 30 seconds. Enforce a maximum deadline below that
// limit, e.g. in case a process is being debugged.
const MAX_DEADLINE = 20000;
if (deadline < MIN_DEADLINE) {
this.deadDeadline = MIN_DEADLINE;
} else if (deadline > MAX_DEADLINE) {
this.deadDeadline = MAX_DEADLINE;
} else {
this.deadDeadline = deadline;
}
}
async waitAndCheckIsProcessAlive(childID) {
Assert.ok(this.isProcessAlive(childID), `Process ${childID} is alive`);
// We want to verify that the extension process does not shut down too soon.
// There is no great way to verify this, other than waiting for a bit and
// verifying that the process is still around.
info(`Waiting for ${this.deadDeadline} ms and process ${childID}`);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => setTimeout(r, this.deadDeadline));
Assert.ok(this.isProcessAlive(childID), `Process ${childID} still alive`);
}
}
// Register early so we catch all terminations.
const processWatcher = new ProcessWatcher();
registerCleanupFunction(() => processWatcher.unregister());
function pidOfContentPage(contentPage) {
return contentPage.browsingContext.currentWindowGlobal.domProcess.childID;
}
function pidOfBackgroundPage(extension) {
return extension.extension.backgroundContext.xulBrowser.browsingContext
.currentWindowGlobal.domProcess.childID;
}
async function loadExtensionAndGetPid() {
let extension = ExtensionTestUtils.loadExtension({
background() {
browser.test.sendMessage("bg_loaded");
},
});
await extension.startup();
await extension.awaitMessage("bg_loaded");
let pid = pidOfBackgroundPage(extension);
await extension.unload();
return pid;
}
add_setup(async function setup_start_and_quit_addon_manager() {
// None of this setup is strictly required for the test file to pass, but
// exists to trigger conditions that were historically associated with bugs
// and test failures.
// As a regression test for bug 1845352: Verify that (simulating) shut down
// of the AddonManager does not break the behavior of extension process
// spawning. For details see bug 1845352 and bug 1845778.
ExtensionTestUtils.mockAppInfo();
AddonTestUtils.init(globalThis);
await AddonTestUtils.promiseStartupManager();
info("Starting an extension to load the extension process");
let extension = ExtensionTestUtils.loadExtension({
background() {
window.onload = () => browser.test.sendMessage("first_run");
},
});
await extension.startup();
await extension.awaitMessage("first_run");
info("Fully loaded initial extension and its process, shutting down now");
await extension.unload();
await AddonTestUtils.promiseShutdownManager();
// Bug 1845352 regression test: the above call broke the test that verified
// process reuse, because unexpectedly the extension process was shut down
// when promiseShutdownManager triggered "quit-application-granted".
});
add_task(
{
// Here we confirm the usual default behavior. We explicitly set the pref
// to 0 because head_remote.js sets the value to 1.
pref_set: [["dom.ipc.keepProcessesAlive.extension", 0]],
},
async function shutdown_extension_process_on_extension_background_unload() {
info("Starting and unloading first extension");
let pid1 = await loadExtensionAndGetPid();
info("Extension process should end after unloading the only extension doc");
await processWatcher.waitForTermination(pid1);
}
);
add_task(
{
// This test verifies that dom.ipc.keepProcessesAlive.extension=1 works,
// because we rely on it in unit tests, mainly to minimize overhead.
pref_set: [["dom.ipc.keepProcessesAlive.extension", 1]],
},
async function extension_process_reused_between_background_page_restarts() {
info("Starting and unloading first extension");
let pid1 = await loadExtensionAndGetPid();
info("Process should be alive after unloading the only extension (1)");
await processWatcher.waitAndCheckIsProcessAlive(pid1);
info("Starting and unloading second extension");
let pid2 = await loadExtensionAndGetPid();
Assert.equal(pid1, pid2, "Extension process was reused");
info("Process should be alive after unloading the only extension (2)");
await processWatcher.waitAndCheckIsProcessAlive(pid1);
// Try again repeatedly for many times to verify that this is not a fluke.
// The number of attempts is arbitrarily chosen.
for (let i = 1; i <= 9; ++i) {
let pid3 = await loadExtensionAndGetPid();
Assert.equal(pid1, pid3, `Extension process was reused at attempt ${i}`);
}
info("Process should be alive after unloading the only extension (3)");
await processWatcher.waitAndCheckIsProcessAlive(pid1);
// Note: while this task started without extension process, we end this
// task with an extension process still running.
}
);
add_task(
{
// Here we confirm the usual default behavior. We explicitly set the pref
// to 0 because head_remote.js sets the value to 1.
pref_set: [["dom.ipc.keepProcessesAlive.extension", 0]],
},
async function shutdown_extension_process_on_last_extension_page_unload() {
let extension = ExtensionTestUtils.loadExtension({
files: {
"page.html": `<!DOCTYPE html><script src="page.js"></script>`,
"page.js": () => browser.test.sendMessage("page_loaded"),
},
});
await extension.startup();
const EXT_PAGE = `moz-extension://${extension.uuid}/page.html`;
async function openOnlyExtensionPageAndGetPid() {
let contentPage = await ExtensionTestUtils.loadContentPage(EXT_PAGE);
await extension.awaitMessage("page_loaded");
let pid = pidOfContentPage(contentPage);
await contentPage.close();
return pid;
}
const timeStart = Date.now();
info("Opening first page");
let contentPage = await ExtensionTestUtils.loadContentPage(EXT_PAGE);
await extension.awaitMessage("page_loaded");
let pid1 = pidOfContentPage(contentPage);
info("Opening and closing second page while the first is open");
let pid2 = await openOnlyExtensionPageAndGetPid();
Assert.equal(pid1, pid2, "Second page should re-use first page's process");
Assert.ok(processWatcher.isProcessAlive(pid1), "Process not dead");
await contentPage.close();
info("Closed last page - extension process should terminate");
// pid1 should have died when we closed ContentPage. But in case shut down
// is not immediate, wait a little bit.
await processWatcher.waitForTermination(pid1);
let pid3 = await openOnlyExtensionPageAndGetPid();
Assert.notEqual(pid2, pid3, "Should get a new extension process");
await extension.unload();
await processWatcher.waitForTermination(pid3);
// By now, we have witnessed:
// 1. extension process spawned.
// 2. first extension tab loaded.
// 3. second extension tab loaded.
// 4. extension process terminated after closing tabs.
// 5. extension process spawned + terminated after opening and closing tab.
// This should be plenty of time for any unexpected process termination to
// have been observed. So wait for that time and not longer.
processWatcher.setExtProcessTerminationDeadline(Date.now() - timeStart);
}
);
add_task(
{
// This test verifies that dom.ipc.keepProcessesAlive.extension=1 works,
// because we rely on it in unit tests, mainly to minimize overhead.
pref_set: [["dom.ipc.keepProcessesAlive.extension", 1]],
},
async function keep_extension_process_on_last_extension_page_unload() {
let extension = ExtensionTestUtils.loadExtension({
files: {
"page.html": `<!DOCTYPE html><script src="page.js"></script>`,
"page.js": () => browser.test.sendMessage("page_loaded"),
},
});
await extension.startup();
const EXT_PAGE = `moz-extension://${extension.uuid}/page.html`;
async function openOnlyExtensionPageAndGetPid() {
let contentPage = await ExtensionTestUtils.loadContentPage(EXT_PAGE);
await extension.awaitMessage("page_loaded");
let pid = pidOfContentPage(contentPage);
await contentPage.close();
return pid;
}
info("Opening and closing first page");
let pid1 = await openOnlyExtensionPageAndGetPid();
info("No extension pages, but extension process should still be alive (1)");
await processWatcher.waitAndCheckIsProcessAlive(pid1);
let pid2 = await openOnlyExtensionPageAndGetPid();
Assert.equal(pid1, pid2, "Extension process is reused by second page");
info("No extension pages, but extension process should still be alive (2)");
await processWatcher.waitAndCheckIsProcessAlive(pid1);
await extension.unload();
info("No extensions around, but extension process should still be alive");
await processWatcher.waitAndCheckIsProcessAlive(pid1);
// Note: while this task started without extension process, we end this
// task with an extension process still running.
}
);