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/. */
6
7 #include "ScheduledTask.h"
8
9 #include <string>
10 #include <time.h>
11
12 #include <comutil.h>
13 #include <taskschd.h>
14
15 #include "readstrings.h"
16 #include "mozilla/RefPtr.h"
17 #include "mozilla/ScopeExit.h"
18 #include "mozilla/UniquePtr.h"
19 #include "mozilla/WinHeaderOnlyUtils.h"
20 #include "WindowsDefaultBrowser.h"
21
22 #include "DefaultBrowser.h"
23
24 const wchar_t* kTaskVendor = L"" MOZ_APP_VENDOR;
25 // kTaskName should have the unique token appended before being used.
26 const wchar_t* kTaskName = L"" MOZ_APP_DISPLAYNAME " Default Browser Agent ";
27
28 // The task scheduler requires its time values to come in the form of a string
29 // in the format YYYY-MM-DDTHH:MM:SSZ. This format string is used to get that
30 // out of the C library wcsftime function.
31 const wchar_t* kTimeFormat = L"%Y-%m-%dT%H:%M:%SZ";
32 // The expanded time string should always be this length, for example:
33 // 2020-02-12T16:59:32Z
34 const size_t kTimeStrMaxLen = 20;
35
36 #define ENSURE(x) \
37 if (FAILED(hr = (x))) { \
38 LOG_ERROR(hr); \
39 return hr; \
40 }
41
42 struct SysFreeStringDeleter {
operator ()SysFreeStringDeleter43 void operator()(BSTR aPtr) { ::SysFreeString(aPtr); }
44 };
45 using BStrPtr = mozilla::UniquePtr<OLECHAR, SysFreeStringDeleter>;
46
GetTaskDescription(mozilla::UniquePtr<wchar_t[]> & description)47 bool GetTaskDescription(mozilla::UniquePtr<wchar_t[]>& description) {
48 mozilla::UniquePtr<wchar_t[]> installPath;
49 bool success = GetInstallDirectory(installPath);
50 if (!success) {
51 LOG_ERROR_MESSAGE(L"Failed to get install directory");
52 return false;
53 }
54 const wchar_t* iniFormat = L"%s\\defaultagent_localized.ini";
55 int bufferSize = _scwprintf(iniFormat, installPath.get());
56 ++bufferSize; // Extra character for terminating null
57 mozilla::UniquePtr<wchar_t[]> iniPath =
58 mozilla::MakeUnique<wchar_t[]>(bufferSize);
59 _snwprintf_s(iniPath.get(), bufferSize, _TRUNCATE, iniFormat,
60 installPath.get());
61
62 IniReader reader(iniPath.get());
63 reader.AddKey("DefaultBrowserAgentTaskDescription", &description);
64 int status = reader.Read();
65 if (status != OK) {
66 LOG_ERROR_MESSAGE(L"Failed to read task description: %d", status);
67 return false;
68 }
69 return true;
70 }
71
RegisterTask(const wchar_t * uniqueToken,BSTR startTime)72 HRESULT RegisterTask(const wchar_t* uniqueToken,
73 BSTR startTime /* = nullptr */) {
74 // Do data migration during the task installation. This might seem like it
75 // belongs in UpdateTask, but we want to be able to call
76 // RemoveTasks();
77 // RegisterTask();
78 // and still have data migration happen. Also, UpdateTask calls this function,
79 // so migration will still get run in that case.
80 MaybeMigrateCurrentDefault();
81
82 // Make sure we don't try to register a task that already exists.
83 RemoveTasks(uniqueToken, WhichTasks::WdbaTaskOnly);
84
85 // If we create a folder and then fail to create the task, we need to
86 // remember to delete the folder so that whatever set of permissions it ends
87 // up with doesn't interfere with trying to create the task again later, and
88 // so that we don't just leave an empty folder behind.
89 bool createdFolder = false;
90
91 HRESULT hr = S_OK;
92 RefPtr<ITaskService> scheduler;
93 ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
94 IID_ITaskService, getter_AddRefs(scheduler)));
95
96 ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
97
98 RefPtr<ITaskFolder> rootFolder;
99 BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
100 ENSURE(
101 scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
102
103 RefPtr<ITaskFolder> taskFolder;
104 BStrPtr vendorBStr = BStrPtr(SysAllocString(kTaskVendor));
105 if (FAILED(rootFolder->GetFolder(vendorBStr.get(),
106 getter_AddRefs(taskFolder)))) {
107 hr = rootFolder->CreateFolder(vendorBStr.get(), VARIANT{},
108 getter_AddRefs(taskFolder));
109 if (SUCCEEDED(hr)) {
110 createdFolder = true;
111 } else if (hr != HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) {
112 // The folder already existing isn't an error, but anything else is.
113 LOG_ERROR(hr);
114 return hr;
115 }
116 }
117
118 auto cleanupFolder =
119 mozilla::MakeScopeExit([hr, createdFolder, &rootFolder, &vendorBStr] {
120 if (createdFolder && FAILED(hr)) {
121 // If this fails, we can't really handle that intelligently, so
122 // don't even bother to check the return code.
123 rootFolder->DeleteFolder(vendorBStr.get(), 0);
124 }
125 });
126
127 RefPtr<ITaskDefinition> newTask;
128 ENSURE(scheduler->NewTask(0, getter_AddRefs(newTask)));
129
130 mozilla::UniquePtr<wchar_t[]> description;
131 if (!GetTaskDescription(description)) {
132 return E_FAIL;
133 }
134 BStrPtr descriptionBstr = BStrPtr(SysAllocString(description.get()));
135
136 RefPtr<IRegistrationInfo> taskRegistration;
137 ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(taskRegistration)));
138 ENSURE(taskRegistration->put_Description(descriptionBstr.get()));
139
140 RefPtr<ITaskSettings> taskSettings;
141 ENSURE(newTask->get_Settings(getter_AddRefs(taskSettings)));
142 ENSURE(taskSettings->put_DisallowStartIfOnBatteries(VARIANT_FALSE));
143 ENSURE(taskSettings->put_MultipleInstances(TASK_INSTANCES_IGNORE_NEW));
144 ENSURE(taskSettings->put_StartWhenAvailable(VARIANT_TRUE));
145 ENSURE(taskSettings->put_StopIfGoingOnBatteries(VARIANT_FALSE));
146 // This cryptic string means "35 minutes". So, if the task runs for longer
147 // than that, the process will be killed, because that should never happen.
148 BStrPtr execTimeLimitBStr = BStrPtr(SysAllocString(L"PT35M"));
149 ENSURE(taskSettings->put_ExecutionTimeLimit(execTimeLimitBStr.get()));
150
151 RefPtr<IRegistrationInfo> regInfo;
152 ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(regInfo)));
153
154 ENSURE(regInfo->put_Author(vendorBStr.get()));
155
156 RefPtr<ITriggerCollection> triggers;
157 ENSURE(newTask->get_Triggers(getter_AddRefs(triggers)));
158
159 RefPtr<ITrigger> newTrigger;
160 ENSURE(triggers->Create(TASK_TRIGGER_DAILY, getter_AddRefs(newTrigger)));
161
162 RefPtr<IDailyTrigger> dailyTrigger;
163 ENSURE(newTrigger->QueryInterface(IID_IDailyTrigger,
164 getter_AddRefs(dailyTrigger)));
165
166 if (startTime) {
167 ENSURE(dailyTrigger->put_StartBoundary(startTime));
168 } else {
169 // The time that the task is scheduled to run at every day is taken from the
170 // time in the trigger's StartBoundary property. We'll set this to the
171 // current time, on the theory that the time at which we're being installed
172 // is a time that the computer is likely to be on other days. If our
173 // theory is wrong and the computer is offline at the scheduled time, then
174 // because we've set StartWhenAvailable above, the task will run whenever
175 // it wakes up. Since our task is entirely in the background and doesn't use
176 // a lot of resources, we're not concerned about it bothering the user if it
177 // runs while they're actively using this computer.
178 time_t now_t = time(nullptr);
179 // Subtract a minute from the current time, to avoid "winning" a potential
180 // race with the scheduler that might have it start the task immediately
181 // after we register it, if we finish doing that and then it evaluates the
182 // trigger during the same second. We haven't seen this happen in practice,
183 // but there's no documented guarantee that it won't, so let's be sure.
184 now_t -= 60;
185
186 tm now_tm;
187 errno_t errno_rv = gmtime_s(&now_tm, &now_t);
188 if (errno_rv != 0) {
189 // The C runtime has a (private) function to convert Win32 error codes to
190 // errno values, but there's nothing that goes the other way, and it
191 // isn't worth including one here for something that's this unlikely to
192 // fail anyway. So just return a generic error.
193 hr = HRESULT_FROM_WIN32(ERROR_INVALID_TIME);
194 LOG_ERROR(hr);
195 return hr;
196 }
197
198 mozilla::UniquePtr<wchar_t[]> timeStr =
199 mozilla::MakeUnique<wchar_t[]>(kTimeStrMaxLen + 1);
200
201 if (wcsftime(timeStr.get(), kTimeStrMaxLen + 1, kTimeFormat, &now_tm) ==
202 0) {
203 hr = E_NOT_SUFFICIENT_BUFFER;
204 LOG_ERROR(hr);
205 return hr;
206 }
207
208 BStrPtr startTimeBStr = BStrPtr(SysAllocString(timeStr.get()));
209 ENSURE(dailyTrigger->put_StartBoundary(startTimeBStr.get()));
210 }
211
212 ENSURE(dailyTrigger->put_DaysInterval(1));
213
214 RefPtr<IActionCollection> actions;
215 ENSURE(newTask->get_Actions(getter_AddRefs(actions)));
216
217 RefPtr<IAction> action;
218 ENSURE(actions->Create(TASK_ACTION_EXEC, getter_AddRefs(action)));
219
220 RefPtr<IExecAction> execAction;
221 ENSURE(action->QueryInterface(IID_IExecAction, getter_AddRefs(execAction)));
222
223 BStrPtr binaryPathBStr =
224 BStrPtr(SysAllocString(mozilla::GetFullBinaryPath().get()));
225 ENSURE(execAction->put_Path(binaryPathBStr.get()));
226
227 std::wstring taskArgs = L"do-task \"";
228 taskArgs += uniqueToken;
229 taskArgs += L"\"";
230 BStrPtr argsBStr = BStrPtr(SysAllocString(taskArgs.c_str()));
231 ENSURE(execAction->put_Arguments(argsBStr.get()));
232
233 std::wstring taskName(kTaskName);
234 taskName += uniqueToken;
235 BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
236
237 RefPtr<IRegisteredTask> registeredTask;
238 ENSURE(taskFolder->RegisterTaskDefinition(
239 taskNameBStr.get(), newTask, TASK_CREATE_OR_UPDATE, VARIANT{}, VARIANT{},
240 TASK_LOGON_INTERACTIVE_TOKEN, VARIANT{}, getter_AddRefs(registeredTask)));
241
242 return hr;
243 }
244
UpdateTask(const wchar_t * uniqueToken)245 HRESULT UpdateTask(const wchar_t* uniqueToken) {
246 RefPtr<ITaskService> scheduler;
247 HRESULT hr = S_OK;
248 ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
249 IID_ITaskService, getter_AddRefs(scheduler)));
250
251 ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
252
253 RefPtr<ITaskFolder> taskFolder;
254 BStrPtr folderBStr = BStrPtr(SysAllocString(kTaskVendor));
255
256 if (FAILED(
257 scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)))) {
258 // If our folder doesn't exist, create it and the task.
259 return RegisterTask(uniqueToken);
260 }
261
262 std::wstring taskName(kTaskName);
263 taskName += uniqueToken;
264 BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
265
266 RefPtr<IRegisteredTask> task;
267 if (FAILED(taskFolder->GetTask(taskNameBStr.get(), getter_AddRefs(task)))) {
268 // If our task doesn't exist at all, just create one.
269 return RegisterTask(uniqueToken);
270 }
271
272 // If we have a task registered already, we need to recreate it because
273 // something might have changed that we need to update. But we don't
274 // want to restart the schedule from now, because that might mean the
275 // task never runs at all for e.g. Nightly. So create a new task, but
276 // first get and preserve the existing trigger.
277 RefPtr<ITaskDefinition> definition;
278 if (FAILED(task->get_Definition(getter_AddRefs(definition)))) {
279 // This task is broken, make a new one.
280 return RegisterTask(uniqueToken);
281 }
282
283 RefPtr<ITriggerCollection> triggerList;
284 if (FAILED(definition->get_Triggers(getter_AddRefs(triggerList)))) {
285 // This task is broken, make a new one.
286 return RegisterTask(uniqueToken);
287 }
288
289 RefPtr<ITrigger> trigger;
290 if (FAILED(triggerList->get_Item(1, getter_AddRefs(trigger)))) {
291 // This task is broken, make a new one.
292 return RegisterTask(uniqueToken);
293 }
294
295 BSTR startTimeBstr;
296 if (FAILED(trigger->get_StartBoundary(&startTimeBstr))) {
297 // This task is broken, make a new one.
298 return RegisterTask(uniqueToken);
299 }
300 BStrPtr startTime(startTimeBstr);
301
302 return RegisterTask(uniqueToken, startTime.get());
303 }
304
EndsWith(const wchar_t * string,const wchar_t * suffix)305 bool EndsWith(const wchar_t* string, const wchar_t* suffix) {
306 size_t string_len = wcslen(string);
307 size_t suffix_len = wcslen(suffix);
308 if (suffix_len > string_len) {
309 return false;
310 }
311 const wchar_t* substring = string + string_len - suffix_len;
312 return wcscmp(substring, suffix) == 0;
313 }
314
RemoveTasks(const wchar_t * uniqueToken,WhichTasks tasksToRemove)315 HRESULT RemoveTasks(const wchar_t* uniqueToken, WhichTasks tasksToRemove) {
316 if (!uniqueToken || wcslen(uniqueToken) == 0) {
317 return E_INVALIDARG;
318 }
319
320 RefPtr<ITaskService> scheduler;
321 HRESULT hr = S_OK;
322 ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
323 IID_ITaskService, getter_AddRefs(scheduler)));
324
325 ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
326
327 RefPtr<ITaskFolder> taskFolder;
328 BStrPtr folderBStr(SysAllocString(kTaskVendor));
329
330 hr = scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder));
331 if (FAILED(hr)) {
332 if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
333 // Don't return an error code if our folder doesn't exist,
334 // because that just means it's been removed already.
335 return S_OK;
336 } else {
337 return hr;
338 }
339 }
340
341 RefPtr<IRegisteredTaskCollection> tasksInFolder;
342 ENSURE(taskFolder->GetTasks(TASK_ENUM_HIDDEN, getter_AddRefs(tasksInFolder)));
343
344 LONG numTasks = 0;
345 ENSURE(tasksInFolder->get_Count(&numTasks));
346
347 std::wstring WdbaTaskName(kTaskName);
348 WdbaTaskName += uniqueToken;
349
350 // This will be set to the last error that we encounter while deleting tasks.
351 // This allows us to keep attempting to remove the remaining tasks, even if
352 // we encounter an error, while still preserving what error we encountered so
353 // we can return it from this function.
354 HRESULT deleteResult = S_OK;
355 // Set to true if we intentionally skip any tasks.
356 bool tasksSkipped = false;
357
358 for (LONG i = 0; i < numTasks; ++i) {
359 RefPtr<IRegisteredTask> task;
360 // IRegisteredTaskCollection's are 1-indexed.
361 hr = tasksInFolder->get_Item(_variant_t(i + 1), getter_AddRefs(task));
362 if (FAILED(hr)) {
363 deleteResult = hr;
364 continue;
365 }
366
367 BSTR taskName;
368 hr = task->get_Name(&taskName);
369 if (FAILED(hr)) {
370 deleteResult = hr;
371 continue;
372 }
373 // Automatically free taskName when we are done with it.
374 BStrPtr uniqueTaskName(taskName);
375
376 if (tasksToRemove == WhichTasks::WdbaTaskOnly) {
377 if (WdbaTaskName.compare(taskName) != 0) {
378 tasksSkipped = true;
379 continue;
380 }
381 } else { // tasksToRemove == WhichTasks::AllTasksForInstallation
382 if (!EndsWith(taskName, uniqueToken)) {
383 tasksSkipped = true;
384 continue;
385 }
386 }
387
388 hr = taskFolder->DeleteTask(taskName, 0 /* flags */);
389 if (FAILED(hr)) {
390 deleteResult = hr;
391 }
392 }
393
394 // If we successfully removed all the tasks, delete the folder too.
395 if (!tasksSkipped && SUCCEEDED(deleteResult)) {
396 RefPtr<ITaskFolder> rootFolder;
397 BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
398 ENSURE(
399 scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
400 ENSURE(rootFolder->DeleteFolder(folderBStr.get(), 0));
401 }
402
403 return deleteResult;
404 }
405