summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_extension_process_alive.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_extension_process_alive.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_extension_process_alive.js312
1 files changed, 312 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_extension_process_alive.js b/toolkit/components/extensions/test/xpcshell/test_extension_process_alive.js
new file mode 100644
index 0000000000..fb82f85140
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_extension_process_alive.js
@@ -0,0 +1,312 @@
+"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.
+ }
+);