c0b65df32e66aee12528139ec29c90dcb18eb59a
[nssm.git] / process.cpp
1 #include "nssm.h"
2
3 extern imports_t imports;
4
5 int get_process_creation_time(HANDLE process_handle, FILETIME *ft) {
6   FILETIME creation_time, exit_time, kernel_time, user_time;
7
8   if (! GetProcessTimes(process_handle, &creation_time, &exit_time, &kernel_time, &user_time)) {
9     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GETPROCESSTIMES_FAILED, error_string(GetLastError()), 0);
10     return 1;
11   }
12
13   memmove(ft, &creation_time, sizeof(creation_time));
14
15   return 0;
16 }
17
18 int get_process_exit_time(HANDLE process_handle, FILETIME *ft) {
19   FILETIME creation_time, exit_time, kernel_time, user_time;
20
21   if (! GetProcessTimes(process_handle, &creation_time, &exit_time, &kernel_time, &user_time)) {
22     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GETPROCESSTIMES_FAILED, error_string(GetLastError()), 0);
23     return 1;
24   }
25
26   if (! (exit_time.dwLowDateTime || exit_time.dwHighDateTime)) return 2;
27   memmove(ft, &exit_time, sizeof(exit_time));
28
29   return 0;
30 }
31
32 int check_parent(nssm_service_t *service, PROCESSENTRY32 *pe, unsigned long ppid) {
33   /* Check parent process ID matches. */
34   if (pe->th32ParentProcessID != ppid) return 1;
35
36   /*
37     Process IDs can be reused so do a sanity check by making sure the child
38     has been running for less time than the parent.
39     Though unlikely, it's possible that the parent exited and its process ID
40     was already reused, so we'll also compare against its exit time.
41   */
42   HANDLE process_handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, pe->th32ProcessID);
43   if (! process_handle) {
44     TCHAR pid_string[16];
45     _sntprintf_s(pid_string, _countof(pid_string), _TRUNCATE, _T("%lu"), pe->th32ProcessID);
46     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENPROCESS_FAILED, pid_string, service->name, error_string(GetLastError()), 0);
47     return 2;
48   }
49
50   FILETIME ft;
51   if (get_process_creation_time(process_handle, &ft)) {
52     CloseHandle(process_handle);
53     return 3;
54   }
55
56   CloseHandle(process_handle);
57
58   /* Verify that the parent's creation time is not later. */
59   if (CompareFileTime(&service->creation_time, &ft) > 0) return 4;
60
61   /* Verify that the parent's exit time is not earlier. */
62   if (CompareFileTime(&service->exit_time, &ft) < 0) return 5;
63
64   return 0;
65 }
66
67 /* Send some window messages and hope the window respects one or more. */
68 int CALLBACK kill_window(HWND window, LPARAM arg) {
69   kill_t *k = (kill_t *) arg;
70
71   unsigned long pid;
72   if (! GetWindowThreadProcessId(window, &pid)) return 1;
73   if (pid != k->pid) return 1;
74
75   /* First try sending WM_CLOSE to request that the window close. */
76   k->signalled |= PostMessage(window, WM_CLOSE, k->exitcode, 0);
77
78   /*
79     Then tell the window that the user is logging off and it should exit
80     without worrying about saving any data.
81   */
82   k->signalled |= PostMessage(window, WM_ENDSESSION, 1, ENDSESSION_CLOSEAPP | ENDSESSION_CRITICAL | ENDSESSION_LOGOFF);
83
84   return 1;
85 }
86
87 /*
88   Try to post a message to the message queues of threads associated with the
89   given process ID.  Not all threads have message queues so there's no
90   guarantee of success, and we don't want to be left waiting for unsignalled
91   processes so this function returns only true if at least one thread was
92   successfully prodded.
93 */
94 int kill_threads(TCHAR *service_name, kill_t *k) {
95   int ret = 0;
96
97   /* Get a snapshot of all threads in the system. */
98   HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
99   if (! snapshot) {
100     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETOOLHELP32SNAPSHOT_THREAD_FAILED, service_name, error_string(GetLastError()), 0);
101     return 0;
102   }
103
104   THREADENTRY32 te;
105   ZeroMemory(&te, sizeof(te));
106   te.dwSize = sizeof(te);
107
108   if (! Thread32First(snapshot, &te)) {
109     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_THREAD_ENUMERATE_FAILED, service_name, error_string(GetLastError()), 0);
110     CloseHandle(snapshot);
111     return 0;
112   }
113
114   /* This thread belongs to the doomed process so signal it. */
115   if (te.th32OwnerProcessID == k->pid) {
116     ret |= PostThreadMessage(te.th32ThreadID, WM_QUIT, k->exitcode, 0);
117   }
118
119   while (true) {
120     /* Try to get the next thread. */
121     if (! Thread32Next(snapshot, &te)) {
122       unsigned long error = GetLastError();
123       if (error == ERROR_NO_MORE_FILES) break;
124       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_THREAD_ENUMERATE_FAILED, service_name, error_string(GetLastError()), 0);
125       CloseHandle(snapshot);
126       return ret;
127     }
128
129     if (te.th32OwnerProcessID == k->pid) {
130       ret |= PostThreadMessage(te.th32ThreadID, WM_QUIT, k->exitcode, 0);
131     }
132   }
133
134   CloseHandle(snapshot);
135
136   return ret;
137 }
138
139 /* Give the process a chance to die gracefully. */
140 int kill_process(nssm_service_t *service, HANDLE process_handle, unsigned long pid, unsigned long exitcode) {
141   /* Shouldn't happen. */
142   if (! service) return 1;
143   if (! pid) return 1;
144   if (! process_handle) return 1;
145
146   unsigned long ret;
147   if (GetExitCodeProcess(process_handle, &ret)) {
148     if (ret != STILL_ACTIVE) return 1;
149   }
150
151   kill_t k = { pid, exitcode, 0 };
152
153   /* Try to send a Control-C event to the console. */
154   if (service->stop_method & NSSM_STOP_METHOD_CONSOLE) {
155     if (! kill_console(service, &k)) return 1;
156   }
157
158   /*
159     Try to post messages to the windows belonging to the given process ID.
160     If the process is a console application it won't have any windows so there's
161     no guarantee of success.
162   */
163   if (service->stop_method & NSSM_STOP_METHOD_WINDOW) {
164     EnumWindows((WNDENUMPROC) kill_window, (LPARAM) &k);
165     if (k.signalled) {
166       if (! await_shutdown(service, _T(__FUNCTION__), service->kill_window_delay)) return 1;
167     }
168   }
169
170   /*
171     Try to post messages to any thread message queues associated with the
172     process.  Console applications might have them (but probably won't) so
173     there's still no guarantee of success.
174   */
175   if (service->stop_method & NSSM_STOP_METHOD_THREADS) {
176     if (kill_threads(service->name, &k)) {
177       if (! await_shutdown(service, _T(__FUNCTION__), service->kill_threads_delay)) return 1;
178     }
179   }
180
181   /* We tried being nice.  Time for extreme prejudice. */
182   if (service->stop_method & NSSM_STOP_METHOD_TERMINATE) {
183     return TerminateProcess(service->process_handle, exitcode);
184   }
185
186   return 0;
187 }
188
189 /* Simulate a Control-C event to our console (shared with the app). */
190 int kill_console(nssm_service_t *service, kill_t *k) {
191   unsigned long ret;
192
193   if (! service) return 1;
194
195   /* Check we loaded AttachConsole(). */
196   if (! imports.AttachConsole) return 4;
197
198   /* Try to attach to the process's console. */
199   if (! imports.AttachConsole(k->pid)) {
200     ret = GetLastError();
201
202     switch (ret) {
203       case ERROR_INVALID_HANDLE:
204         /* The app doesn't have a console. */
205         return 1;
206
207       case ERROR_GEN_FAILURE:
208         /* The app already exited. */
209         return 2;
210
211       case ERROR_ACCESS_DENIED:
212       default:
213         /* We already have a console. */
214         log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_ATTACHCONSOLE_FAILED, service->name, error_string(ret), 0);
215         return 3;
216     }
217   }
218
219   /* Ignore the event ourselves. */
220   ret = 0;
221   if (! SetConsoleCtrlHandler(0, TRUE)) {
222     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_SETCONSOLECTRLHANDLER_FAILED, service->name, error_string(GetLastError()), 0);
223     ret = 4;
224   }
225
226   /* Send the event. */
227   if (! ret) {
228     if (! GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) {
229       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GENERATECONSOLECTRLEVENT_FAILED, service->name, error_string(GetLastError()), 0);
230       ret = 5;
231     }
232   }
233
234   /* Detach from the console. */
235   if (! FreeConsole()) {
236     log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_FREECONSOLE_FAILED, service->name, error_string(GetLastError()), 0);
237   }
238
239   /* Wait for process to exit. */
240   if (await_shutdown(service, _T(__FUNCTION__), service->kill_console_delay)) ret = 6;
241
242   return ret;
243 }
244
245 void kill_process_tree(nssm_service_t *service, unsigned long pid, unsigned long exitcode, unsigned long ppid) {
246   /* Shouldn't happen unless the service failed to start. */
247   if (! pid) return;
248
249   TCHAR pid_string[16], code[16];
250   _sntprintf_s(pid_string, _countof(pid_string), _TRUNCATE, _T("%lu"), pid);
251   _sntprintf_s(code, _countof(code), _TRUNCATE, _T("%lu"), exitcode);
252   log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILLING, service->name, pid_string, code, 0);
253
254   /* Get a snapshot of all processes in the system. */
255   HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
256   if (! snapshot) {
257     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETOOLHELP32SNAPSHOT_PROCESS_FAILED, service->name, error_string(GetLastError()), 0);
258     return;
259   }
260
261   PROCESSENTRY32 pe;
262   ZeroMemory(&pe, sizeof(pe));
263   pe.dwSize = sizeof(pe);
264
265   if (! Process32First(snapshot, &pe)) {
266     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_PROCESS_ENUMERATE_FAILED, service->name, error_string(GetLastError()), 0);
267     CloseHandle(snapshot);
268     return;
269   }
270
271   /* This is a child of the doomed process so kill it. */
272   if (! check_parent(service, &pe, pid)) kill_process_tree(service, pe.th32ProcessID, exitcode, ppid);
273
274   while (true) {
275     /* Try to get the next process. */
276     if (! Process32Next(snapshot, &pe)) {
277       unsigned long ret = GetLastError();
278       if (ret == ERROR_NO_MORE_FILES) break;
279       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_PROCESS_ENUMERATE_FAILED, service->name, error_string(GetLastError()), 0);
280       CloseHandle(snapshot);
281       return;
282     }
283
284     if (! check_parent(service, &pe, pid)) kill_process_tree(service, pe.th32ProcessID, exitcode, ppid);
285   }
286
287   CloseHandle(snapshot);
288
289   /* We will need a process handle in order to call TerminateProcess() later. */
290   HANDLE process_handle = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, false, pid);
291   if (! process_handle) {
292     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENPROCESS_FAILED, pid_string, service->name, error_string(GetLastError()), 0);
293     return;
294   }
295
296   TCHAR ppid_string[16];
297   _sntprintf_s(ppid_string, _countof(ppid_string), _TRUNCATE, _T("%lu"), ppid);
298   log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILL_PROCESS_TREE, pid_string, ppid_string, service->name, 0);
299   if (! kill_process(service, process_handle, pid, exitcode)) {
300     /* Maybe it already died. */
301     unsigned long ret;
302     if (! GetExitCodeProcess(process_handle, &ret) || ret == STILL_ACTIVE) {
303       if (service->stop_method & NSSM_STOP_METHOD_TERMINATE) log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_TERMINATEPROCESS_FAILED, pid_string, service->name, error_string(GetLastError()), 0);
304       else log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_PROCESS_STILL_ACTIVE, service->name, pid_string, NSSM, NSSM_REG_STOP_METHOD_SKIP, 0);
305     }
306   }
307
308   CloseHandle(process_handle);
309 }