Try to kill the process tree gracefully.
[nssm.git] / process.cpp
index 15fc83d..528b981 100644 (file)
 #include "nssm.h"
 
+/* Send some window messages and hope the window respects one or more. */
+int CALLBACK kill_window(HWND window, LPARAM arg) {
+  kill_t *k = (kill_t *) arg;
+
+  unsigned long pid;
+  if (! GetWindowThreadProcessId(window, &pid)) return 1;
+  if (pid != k->pid) return 1;
+
+  /* First try sending WM_CLOSE to request that the window close. */
+  k->signalled |= PostMessage(window, WM_CLOSE, k->exitcode, 0);
+
+  /*
+    Then tell the window that the user is logging off and it should exit
+    without worrying about saving any data.
+  */
+  k->signalled |= PostMessage(window, WM_ENDSESSION, 1, ENDSESSION_CLOSEAPP | ENDSESSION_CRITICAL | ENDSESSION_LOGOFF);
+
+  return 1;
+}
+
+/*
+  Try to post a message to the message queues of threads associated with the
+  given process ID.  Not all threads have message queues so there's no
+  guarantee of success, and we don't want to be left waiting for unsignalled
+  processes so this function returns only true if at least one thread was
+  successfully prodded.
+*/
+int kill_threads(char *service_name, kill_t *k) {
+  int ret = 0;
+
+  /* Get a snapshot of all threads in the system. */
+  HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
+  if (! snapshot) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETOOLHELP32SNAPSHOT_THREAD_FAILED, service_name, GetLastError(), 0);
+    return 0;
+  }
+
+  THREADENTRY32 te;
+  ZeroMemory(&te, sizeof(te));
+  te.dwSize = sizeof(te);
+
+  if (! Thread32First(snapshot, &te)) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_THREAD_ENUMERATE_FAILED, service_name, GetLastError(), 0);
+    return 0;
+  }
+
+  /* This thread belongs to the doomed process so signal it. */
+  if (te.th32OwnerProcessID == k->pid) {
+    ret |= PostThreadMessage(te.th32ThreadID, WM_QUIT, k->exitcode, 0);
+  }
+
+  while (true) {
+    /* Try to get the next thread. */
+    if (! Thread32Next(snapshot, &te)) {
+      unsigned long error = GetLastError();
+      if (error == ERROR_NO_MORE_FILES) break;
+      log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_THREAD_ENUMERATE_FAILED, service_name, GetLastError(), 0);
+      return ret;
+    }
+
+    if (te.th32OwnerProcessID == k->pid) {
+      ret |= PostThreadMessage(te.th32ThreadID, WM_QUIT, k->exitcode, 0);
+    }
+  }
+
+  return ret;
+}
+
+/* Give the process a chance to die gracefully. */
+int kill_process(char *service_name, HANDLE process_handle, unsigned long pid, unsigned long exitcode) {
+  /* Shouldn't happen. */
+  if (! pid) return 1;
+
+  kill_t k = { pid, exitcode, 0 };
+
+  /*
+    Try to post messages to the windows belonging to the given process ID.
+    If the process is a console application it won't have any windows so there's
+    no guarantee of success.
+  */
+  EnumWindows((WNDENUMPROC) kill_window, (LPARAM) &k);
+  if (k.signalled) {
+    if (! WaitForSingleObject(process_handle, NSSM_KILL_WINDOW_GRACE_PERIOD)) return 1;
+  }
+
+  /*
+    Try to post messages to any thread message queues associated with the
+    process.  Console applications might have them (but probably won't) so
+    there's still no guarantee of success.
+  */
+  if (kill_threads(service_name, &k)) {
+    if (! WaitForSingleObject(process_handle, NSSM_KILL_THREADS_GRACE_PERIOD)) return 1;
+  }
+
+  /* We tried being nice.  Time for extreme prejudice. */
+  return TerminateProcess(process_handle, exitcode);
+}
+
 void kill_process_tree(char *service_name, unsigned long pid, unsigned long exitcode, unsigned long ppid) {
   log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILLING, service_name, pid, exitcode, 0);
 
   /* Shouldn't happen. */
   if (! pid) return;
 
+  /* Get a snapshot of all processes in the system. */
   HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
   if (! snapshot) {
-    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETOOLHELP32SNAPSHOT_FAILED, service_name, GetLastError(), 0);
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETOOLHELP32SNAPSHOT_PROCESS_FAILED, service_name, GetLastError(), 0);
     return;
   }
 
@@ -21,6 +120,7 @@ void kill_process_tree(char *service_name, unsigned long pid, unsigned long exit
     return;
   }
 
+  /* This is a child of the doomed process so kill it. */
   if (pe.th32ParentProcessID == pid) kill_process_tree(service_name, pe.th32ProcessID, exitcode, ppid);
 
   while (true) {
@@ -35,6 +135,7 @@ void kill_process_tree(char *service_name, unsigned long pid, unsigned long exit
     if (pe.th32ParentProcessID == pid) kill_process_tree(service_name, pe.th32ProcessID, exitcode, ppid);
   }
 
+  /* We will need a process handle in order to call TerminateProcess() later. */
   HANDLE process_handle = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, false, pid);
   if (! process_handle) {
     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENPROCESS_FAILED, pid, service_name, GetLastError(), 0);
@@ -42,7 +143,7 @@ void kill_process_tree(char *service_name, unsigned long pid, unsigned long exit
   }
 
   log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILL_PROCESS_TREE, pid, ppid, service_name, 0);
-  if (! TerminateProcess(process_handle, exitcode)) {
+  if (! kill_process(service_name, process_handle, pid, exitcode)) {
     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_TERMINATEPROCESS_FAILED, pid, service_name, GetLastError(), 0);
     return;
   }