diff options
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.js | 312 |
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. + } +); |