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