diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 46777f50438c..b4a93fd1cdec 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -13494,6 +13494,12 @@ # Prefs starting with "toolkit." #--------------------------------------------------------------------------- +# Makes removeDirectory background task wait for the given milliseconds before removal. +- name: toolkit.background_tasks.remove_directory.testing.sleep_ms + type: RelaxedAtomicUint32 + value: 0 + mirror: always + # Returns true if BHR is disabled. - name: toolkit.content-background-hang-monitor.disabled type: bool diff --git a/netwerk/cache2/CacheFileIOManager.cpp b/netwerk/cache2/CacheFileIOManager.cpp index 812c2acccc1a..0e0c21fe5607 100644 --- a/netwerk/cache2/CacheFileIOManager.cpp +++ b/netwerk/cache2/CacheFileIOManager.cpp @@ -23,6 +23,7 @@ #include "nsIObserverService.h" #include "nsISizeOf.h" #include "mozilla/net/MozURL.h" +#include "mozilla/BackgroundTasksRunner.h" #include "mozilla/Telemetry.h" #include "mozilla/DebugOnly.h" #include "mozilla/Services.h" @@ -35,7 +36,6 @@ #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/Preferences.h" #include "nsNetUtil.h" -#include "prproces.h" // include files for ftruncate (or equivalent) #if defined(XP_UNIX) @@ -4096,14 +4096,6 @@ nsresult CacheFileIOManager::DispatchPurgeTask( rv = XRE_GetBinaryPath(getter_AddRefs(lf)); NS_ENSURE_SUCCESS(rv, rv); - nsAutoCString exePath; -#if !defined(XP_WIN) - rv = lf->GetNativePath(exePath); -#else - rv = lf->GetNativeTarget(exePath); -#endif - NS_ENSURE_SUCCESS(rv, rv); - nsAutoCString path; #if !defined(XP_WIN) rv = profileDir->GetNativePath(path); @@ -4112,17 +4104,8 @@ nsresult CacheFileIOManager::DispatchPurgeTask( #endif NS_ENSURE_SUCCESS(rv, rv); - const char* const argv[] = {exePath.get(), "--backgroundtask", - "removeDirectory", path.get(), - aCacheDirName.get(), aSecondsToWait.get(), - aPurgeExtension.get(), nullptr}; - if (NS_WARN_IF(PR_FAILURE == PR_CreateProcessDetached(exePath.get(), - (char* const*)argv, - nullptr, nullptr))) { - return NS_ERROR_FAILURE; - } - - return NS_OK; + return BackgroundTasksRunner::RemoveDirectoryInDetachedProcess( + path, aCacheDirName, aSecondsToWait, aPurgeExtension); } void CacheFileIOManager::SyncRemoveAllCacheFiles() { diff --git a/netwerk/test/marionette/manifest.ini b/netwerk/test/marionette/manifest.ini new file mode 100644 index 000000000000..e2025ab3c923 --- /dev/null +++ b/netwerk/test/marionette/manifest.ini @@ -0,0 +1 @@ +[test_purge_http_cache_at_shutdown.py] diff --git a/netwerk/test/marionette/test_purge_http_cache_at_shutdown.py b/netwerk/test/marionette/test_purge_http_cache_at_shutdown.py new file mode 100644 index 000000000000..d7ade93f63a8 --- /dev/null +++ b/netwerk/test/marionette/test_purge_http_cache_at_shutdown.py @@ -0,0 +1,74 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from pathlib import Path + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase + + +class PurgeHTTPCacheAtShutdownTestCase(MarionetteTestCase): + def setUp(self): + super().setUp() + self.marionette.enforce_gecko_prefs( + { + "privacy.sanitize.sanitizeOnShutdown": True, + "privacy.clearOnShutdown.cache": True, + "network.cache.shutdown_purge_in_background_task": True, + } + ) + + self.profile_path = Path(self.marionette.profile_path) + self.cache_path = self.profile_path.joinpath("cache2") + + def tearDown(self): + self.marionette.cleanup() + super().tearDown() + + def cacheDirExists(self): + return self.cache_path.exists() + + def renamedDirExists(self): + return any( + child.name.endswith(".purge.bg_rm") for child in self.profile_path.iterdir() + ) + + def test_ensure_cache_purge_after_in_app_quit(self): + self.assertTrue(self.cacheDirExists(), "Cache directory must exist") + + self.marionette.quit() + + Wait(self.marionette, timeout=60).until( + lambda _: not self.cacheDirExists() and not self.renamedDirExists(), + message="Cache directory must be removed after orderly shutdown", + ) + + def test_longstanding_cache_purge_after_in_app_quit(self): + self.assertTrue(self.cacheDirExists(), "Cache directory must exist") + + self.marionette.set_pref( + "toolkit.background_tasks.remove_directory.testing.sleep_ms", 5000 + ) + + self.marionette.quit() + + Wait(self.marionette, timeout=60).until( + lambda _: not self.cacheDirExists() and not self.renamedDirExists(), + message="Cache directory must be removed after orderly shutdown", + ) + + def test_ensure_cache_purge_after_forced_restart(self): + """ + Doing forced restart here to prevent the shutdown phase purging and only allow startup + phase one, via `CacheFileIOManager::OnDelayedStartupFinished`. + """ + self.profile_path.joinpath("foo.purge.bg_rm").mkdir() + + self.marionette.restart(in_app=False) + + Wait(self.marionette, timeout=60).until( + lambda _: not self.renamedDirExists(), + message="Directories with .purge.bg_rm postfix must be removed at startup after" + "disorderly shutdown", + ) diff --git a/netwerk/test/moz.build b/netwerk/test/moz.build index 72c7365c343f..b37a0d358538 100644 --- a/netwerk/test/moz.build +++ b/netwerk/test/moz.build @@ -26,5 +26,9 @@ TESTING_JS_MODULES += [ PERFTESTS_MANIFESTS += ["perf/perftest.ini", "unit/perftest.ini"] +MARIONETTE_UNIT_MANIFESTS += [ + "marionette/manifest.ini", +] + if CONFIG["FUZZING_INTERFACES"]: TEST_DIRS += ["fuzz"] diff --git a/testing/marionette/harness/marionette_harness/tests/unit-tests.ini b/testing/marionette/harness/marionette_harness/tests/unit-tests.ini index 4b2e580f6289..531ad22daefe 100644 --- a/testing/marionette/harness/marionette_harness/tests/unit-tests.ini +++ b/testing/marionette/harness/marionette_harness/tests/unit-tests.ini @@ -17,6 +17,9 @@ # layout tests [include:../../../../../layout/base/tests/marionette/manifest.ini] +# netwerk tests +[include:../../../../../netwerk/test/marionette/manifest.ini] + # toolkit tests [include:../../../../../toolkit/components/cleardata/tests/marionette/manifest.ini] [include:../../../../../toolkit/components/extensions/test/marionette/manifest.ini] diff --git a/toolkit/components/backgroundtasks/BackgroundTask_removeDirectory.sys.mjs b/toolkit/components/backgroundtasks/BackgroundTask_removeDirectory.sys.mjs index bc19005c7a3d..b024e2bdaa6d 100644 --- a/toolkit/components/backgroundtasks/BackgroundTask_removeDirectory.sys.mjs +++ b/toolkit/components/backgroundtasks/BackgroundTask_removeDirectory.sys.mjs @@ -166,7 +166,7 @@ async function cleanupOtherDirectories(parentDirPath, otherFoldersSuffix) { } // Usage: -// removeDirectory parentDirPath childDirName secondsToWait [otherFoldersSuffix] +// removeDirectory parentDirPath childDirName secondsToWait [otherFoldersSuffix] [--test-sleep testSleep] // arg0 arg1 arg2 arg3 // parentDirPath - The path to the parent directory that includes the target directory // childDirName - The "leaf name" of the moved cache directory @@ -175,7 +175,13 @@ async function cleanupOtherDirectories(parentDirPath, otherFoldersSuffix) { // otherFoldersSuffix - [optional] The suffix of directories that should be removed // When not empty, this task will also attempt to remove all directories in // the parent dir that end with this suffix +// testSleep - [optional] A test-only argument to sleep for a given milliseconds before removal. +// This exists to test whether a long-running task can survive. export async function runBackgroundTask(commandLine) { + const testSleep = Number.parseInt( + commandLine.handleFlagWithParam("test-sleep", false) + ); + if (commandLine.length < 3) { throw new Error("Insufficient arguments"); } @@ -186,13 +192,26 @@ export async function runBackgroundTask(commandLine) { if (isNaN(secondsToWait)) { secondsToWait = 10; } + commandLine.removeArguments(0, 2); + let otherFoldersSuffix = ""; - if (commandLine.length >= 4) { - otherFoldersSuffix = commandLine.getArgument(3); + if (commandLine.length) { + otherFoldersSuffix = commandLine.getArgument(0); + commandLine.removeArguments(0, 0); + } + + if (commandLine.length) { + throw new Error( + `${commandLine.length} unknown command args exist, closing.` + ); } console.error(parentDirPath, childDirName, secondsToWait, otherFoldersSuffix); + if (!Number.isNaN(testSleep)) { + await new Promise(resolve => lazy.setTimeout(resolve, testSleep)); + } + await deleteChildDirectory(parentDirPath, childDirName, secondsToWait); await cleanupOtherDirectories(parentDirPath, otherFoldersSuffix); diff --git a/toolkit/components/backgroundtasks/BackgroundTasksRunner.cpp b/toolkit/components/backgroundtasks/BackgroundTasksRunner.cpp new file mode 100644 index 000000000000..202ea4b856e6 --- /dev/null +++ b/toolkit/components/backgroundtasks/BackgroundTasksRunner.cpp @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/BackgroundTasksRunner.h" + +#include "base/process_util.h" +#include "mozilla/StaticPrefs_toolkit.h" +#include "nsIFile.h" + +#ifdef XP_WIN +# include "mozilla/AssembleCmdLine.h" +#endif + +namespace mozilla { + +nsresult BackgroundTasksRunner::RunInDetachedProcess( + const nsACString& aTaskName, const nsTArray& aArgs) { + nsCOMPtr lf; + nsresult rv = XRE_GetBinaryPath(getter_AddRefs(lf)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString exePath; +#if !defined(XP_WIN) + rv = lf->GetNativePath(exePath); +#else + rv = lf->GetNativeTarget(exePath); +#endif + NS_ENSURE_SUCCESS(rv, rv); + + base::LaunchOptions options; +#ifdef XP_WIN + options.start_independent = true; + + nsTArray argv = {exePath.Data(), "--backgroundtask", + aTaskName.Data()}; + for (const nsCString& str : aArgs) { + argv.AppendElement(str.get()); + } + argv.AppendElement(nullptr); + + wchar_t* assembledCmdLine = nullptr; + if (assembleCmdLine(argv.Elements(), &assembledCmdLine, CP_UTF8) == -1) { + return NS_ERROR_FAILURE; + } + + if (!base::LaunchApp(assembledCmdLine, options, nullptr)) { + return NS_ERROR_FAILURE; + } +#else + std::vector argv = {exePath.Data(), "--backgroundtask", + aTaskName.Data()}; + for (const nsCString& str : aArgs) { + argv.push_back(str.get()); + } + + if (!base::LaunchApp(argv, options, nullptr)) { + return NS_ERROR_FAILURE; + } +#endif + + return NS_OK; +} + +nsresult BackgroundTasksRunner::RemoveDirectoryInDetachedProcess( + const nsCString& aParentDirPath, const nsCString& aChildDirName, + const nsCString& aSecondsToWait, const nsCString& aOtherFoldersSuffix) { + nsTArray argv = {aParentDirPath, aChildDirName, aSecondsToWait, + aOtherFoldersSuffix}; + + uint32_t testingSleepMs = + StaticPrefs::toolkit_background_tasks_remove_directory_testing_sleep_ms(); + if (testingSleepMs > 0) { + argv.AppendElement("--test-sleep"); + nsAutoCString sleep; + sleep.AppendInt(testingSleepMs); + argv.AppendElement(sleep); + } + + return RunInDetachedProcess("removeDirectory"_ns, argv); +} + +} // namespace mozilla diff --git a/toolkit/components/backgroundtasks/BackgroundTasksRunner.h b/toolkit/components/backgroundtasks/BackgroundTasksRunner.h new file mode 100644 index 000000000000..b23fb152e7e1 --- /dev/null +++ b/toolkit/components/backgroundtasks/BackgroundTasksRunner.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TOOLKIT_COMPONENTS_BACKGROUNDTASKS_BACKGROUNDTASKSRUNNER_H_ +#define TOOLKIT_COMPONENTS_BACKGROUNDTASKS_BACKGROUNDTASKSRUNNER_H_ + +#include "nsString.h" + +namespace mozilla { + +class BackgroundTasksRunner final { + public: + /** + * Runs a background process in an independent detached process. Any process + * opened by this function can outlive the main process. + * + * This function is thread-safe. + * + * @param aTaskName The name of the background task. + * (BackgroundTask_{name}.sys.mjs) + * @param aArgs The arguments that will be passed to the task process. Any + * needed escape will happen automatically. + */ + static nsresult RunInDetachedProcess(const nsACString& aTaskName, + const nsTArray& aArgs); + + /** + * Runs removeDirectory background task. + * `toolkit.background_tasks.remove_directory.testing.sleep_ms` can be set to + * make it wait for the given milliseconds for testing purpose. + * + * See BackgroundTask_removeDirectory.sys.mjs for details about the arguments. + */ + static nsresult RemoveDirectoryInDetachedProcess( + const nsCString& aParentDirPath, const nsCString& aChildDirName, + const nsCString& aSecondsToWait, const nsCString& aOtherFoldersSuffix); +}; + +} // namespace mozilla + +#endif // TOOLKIT_COMPONENTS_BACKGROUNDTASKS_BACKGROUNDTASKSRUNNER_H_ diff --git a/toolkit/components/backgroundtasks/docs/index.md b/toolkit/components/backgroundtasks/docs/index.md index 6cc59690deeb..706b52c4d719 100644 --- a/toolkit/components/backgroundtasks/docs/index.md +++ b/toolkit/components/backgroundtasks/docs/index.md @@ -59,6 +59,18 @@ For more details, see [`XPCSHELL_TESTING_MODULES_URI`](https://searchfox.org/moz Background task mode supports using the JavaScript debugger and the Firefox Devtools and Browser Toolbox. When invoked with the command line parameters `--jsdebugger` (and optionally `--wait-for-jsdebugger`), the background task framework will launch a Browser Toolbox, connect to the background task, and pause execution at the first line of the task implementation. The Browser Toolbox is launched with a temporary profile (sibling to the ephemeral temporary profile the background task itself creates.) The Browser Toolbox profile's preferences are copied from the default browsing profile, allowing to configure devtools preferences. (The `--start-debugger-server` command line option is also recognized; see the output of `firefox --backgroundtask success --attach-console --help` for details.) +## Invoking background tasks + +Use `BackgroundTasksRunner::RunInDetachedProcess` is a helper to open a new background process within Gecko. It automatically manages the configuration 1) to let the new process outlive the launching process and 2) to escape the arguments properly. The function is safe to be called in a non-main process. + +## Existing background tasks + +* `BackgroundTask_removeDirectory` + + Removes the child directory with the given name and/or child directories with the given postfix, all in the given parent directory. It's recommended to run it via the corresponding helper function `BackgroundTasksRunner::RemoveDirectoryInDetachedProcess`. + + Tests can use `toolkit.background_tasks.remove_directory.testing.sleep_ms` to see whether a longstanding task can finish the work even after the launching process is closed. + ## The background task mode runtime environment ### Most background tasks run in ephemeral temporary profiles diff --git a/toolkit/components/backgroundtasks/moz.build b/toolkit/components/backgroundtasks/moz.build index 4f54da8ec333..d4d41173fcc8 100644 --- a/toolkit/components/backgroundtasks/moz.build +++ b/toolkit/components/backgroundtasks/moz.build @@ -17,10 +17,12 @@ for var in ("MOZ_APP_VENDOR",): UNIFIED_SOURCES += [ "BackgroundTasks.cpp", + "BackgroundTasksRunner.cpp", ] EXPORTS.mozilla += [ "BackgroundTasks.h", + "BackgroundTasksRunner.h", ] XPCOM_MANIFESTS += [ @@ -92,3 +94,6 @@ if CONFIG["MOZ_BUILD_APP"] == "browser": FINAL_TARGET_FILES.defaults.backgroundtasks += [ "defaults/backgroundtasks.js", ] + +# For base::LaunchApp +include("/ipc/chromium/chromium-config.mozbuild")