Bug 1867190 - Add prefs for PHC probablities r=glandium
[gecko.git] / toolkit / mozapps / defaultagent / ScheduledTask.cpp
bloba9cd647c0367a09050e1e6005906ab76a1a3aa80
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "ScheduledTask.h"
8 #include "ScheduledTaskRemove.h"
10 #include <string>
11 #include <time.h>
13 #include <comutil.h>
14 #include <taskschd.h>
16 #include "readstrings.h"
17 #include "updatererrors.h"
18 #include "EventLog.h"
19 #include "mozilla/RefPtr.h"
20 #include "mozilla/ScopeExit.h"
21 #include "mozilla/UniquePtr.h"
22 #include "mozilla/WinHeaderOnlyUtils.h"
23 #include "WindowsDefaultBrowser.h"
25 #include "DefaultBrowser.h"
27 #include "mozilla/ErrorResult.h"
28 #include "mozilla/intl/Localization.h"
29 #include "nsString.h"
30 #include "nsTArray.h"
31 using mozilla::intl::Localization;
33 namespace mozilla::default_agent {
35 // The task scheduler requires its time values to come in the form of a string
36 // in the format YYYY-MM-DDTHH:MM:SSZ. This format string is used to get that
37 // out of the C library wcsftime function.
38 const wchar_t* kTimeFormat = L"%Y-%m-%dT%H:%M:%SZ";
39 // The expanded time string should always be this length, for example:
40 // 2020-02-12T16:59:32Z
41 const size_t kTimeStrMaxLen = 20;
43 #define ENSURE(x) \
44 if (FAILED(hr = (x))) { \
45 LOG_ERROR(hr); \
46 return hr; \
49 bool GetTaskDescription(mozilla::UniquePtr<wchar_t[]>& description) {
50 mozilla::UniquePtr<wchar_t[]> installPath;
51 bool success = GetInstallDirectory(installPath);
52 if (!success) {
53 LOG_ERROR_MESSAGE(L"Failed to get install directory");
54 return false;
56 nsTArray<nsCString> resIds = {"branding/brand.ftl"_ns,
57 "browser/backgroundtasks/defaultagent.ftl"_ns};
58 RefPtr<Localization> l10n = Localization::Create(resIds, true);
59 nsAutoCString daTaskDesc;
60 mozilla::ErrorResult rv;
61 l10n->FormatValueSync("default-browser-agent-task-description"_ns, {},
62 daTaskDesc, rv);
63 if (rv.Failed()) {
64 LOG_ERROR_MESSAGE(L"Failed to read task description");
65 return false;
67 NS_ConvertUTF8toUTF16 daTaskDescW(daTaskDesc);
68 description = mozilla::MakeUnique<wchar_t[]>(daTaskDescW.Length() + 1);
69 wcsncpy(description.get(), daTaskDescW.get(), daTaskDescW.Length() + 1);
70 return true;
73 HRESULT RegisterTask(const wchar_t* uniqueToken,
74 BSTR startTime /* = nullptr */) {
75 // Do data migration during the task installation. This might seem like it
76 // belongs in UpdateTask, but we want to be able to call
77 // RemoveTasks();
78 // RegisterTask();
79 // and still have data migration happen. Also, UpdateTask calls this function,
80 // so migration will still get run in that case.
81 MaybeMigrateCurrentDefault();
83 // Make sure we don't try to register a task that already exists.
84 RemoveTasks(uniqueToken, WhichTasks::WdbaTaskOnly);
86 // If we create a folder and then fail to create the task, we need to
87 // remember to delete the folder so that whatever set of permissions it ends
88 // up with doesn't interfere with trying to create the task again later, and
89 // so that we don't just leave an empty folder behind.
90 bool createdFolder = false;
92 HRESULT hr = S_OK;
93 RefPtr<ITaskService> scheduler;
94 ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
95 IID_ITaskService, getter_AddRefs(scheduler)));
97 ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
99 RefPtr<ITaskFolder> rootFolder;
100 BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
101 ENSURE(
102 scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
104 RefPtr<ITaskFolder> taskFolder;
105 BStrPtr vendorBStr = BStrPtr(SysAllocString(kTaskVendor));
106 if (FAILED(rootFolder->GetFolder(vendorBStr.get(),
107 getter_AddRefs(taskFolder)))) {
108 hr = rootFolder->CreateFolder(vendorBStr.get(), VARIANT{},
109 getter_AddRefs(taskFolder));
111 if (SUCCEEDED(hr)) {
112 createdFolder = true;
113 } else if (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) {
114 // `CreateFolder` doesn't assign to the out pointer on
115 // `ERROR_ALREADY_EXISTS`, so try to get the folder again. This behavior
116 // is undocumented but was verified in a debugger.
117 HRESULT priorHr = hr;
118 hr = rootFolder->GetFolder(vendorBStr.get(), getter_AddRefs(taskFolder));
120 if (FAILED(hr)) {
121 LOG_ERROR(priorHr);
122 LOG_ERROR(hr);
123 return hr;
125 } else {
126 LOG_ERROR(hr);
127 return hr;
131 auto cleanupFolder =
132 mozilla::MakeScopeExit([&hr, createdFolder, &rootFolder, &vendorBStr] {
133 if (createdFolder && FAILED(hr)) {
134 // If this fails, we can't really handle that intelligently, so
135 // don't even bother to check the return code.
136 rootFolder->DeleteFolder(vendorBStr.get(), 0);
140 RefPtr<ITaskDefinition> newTask;
141 ENSURE(scheduler->NewTask(0, getter_AddRefs(newTask)));
143 mozilla::UniquePtr<wchar_t[]> description;
144 if (!GetTaskDescription(description)) {
145 return E_FAIL;
147 BStrPtr descriptionBstr = BStrPtr(SysAllocString(description.get()));
149 RefPtr<IRegistrationInfo> taskRegistration;
150 ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(taskRegistration)));
151 ENSURE(taskRegistration->put_Description(descriptionBstr.get()));
153 RefPtr<ITaskSettings> taskSettings;
154 ENSURE(newTask->get_Settings(getter_AddRefs(taskSettings)));
155 ENSURE(taskSettings->put_DisallowStartIfOnBatteries(VARIANT_FALSE));
156 ENSURE(taskSettings->put_MultipleInstances(TASK_INSTANCES_IGNORE_NEW));
157 ENSURE(taskSettings->put_StartWhenAvailable(VARIANT_TRUE));
158 ENSURE(taskSettings->put_StopIfGoingOnBatteries(VARIANT_FALSE));
159 // This cryptic string means "12 hours 5 minutes". So, if the task runs for
160 // longer than that, the process will be killed, because that should never
161 // happen. See
162 // https://docs.microsoft.com/en-us/windows/win32/taskschd/tasksettings-executiontimelimit
163 // for a detailed explanation of these strings.
164 BStrPtr execTimeLimitBStr = BStrPtr(SysAllocString(L"PT12H5M"));
165 ENSURE(taskSettings->put_ExecutionTimeLimit(execTimeLimitBStr.get()));
167 RefPtr<IRegistrationInfo> regInfo;
168 ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(regInfo)));
170 ENSURE(regInfo->put_Author(vendorBStr.get()));
172 RefPtr<ITriggerCollection> triggers;
173 ENSURE(newTask->get_Triggers(getter_AddRefs(triggers)));
175 RefPtr<ITrigger> newTrigger;
176 ENSURE(triggers->Create(TASK_TRIGGER_DAILY, getter_AddRefs(newTrigger)));
178 RefPtr<IDailyTrigger> dailyTrigger;
179 ENSURE(newTrigger->QueryInterface(IID_IDailyTrigger,
180 getter_AddRefs(dailyTrigger)));
182 if (startTime) {
183 ENSURE(dailyTrigger->put_StartBoundary(startTime));
184 } else {
185 // The time that the task is scheduled to run at every day is taken from the
186 // time in the trigger's StartBoundary property. We'll set this to the
187 // current time, on the theory that the time at which we're being installed
188 // is a time that the computer is likely to be on other days. If our
189 // theory is wrong and the computer is offline at the scheduled time, then
190 // because we've set StartWhenAvailable above, the task will run whenever
191 // it wakes up. Since our task is entirely in the background and doesn't use
192 // a lot of resources, we're not concerned about it bothering the user if it
193 // runs while they're actively using this computer.
194 time_t now_t = time(nullptr);
195 // Subtract a minute from the current time, to avoid "winning" a potential
196 // race with the scheduler that might have it start the task immediately
197 // after we register it, if we finish doing that and then it evaluates the
198 // trigger during the same second. We haven't seen this happen in practice,
199 // but there's no documented guarantee that it won't, so let's be sure.
200 now_t -= 60;
202 tm now_tm;
203 errno_t errno_rv = gmtime_s(&now_tm, &now_t);
204 if (errno_rv != 0) {
205 // The C runtime has a (private) function to convert Win32 error codes to
206 // errno values, but there's nothing that goes the other way, and it
207 // isn't worth including one here for something that's this unlikely to
208 // fail anyway. So just return a generic error.
209 hr = HRESULT_FROM_WIN32(ERROR_INVALID_TIME);
210 LOG_ERROR(hr);
211 return hr;
214 mozilla::UniquePtr<wchar_t[]> timeStr =
215 mozilla::MakeUnique<wchar_t[]>(kTimeStrMaxLen + 1);
217 if (wcsftime(timeStr.get(), kTimeStrMaxLen + 1, kTimeFormat, &now_tm) ==
218 0) {
219 hr = E_NOT_SUFFICIENT_BUFFER;
220 LOG_ERROR(hr);
221 return hr;
224 BStrPtr startTimeBStr = BStrPtr(SysAllocString(timeStr.get()));
225 ENSURE(dailyTrigger->put_StartBoundary(startTimeBStr.get()));
228 ENSURE(dailyTrigger->put_DaysInterval(1));
230 RefPtr<IActionCollection> actions;
231 ENSURE(newTask->get_Actions(getter_AddRefs(actions)));
233 RefPtr<IAction> action;
234 ENSURE(actions->Create(TASK_ACTION_EXEC, getter_AddRefs(action)));
236 RefPtr<IExecAction> execAction;
237 ENSURE(action->QueryInterface(IID_IExecAction, getter_AddRefs(execAction)));
239 // Register proxy instead of Firefox background task.
240 mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
241 if (!PathRemoveFileSpecW(installPath.get())) {
242 return E_FAIL;
244 std::wstring proxyPath(installPath.get());
245 proxyPath += L"\\default-browser-agent.exe";
247 BStrPtr binaryPathBStr = BStrPtr(SysAllocString(proxyPath.c_str()));
248 ENSURE(execAction->put_Path(binaryPathBStr.get()));
250 std::wstring taskArgs = L"do-task \"";
251 taskArgs += uniqueToken;
252 taskArgs += L"\"";
253 BStrPtr argsBStr = BStrPtr(SysAllocString(taskArgs.c_str()));
254 ENSURE(execAction->put_Arguments(argsBStr.get()));
256 std::wstring taskName(kTaskName);
257 taskName += uniqueToken;
258 BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
260 RefPtr<IRegisteredTask> registeredTask;
261 ENSURE(taskFolder->RegisterTaskDefinition(
262 taskNameBStr.get(), newTask, TASK_CREATE_OR_UPDATE, VARIANT{}, VARIANT{},
263 TASK_LOGON_INTERACTIVE_TOKEN, VARIANT{}, getter_AddRefs(registeredTask)));
265 return hr;
268 HRESULT UpdateTask(const wchar_t* uniqueToken) {
269 RefPtr<ITaskService> scheduler;
270 HRESULT hr = S_OK;
271 ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
272 IID_ITaskService, getter_AddRefs(scheduler)));
274 ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
276 RefPtr<ITaskFolder> taskFolder;
277 BStrPtr folderBStr = BStrPtr(SysAllocString(kTaskVendor));
279 if (FAILED(
280 scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)))) {
281 // If our folder doesn't exist, create it and the task.
282 return RegisterTask(uniqueToken);
285 std::wstring taskName(kTaskName);
286 taskName += uniqueToken;
287 BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
289 RefPtr<IRegisteredTask> task;
290 if (FAILED(taskFolder->GetTask(taskNameBStr.get(), getter_AddRefs(task)))) {
291 // If our task doesn't exist at all, just create one.
292 return RegisterTask(uniqueToken);
295 // If we have a task registered already, we need to recreate it because
296 // something might have changed that we need to update. But we don't
297 // want to restart the schedule from now, because that might mean the
298 // task never runs at all for e.g. Nightly. So create a new task, but
299 // first get and preserve the existing trigger.
300 RefPtr<ITaskDefinition> definition;
301 if (FAILED(task->get_Definition(getter_AddRefs(definition)))) {
302 // This task is broken, make a new one.
303 return RegisterTask(uniqueToken);
306 RefPtr<ITriggerCollection> triggerList;
307 if (FAILED(definition->get_Triggers(getter_AddRefs(triggerList)))) {
308 // This task is broken, make a new one.
309 return RegisterTask(uniqueToken);
312 RefPtr<ITrigger> trigger;
313 if (FAILED(triggerList->get_Item(1, getter_AddRefs(trigger)))) {
314 // This task is broken, make a new one.
315 return RegisterTask(uniqueToken);
318 BSTR startTimeBstr;
319 if (FAILED(trigger->get_StartBoundary(&startTimeBstr))) {
320 // This task is broken, make a new one.
321 return RegisterTask(uniqueToken);
323 BStrPtr startTime(startTimeBstr);
325 return RegisterTask(uniqueToken, startTime.get());
328 } // namespace mozilla::default_agent