Moved environment functions to a new file.
[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   /* Close the stdin pipe. */
153   if (service->stdin_pipe) {
154     CloseHandle(service->stdin_pipe);
155     service->stdin_pipe = 0;
156     if (! await_shutdown(service, _T(__FUNCTION__), service->kill_console_delay)) return 1;
157   }
158
159   /* Try to send a Control-C event to the console. */
160   if (service->stop_method & NSSM_STOP_METHOD_CONSOLE) {
161     if (! kill_console(service)) return 1;
162   }
163
164   /*
165     Try to post messages to the windows belonging to the given process ID.
166     If the process is a console application it won't have any windows so there's
167     no guarantee of success.
168   */
169   if (service->stop_method & NSSM_STOP_METHOD_WINDOW) {
170     EnumWindows((WNDENUMPROC) kill_window, (LPARAM) &k);
171     if (k.signalled) {
172       if (! await_shutdown(service, _T(__FUNCTION__), service->kill_window_delay)) return 1;
173     }
174   }
175
176   /*
177     Try to post messages to any thread message queues associated with the
178     process.  Console applications might have them (but probably won't) so
179     there's still no guarantee of success.
180   */
181   if (service->stop_method & NSSM_STOP_METHOD_THREADS) {
182     if (kill_threads(service->name, &k)) {
183       if (! await_shutdown(service, _T(__FUNCTION__), service->kill_threads_delay)) return 1;
184     }
185   }
186
187   /* We tried being nice.  Time for extreme prejudice. */
188   if (service->stop_method & NSSM_STOP_METHOD_TERMINATE) {
189     return TerminateProcess(service->process_handle, exitcode);
190   }
191
192   return 0;
193 }
194
195 /* Simulate a Control-C event to our console (shared with the app). */
196 int kill_console(nssm_service_t *service) {
197   unsigned long ret;
198
199   if (! service) return 1;
200
201   /* Check we loaded AttachConsole(). */
202   if (! imports.AttachConsole) return 4;
203
204   /* Try to attach to the process's console. */
205   if (! imports.AttachConsole(service->pid)) {
206     ret = GetLastError();
207
208     switch (ret) {
209       case ERROR_INVALID_HANDLE:
210         /* The app doesn't have a console. */
211         return 1;
212
213       case ERROR_GEN_FAILURE:
214         /* The app already exited. */
215         return 2;
216
217       case ERROR_ACCESS_DENIED:
218       default:
219         /* We already have a console. */
220         log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_ATTACHCONSOLE_FAILED, service->name, error_string(ret), 0);
221         return 3;
222     }
223   }
224
225   /* Ignore the event ourselves. */
226   ret = 0;
227   if (! SetConsoleCtrlHandler(0, TRUE)) {
228     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_SETCONSOLECTRLHANDLER_FAILED, service->name, error_string(GetLastError()), 0);
229     ret = 4;
230   }
231
232   /* Send the event. */
233   if (! ret) {
234     if (! GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) {
235       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GENERATECONSOLECTRLEVENT_FAILED, service->name, error_string(GetLastError()), 0);
236       ret = 5;
237     }
238   }
239
240   /* Detach from the console. */
241   if (! FreeConsole()) {
242     log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_FREECONSOLE_FAILED, service->name, error_string(GetLastError()), 0);
243   }
244
245   /* Wait for process to exit. */
246   if (await_shutdown(service, _T(__FUNCTION__), service->kill_console_delay)) ret = 6;
247
248   return ret;
249 }
250
251 void kill_process_tree(nssm_service_t *service, unsigned long pid, unsigned long exitcode, unsigned long ppid) {
252   /* Shouldn't happen unless the service failed to start. */
253   if (! pid) return;
254
255   TCHAR pid_string[16], code[16];
256   _sntprintf_s(pid_string, _countof(pid_string), _TRUNCATE, _T("%lu"), pid);
257   _sntprintf_s(code, _countof(code), _TRUNCATE, _T("%lu"), exitcode);
258   log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILLING, service->name, pid_string, code, 0);
259
260   /* Get a snapshot of all processes in the system. */
261   HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
262   if (! snapshot) {
263     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETOOLHELP32SNAPSHOT_PROCESS_FAILED, service->name, error_string(GetLastError()), 0);
264     return;
265   }
266
267   PROCESSENTRY32 pe;
268   ZeroMemory(&pe, sizeof(pe));
269   pe.dwSize = sizeof(pe);
270
271   if (! Process32First(snapshot, &pe)) {
272     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_PROCESS_ENUMERATE_FAILED, service->name, error_string(GetLastError()), 0);
273     CloseHandle(snapshot);
274     return;
275   }
276
277   /* This is a child of the doomed process so kill it. */
278   if (! check_parent(service, &pe, pid)) kill_process_tree(service, pe.th32ProcessID, exitcode, ppid);
279
280   while (true) {
281     /* Try to get the next process. */
282     if (! Process32Next(snapshot, &pe)) {
283       unsigned long ret = GetLastError();
284       if (ret == ERROR_NO_MORE_FILES) break;
285       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_PROCESS_ENUMERATE_FAILED, service->name, error_string(GetLastError()), 0);
286       CloseHandle(snapshot);
287       return;
288     }
289
290     if (! check_parent(service, &pe, pid)) kill_process_tree(service, pe.th32ProcessID, exitcode, ppid);
291   }
292
293   CloseHandle(snapshot);
294
295   /* We will need a process handle in order to call TerminateProcess() later. */
296   HANDLE process_handle = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, false, pid);
297   if (! process_handle) {
298     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENPROCESS_FAILED, pid_string, service->name, error_string(GetLastError()), 0);
299     return;
300   }
301
302   TCHAR ppid_string[16];
303   _sntprintf_s(ppid_string, _countof(ppid_string), _TRUNCATE, _T("%lu"), ppid);
304   log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILL_PROCESS_TREE, pid_string, ppid_string, service->name, 0);
305   if (! kill_process(service, process_handle, pid, exitcode)) {
306     /* Maybe it already died. */
307     unsigned long ret;
308     if (! GetExitCodeProcess(process_handle, &ret) || ret == STILL_ACTIVE) {
309       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);
310       else log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_PROCESS_STILL_ACTIVE, service->name, pid_string, NSSM, NSSM_REG_STOP_METHOD_SKIP, 0);
311     }
312   }
313
314   CloseHandle(process_handle);
315 }