Command arguments no longer called options.
[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   /* We will need a process handle in order to call TerminateProcess() later. */
255   HANDLE process_handle = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, false, pid);
256   if (process_handle) {
257     /* Kill this process first, then its descendents. */
258     TCHAR ppid_string[16];
259     _sntprintf_s(ppid_string, _countof(ppid_string), _TRUNCATE, _T("%lu"), ppid);
260     log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILL_PROCESS_TREE, pid_string, ppid_string, service->name, 0);
261     if (! kill_process(service, process_handle, pid, exitcode)) {
262       /* Maybe it already died. */
263       unsigned long ret;
264       if (! GetExitCodeProcess(process_handle, &ret) || ret == STILL_ACTIVE) {
265         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);
266         else log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_PROCESS_STILL_ACTIVE, service->name, pid_string, NSSM, NSSM_REG_STOP_METHOD_SKIP, 0);
267       }
268     }
269
270     CloseHandle(process_handle);
271   }
272   else log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENPROCESS_FAILED, pid_string, service->name, error_string(GetLastError()), 0);
273
274   /* Get a snapshot of all processes in the system. */
275   HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
276   if (! snapshot) {
277     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETOOLHELP32SNAPSHOT_PROCESS_FAILED, service->name, error_string(GetLastError()), 0);
278     return;
279   }
280
281   PROCESSENTRY32 pe;
282   ZeroMemory(&pe, sizeof(pe));
283   pe.dwSize = sizeof(pe);
284
285   if (! Process32First(snapshot, &pe)) {
286     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_PROCESS_ENUMERATE_FAILED, service->name, error_string(GetLastError()), 0);
287     CloseHandle(snapshot);
288     return;
289   }
290
291   /* This is a child of the doomed process so kill it. */
292   if (! check_parent(service, &pe, pid)) kill_process_tree(service, pe.th32ProcessID, exitcode, ppid);
293
294   while (true) {
295     /* Try to get the next process. */
296     if (! Process32Next(snapshot, &pe)) {
297       unsigned long ret = GetLastError();
298       if (ret == ERROR_NO_MORE_FILES) break;
299       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_PROCESS_ENUMERATE_FAILED, service->name, error_string(GetLastError()), 0);
300       CloseHandle(snapshot);
301       return;
302     }
303
304     if (! check_parent(service, &pe, pid)) kill_process_tree(service, pe.th32ProcessID, exitcode, ppid);
305   }
306
307   CloseHandle(snapshot);
308 }