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