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