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