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