Bug 1791675 - Part 4: Make sure purgeHTTPCache runs after shutdown r=webdriver-reviewers,necko-reviewers,valentin,nalexander,whimboo

This adds BackgroundTasksRunner utility class as a generic way to properly run background tasks. A few argument for not extending existing BackgroundTasksUtils:

1. Simply because the existing use case is in C++.
2. I have another use case from JSM and thus I'll ultimately convert this an XPCOM component. And `CacheFileIOManager::DispatchPurgeTask` cannot get a JSM-written XPCOM instance which is required to be main-thread only.

Depends on D157998

Differential Revision: https://phabricator.services.mozilla.com/D157757
This commit is contained in:
Kagami Sascha Rosylight 2022-11-08 15:42:24 +00:00
parent a3364bdccc
commit d20dd96f8f
11 changed files with 256 additions and 23 deletions

View file

@ -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

View file

@ -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() {

View file

@ -0,0 +1 @@
[test_purge_http_cache_at_shutdown.py]

View file

@ -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",
)

View file

@ -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"]

View file

@ -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]

View file

@ -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);

View file

@ -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<nsCString>& aArgs) {
nsCOMPtr<nsIFile> 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<const char*> 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<std::string> 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<nsCString> 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

View file

@ -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<nsCString>& 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_

View file

@ -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

View file

@ -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")