diff options
Diffstat (limited to 'toolkit/components/backgroundtasks/BackgroundTasks.cpp')
-rw-r--r-- | toolkit/components/backgroundtasks/BackgroundTasks.cpp | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/toolkit/components/backgroundtasks/BackgroundTasks.cpp b/toolkit/components/backgroundtasks/BackgroundTasks.cpp new file mode 100644 index 0000000000..302bab2e35 --- /dev/null +++ b/toolkit/components/backgroundtasks/BackgroundTasks.cpp @@ -0,0 +1,499 @@ +/* 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/BackgroundTasks.h" + +#include "nsIBackgroundTasksManager.h" +#include "nsICommandLine.h" +#include "nsIFile.h" +#include "nsImportModule.h" +#include "nsPrintfCString.h" +#include "nsProfileLock.h" +#include "nsTSubstring.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "prenv.h" +#include "prtime.h" + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/LateWriteChecks.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(BackgroundTasks, nsIBackgroundTasks); + +BackgroundTasks::BackgroundTasks(Maybe<nsCString> aBackgroundTask) + : mBackgroundTask(std::move(aBackgroundTask)), mIsEphemeralProfile(false) { + // Log when a background task is created. + if (mBackgroundTask.isSome()) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Created background task: %s", mBackgroundTask->get())); + } +} + +void BackgroundTasks::Init(Maybe<nsCString> aBackgroundTask) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + + MOZ_RELEASE_ASSERT(!sSingleton, + "BackgroundTasks singleton already initialized"); + // The singleton will be cleaned up by `Shutdown()`. + sSingleton = new BackgroundTasks(std::move(aBackgroundTask)); +} + +void BackgroundTasks::Shutdown() { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + + MOZ_LOG(sBackgroundTasksLog, LogLevel::Info, ("Shutdown")); + + if (!sSingleton) { + return; + } + + if (sSingleton->mProfD && + !EnvHasValue("MOZ_BACKGROUNDTASKS_NO_REMOVE_PROFILE")) { + AutoSuspendLateWriteChecks suspend; + + if (sSingleton->mIsEphemeralProfile) { + // Log that the ephemeral profile is being removed. + if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Info)) { + nsAutoString path; + if (NS_SUCCEEDED(sSingleton->mProfD->GetPath(path))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Removing profile: %s", + NS_LossyConvertUTF16toASCII(path).get())); + } + } + + Unused << sSingleton->mProfD->Remove(/* aRecursive */ true); + } else { + // Log that the non-ephemeral profile is not being removed. + if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Debug)) { + nsAutoString path; + if (NS_SUCCEEDED(sSingleton->mProfD->GetPath(path))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Debug, + ("Not removing non-ephemeral profile: %s", + NS_LossyConvertUTF16toASCII(path).get())); + } + } + } + } + + sSingleton = nullptr; +} + +BackgroundTasks* BackgroundTasks::GetSingleton() { + if (!sSingleton) { + // xpcshell doesn't set up background tasks: default to no background + // task. + Init(Nothing()); + } + + MOZ_RELEASE_ASSERT(sSingleton, + "BackgroundTasks singleton should have been initialized"); + + return sSingleton.get(); +} + +already_AddRefed<BackgroundTasks> BackgroundTasks::GetSingletonAddRefed() { + return RefPtr<BackgroundTasks>(GetSingleton()).forget(); +} + +Maybe<nsCString> BackgroundTasks::GetBackgroundTasks() { + if (!XRE_IsParentProcess()) { + return Nothing(); + } + + return GetSingleton()->mBackgroundTask; +} + +bool BackgroundTasks::IsBackgroundTaskMode() { + if (!XRE_IsParentProcess()) { + return false; + } + + return GetBackgroundTasks().isSome(); +} + +nsresult BackgroundTasks::CreateEphemeralProfileDirectory( + nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + Maybe<nsCString> task = GetBackgroundTasks(); + sSingleton->mIsEphemeralProfile = + task.isSome() && IsEphemeralProfileTaskName(task.ref()); + + MOZ_RELEASE_ASSERT(sSingleton->mIsEphemeralProfile); + + nsresult rv = sSingleton->CreateEphemeralProfileDirectoryImpl( + aRootDir, aProfilePrefix, aFile); + + // Log whether the ephemeral profile was created. + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning, + ("Failed to create ephemeral profile directory!")); + } else { + if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Info)) { + nsAutoString path; + if (aFile && *aFile && NS_SUCCEEDED((*aFile)->GetPath(path))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Created ephemeral profile directory: %s", + NS_LossyConvertUTF16toASCII(path).get())); + } + } + } + + return rv; +} + +nsresult BackgroundTasks::CreateNonEphemeralProfileDirectory( + nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + Maybe<nsCString> task = GetBackgroundTasks(); + sSingleton->mIsEphemeralProfile = + task.isSome() && IsEphemeralProfileTaskName(task.ref()); + + MOZ_RELEASE_ASSERT(!sSingleton->mIsEphemeralProfile); + + nsresult rv = sSingleton->CreateNonEphemeralProfileDirectoryImpl( + aRootDir, aProfilePrefix, aFile); + + // Log whether the non-ephemeral profile was created. + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning, + ("Failed to create non-ephemeral profile directory!")); + } else { + if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Info)) { + nsAutoString path; + if (aFile && *aFile && NS_SUCCEEDED((*aFile)->GetPath(path))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Non-ephemeral profile directory existed or was created: %s", + NS_LossyConvertUTF16toASCII(path).get())); + } + } + } + + return rv; +} + +bool BackgroundTasks::IsEphemeralProfile() { + return sSingleton && sSingleton->mIsEphemeralProfile && sSingleton->mProfD; +} + +class BackgroundTaskLaunchRunnable : public Runnable { + public: + explicit BackgroundTaskLaunchRunnable(nsIBackgroundTasksManager* aManager, + const char* aTaskName, + nsICommandLine* aCmdLine) + : Runnable("BackgroundTaskLaunchRunnable"), + mManager(aManager), + mTaskName(aTaskName), + mCmdLine(aCmdLine) {} + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + return mManager->RunBackgroundTaskNamed(mTaskName, mCmdLine); + } + + private: + nsCOMPtr<nsIBackgroundTasksManager> mManager; + NS_ConvertASCIItoUTF16 mTaskName; + nsCOMPtr<nsICommandLine> mCmdLine; +}; + +nsresult BackgroundTasks::RunBackgroundTask(nsICommandLine* aCmdLine) { + Maybe<nsCString> task = GetBackgroundTasks(); + if (task.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIBackgroundTasksManager> manager = + do_GetService("@mozilla.org/backgroundtasksmanager;1"); + + MOZ_RELEASE_ASSERT(manager, "Could not get background tasks manager service"); + + // Give the initial storm of startup runnables a chance to run before our + // payload is going to potentially block the main thread for a while. + auto r = MakeRefPtr<BackgroundTaskLaunchRunnable>(manager, task.ref().get(), + aCmdLine); + return GetCurrentSerialEventTarget()->DelayedDispatch(r.forget(), 100); +} + +bool BackgroundTasks::IsUpdatingTaskName(const nsCString& aName) { + return aName.EqualsLiteral("backgroundupdate") || + aName.EqualsLiteral("shouldprocessupdates"); +} + +bool BackgroundTasks::IsEphemeralProfileTaskName(const nsCString& aName) { + return !(aName.EqualsLiteral("backgroundupdate") || + aName.EqualsLiteral("defaultagent") || + aName.EqualsLiteral("message") || // Just for development. + aName.EqualsLiteral("not_ephemeral_profile")); // Just for testing. +} + +bool BackgroundTasks::IsNoOutputTaskName(const nsCString& aName) { + return aName.EqualsLiteral("pingsender") || + aName.EqualsLiteral("removeDirectory") || + aName.EqualsLiteral("no_output"); // Just for testing. +} + +nsCString BackgroundTasks::GetProfilePrefix(const nsCString& aInstallHash) { + return nsPrintfCString("%sBackgroundTask-%s-%s", MOZ_APP_VENDOR, + aInstallHash.get(), GetBackgroundTasks().ref().get()); +} + +nsresult BackgroundTasks::CreateNonEphemeralProfileDirectoryImpl( + nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile) { + if (mBackgroundTask.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv; + + nsCOMPtr<nsIFile> file; + if (mProfD) { + rv = mProfD->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + } else { + file = aRootDir; + + // The base path is + // /{UAppData}/Background Tasks + // Profiles/[salt].[vendor]BackgroundTask-[pathHash]-[taskName]. + rv = file->AppendNative(aProfilePrefix); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the persistent profile directory if it does not exist. + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = file->Clone(getter_AddRefs(mProfD)); + NS_ENSURE_SUCCESS(rv, rv); + } + + file.forget(aFile); + return NS_OK; +} + +nsresult BackgroundTasks::CreateEphemeralProfileDirectoryImpl( + nsIFile* aRootDir, const nsCString& aProfilePrefix, nsIFile** aFile) { + if (mBackgroundTask.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv; + + nsCOMPtr<nsIFile> file; + if (mProfD) { + rv = mProfD->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + } else { + file = aRootDir; + + // Windows file cleanup is unreliable, so let's take a moment to clean up + // any prior background task profiles. We can continue if there was an error + // as creating a new ephemeral profile does not require cleaning up the old. + rv = RemoveStaleEphemeralProfileDirectories(file, aProfilePrefix); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning, + ("Error cleaning up stale ephemeral profile directories.")); + } + + // The base path is /tmp/[vendor]BackgroundTask-[pathHash]-[taskName]. + rv = file->AppendNative(aProfilePrefix); + NS_ENSURE_SUCCESS(rv, rv); + + // Create a unique profile directory. This can fail if there are too many + // (thousands) of existing directories, which is unlikely to happen. + rv = file->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->Clone(getter_AddRefs(mProfD)); + NS_ENSURE_SUCCESS(rv, rv); + } + + file.forget(aFile); + return NS_OK; +} + +nsresult BackgroundTasks::RemoveStaleEphemeralProfileDirectories( + nsIFile* const aRoot, const nsCString& aPrefix) { + nsresult rv; + + if (MOZ_LOG_TEST(sBackgroundTasksLog, LogLevel::Info)) { + nsAutoString path; + if (NS_SUCCEEDED(aRoot->GetPath(path))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Checking \"%s\" for stale profiles matching \"%s\".", + NS_LossyConvertUTF16toASCII(path.get()).get(), aPrefix.get())); + } + } + + // Check how old a background task should be before being deleted. + PRTime timeoutMillis = 60 * 1000; // Default to 1 minute. + bool deleteAll = false; + // MOZ_BACKGROUNDTASKS_PURGE_STALE_PROFILES = ["0"+ in ms, "always", "never"] + nsAutoCString envTimeoutStr( + PR_GetEnv("MOZ_BACKGROUNDTASKS_PURGE_STALE_PROFILES")); + + if (!envTimeoutStr.IsEmpty()) { + int64_t envTimeoutMillis = envTimeoutStr.ToInteger64(&rv); + if (NS_SUCCEEDED(rv)) { + if (envTimeoutMillis >= 0) { + timeoutMillis = envTimeoutMillis; + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Setting stale profile age to %sms", envTimeoutStr.get())); + } else { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning, + ("MOZ_BACKGROUNDTASKS_PURGE_STALE_PROFILES is set less than 0, " + "using default timeout instead.")); + } + } else { + if (envTimeoutStr.Equals("always")) { + deleteAll = true; + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Deleting profiles regardless of age.")); + } else if (envTimeoutStr.Equals("never")) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Skipping cleanup of stale background task profiles.")); + return NS_OK; + } else { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning, + ("MOZ_BACKGROUNDTASKS_PURGE_STALE_PROFILES is set to invalid " + "value, using default timeout instead.")); + } + } + } + + nsCOMPtr<nsIDirectoryEnumerator> entries; + rv = aRoot->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> entry; + int removedProfiles = 0; + // Limit the number of stale ephemeral profiles we clean up so that we don't + // timeout the background task. + const int kMaxRemovedProfiles = 5; + + // Loop over the ephemeral directory entries, deleting folders matching our + // profile prefix. Continue if there is an error interacting with the entry to + // more reliably make progress on cleaning up stale ephemeral profiles. + while (removedProfiles < kMaxRemovedProfiles && + NS_SUCCEEDED(rv = entries->GetNextFile(getter_AddRefs(entry))) && + entry) { + nsCString entryName; + rv = entry->GetNativeLeafName(entryName); + if (NS_FAILED(rv)) { + continue; + } + + // Find profile folders matching our prefix. + if (!StringBeginsWith(entryName, aPrefix)) { + continue; + } + + if (!deleteAll) { + // Skip profiles that were recently created to prevent deleting a profile + // after creating the directory but before creating the lockfile. + PRTime profileModifyTime; + if (NS_FAILED(entry->GetLastModifiedTime(&profileModifyTime))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning, + ("Skipping deletion of %s, unable to retrieve when profile was " + "last modified.", + entryName.get())); + continue; + } + PRTime now = PR_Now() / PR_USEC_PER_MSEC; + // Timeout only needs to be large enough to prevent deleting a ephemeral + // profile between it being created and locked. + if (now - profileModifyTime < timeoutMillis) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Debug, + ("Skipping deletion of %s, profile is not yet stale.", + entryName.get())); + continue; + } + } + + // Check if the profile is locked. If successful drop the lock so we can + // delete the folder. Background tasks' ephemeral profiles are not reused or + // remembered once released, so we don't need to hold this lock while + // deleting it. + nsProfileLock lock; + if (NS_FAILED(lock.Lock(entry, nullptr)) || NS_FAILED(lock.Unlock())) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning, + ("Skipping deletion of %s, unable to lock/unlock profile.", + entryName.get())); + continue; + } + + rv = entry->Remove(true); + if (NS_FAILED(rv)) { + if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Warning)) { + nsAutoString path; + if (NS_SUCCEEDED(entry->GetPath(path))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Warning, + ("Error removing stale ephemeral profile directory: %s", + NS_LossyConvertUTF16toASCII(path).get())); + } + } + + continue; + } + + if (MOZ_LOG_TEST(sBackgroundTasksLog, mozilla::LogLevel::Info)) { + nsAutoString path; + if (NS_SUCCEEDED(entry->GetPath(path))) { + MOZ_LOG(sBackgroundTasksLog, mozilla::LogLevel::Info, + ("Removed stale ephemeral profile directory: %s", + NS_LossyConvertUTF16toASCII(path).get())); + } + } + + removedProfiles++; + } + + return NS_OK; +} + +nsresult BackgroundTasks::GetIsBackgroundTaskMode(bool* result) { + *result = mBackgroundTask.isSome(); + return NS_OK; +} + +nsresult BackgroundTasks::BackgroundTaskName(nsAString& name) { + name.SetIsVoid(true); + if (mBackgroundTask.isSome()) { + name.AssignASCII(mBackgroundTask.ref()); + } + return NS_OK; +} + +nsresult BackgroundTasks::OverrideBackgroundTaskNameForTesting( + const nsAString& name) { + if (name.IsVoid()) { + mBackgroundTask = Nothing(); + } else { + mBackgroundTask = Some(NS_LossyConvertUTF16toASCII(name)); + } + return NS_OK; +} + +StaticRefPtr<BackgroundTasks> BackgroundTasks::sSingleton; + +LazyLogModule BackgroundTasks::sBackgroundTasksLog("BackgroundTasks"); + +} // namespace mozilla |