diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/mozapps/defaultagent/ScheduledTask.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | toolkit/mozapps/defaultagent/ScheduledTask.cpp | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/toolkit/mozapps/defaultagent/ScheduledTask.cpp b/toolkit/mozapps/defaultagent/ScheduledTask.cpp new file mode 100644 index 0000000000..9c77dc15ed --- /dev/null +++ b/toolkit/mozapps/defaultagent/ScheduledTask.cpp @@ -0,0 +1,409 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "ScheduledTask.h" + +#include <string> +#include <time.h> + +#include <comutil.h> +#include <taskschd.h> + +#include "readstrings.h" +#include "updatererrors.h" +#include "EventLog.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "WindowsDefaultBrowser.h" + +#include "DefaultBrowser.h" + +const wchar_t* kTaskVendor = L"" MOZ_APP_VENDOR; +// kTaskName should have the unique token appended before being used. +const wchar_t* kTaskName = L"" MOZ_APP_DISPLAYNAME " Default Browser Agent "; + +// The task scheduler requires its time values to come in the form of a string +// in the format YYYY-MM-DDTHH:MM:SSZ. This format string is used to get that +// out of the C library wcsftime function. +const wchar_t* kTimeFormat = L"%Y-%m-%dT%H:%M:%SZ"; +// The expanded time string should always be this length, for example: +// 2020-02-12T16:59:32Z +const size_t kTimeStrMaxLen = 20; + +#define ENSURE(x) \ + if (FAILED(hr = (x))) { \ + LOG_ERROR(hr); \ + return hr; \ + } + +struct SysFreeStringDeleter { + void operator()(BSTR aPtr) { ::SysFreeString(aPtr); } +}; +using BStrPtr = mozilla::UniquePtr<OLECHAR, SysFreeStringDeleter>; + +bool GetTaskDescription(mozilla::UniquePtr<wchar_t[]>& description) { + mozilla::UniquePtr<wchar_t[]> installPath; + bool success = GetInstallDirectory(installPath); + if (!success) { + LOG_ERROR_MESSAGE(L"Failed to get install directory"); + return false; + } + const wchar_t* iniFormat = L"%s\\defaultagent_localized.ini"; + int bufferSize = _scwprintf(iniFormat, installPath.get()); + ++bufferSize; // Extra character for terminating null + mozilla::UniquePtr<wchar_t[]> iniPath = + mozilla::MakeUnique<wchar_t[]>(bufferSize); + _snwprintf_s(iniPath.get(), bufferSize, _TRUNCATE, iniFormat, + installPath.get()); + + IniReader reader(iniPath.get()); + reader.AddKey("DefaultBrowserAgentTaskDescription", &description); + int status = reader.Read(); + if (status != OK) { + LOG_ERROR_MESSAGE(L"Failed to read task description: %d", status); + return false; + } + return true; +} + +HRESULT RegisterTask(const wchar_t* uniqueToken, + BSTR startTime /* = nullptr */) { + // Do data migration during the task installation. This might seem like it + // belongs in UpdateTask, but we want to be able to call + // RemoveTasks(); + // RegisterTask(); + // and still have data migration happen. Also, UpdateTask calls this function, + // so migration will still get run in that case. + MaybeMigrateCurrentDefault(); + + // Make sure we don't try to register a task that already exists. + RemoveTasks(uniqueToken, WhichTasks::WdbaTaskOnly); + + // If we create a folder and then fail to create the task, we need to + // remember to delete the folder so that whatever set of permissions it ends + // up with doesn't interfere with trying to create the task again later, and + // so that we don't just leave an empty folder behind. + bool createdFolder = false; + + HRESULT hr = S_OK; + RefPtr<ITaskService> scheduler; + ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER, + IID_ITaskService, getter_AddRefs(scheduler))); + + ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{})); + + RefPtr<ITaskFolder> rootFolder; + BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\")); + ENSURE( + scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder))); + + RefPtr<ITaskFolder> taskFolder; + BStrPtr vendorBStr = BStrPtr(SysAllocString(kTaskVendor)); + if (FAILED(rootFolder->GetFolder(vendorBStr.get(), + getter_AddRefs(taskFolder)))) { + hr = rootFolder->CreateFolder(vendorBStr.get(), VARIANT{}, + getter_AddRefs(taskFolder)); + if (SUCCEEDED(hr)) { + createdFolder = true; + } else if (hr != HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) { + // The folder already existing isn't an error, but anything else is. + LOG_ERROR(hr); + return hr; + } + } + + auto cleanupFolder = + mozilla::MakeScopeExit([hr, createdFolder, &rootFolder, &vendorBStr] { + if (createdFolder && FAILED(hr)) { + // If this fails, we can't really handle that intelligently, so + // don't even bother to check the return code. + rootFolder->DeleteFolder(vendorBStr.get(), 0); + } + }); + + RefPtr<ITaskDefinition> newTask; + ENSURE(scheduler->NewTask(0, getter_AddRefs(newTask))); + + mozilla::UniquePtr<wchar_t[]> description; + if (!GetTaskDescription(description)) { + return E_FAIL; + } + BStrPtr descriptionBstr = BStrPtr(SysAllocString(description.get())); + + RefPtr<IRegistrationInfo> taskRegistration; + ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(taskRegistration))); + ENSURE(taskRegistration->put_Description(descriptionBstr.get())); + + RefPtr<ITaskSettings> taskSettings; + ENSURE(newTask->get_Settings(getter_AddRefs(taskSettings))); + ENSURE(taskSettings->put_DisallowStartIfOnBatteries(VARIANT_FALSE)); + ENSURE(taskSettings->put_MultipleInstances(TASK_INSTANCES_IGNORE_NEW)); + ENSURE(taskSettings->put_StartWhenAvailable(VARIANT_TRUE)); + ENSURE(taskSettings->put_StopIfGoingOnBatteries(VARIANT_FALSE)); + // This cryptic string means "12 hours 5 minutes". So, if the task runs for + // longer than that, the process will be killed, because that should never + // happen. See + // https://docs.microsoft.com/en-us/windows/win32/taskschd/tasksettings-executiontimelimit + // for a detailed explanation of these strings. + BStrPtr execTimeLimitBStr = BStrPtr(SysAllocString(L"PT12H5M")); + ENSURE(taskSettings->put_ExecutionTimeLimit(execTimeLimitBStr.get())); + + RefPtr<IRegistrationInfo> regInfo; + ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(regInfo))); + + ENSURE(regInfo->put_Author(vendorBStr.get())); + + RefPtr<ITriggerCollection> triggers; + ENSURE(newTask->get_Triggers(getter_AddRefs(triggers))); + + RefPtr<ITrigger> newTrigger; + ENSURE(triggers->Create(TASK_TRIGGER_DAILY, getter_AddRefs(newTrigger))); + + RefPtr<IDailyTrigger> dailyTrigger; + ENSURE(newTrigger->QueryInterface(IID_IDailyTrigger, + getter_AddRefs(dailyTrigger))); + + if (startTime) { + ENSURE(dailyTrigger->put_StartBoundary(startTime)); + } else { + // The time that the task is scheduled to run at every day is taken from the + // time in the trigger's StartBoundary property. We'll set this to the + // current time, on the theory that the time at which we're being installed + // is a time that the computer is likely to be on other days. If our + // theory is wrong and the computer is offline at the scheduled time, then + // because we've set StartWhenAvailable above, the task will run whenever + // it wakes up. Since our task is entirely in the background and doesn't use + // a lot of resources, we're not concerned about it bothering the user if it + // runs while they're actively using this computer. + time_t now_t = time(nullptr); + // Subtract a minute from the current time, to avoid "winning" a potential + // race with the scheduler that might have it start the task immediately + // after we register it, if we finish doing that and then it evaluates the + // trigger during the same second. We haven't seen this happen in practice, + // but there's no documented guarantee that it won't, so let's be sure. + now_t -= 60; + + tm now_tm; + errno_t errno_rv = gmtime_s(&now_tm, &now_t); + if (errno_rv != 0) { + // The C runtime has a (private) function to convert Win32 error codes to + // errno values, but there's nothing that goes the other way, and it + // isn't worth including one here for something that's this unlikely to + // fail anyway. So just return a generic error. + hr = HRESULT_FROM_WIN32(ERROR_INVALID_TIME); + LOG_ERROR(hr); + return hr; + } + + mozilla::UniquePtr<wchar_t[]> timeStr = + mozilla::MakeUnique<wchar_t[]>(kTimeStrMaxLen + 1); + + if (wcsftime(timeStr.get(), kTimeStrMaxLen + 1, kTimeFormat, &now_tm) == + 0) { + hr = E_NOT_SUFFICIENT_BUFFER; + LOG_ERROR(hr); + return hr; + } + + BStrPtr startTimeBStr = BStrPtr(SysAllocString(timeStr.get())); + ENSURE(dailyTrigger->put_StartBoundary(startTimeBStr.get())); + } + + ENSURE(dailyTrigger->put_DaysInterval(1)); + + RefPtr<IActionCollection> actions; + ENSURE(newTask->get_Actions(getter_AddRefs(actions))); + + RefPtr<IAction> action; + ENSURE(actions->Create(TASK_ACTION_EXEC, getter_AddRefs(action))); + + RefPtr<IExecAction> execAction; + ENSURE(action->QueryInterface(IID_IExecAction, getter_AddRefs(execAction))); + + BStrPtr binaryPathBStr = + BStrPtr(SysAllocString(mozilla::GetFullBinaryPath().get())); + ENSURE(execAction->put_Path(binaryPathBStr.get())); + + std::wstring taskArgs = L"do-task \""; + taskArgs += uniqueToken; + taskArgs += L"\""; + BStrPtr argsBStr = BStrPtr(SysAllocString(taskArgs.c_str())); + ENSURE(execAction->put_Arguments(argsBStr.get())); + + std::wstring taskName(kTaskName); + taskName += uniqueToken; + BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str())); + + RefPtr<IRegisteredTask> registeredTask; + ENSURE(taskFolder->RegisterTaskDefinition( + taskNameBStr.get(), newTask, TASK_CREATE_OR_UPDATE, VARIANT{}, VARIANT{}, + TASK_LOGON_INTERACTIVE_TOKEN, VARIANT{}, getter_AddRefs(registeredTask))); + + return hr; +} + +HRESULT UpdateTask(const wchar_t* uniqueToken) { + RefPtr<ITaskService> scheduler; + HRESULT hr = S_OK; + ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER, + IID_ITaskService, getter_AddRefs(scheduler))); + + ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{})); + + RefPtr<ITaskFolder> taskFolder; + BStrPtr folderBStr = BStrPtr(SysAllocString(kTaskVendor)); + + if (FAILED( + scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)))) { + // If our folder doesn't exist, create it and the task. + return RegisterTask(uniqueToken); + } + + std::wstring taskName(kTaskName); + taskName += uniqueToken; + BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str())); + + RefPtr<IRegisteredTask> task; + if (FAILED(taskFolder->GetTask(taskNameBStr.get(), getter_AddRefs(task)))) { + // If our task doesn't exist at all, just create one. + return RegisterTask(uniqueToken); + } + + // If we have a task registered already, we need to recreate it because + // something might have changed that we need to update. But we don't + // want to restart the schedule from now, because that might mean the + // task never runs at all for e.g. Nightly. So create a new task, but + // first get and preserve the existing trigger. + RefPtr<ITaskDefinition> definition; + if (FAILED(task->get_Definition(getter_AddRefs(definition)))) { + // This task is broken, make a new one. + return RegisterTask(uniqueToken); + } + + RefPtr<ITriggerCollection> triggerList; + if (FAILED(definition->get_Triggers(getter_AddRefs(triggerList)))) { + // This task is broken, make a new one. + return RegisterTask(uniqueToken); + } + + RefPtr<ITrigger> trigger; + if (FAILED(triggerList->get_Item(1, getter_AddRefs(trigger)))) { + // This task is broken, make a new one. + return RegisterTask(uniqueToken); + } + + BSTR startTimeBstr; + if (FAILED(trigger->get_StartBoundary(&startTimeBstr))) { + // This task is broken, make a new one. + return RegisterTask(uniqueToken); + } + BStrPtr startTime(startTimeBstr); + + return RegisterTask(uniqueToken, startTime.get()); +} + +bool EndsWith(const wchar_t* string, const wchar_t* suffix) { + size_t string_len = wcslen(string); + size_t suffix_len = wcslen(suffix); + if (suffix_len > string_len) { + return false; + } + const wchar_t* substring = string + string_len - suffix_len; + return wcscmp(substring, suffix) == 0; +} + +HRESULT RemoveTasks(const wchar_t* uniqueToken, WhichTasks tasksToRemove) { + if (!uniqueToken || wcslen(uniqueToken) == 0) { + return E_INVALIDARG; + } + + RefPtr<ITaskService> scheduler; + HRESULT hr = S_OK; + ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER, + IID_ITaskService, getter_AddRefs(scheduler))); + + ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{})); + + RefPtr<ITaskFolder> taskFolder; + BStrPtr folderBStr(SysAllocString(kTaskVendor)); + + hr = scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)); + if (FAILED(hr)) { + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { + // Don't return an error code if our folder doesn't exist, + // because that just means it's been removed already. + return S_OK; + } else { + return hr; + } + } + + RefPtr<IRegisteredTaskCollection> tasksInFolder; + ENSURE(taskFolder->GetTasks(TASK_ENUM_HIDDEN, getter_AddRefs(tasksInFolder))); + + LONG numTasks = 0; + ENSURE(tasksInFolder->get_Count(&numTasks)); + + std::wstring WdbaTaskName(kTaskName); + WdbaTaskName += uniqueToken; + + // This will be set to the last error that we encounter while deleting tasks. + // This allows us to keep attempting to remove the remaining tasks, even if + // we encounter an error, while still preserving what error we encountered so + // we can return it from this function. + HRESULT deleteResult = S_OK; + // Set to true if we intentionally skip any tasks. + bool tasksSkipped = false; + + for (LONG i = 0; i < numTasks; ++i) { + RefPtr<IRegisteredTask> task; + // IRegisteredTaskCollection's are 1-indexed. + hr = tasksInFolder->get_Item(_variant_t(i + 1), getter_AddRefs(task)); + if (FAILED(hr)) { + deleteResult = hr; + continue; + } + + BSTR taskName; + hr = task->get_Name(&taskName); + if (FAILED(hr)) { + deleteResult = hr; + continue; + } + // Automatically free taskName when we are done with it. + BStrPtr uniqueTaskName(taskName); + + if (tasksToRemove == WhichTasks::WdbaTaskOnly) { + if (WdbaTaskName.compare(taskName) != 0) { + tasksSkipped = true; + continue; + } + } else { // tasksToRemove == WhichTasks::AllTasksForInstallation + if (!EndsWith(taskName, uniqueToken)) { + tasksSkipped = true; + continue; + } + } + + hr = taskFolder->DeleteTask(taskName, 0 /* flags */); + if (FAILED(hr)) { + deleteResult = hr; + } + } + + // If we successfully removed all the tasks, delete the folder too. + if (!tasksSkipped && SUCCEEDED(deleteResult)) { + RefPtr<ITaskFolder> rootFolder; + BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\")); + ENSURE( + scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder))); + ENSURE(rootFolder->DeleteFolder(folderBStr.get(), 0)); + } + + return deleteResult; +} |