1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 #include <windows.h>
6 #include <shlwapi.h>
7 #include <stdio.h>
8 #include <wchar.h>
9 #include <shlobj.h>
10
11 #include "serviceinstall.hxx"
12 #include "maintenanceservice.hxx"
13 #include "servicebase.hxx"
14 #include "workmonitor.hxx"
15 #include "uachelper.h"
16 #include "updatehelper.h"
17
18 // Link w/ subsystem window so we don't get a console when executing
19 // this binary through the installer.
20 #pragma comment(linker, "/SUBSYSTEM:windows")
21
22 SERVICE_STATUS gSvcStatus = { 0 };
23 SERVICE_STATUS_HANDLE gSvcStatusHandle = nullptr;
24 HANDLE gWorkDoneEvent = nullptr;
25 HANDLE gThread = nullptr;
26 bool gServiceControlStopping = false;
27
28 // logs are pretty small, about 20 lines, so 10 seems reasonable.
29 #define LOGS_TO_KEEP 10
30
31 BOOL GetLogDirectoryPath(WCHAR *path);
32
33 int
wmain(int argc,WCHAR ** argv)34 wmain(int argc, WCHAR **argv)
35 {
36 if (argc < 2)
37 {
38 LOG_WARN(("missing mandatory command line argument"));
39 return 1;
40 }
41 // If command-line parameter is "install", install the service
42 // or upgrade if already installed
43 // If command line parameter is "forceinstall", install the service
44 // even if it is older than what is already installed.
45 // If command-line parameter is "upgrade", upgrade the service
46 // but do not install it if it is not already installed.
47 // If command line parameter is "uninstall", uninstall the service.
48 // Otherwise, the service is probably being started by the SCM.
49 bool forceInstall = !lstrcmpi(argv[1], L"forceinstall");
50 if (!lstrcmpi(argv[1], L"install") || forceInstall)
51 {
52 WCHAR updatePath[MAX_PATH + 1];
53 if (GetLogDirectoryPath(updatePath))
54 {
55 LogInit(updatePath, L"maintenanceservice-install.log");
56 }
57
58 SvcInstallAction action = InstallSvc;
59 if (forceInstall)
60 {
61 action = ForceInstallSvc;
62 LOG(("Installing service with force specified..."));
63 }
64 else
65 {
66 LOG(("Installing service..."));
67 }
68
69 bool ret = SvcInstall(action);
70 if (!ret)
71 {
72 LOG_WARN(("Could not install service. (%d)", GetLastError()));
73 LogFinish();
74 return 1;
75 }
76
77 LOG(("The service was installed successfully"));
78 LogFinish();
79 return 0;
80 }
81
82 if (!lstrcmpi(argv[1], L"upgrade"))
83 {
84 WCHAR updatePath[MAX_PATH + 1];
85 if (GetLogDirectoryPath(updatePath))
86 {
87 LogInit(updatePath, L"maintenanceservice-install.log");
88 }
89
90 LOG(("Upgrading service if installed..."));
91 if (!SvcInstall(UpgradeSvc))
92 {
93 LOG_WARN(("Could not upgrade service. (%d)", GetLastError()));
94 LogFinish();
95 return 1;
96 }
97
98 LOG(("The service was upgraded successfully"));
99 LogFinish();
100 return 0;
101 }
102
103 if (!lstrcmpi(argv[1], L"uninstall"))
104 {
105 WCHAR updatePath[MAX_PATH + 1];
106 if (GetLogDirectoryPath(updatePath))
107 {
108 LogInit(updatePath, L"maintenanceservice-uninstall.log");
109 }
110 LOG(("Uninstalling service..."));
111 if (!SvcUninstall())
112 {
113 LOG_WARN(("Could not uninstall service. (%d)", GetLastError()));
114 LogFinish();
115 return 1;
116 }
117 LOG(("The service was uninstalled successfully"));
118 LogFinish();
119 return 0;
120 }
121
122 SERVICE_TABLE_ENTRYW DispatchTable[] =
123 {
124 { SVC_NAME, (LPSERVICE_MAIN_FUNCTIONW) SvcMain },
125 { nullptr, nullptr }
126 };
127
128 // This call returns when the service has stopped.
129 // The process should simply terminate when the call returns.
130 if (!StartServiceCtrlDispatcherW(DispatchTable))
131 {
132 LOG_WARN(("StartServiceCtrlDispatcher failed. (%d)", GetLastError()));
133 }
134
135 return 0;
136 }
137
138 /**
139 * Obtains the base path where logs should be stored
140 *
141 * @param path The out buffer for the backup log path of size MAX_PATH + 1
142 * @return TRUE if successful.
143 */
144 BOOL
GetLogDirectoryPath(WCHAR * path)145 GetLogDirectoryPath(WCHAR *path)
146 {
147 HRESULT hr = SHGetFolderPathW(nullptr, CSIDL_COMMON_APPDATA, nullptr,
148 SHGFP_TYPE_CURRENT, path);
149 if (FAILED(hr))
150 {
151 return FALSE;
152 }
153
154 if (!PathAppendSafe(path, L"Mozilla"))
155 {
156 return FALSE;
157 }
158 // The directory should already be created from the installer, but
159 // just to be safe in case someone deletes.
160 CreateDirectoryW(path, nullptr);
161
162 if (!PathAppendSafe(path, L"logs"))
163 {
164 return FALSE;
165 }
166 CreateDirectoryW(path, nullptr);
167 return TRUE;
168 }
169
170 /**
171 * Calculated a backup path based on the log number.
172 *
173 * @param path The out buffer to store the log path of size MAX_PATH + 1
174 * @param basePath The base directory where the calculated path should go
175 * @param logNumber The log number, 0 == updater.log
176 * @return TRUE if successful.
177 */
178 BOOL
GetBackupLogPath(LPWSTR path,LPCWSTR basePath,int logNumber)179 GetBackupLogPath(LPWSTR path, LPCWSTR basePath, int logNumber)
180 {
181 WCHAR logName[64] = { L'\0' };
182 wcsncpy(path, basePath, sizeof(logName) / sizeof(logName[0]) - 1);
183 if (logNumber <= 0)
184 {
185 swprintf(logName, sizeof(logName) / sizeof(logName[0]),
186 L"maintenanceservice.log");
187 }
188 else
189 {
190 swprintf(logName, sizeof(logName) / sizeof(logName[0]),
191 L"maintenanceservice-%d.log", logNumber);
192 }
193 return PathAppendSafe(path, logName);
194 }
195
196 /**
197 * Moves the old log files out of the way before a new one is written.
198 * If you for example keep 3 logs, then this function will do:
199 * updater2.log -> updater3.log
200 * updater1.log -> updater2.log
201 * updater.log -> updater1.log
202 * Which clears room for a new updater.log in the basePath directory
203 *
204 * @param basePath The base directory path where log files are stored
205 * @param numLogsToKeep The number of logs to keep
206 */
207 void
BackupOldLogs(LPCWSTR basePath,int numLogsToKeep)208 BackupOldLogs(LPCWSTR basePath, int numLogsToKeep)
209 {
210 WCHAR oldPath[MAX_PATH + 1];
211 WCHAR newPath[MAX_PATH + 1];
212 for (int i = numLogsToKeep; i >= 1; i--)
213 {
214 if (!GetBackupLogPath(oldPath, basePath, i -1))
215 {
216 continue;
217 }
218
219 if (!GetBackupLogPath(newPath, basePath, i))
220 {
221 continue;
222 }
223
224 if (!MoveFileExW(oldPath, newPath, MOVEFILE_REPLACE_EXISTING))
225 {
226 continue;
227 }
228 }
229 }
230
231 /**
232 * Ensures the service is shutdown once all work is complete.
233 * There is an issue on XP SP2 and below where the service can hang
234 * in a stop pending state even though the SCM is notified of a stopped
235 * state. Control *should* be returned to StartServiceCtrlDispatcher from the
236 * call to SetServiceStatus on a stopped state in the wmain thread.
237 * Sometimes this is not the case though. This thread will terminate the process
238 * if it has been 5 seconds after all work is done and the process is still not
239 * terminated. This thread is only started once a stopped state was sent to the
240 * SCM. The stop pending hang can be reproduced intermittently even if you set
241 * a stopped state directly and never set a stop pending state. It is safe to
242 * forcefully terminate the process ourselves since all work is done once we
243 * start this thread.
244 */
245 DWORD WINAPI
EnsureProcessTerminatedThread(LPVOID)246 EnsureProcessTerminatedThread(LPVOID)
247 {
248 Sleep(5000);
249 exit(0);
250 }
251
252 void
StartTerminationThread()253 StartTerminationThread()
254 {
255 // If the process does not self terminate like it should, this thread
256 // will terminate the process after 5 seconds.
257 HANDLE thread = CreateThread(nullptr, 0, EnsureProcessTerminatedThread,
258 nullptr, 0, nullptr);
259 if (thread)
260 {
261 CloseHandle(thread);
262 }
263 }
264
265 /**
266 * Main entry point when running as a service.
267 */
268 void WINAPI
SvcMain(DWORD argc,LPWSTR * argv)269 SvcMain(DWORD argc, LPWSTR *argv)
270 {
271 // Setup logging, and backup the old logs
272 WCHAR updatePath[MAX_PATH + 1];
273 if (GetLogDirectoryPath(updatePath))
274 {
275 BackupOldLogs(updatePath, LOGS_TO_KEEP);
276 LogInit(updatePath, L"maintenanceservice.log");
277 }
278
279 // Disable every privilege we don't need. Processes started using
280 // CreateProcess will use the same token as this process.
281 UACHelper::DisablePrivileges(nullptr);
282
283 // Register the handler function for the service
284 gSvcStatusHandle = RegisterServiceCtrlHandlerW(SVC_NAME, SvcCtrlHandler);
285 if (!gSvcStatusHandle)
286 {
287 LOG_WARN(("RegisterServiceCtrlHandler failed. (%d)", GetLastError()));
288 ExecuteServiceCommand(argc, argv);
289 LogFinish();
290 exit(1);
291 }
292
293 // These values will be re-used later in calls involving gSvcStatus
294 gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
295 gSvcStatus.dwServiceSpecificExitCode = 0;
296
297 // Report initial status to the SCM
298 ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
299
300 // This event will be used to tell the SvcCtrlHandler when the work is
301 // done for when a stop command is manually issued.
302 gWorkDoneEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
303 if (!gWorkDoneEvent)
304 {
305 ReportSvcStatus(SERVICE_STOPPED, 1, 0);
306 StartTerminationThread();
307 return;
308 }
309
310 // Initialization complete and we're about to start working on
311 // the actual command. Report the service state as running to the SCM.
312 ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
313
314 // The service command was executed, stop logging and set an event
315 // to indicate the work is done in case someone is waiting on a
316 // service stop operation.
317 ExecuteServiceCommand(argc, argv);
318 LogFinish();
319
320 SetEvent(gWorkDoneEvent);
321
322 // If we aren't already in a stopping state then tell the SCM we're stopped
323 // now. If we are already in a stopping state then the SERVICE_STOPPED state
324 // will be set by the SvcCtrlHandler.
325 if (!gServiceControlStopping)
326 {
327 ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
328 StartTerminationThread();
329 }
330 }
331
332 /**
333 * Sets the current service status and reports it to the SCM.
334 *
335 * @param currentState The current state (see SERVICE_STATUS)
336 * @param exitCode The system error code
337 * @param waitHint Estimated time for pending operation in milliseconds
338 */
339 void
ReportSvcStatus(DWORD currentState,DWORD exitCode,DWORD waitHint)340 ReportSvcStatus(DWORD currentState,
341 DWORD exitCode,
342 DWORD waitHint)
343 {
344 static DWORD dwCheckPoint = 1;
345
346 gSvcStatus.dwCurrentState = currentState;
347 gSvcStatus.dwWin32ExitCode = exitCode;
348 gSvcStatus.dwWaitHint = waitHint;
349
350 if (SERVICE_START_PENDING == currentState ||
351 SERVICE_STOP_PENDING == currentState)
352 {
353 gSvcStatus.dwControlsAccepted = 0;
354 }
355 else
356 {
357 gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP |
358 SERVICE_ACCEPT_SHUTDOWN;
359 }
360
361 if ((SERVICE_RUNNING == currentState) ||
362 (SERVICE_STOPPED == currentState))
363 {
364 gSvcStatus.dwCheckPoint = 0;
365 }
366 else
367 {
368 gSvcStatus.dwCheckPoint = dwCheckPoint++;
369 }
370
371 // Report the status of the service to the SCM.
372 SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
373 }
374
375 /**
376 * Since the SvcCtrlHandler should only spend at most 30 seconds before
377 * returning, this function does the service stop work for the SvcCtrlHandler.
378 */
379 DWORD WINAPI
StopServiceAndWaitForCommandThread(LPVOID)380 StopServiceAndWaitForCommandThread(LPVOID)
381 {
382 do
383 {
384 ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000);
385 }
386 while (WaitForSingleObject(gWorkDoneEvent, 100) == WAIT_TIMEOUT);
387 CloseHandle(gWorkDoneEvent);
388 gWorkDoneEvent = nullptr;
389 ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
390 StartTerminationThread();
391 return 0;
392 }
393
394 /**
395 * Called by SCM whenever a control code is sent to the service
396 * using the ControlService function.
397 */
398 void WINAPI
SvcCtrlHandler(DWORD dwCtrl)399 SvcCtrlHandler(DWORD dwCtrl)
400 {
401 // After a SERVICE_CONTROL_STOP there should be no more commands sent to
402 // the SvcCtrlHandler.
403 if (gServiceControlStopping)
404 {
405 return;
406 }
407
408 // Handle the requested control code.
409 switch (dwCtrl)
410 {
411 case SERVICE_CONTROL_SHUTDOWN:
412 case SERVICE_CONTROL_STOP:
413 {
414 gServiceControlStopping = true;
415 ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000);
416
417 // The SvcCtrlHandler thread should not spend more than 30 seconds in
418 // shutdown so we spawn a new thread for stopping the service
419 HANDLE thread = CreateThread(nullptr, 0,
420 StopServiceAndWaitForCommandThread,
421 nullptr, 0, nullptr);
422 if (thread)
423 {
424 CloseHandle(thread);
425 }
426 else
427 {
428 // Couldn't start the thread so just call the stop ourselves.
429 // If it happens to take longer than 30 seconds the caller will
430 // get an error.
431 StopServiceAndWaitForCommandThread(nullptr);
432 }
433 }
434 break;
435 default:
436 break;
437 }
438 }
439