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