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