/* 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 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 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::GetSingletonAddRefed() { return RefPtr(GetSingleton()).forget(); } Maybe 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 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 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 mManager; NS_ConvertASCIItoUTF16 mTaskName; nsCOMPtr mCmdLine; }; nsresult BackgroundTasks::RunBackgroundTask(nsICommandLine* aCmdLine) { Maybe task = GetBackgroundTasks(); if (task.isNothing()) { return NS_ERROR_NOT_AVAILABLE; } nsCOMPtr 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(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("message") || // Just for development. aName.EqualsLiteral("not_ephemeral_profile")); } 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 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 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 entries; rv = aRoot->GetDirectoryEntries(getter_AddRefs(entries)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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::sSingleton; LazyLogModule BackgroundTasks::sBackgroundTasksLog("BackgroundTasks"); } // namespace mozilla