Browse Source

Run hooks in response to certain events.

User-configurable hooks will be run before and/or after certain events,
eg after the application starts successfully.

Hooks will run with the environment of the application, with additional
environment variables providing information about the service status and
the hook which has been called.
Iain Patterson 9 years ago
parent
commit
2f2f64b076
14 changed files with 964 additions and 16 deletions
  1. 114 0
      README.txt
  2. 154 1
      gui.cpp
  3. 405 0
      hook.cpp
  4. 75 0
      hook.h
  5. BIN
      messages.mc
  6. 7 0
      nssm.h
  7. BIN
      nssm.rc
  8. 8 0
      nssm.vcproj
  9. 54 0
      registry.cpp
  10. 3 0
      registry.h
  11. 7 2
      resource.h
  12. 84 13
      service.cpp
  13. 8 0
      service.h
  14. 45 0
      settings.cpp

+ 114 - 0
README.txt

@@ -66,6 +66,8 @@ type, log on details and dependencies.
 
 Since version 2.22, NSSM can manage existing services.
 
+Since version 2.25, NSSM can execute commands in response to service events.
+
 
 Usage
 -----
@@ -405,6 +407,118 @@ Most people will want to use AppEnvironmentExtra exclusively.  srvany only
 supports AppEnvironment.
 
 
+Event hooks
+-----------
+NSSM can run user-configurable commands in response to application events.
+These commands are referred to as "hooks" below.
+
+All hooks are optional.  Any hooks which are run will be launched with the
+environment configured for the service.  NSSM will place additional
+variables into the environment which hooks can query to learn how and why
+they were called.
+
+Hooks are categorised by Event and Action.  Some hooks are run synchronously
+and some are run asynchronously.  Hooks prefixed with an *asterisk are run
+synchronously.  NSSM will wait for these hooks to complete before continuing
+its work.  Note, however, that ALL hooks are subject to a deadline after which
+they will be killed, regardless of whether they are run asynchronously
+or not.
+
+  Event: Start - Triggered when the service is requested to start.
+   *Action: Pre - Called before NSSM attempts to launch the application.
+    Action: Post - Called after the application successfully starts.
+
+  Event: Stop - Triggered when the service is requested to stop.
+   *Action: Pre - Called before NSSM attempts to kill the application.
+
+  Event: Exit - Triggered when the application exits.
+   *Action: Post - Called after NSSM has cleaned up the application.
+
+  Event: Rotate - Triggered when online log rotation is requested.
+   *Action: Pre - Called before NSSM rotates logs.
+    Action: Post - Called after NSSM rotates logs.
+
+  Event: Power
+    Action: Change - Called when the system power status has changed.
+    Action: Resume - Called when the system has resumed from standby.
+
+Note that there is no Stop/Post hook.  This is because Exit/Post is called
+when the application exits, regardless of whether it did so in response to
+a service shutdown request.  Stop/Pre is only called before a graceful
+shutdown attempt.
+
+NSSM sets the environment variable NSSM_HOOK_VERSION to a positive number.
+Hooks can check the value of the number to determine which other environment
+variables are available to them.
+
+If NSSM_HOOK_VERSION is 1 or greater, these variables are provided:
+
+  NSSM_EXE - Path to NSSM itself.
+  NSSM_CONFIGURATION - Build information for the NSSM executable,
+    eg 64-bit debug.
+  NSSM_VERSION - Version of the NSSM executable.
+  NSSM_BUILD_DATE - Build date of NSSM.
+  NSSM_PID - Process ID of the running NSSM executable.
+  NSSM_DEADLINE - Deadline number of milliseconds after which NSSM will
+    kill the hook if it is still running.
+  NSSM_SERVICE_NAME - Name of the service controlled by NSSM.
+  NSSM_SERVICE_DISPLAYNAME - Display name of the service.
+  NSSM_COMMAND_LINE - Command line used to launch the application.
+  NSSM_APPLICATION_PID - Process ID of the primary application process.
+    May be blank if the process is not running.
+  NSSM_EVENT - Event class triggering the hook.
+  NSSM_ACTION - Event action triggering the hook.
+  NSSM_TRIGGER - Service control triggering the hook.  May be blank if
+    the hook was not triggered by a service control, eg Exit/Post.
+  NSSM_LAST_CONTROL - Last service control handled by NSSM.
+  NSSM_START_REQUESTED_COUNT - Number of times the application was
+    requested to start.
+  NSSM_START_COUNT - Number of times the application successfully started.
+  NSSM_THROTTLE_COUNT - Number of times the application ran for less than
+    the throttle period.  Reset to zero on successful start or when the
+    service is explicitly unpaused.
+  NSSM_EXIT_COUNT - Number of times the application exited.
+  NSSM_EXITCODE - Exit code of the application.  May be blank if the
+    application is still running or has not started yet.
+  NSSM_RUNTIME - Number of milliseconds for which the NSSM executable has
+    been running.
+  NSSM_APPLICATION_RUNTIME - Number of milliseconds for which the
+    application has been running since it was last started.  May be blank
+    if the application has not been started yet.
+
+Future versions of NSSM may provide more environment variables, in which
+case NSSM_HOOK_VERSION will be set to a higher number.
+
+Hooks are configured by creating string (REG_EXPAND_SZ) values in the
+registry named after the hook action and placed under
+HKLM\SYSTEM\CurrentControlSet\Services\<service>\Parameters\AppEvents\<event>.
+
+For example the service could be configured to restart when the system
+resumes from standby by setting AppEvents\Power\Resume to:
+
+    %NSSM_EXE% restart %NSSM_SERVICE_NAME%
+
+Note that NSSM will abort the startup of the application if a Start/Pre hook
+returns exit code of 99.
+
+A service will normally run hooks in the following order:
+
+  Start/Pre
+  Start/Post
+  Stop/Pre
+  Exit/Post
+
+If the application crashes and is restarted by NSSM, the order might be:
+
+  Start/Pre
+  Start/Post
+  Exit/Post
+  Start/Pre
+  Start/Post
+  Stop/Pre
+  Exit/Post
+
+
 Managing services using the GUI
 -------------------------------
 NSSM can edit the settings of existing services with the same GUI that is

+ 154 - 1
gui.cpp

@@ -1,7 +1,9 @@
 #include "nssm.h"
 
-static enum { NSSM_TAB_APPLICATION, NSSM_TAB_DETAILS, NSSM_TAB_LOGON, NSSM_TAB_DEPENDENCIES, NSSM_TAB_PROCESS, NSSM_TAB_SHUTDOWN, NSSM_TAB_EXIT, NSSM_TAB_IO, NSSM_TAB_ROTATION, NSSM_TAB_ENVIRONMENT, NSSM_NUM_TABS };
+static enum { NSSM_TAB_APPLICATION, NSSM_TAB_DETAILS, NSSM_TAB_LOGON, NSSM_TAB_DEPENDENCIES, NSSM_TAB_PROCESS, NSSM_TAB_SHUTDOWN, NSSM_TAB_EXIT, NSSM_TAB_IO, NSSM_TAB_ROTATION, NSSM_TAB_ENVIRONMENT, NSSM_TAB_HOOKS, NSSM_NUM_TABS };
 static HWND tablist[NSSM_NUM_TABS];
+static const TCHAR *hook_event_strings[] = { NSSM_HOOK_EVENT_START, NSSM_HOOK_EVENT_STOP, NSSM_HOOK_EVENT_EXIT, NSSM_HOOK_EVENT_POWER, NSSM_HOOK_EVENT_ROTATE, NULL };
+static const TCHAR *hook_action_strings[] = { NSSM_HOOK_ACTION_PRE, NSSM_HOOK_ACTION_POST, NSSM_HOOK_ACTION_CHANGE, NSSM_HOOK_ACTION_RESUME, NULL };
 static int selected_tab;
 
 static HWND dialog(const TCHAR *templ, HWND parent, DLGPROC function, LPARAM l) {
@@ -279,6 +281,100 @@ static inline void set_rotation_enabled(unsigned char enabled) {
   EnableWindow(GetDlgItem(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_BYTES_LOW), enabled);
 }
 
+static inline int hook_env(const TCHAR *hook_event, const TCHAR *hook_action, TCHAR *buffer, unsigned long buflen) {
+  return _sntprintf_s(buffer, buflen, _TRUNCATE, _T("NSSM_HOOK_%s_%s"), hook_event, hook_action);
+}
+
+static inline void set_hook_tab(int event_index, int action_index, bool changed) {
+  int first_event = NSSM_GUI_HOOK_EVENT_START;
+  HWND combo;
+  combo = GetDlgItem(tablist[NSSM_TAB_HOOKS], IDC_HOOK_EVENT);
+  SendMessage(combo, CB_SETCURSEL, event_index, 0);
+  combo = GetDlgItem(tablist[NSSM_TAB_HOOKS], IDC_HOOK_ACTION);
+  SendMessage(combo, CB_RESETCONTENT, 0, 0);
+
+  const TCHAR *hook_event = hook_event_strings[event_index];
+  TCHAR *hook_action;
+  int i;
+  switch (event_index + first_event) {
+    case NSSM_GUI_HOOK_EVENT_ROTATE:
+      i = 0;
+      SendMessage(combo, CB_INSERTSTRING, i, (LPARAM) message_string(NSSM_GUI_HOOK_ACTION_ROTATE_PRE));
+      if (action_index == i++) hook_action = NSSM_HOOK_ACTION_PRE;
+      SendMessage(combo, CB_INSERTSTRING, i, (LPARAM) message_string(NSSM_GUI_HOOK_ACTION_ROTATE_POST));
+      if (action_index == i++) hook_action = NSSM_HOOK_ACTION_POST;
+      break;
+
+    case NSSM_GUI_HOOK_EVENT_START:
+      i = 0;
+      SendMessage(combo, CB_INSERTSTRING, i, (LPARAM) message_string(NSSM_GUI_HOOK_ACTION_START_PRE));
+      if (action_index == i++) hook_action = NSSM_HOOK_ACTION_PRE;
+      SendMessage(combo, CB_INSERTSTRING, i, (LPARAM) message_string(NSSM_GUI_HOOK_ACTION_START_POST));
+      if (action_index == i++) hook_action = NSSM_HOOK_ACTION_POST;
+      break;
+
+    case NSSM_GUI_HOOK_EVENT_STOP:
+      i = 0;
+      SendMessage(combo, CB_INSERTSTRING, i, (LPARAM) message_string(NSSM_GUI_HOOK_ACTION_STOP_PRE));
+      if (action_index == i++) hook_action = NSSM_HOOK_ACTION_PRE;
+      break;
+
+    case NSSM_GUI_HOOK_EVENT_EXIT:
+      i = 0;
+      SendMessage(combo, CB_INSERTSTRING, i, (LPARAM) message_string(NSSM_GUI_HOOK_ACTION_EXIT_POST));
+      if (action_index == i++) hook_action = NSSM_HOOK_ACTION_POST;
+      break;
+
+    case NSSM_GUI_HOOK_EVENT_POWER:
+      i = 0;
+      SendMessage(combo, CB_INSERTSTRING, i, (LPARAM) message_string(NSSM_GUI_HOOK_ACTION_POWER_CHANGE));
+      if (action_index == i++) hook_action = NSSM_HOOK_ACTION_CHANGE;
+      SendMessage(combo, CB_INSERTSTRING, i, (LPARAM) message_string(NSSM_GUI_HOOK_ACTION_POWER_RESUME));
+      if (action_index == i++) hook_action = NSSM_HOOK_ACTION_RESUME;
+      break;
+  }
+
+  SendMessage(combo, CB_SETCURSEL, action_index, 0);
+
+  TCHAR hook_name[HOOK_NAME_LENGTH];
+  hook_env(hook_event, hook_action, hook_name, _countof(hook_name));
+
+  if (! *hook_name) return;
+
+  TCHAR cmd[CMD_LENGTH];
+  if (changed) {
+    GetDlgItemText(tablist[NSSM_TAB_HOOKS], IDC_HOOK, cmd, _countof(cmd));
+    SetEnvironmentVariable(hook_name, cmd);
+  }
+  else {
+    GetEnvironmentVariable(hook_name, cmd, _countof(cmd));
+    SetDlgItemText(tablist[NSSM_TAB_HOOKS], IDC_HOOK, cmd);
+  }
+}
+
+static inline int update_hook(TCHAR *service_name, const TCHAR *hook_event, const TCHAR *hook_action) {
+  TCHAR hook_name[HOOK_NAME_LENGTH];
+  if (hook_env(hook_event, hook_action, hook_name, _countof(hook_name)) < 0) return 1;
+  TCHAR cmd[CMD_LENGTH];
+  ZeroMemory(cmd, sizeof(cmd));
+  GetEnvironmentVariable(hook_name, cmd, _countof(cmd));
+  if (set_hook(service_name, hook_event, hook_action, cmd)) return 2;
+  return 0;
+}
+
+static inline int update_hooks(TCHAR *service_name) {
+  int ret = 0;
+  ret += update_hook(service_name, NSSM_HOOK_EVENT_START, NSSM_HOOK_ACTION_PRE);
+  ret += update_hook(service_name, NSSM_HOOK_EVENT_START, NSSM_HOOK_ACTION_POST);
+  ret += update_hook(service_name, NSSM_HOOK_EVENT_STOP, NSSM_HOOK_ACTION_PRE);
+  ret += update_hook(service_name, NSSM_HOOK_EVENT_EXIT, NSSM_HOOK_ACTION_POST);
+  ret += update_hook(service_name, NSSM_HOOK_EVENT_POWER, NSSM_HOOK_ACTION_CHANGE);
+  ret += update_hook(service_name, NSSM_HOOK_EVENT_POWER, NSSM_HOOK_ACTION_RESUME);
+  ret += update_hook(service_name, NSSM_HOOK_EVENT_ROTATE, NSSM_HOOK_ACTION_PRE);
+  ret += update_hook(service_name, NSSM_HOOK_EVENT_ROTATE, NSSM_HOOK_ACTION_POST);
+  return ret;
+}
+
 static inline void check_io(HWND owner, TCHAR *name, TCHAR *buffer, unsigned long len, unsigned long control) {
   if (! SendMessage(GetDlgItem(tablist[NSSM_TAB_IO], control), WM_GETTEXTLENGTH, 0, 0)) return;
   if (GetDlgItemText(tablist[NSSM_TAB_IO], control, buffer, (int) len)) return;
@@ -671,6 +767,8 @@ int install(HWND window) {
       return 6;
   }
 
+  update_hooks(service->name);
+
   popup_message(window, MB_OK, NSSM_MESSAGE_SERVICE_INSTALLED, service->name);
   cleanup_nssm_service(service);
   return 0;
@@ -756,6 +854,8 @@ int edit(HWND window, nssm_service_t *orig_service) {
       return 6;
   }
 
+  update_hooks(service->name);
+
   popup_message(window, MB_OK, NSSM_MESSAGE_SERVICE_EDITED, service->name);
   cleanup_nssm_service(service);
   return 0;
@@ -929,6 +1029,28 @@ INT_PTR CALLBACK tab_dlg(HWND tab, UINT message, WPARAM w, LPARAM l) {
           else enabled = 0;
           set_rotation_enabled(enabled);
           break;
+
+        /* Hook event. */
+        case IDC_HOOK_EVENT:
+          if (HIWORD(w) == CBN_SELCHANGE) set_hook_tab((int) SendMessage(GetDlgItem(tab, IDC_HOOK_EVENT), CB_GETCURSEL, 0, 0), 0, false);
+          break;
+
+        /* Hook action. */
+        case IDC_HOOK_ACTION:
+          if (HIWORD(w) == CBN_SELCHANGE) set_hook_tab((int) SendMessage(GetDlgItem(tab, IDC_HOOK_EVENT), CB_GETCURSEL, 0, 0), (int) SendMessage(GetDlgItem(tab, IDC_HOOK_ACTION), CB_GETCURSEL, 0, 0), false);
+          break;
+
+        /* Browse for hook. */
+        case IDC_BROWSE_HOOK:
+          dlg = GetDlgItem(tab, IDC_HOOK);
+          GetDlgItemText(tab, IDC_HOOK, buffer, _countof(buffer));
+          browse(dlg, _T(""), OFN_FILEMUSTEXIST, NSSM_GUI_BROWSE_FILTER_ALL_FILES, 0);
+          break;
+
+        /* Hook. */
+        case IDC_HOOK:
+          set_hook_tab((int) SendMessage(GetDlgItem(tab, IDC_HOOK_EVENT), CB_GETCURSEL, 0, 0), (int) SendMessage(GetDlgItem(tab, IDC_HOOK_ACTION), CB_GETCURSEL, 0, 0), true);
+          break;
       }
       return 1;
   }
@@ -1121,6 +1243,37 @@ INT_PTR CALLBACK nssm_dlg(HWND window, UINT message, WPARAM w, LPARAM l) {
       tablist[NSSM_TAB_ENVIRONMENT] = dialog(MAKEINTRESOURCE(IDD_ENVIRONMENT), window, tab_dlg);
       ShowWindow(tablist[NSSM_TAB_ENVIRONMENT], SW_HIDE);
 
+      /* Hooks tab. */
+      tab.pszText = message_string(NSSM_GUI_TAB_HOOKS);
+      tab.cchTextMax = (int) _tcslen(tab.pszText) + 1;
+      SendMessage(tabs, TCM_INSERTITEM, NSSM_TAB_HOOKS, (LPARAM) &tab);
+      tablist[NSSM_TAB_HOOKS] = dialog(MAKEINTRESOURCE(IDD_HOOKS), window, tab_dlg);
+      ShowWindow(tablist[NSSM_TAB_HOOKS], SW_HIDE);
+
+      /* Set defaults. */
+      combo = GetDlgItem(tablist[NSSM_TAB_HOOKS], IDC_HOOK_EVENT);
+      SendMessage(combo, CB_INSERTSTRING, -1, (LPARAM) message_string(NSSM_GUI_HOOK_EVENT_START));
+      SendMessage(combo, CB_INSERTSTRING, -1, (LPARAM) message_string(NSSM_GUI_HOOK_EVENT_STOP));
+      SendMessage(combo, CB_INSERTSTRING, -1, (LPARAM) message_string(NSSM_GUI_HOOK_EVENT_EXIT));
+      SendMessage(combo, CB_INSERTSTRING, -1, (LPARAM) message_string(NSSM_GUI_HOOK_EVENT_POWER));
+      SendMessage(combo, CB_INSERTSTRING, -1, (LPARAM) message_string(NSSM_GUI_HOOK_EVENT_ROTATE));
+      if (_tcslen(service->name)) {
+        TCHAR hook_name[HOOK_NAME_LENGTH];
+        TCHAR cmd[CMD_LENGTH];
+        for (i = 0; hook_event_strings[i]; i++) {
+          const TCHAR *hook_event = hook_event_strings[i];
+          int j;
+          for (j = 0; hook_action_strings[j]; j++) {
+            const TCHAR *hook_action = hook_action_strings[j];
+            if (! valid_hook_name(hook_event, hook_action, true)) continue;
+            if (get_hook(service->name, hook_event, hook_action, cmd, sizeof(cmd))) continue;
+            if (hook_env(hook_event, hook_action, hook_name, _countof(hook_name)) < 0) continue;
+            SetEnvironmentVariable(hook_name, cmd);
+          }
+        }
+      }
+      set_hook_tab(0, 0, false);
+
       return 1;
 
     /* Tab change. */

+ 405 - 0
hook.cpp

@@ -0,0 +1,405 @@
+#include "nssm.h"
+
+typedef struct {
+  TCHAR *name;
+  HANDLE process_handle;
+  unsigned long pid;
+  unsigned long deadline;
+  FILETIME creation_time;
+  kill_t k;
+} hook_t;
+
+static unsigned long WINAPI await_hook(void *arg) {
+  hook_t *hook = (hook_t *) arg;
+  if (! hook) return NSSM_HOOK_STATUS_ERROR;
+
+  int ret = 0;
+  if (WaitForSingleObject(hook->process_handle, hook->deadline) == WAIT_TIMEOUT) ret = NSSM_HOOK_STATUS_TIMEOUT;
+
+  /* Tidy up hook process tree. */
+  if (hook->name) hook->k.name = hook->name;
+  else hook->k.name = _T("hook");
+  hook->k.process_handle = hook->process_handle;
+  hook->k.pid = hook->pid;
+  hook->k.stop_method = ~0;
+  hook->k.kill_console_delay = NSSM_KILL_CONSOLE_GRACE_PERIOD;
+  hook->k.kill_window_delay = NSSM_KILL_WINDOW_GRACE_PERIOD;
+  hook->k.kill_threads_delay = NSSM_KILL_THREADS_GRACE_PERIOD;
+  hook->k.creation_time = hook->creation_time;
+  GetSystemTimeAsFileTime(&hook->k.exit_time);
+  kill_process_tree(&hook->k, hook->pid);
+
+  if (ret) {
+    CloseHandle(hook->process_handle);
+    if (hook->name) HeapFree(GetProcessHeap(), 0, hook->name);
+    HeapFree(GetProcessHeap(), 0, hook);
+    return ret;
+  }
+
+  unsigned long exitcode;
+  GetExitCodeProcess(hook->process_handle, &exitcode);
+  CloseHandle(hook->process_handle);
+
+  if (hook->name) HeapFree(GetProcessHeap(), 0, hook->name);
+  HeapFree(GetProcessHeap(), 0, hook);
+
+  if (exitcode == NSSM_HOOK_STATUS_ABORT) return NSSM_HOOK_STATUS_ABORT;
+  if (exitcode) return NSSM_HOOK_STATUS_FAILED;
+
+  return NSSM_HOOK_STATUS_SUCCESS;
+}
+
+static void set_hook_runtime(TCHAR *v, FILETIME *start, FILETIME *now) {
+  if (start && now) {
+    ULARGE_INTEGER s;
+    s.LowPart = start->dwLowDateTime;
+    s.HighPart = start->dwHighDateTime;
+    if (s.QuadPart) {
+      ULARGE_INTEGER t;
+      t.LowPart = now->dwLowDateTime;
+      t.HighPart = now->dwHighDateTime;
+      if (t.QuadPart && t.QuadPart >= s.QuadPart) {
+        t.QuadPart -= s.QuadPart;
+        t.QuadPart /= 10000LL;
+        TCHAR number[16];
+        _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%llu"), t.QuadPart);
+        SetEnvironmentVariable(v, number);
+        return;
+      }
+    }
+  }
+  SetEnvironmentVariable(v, _T(""));
+}
+
+static void add_thread_handle(hook_thread_t *hook_threads, HANDLE thread_handle, TCHAR *name) {
+  if (! hook_threads) return;
+
+  int num_threads = hook_threads->num_threads + 1;
+  hook_thread_data_t *data = (hook_thread_data_t *) HeapAlloc(GetProcessHeap(), 0, num_threads * sizeof(hook_thread_data_t));
+  if (! data) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("hook_thread_t"), _T("add_thread_handle()"), 0);
+    return;
+  }
+
+  int i;
+  for (i = 0; i < hook_threads->num_threads; i++) memmove(&data[i], &hook_threads->data[i], sizeof(data[i]));
+  memmove(data[i].name, name, sizeof(data[i].name));
+  data[i].thread_handle = thread_handle;
+
+  if (hook_threads->data) HeapFree(GetProcessHeap(), 0, hook_threads->data);
+  hook_threads->data = data;
+  hook_threads->num_threads = num_threads;
+}
+
+bool valid_hook_name(const TCHAR *hook_event, const TCHAR *hook_action, bool quiet) {
+  bool valid_event = false;
+  bool valid_action = false;
+
+  /* Exit/Post */
+  if (str_equiv(hook_event, NSSM_HOOK_EVENT_EXIT)) {
+    if (str_equiv(hook_action, NSSM_HOOK_ACTION_POST)) return true;
+    if (quiet) return false;
+    print_message(stderr, NSSM_MESSAGE_INVALID_HOOK_ACTION, hook_event);
+    _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_ACTION_POST);
+    return false;
+  }
+
+  /* Power/{Change,Resume} */
+  if (str_equiv(hook_event, NSSM_HOOK_EVENT_POWER)) {
+    if (str_equiv(hook_action, NSSM_HOOK_ACTION_CHANGE)) return true;
+    if (str_equiv(hook_action, NSSM_HOOK_ACTION_RESUME)) return true;
+    if (quiet) return false;
+    print_message(stderr, NSSM_MESSAGE_INVALID_HOOK_ACTION, hook_event);
+    _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_ACTION_CHANGE);
+    _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_ACTION_RESUME);
+    return false;
+  }
+
+  /* Rotate/{Pre,Post} */
+  if (str_equiv(hook_event, NSSM_HOOK_EVENT_ROTATE)) {
+    if (str_equiv(hook_action, NSSM_HOOK_ACTION_PRE)) return true;
+    if (str_equiv(hook_action, NSSM_HOOK_ACTION_POST)) return true;
+    if (quiet) return false;
+    print_message(stderr, NSSM_MESSAGE_INVALID_HOOK_ACTION, hook_event);
+    _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_ACTION_PRE);
+    _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_ACTION_POST);
+    return false;
+  }
+
+  /* Start/{Pre,Post} */
+  if (str_equiv(hook_event, NSSM_HOOK_EVENT_START)) {
+    if (str_equiv(hook_action, NSSM_HOOK_ACTION_PRE)) return true;
+    if (str_equiv(hook_action, NSSM_HOOK_ACTION_POST)) return true;
+    if (quiet) return false;
+    print_message(stderr, NSSM_MESSAGE_INVALID_HOOK_ACTION, hook_event);
+    _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_ACTION_PRE);
+    _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_ACTION_POST);
+    return false;
+  }
+
+  /* Stop/Pre */
+  if (str_equiv(hook_event, NSSM_HOOK_EVENT_STOP)) {
+    if (str_equiv(hook_action, NSSM_HOOK_ACTION_PRE)) return true;
+    if (quiet) return false;
+    print_message(stderr, NSSM_MESSAGE_INVALID_HOOK_ACTION, hook_event);
+    _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_ACTION_PRE);
+    return false;
+  }
+
+  if (quiet) return false;
+  print_message(stderr, NSSM_MESSAGE_INVALID_HOOK_EVENT);
+  _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_EVENT_EXIT);
+  _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_EVENT_POWER);
+  _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_EVENT_ROTATE);
+  _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_EVENT_START);
+  _ftprintf(stderr, _T("%s\n"), NSSM_HOOK_EVENT_STOP);
+  return false;
+}
+
+void await_hook_threads(hook_thread_t *hook_threads, SERVICE_STATUS_HANDLE status_handle, SERVICE_STATUS *status, unsigned long deadline) {
+  if (! hook_threads) return;
+  if (! hook_threads->num_threads) return;
+
+  int *retain = (int *) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, hook_threads->num_threads * sizeof(int));
+  if (! retain) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("retain"), _T("await_hook_threads()"), 0);
+    return;
+  }
+
+  /*
+    We could use WaitForMultipleObjects() but await_single_object() can update
+    the service status as well.
+  */
+  int num_threads = 0;
+  int i;
+  for (i = 0; i < hook_threads->num_threads; i++) {
+    if (deadline) {
+      if (await_single_handle(status_handle, status, hook_threads->data[i].thread_handle, hook_threads->data[i].name, _T(__FUNCTION__), deadline) != 1) {
+        CloseHandle(hook_threads->data[i].thread_handle);
+        continue;
+      }
+    }
+    else if (WaitForSingleObject(hook_threads->data[i].thread_handle, 0) != WAIT_TIMEOUT) {
+      CloseHandle(hook_threads->data[i].thread_handle);
+      continue;
+    }
+
+    retain[num_threads++]= i;
+  }
+
+  if (num_threads) {
+    hook_thread_data_t *data = (hook_thread_data_t *) HeapAlloc(GetProcessHeap(), 0, num_threads * sizeof(hook_thread_data_t));
+    if (! data) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("data"), _T("await_hook_threads()"), 0);
+      HeapFree(GetProcessHeap(), 0, retain);
+      return;
+    }
+
+    for (i = 0; i < num_threads; i++) memmove(&data[i], &hook_threads->data[retain[i]], sizeof(data[i]));
+
+    HeapFree(GetProcessHeap(), 0, hook_threads->data);
+    hook_threads->data = data;
+    hook_threads->num_threads = num_threads;
+  }
+  else {
+    HeapFree(GetProcessHeap(), 0, hook_threads->data);
+    ZeroMemory(hook_threads, sizeof(*hook_threads));
+  }
+
+  HeapFree(GetProcessHeap(), 0, retain);
+}
+
+/*
+   Returns:
+   NSSM_HOOK_STATUS_SUCCESS  if the hook ran successfully.
+   NSSM_HOOK_STATUS_NOTFOUND if no hook was found.
+   NSSM_HOOK_STATUS_ABORT    if the hook failed and we should cancel service start.
+   NSSM_HOOK_STATUS_ERROR    on error.
+   NSSM_HOOK_STATUS_NOTRUN   if the hook didn't run.
+   NSSM_HOOK_STATUS_TIMEOUT  if the hook timed out.
+   NSSM_HOOK_STATUS_FAILED   if the hook failed.
+*/
+int nssm_hook(hook_thread_t *hook_threads, nssm_service_t *service, TCHAR *hook_event, TCHAR *hook_action, unsigned long *hook_control, unsigned long deadline, bool async) {
+  int ret = 0;
+
+  hook_t *hook = (hook_t *) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(hook_t));
+  if (! hook) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("hook"), _T("nssm_hook()"), 0);
+    return NSSM_HOOK_STATUS_ERROR;
+  }
+
+  FILETIME now;
+  GetSystemTimeAsFileTime(&now);
+
+  EnterCriticalSection(&service->hook_section);
+
+  /* Set the environment. */
+  if (service->env) duplicate_environment(service->env);
+  if (service->env_extra) set_environment_block(service->env_extra);
+
+  /* ABI version. */
+  TCHAR number[16];
+  _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), NSSM_HOOK_VERSION);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_VERSION, number);
+
+  /* Event triggering this action. */
+  SetEnvironmentVariable(NSSM_HOOK_ENV_EVENT, hook_event);
+
+  /* Hook action. */
+  SetEnvironmentVariable(NSSM_HOOK_ENV_ACTION, hook_action);
+
+  /* Control triggering this action.  May be empty. */
+  if (hook_control) SetEnvironmentVariable(NSSM_HOOK_ENV_TRIGGER, service_control_text(*hook_control));
+  else SetEnvironmentVariable(NSSM_HOOK_ENV_TRIGGER, _T(""));
+
+  /* Last control handled. */
+  SetEnvironmentVariable(NSSM_HOOK_ENV_LAST_CONTROL, service_control_text(service->last_control));
+
+  /* Path to NSSM. */
+  TCHAR path[PATH_LENGTH];
+  GetModuleFileName(0, path, _countof(path));
+  SetEnvironmentVariable(NSSM_HOOK_ENV_IMAGE_PATH, path);
+
+  /* NSSM version. */
+  SetEnvironmentVariable(NSSM_HOOK_ENV_NSSM_CONFIGURATION, NSSM_CONFIGURATION);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_NSSM_VERSION, NSSM_VERSION);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_BUILD_DATE, NSSM_DATE);
+
+  /* NSSM PID. */
+  _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), GetCurrentProcessId());
+  SetEnvironmentVariable(NSSM_HOOK_ENV_PID, number);
+
+  /* NSSM runtime. */
+  set_hook_runtime(NSSM_HOOK_ENV_RUNTIME, &service->nssm_creation_time, &now);
+
+  /* Application PID. */
+  if (service->pid) {
+    _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), service->pid);
+    SetEnvironmentVariable(NSSM_HOOK_ENV_APPLICATION_PID, number);
+    /* Application runtime. */
+    set_hook_runtime(NSSM_HOOK_ENV_APPLICATION_RUNTIME, &service->creation_time, &now);
+    /* Exit code. */
+    SetEnvironmentVariable(NSSM_HOOK_ENV_EXITCODE, _T(""));
+  }
+  else {
+    SetEnvironmentVariable(NSSM_HOOK_ENV_APPLICATION_PID, _T(""));
+    if (str_equiv(hook_event, NSSM_HOOK_EVENT_START) && str_equiv(hook_action, NSSM_HOOK_ACTION_PRE)) {
+      SetEnvironmentVariable(NSSM_HOOK_ENV_APPLICATION_RUNTIME, _T(""));
+      SetEnvironmentVariable(NSSM_HOOK_ENV_EXITCODE, _T(""));
+    }
+    else {
+      set_hook_runtime(NSSM_HOOK_ENV_APPLICATION_RUNTIME, &service->creation_time, &service->exit_time);
+      /* Exit code. */
+      _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), service->exitcode);
+      SetEnvironmentVariable(NSSM_HOOK_ENV_EXITCODE, number);
+    }
+  }
+
+  /* Deadline for this script. */
+  _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), deadline);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_DEADLINE, number);
+
+  /* Service name. */
+  SetEnvironmentVariable(NSSM_HOOK_ENV_SERVICE_NAME, service->name);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_SERVICE_DISPLAYNAME, service->displayname);
+
+  /* Times the service was asked to start. */
+  _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), service->start_requested_count);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_START_REQUESTED_COUNT, number);
+
+  /* Times the service actually did start. */
+  _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), service->start_count);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_START_COUNT, number);
+
+  /* Times the service exited. */
+  _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), service->exit_count);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_EXIT_COUNT, number);
+
+  /* Throttled count. */
+  _sntprintf_s(number, _countof(number), _TRUNCATE, _T("%lu"), service->throttle);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_THROTTLE_COUNT, number);
+
+  /* Command line. */
+  TCHAR app[CMD_LENGTH];
+  _sntprintf_s(app, _countof(app), _TRUNCATE, _T("\"%s\" %s"), service->exe, service->flags);
+  SetEnvironmentVariable(NSSM_HOOK_ENV_COMMAND_LINE, app);
+
+  TCHAR cmd[CMD_LENGTH];
+  if (get_hook(service->name, hook_event, hook_action, cmd, sizeof(cmd))) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GET_HOOK_FAILED, hook_event, hook_action, service->name, 0);
+    duplicate_environment_strings(service->initial_env);
+    LeaveCriticalSection(&service->hook_section);
+    HeapFree(GetProcessHeap(), 0, hook);
+    return NSSM_HOOK_STATUS_ERROR;
+  }
+
+  /* No hook. */
+  if (! _tcslen(cmd)) {
+    duplicate_environment_strings(service->initial_env);
+    LeaveCriticalSection(&service->hook_section);
+    HeapFree(GetProcessHeap(), 0, hook);
+    return NSSM_HOOK_STATUS_NOTFOUND;
+  }
+
+  /* Run the command. */
+  STARTUPINFO si;
+  ZeroMemory(&si, sizeof(si));
+  si.cb = sizeof(si);
+  PROCESS_INFORMATION pi;
+  ZeroMemory(&pi, sizeof(pi));
+  unsigned long flags = 0;
+#ifdef UNICODE
+  flags |= CREATE_UNICODE_ENVIRONMENT;
+#endif
+  ret = NSSM_HOOK_STATUS_NOTRUN;
+  if (CreateProcess(0, cmd, 0, 0, false, flags, 0, service->dir, &si, &pi)) {
+    hook->name = (TCHAR *) HeapAlloc(GetProcessHeap(), 0, HOOK_NAME_LENGTH * sizeof(TCHAR));
+    if (hook->name) _sntprintf_s(hook->name, HOOK_NAME_LENGTH, _TRUNCATE, _T("%s (%s/%s)"), service->name, hook_event, hook_action);
+    hook->process_handle = pi.hProcess;
+    hook->pid = pi.dwProcessId;
+    hook->deadline = deadline;
+    if (get_process_creation_time(hook->process_handle, &hook->creation_time)) GetSystemTimeAsFileTime(&hook->creation_time);
+
+    unsigned long tid;
+    HANDLE thread_handle = CreateThread(NULL, 0, await_hook, (void *) hook, 0, &tid);
+    if (thread_handle) {
+      if (async) {
+        ret = 0;
+        await_hook_threads(hook_threads, service->status_handle, &service->status, 0);
+        add_thread_handle(hook_threads, thread_handle, hook->name);
+      }
+      else {
+        await_single_handle(service->status_handle, &service->status, thread_handle, hook->name, _T(__FUNCTION__), deadline + NSSM_SERVICE_STATUS_DEADLINE);
+        unsigned long exitcode;
+        GetExitCodeThread(thread_handle, &exitcode);
+        ret = (int) exitcode;
+        CloseHandle(thread_handle);
+      }
+    }
+    else {
+      log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETHREAD_FAILED, error_string(GetLastError()), 0);
+      await_hook(hook);
+      if (hook->name) HeapFree(GetProcessHeap(), 0, hook->name);
+      HeapFree(GetProcessHeap(), 0, hook);
+    }
+  }
+  else {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_HOOK_CREATEPROCESS_FAILED, hook_event, hook_action, service->name, cmd, error_string(GetLastError()), 0);
+    HeapFree(GetProcessHeap(), 0, hook);
+  }
+
+  /* Restore our environment. */
+  duplicate_environment_strings(service->initial_env);
+
+  LeaveCriticalSection(&service->hook_section);
+
+  return ret;
+}
+
+int nssm_hook(hook_thread_t *hook_threads, nssm_service_t *service, TCHAR *hook_event, TCHAR *hook_action, unsigned long *hook_control, unsigned long deadline) {
+  return nssm_hook(hook_threads, service, hook_event, hook_action, hook_control, deadline, true);
+}
+
+int nssm_hook(hook_thread_t *hook_threads, nssm_service_t *service, TCHAR *hook_event, TCHAR *hook_action, unsigned long *hook_control) {
+  return nssm_hook(hook_threads, service, hook_event, hook_action, hook_control, NSSM_HOOK_DEADLINE);
+}

+ 75 - 0
hook.h

@@ -0,0 +1,75 @@
+#ifndef HOOK_H
+#define HOOK_H
+
+#define NSSM_HOOK_EVENT_START _T("Start")
+#define NSSM_HOOK_EVENT_STOP _T("Stop")
+#define NSSM_HOOK_EVENT_EXIT _T("Exit")
+#define NSSM_HOOK_EVENT_POWER _T("Power")
+#define NSSM_HOOK_EVENT_ROTATE _T("Rotate")
+
+#define NSSM_HOOK_ACTION_PRE _T("Pre")
+#define NSSM_HOOK_ACTION_POST _T("Post")
+#define NSSM_HOOK_ACTION_CHANGE _T("Change")
+#define NSSM_HOOK_ACTION_RESUME _T("Resume")
+
+/* Hook name will be "<service> (<event>/<action>)" */
+#define HOOK_NAME_LENGTH SERVICE_NAME_LENGTH * 2
+
+#define NSSM_HOOK_VERSION 1
+
+/* Hook ran successfully. */
+#define NSSM_HOOK_STATUS_SUCCESS 0
+/* No hook configured. */
+#define NSSM_HOOK_STATUS_NOTFOUND 1
+/* Hook requested abort. */
+#define NSSM_HOOK_STATUS_ABORT 99
+/* Internal error launching hook. */
+#define NSSM_HOOK_STATUS_ERROR 100
+/* Hook was not run. */
+#define NSSM_HOOK_STATUS_NOTRUN 101
+/* Hook timed out. */
+#define NSSM_HOOK_STATUS_TIMEOUT 102
+/* Hook returned non-zero. */
+#define NSSM_HOOK_STATUS_FAILED 111
+
+/* Version 1. */
+#define NSSM_HOOK_ENV_VERSION _T("NSSM_HOOK_VERSION")
+#define NSSM_HOOK_ENV_IMAGE_PATH _T("NSSM_EXE")
+#define NSSM_HOOK_ENV_NSSM_CONFIGURATION _T("NSSM_CONFIGURATION")
+#define NSSM_HOOK_ENV_NSSM_VERSION _T("NSSM_VERSION")
+#define NSSM_HOOK_ENV_BUILD_DATE _T("NSSM_BUILD_DATE")
+#define NSSM_HOOK_ENV_PID _T("NSSM_PID")
+#define NSSM_HOOK_ENV_DEADLINE _T("NSSM_DEADLINE")
+#define NSSM_HOOK_ENV_SERVICE_NAME _T("NSSM_SERVICE_NAME")
+#define NSSM_HOOK_ENV_SERVICE_DISPLAYNAME _T("NSSM_SERVICE_DISPLAYNAME")
+#define NSSM_HOOK_ENV_COMMAND_LINE _T("NSSM_COMMAND_LINE")
+#define NSSM_HOOK_ENV_APPLICATION_PID _T("NSSM_APPLICATION_PID")
+#define NSSM_HOOK_ENV_EVENT _T("NSSM_EVENT")
+#define NSSM_HOOK_ENV_ACTION _T("NSSM_ACTION")
+#define NSSM_HOOK_ENV_TRIGGER _T("NSSM_TRIGGER")
+#define NSSM_HOOK_ENV_LAST_CONTROL _T("NSSM_LAST_CONTROL")
+#define NSSM_HOOK_ENV_START_REQUESTED_COUNT _T("NSSM_START_REQUESTED_COUNT")
+#define NSSM_HOOK_ENV_START_COUNT _T("NSSM_START_COUNT")
+#define NSSM_HOOK_ENV_THROTTLE_COUNT _T("NSSM_THROTTLE_COUNT")
+#define NSSM_HOOK_ENV_EXIT_COUNT _T("NSSM_EXIT_COUNT")
+#define NSSM_HOOK_ENV_EXITCODE _T("NSSM_EXITCODE")
+#define NSSM_HOOK_ENV_RUNTIME _T("NSSM_RUNTIME")
+#define NSSM_HOOK_ENV_APPLICATION_RUNTIME _T("NSSM_APPLICATION_RUNTIME")
+
+typedef struct {
+  TCHAR name[HOOK_NAME_LENGTH];
+  HANDLE thread_handle;
+} hook_thread_data_t;
+
+typedef struct {
+  hook_thread_data_t *data;
+  int num_threads;
+} hook_thread_t;
+
+bool valid_hook_name(const TCHAR *, const TCHAR *, bool);
+void await_hook_threads(hook_thread_t *, SERVICE_STATUS_HANDLE, SERVICE_STATUS *, unsigned long);
+int nssm_hook(hook_thread_t *, nssm_service_t *, TCHAR *, TCHAR *, unsigned long *, unsigned long, bool);
+int nssm_hook(hook_thread_t *, nssm_service_t *, TCHAR *, TCHAR *, unsigned long *, unsigned long);
+int nssm_hook(hook_thread_t *, nssm_service_t *, TCHAR *, TCHAR *, unsigned long *);
+
+#endif

BIN
messages.mc


+ 7 - 0
nssm.h

@@ -45,6 +45,7 @@
 #include "console.h"
 #include "env.h"
 #include "event.h"
+#include "hook.h"
 #include "imports.h"
 #include "messages.h"
 #include "process.h"
@@ -136,4 +137,10 @@ int usage(int);
 #define NSSM_SERVICE_CONTROL_START 0
 #define NSSM_SERVICE_CONTROL_ROTATE 128
 
+/* How many milliseconds to wait for a hook. */
+#define NSSM_HOOK_DEADLINE 60000
+
+/* How many milliseconds to wait for outstanding hooks. */
+#define NSSM_HOOK_THREAD_DEADLINE 80000
+
 #endif

BIN
nssm.rc


+ 8 - 0
nssm.vcproj

@@ -486,6 +486,10 @@
 					/>
 				</FileConfiguration>
 			</File>
+			<File
+				RelativePath="hook.cpp"
+				>
+			</File>
 			<File
 				RelativePath="imports.cpp"
 				>
@@ -667,6 +671,10 @@
 				RelativePath="gui.h"
 				>
 			</File>
+			<File
+				RelativePath="hook.h"
+				>
+			</File>
 			<File
 				RelativePath="imports.h"
 				>

+ 54 - 0
registry.cpp

@@ -735,3 +735,57 @@ int get_exit_action(const TCHAR *service_name, unsigned long *ret, TCHAR *action
 
   return 0;
 }
+
+int set_hook(const TCHAR *service_name, const TCHAR *hook_event, const TCHAR *hook_action, TCHAR *cmd) {
+  /* Try to open the registry */
+  TCHAR registry[KEY_LENGTH];
+  if (_sntprintf_s(registry, _countof(registry), _TRUNCATE, _T("%s\\%s"), NSSM_REG_HOOK, hook_event) < 0) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("hook registry"), _T("set_hook()"), 0);
+    return 1;
+  }
+
+  HKEY key;
+  long error;
+
+  /* Don't create keys needlessly. */
+  if (! _tcslen(cmd)) {
+    key = open_registry(service_name, registry, KEY_READ, false);
+    if (! key) return 0;
+    error = RegQueryValueEx(key, hook_action, 0, 0, 0, 0);
+    RegCloseKey(key);
+    if (error == ERROR_FILE_NOT_FOUND) return 0;
+  }
+
+  key = open_registry(service_name, registry, KEY_WRITE);
+  if (! key) return 1;
+
+  int ret = 1;
+  if (_tcslen(cmd)) ret = set_string(key, (TCHAR *) hook_action, cmd, true);
+  else {
+    error = RegDeleteValue(key, hook_action);
+    if (error == ERROR_SUCCESS || error == ERROR_FILE_NOT_FOUND) ret = 0;
+  }
+
+  /* Close registry */
+  RegCloseKey(key);
+
+  return ret;
+}
+
+int get_hook(const TCHAR *service_name, const TCHAR *hook_event, const TCHAR *hook_action, TCHAR *buffer, unsigned long buflen) {
+  /* Try to open the registry */
+  TCHAR registry[KEY_LENGTH];
+  if (_sntprintf_s(registry, _countof(registry), _TRUNCATE, _T("%s\\%s"), NSSM_REG_HOOK, hook_event) < 0) {
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("hook registry"), _T("get_hook()"), 0);
+    return 1;
+  }
+  HKEY key = open_registry(service_name, registry, KEY_READ, false);
+  if (! key) return 1;
+
+  int ret = expand_parameter(key, (TCHAR *) hook_action, buffer, buflen, true, false);
+
+  /* Close registry */
+  RegCloseKey(key);
+
+  return ret;
+}

+ 3 - 0
registry.h

@@ -33,6 +33,7 @@
 #define NSSM_REG_PRIORITY _T("AppPriority")
 #define NSSM_REG_AFFINITY _T("AppAffinity")
 #define NSSM_REG_NO_CONSOLE _T("AppNoConsole")
+#define NSSM_REG_HOOK _T("AppEvents")
 #define NSSM_STDIO_LENGTH 29
 
 HKEY open_registry(const TCHAR *, const TCHAR *, REGSAM sam, bool);
@@ -58,5 +59,7 @@ void override_milliseconds(TCHAR *, HKEY, TCHAR *, unsigned long *, unsigned lon
 int get_io_parameters(nssm_service_t *, HKEY);
 int get_parameters(nssm_service_t *, STARTUPINFO *);
 int get_exit_action(const TCHAR *, unsigned long *, TCHAR *, bool *);
+int set_hook(const TCHAR *, const TCHAR *, const TCHAR *, TCHAR *);
+int get_hook(const TCHAR *, const TCHAR *, const TCHAR *, TCHAR *, unsigned long);
 
 #endif

+ 7 - 2
resource.h

@@ -18,6 +18,7 @@
 #define IDD_NATIVE                      113
 #define IDD_PROCESS                     114
 #define IDD_DEPENDENCIES                115
+#define IDD_HOOKS                       116
 #define IDC_PATH                        1000
 #define IDC_TAB1                        1001
 #define IDC_CANCEL                      1002
@@ -65,14 +66,18 @@
 #define IDC_CONSOLE                     1045
 #define IDC_DEPENDENCIES                1046
 #define IDC_KILL_PROCESS_TREE           1047
+#define IDC_HOOK_EVENT                  1048
+#define IDC_HOOK_ACTION                 1049
+#define IDC_HOOK                        1050
+#define IDC_BROWSE_HOOK                 1051
 
 // Next default values for new objects
 // 
 #ifdef APSTUDIO_INVOKED
 #ifndef APSTUDIO_READONLY_SYMBOLS
-#define _APS_NEXT_RESOURCE_VALUE        115
+#define _APS_NEXT_RESOURCE_VALUE        117
 #define _APS_NEXT_COMMAND_VALUE         40001
-#define _APS_NEXT_CONTROL_VALUE         1048
+#define _APS_NEXT_CONTROL_VALUE         1052
 #define _APS_NEXT_SYMED_VALUE           101
 #endif
 #endif

+ 84 - 13
service.cpp

@@ -10,6 +10,8 @@ const TCHAR *exit_action_strings[] = { _T("Restart"), _T("Ignore"), _T("Exit"),
 const TCHAR *startup_strings[] = { _T("SERVICE_AUTO_START"), _T("SERVICE_DELAYED_AUTO_START"), _T("SERVICE_DEMAND_START"), _T("SERVICE_DISABLED"), 0 };
 const TCHAR *priority_strings[] = { _T("REALTIME_PRIORITY_CLASS"), _T("HIGH_PRIORITY_CLASS"), _T("ABOVE_NORMAL_PRIORITY_CLASS"), _T("NORMAL_PRIORITY_CLASS"), _T("BELOW_NORMAL_PRIORITY_CLASS"), _T("IDLE_PRIORITY_CLASS"), 0 };
 
+static hook_thread_t hook_threads = { NULL, 0 };
+
 typedef struct {
   int first;
   int last;
@@ -101,6 +103,25 @@ static inline int await_service_control_response(unsigned long control, SC_HANDL
   return -1;
 }
 
+static inline void wait_for_hooks(nssm_service_t *service, bool notify) {
+  SERVICE_STATUS_HANDLE status_handle;
+  SERVICE_STATUS *status;
+
+  /* On a clean shutdown we need to keep the service's status up-to-date. */
+  if (notify) {
+    status_handle = service->status_handle;
+    status = &service->status;
+  }
+  else {
+    status_handle = NULL;
+    status = NULL;
+  }
+
+  EnterCriticalSection(&service->hook_section);
+  await_hook_threads(&hook_threads, status_handle, status, NSSM_HOOK_THREAD_DEADLINE);
+  LeaveCriticalSection(&service->hook_section);
+}
+
 int affinity_mask_to_string(__int64 mask, TCHAR **string) {
   if (! string) return 1;
   if (! mask) {
@@ -246,6 +267,7 @@ unsigned long priority_index_to_constant(int index) {
 }
 
 static inline unsigned long throttle_milliseconds(unsigned long throttle) {
+  if (throttle > 7) throttle = 8;
   /* pow() operates on doubles. */
   unsigned long ret = 1; for (unsigned long i = 1; i < throttle; i++) ret *= 2;
   return ret * 1000;
@@ -722,6 +744,7 @@ void cleanup_nssm_service(nssm_service_t *service) {
   if (service->wait_handle) UnregisterWait(service->wait_handle);
   if (service->throttle_section_initialised) DeleteCriticalSection(&service->throttle_section);
   if (service->throttle_timer) CloseHandle(service->throttle_timer);
+  if (service->hook_section_initialised) DeleteCriticalSection(&service->hook_section);
   if (service->initial_env) FreeEnvironmentStrings(service->initial_env);
   HeapFree(GetProcessHeap(), 0, service);
 }
@@ -1402,6 +1425,10 @@ void WINAPI service_main(unsigned long argc, TCHAR **argv) {
       service->handle = open_service(services, service->name, SERVICE_CHANGE_CONFIG, 0, 0);
       set_service_recovery(service);
 
+      /* Remember our display name. */
+      unsigned long displayname_len = _countof(service->displayname);
+      GetServiceDisplayName(services, service->name, service->displayname, &displayname_len);
+
       CloseServiceHandle(services);
     }
   }
@@ -1418,9 +1445,16 @@ void WINAPI service_main(unsigned long argc, TCHAR **argv) {
     }
   }
 
+  /* Critical section for hooks. */
+  InitializeCriticalSection(&service->hook_section);
+  service->hook_section_initialised = true;
+
   /* Remember our initial environment. */
   service->initial_env = GetEnvironmentStrings();
 
+  /* Remember our creation time. */
+  if (get_process_creation_time(GetCurrentProcess(), &service->nssm_creation_time)) ZeroMemory(&service->nssm_creation_time, sizeof(service->nssm_creation_time));
+
   monitor_service(service);
 }
 
@@ -1527,7 +1561,14 @@ unsigned long WINAPI service_control_handler(unsigned long control, unsigned lon
 
     case SERVICE_CONTROL_SHUTDOWN:
     case SERVICE_CONTROL_STOP:
+      service->last_control = control;
       log_service_control(service->name, control, true);
+
+      /* Pre-stop hook. */
+      service->status.dwCurrentState = SERVICE_STOP_PENDING;
+      SetServiceStatus(service->status_handle, &service->status);
+      nssm_hook(&hook_threads, service, NSSM_HOOK_EVENT_STOP, NSSM_HOOK_ACTION_PRE, &control, NSSM_SERVICE_STATUS_DEADLINE, false);
+
       /*
         We MUST acknowledge the stop request promptly but we're committed to
         waiting for the application to exit.  Spawn a new thread to wait
@@ -1549,6 +1590,7 @@ unsigned long WINAPI service_control_handler(unsigned long control, unsigned lon
       return NO_ERROR;
 
     case SERVICE_CONTROL_CONTINUE:
+      service->last_control = control;
       log_service_control(service->name, control, true);
       service->throttle = 0;
       if (use_critical_section) imports.WakeConditionVariable(&service->throttle_condition);
@@ -1573,18 +1615,31 @@ unsigned long WINAPI service_control_handler(unsigned long control, unsigned lon
       return ERROR_CALL_NOT_IMPLEMENTED;
 
     case NSSM_SERVICE_CONTROL_ROTATE:
+      service->last_control = control;
       log_service_control(service->name, control, true);
+      (void) nssm_hook(&hook_threads, service, NSSM_HOOK_EVENT_ROTATE, NSSM_HOOK_ACTION_PRE, &control, NSSM_HOOK_DEADLINE, false);
       if (service->rotate_stdout_online == NSSM_ROTATE_ONLINE) service->rotate_stdout_online = NSSM_ROTATE_ONLINE_ASAP;
       if (service->rotate_stderr_online == NSSM_ROTATE_ONLINE) service->rotate_stderr_online = NSSM_ROTATE_ONLINE_ASAP;
+      (void) nssm_hook(&hook_threads, service, NSSM_HOOK_EVENT_ROTATE, NSSM_HOOK_ACTION_POST, &control);
       return NO_ERROR;
 
     case SERVICE_CONTROL_POWEREVENT:
-      if (event != PBT_APMRESUMEAUTOMATIC) {
-        log_service_control(service->name, control, false);
+      /* Resume from suspend. */
+      if (event == PBT_APMRESUMEAUTOMATIC) {
+        service->last_control = control;
+        log_service_control(service->name, control, true);
+        (void) nssm_hook(&hook_threads, service, NSSM_HOOK_EVENT_POWER, NSSM_HOOK_ACTION_RESUME, &control);
         return NO_ERROR;
       }
-      log_service_control(service->name, control, true);
-      end_service((void *) service, false);
+
+      /* Battery low or changed to A/C power or something. */
+      if (event == PBT_APMPOWERSTATUSCHANGE) {
+        service->last_control = control;
+        log_service_control(service->name, control, true);
+        (void) nssm_hook(&hook_threads, service, NSSM_HOOK_EVENT_POWER, NSSM_HOOK_ACTION_CHANGE, &control);
+        return NO_ERROR;
+      }
+      log_service_control(service->name, control, false);
       return NO_ERROR;
   }
 
@@ -1599,6 +1654,7 @@ int start_service(nssm_service_t *service) {
   service->allow_restart = true;
 
   if (service->process_handle) return 0;
+  service->start_requested_count++;
 
   /* Allocate a STARTUPINFO structure for a new process */
   STARTUPINFO si;
@@ -1629,6 +1685,17 @@ int start_service(nssm_service_t *service) {
   if (service->env) duplicate_environment(service->env);
   if (service->env_extra) set_environment_block(service->env_extra);
 
+  /* Pre-start hook. */
+  unsigned long control = NSSM_SERVICE_CONTROL_START;
+  service->status.dwCurrentState = SERVICE_START_PENDING;
+  service->status.dwControlsAccepted = SERVICE_ACCEPT_POWEREVENT | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
+  if (nssm_hook(&hook_threads, service, NSSM_HOOK_EVENT_START, NSSM_HOOK_ACTION_PRE, &control, NSSM_SERVICE_STATUS_DEADLINE, false) == NSSM_HOOK_STATUS_ABORT) {
+    TCHAR code[16];
+    _sntprintf_s(code, _countof(code), _TRUNCATE, _T("%lu"), NSSM_HOOK_STATUS_ABORT);
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_PRESTART_HOOK_ABORT, NSSM_HOOK_EVENT_START, NSSM_HOOK_ACTION_PRE, service->name, code, 0);
+    return stop_service(service, 5, true, true);
+  }
+
   /* Set up I/O redirection. */
   if (get_output_handles(service, &si)) {
     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GET_OUTPUT_HANDLES_FAILED, service->name, 0);
@@ -1649,6 +1716,7 @@ int start_service(nssm_service_t *service) {
     duplicate_environment_strings(service->initial_env);
     return stop_service(service, exitcode, true, true);
   }
+  service->start_count++;
   service->process_handle = pi.hProcess;
   service->pid = pi.dwProcessId;
 
@@ -1696,7 +1764,6 @@ int start_service(nssm_service_t *service) {
     so abandon the wait before too much time has elapsed.
   */
   service->status.dwCurrentState = SERVICE_START_PENDING;
-  service->status.dwControlsAccepted = SERVICE_ACCEPT_POWEREVENT | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
   if (await_single_handle(service->status_handle, &service->status, service->process_handle, service->name, _T("start_service"), service->throttle_delay) == 1) service->throttle = 0;
 
   /* Signal successful start */
@@ -1704,12 +1771,9 @@ int start_service(nssm_service_t *service) {
   service->status.dwControlsAccepted &= ~SERVICE_ACCEPT_PAUSE_CONTINUE;
   SetServiceStatus(service->status_handle, &service->status);
 
-  /* Continue waiting for a clean startup. */
-  if (deadline == WAIT_TIMEOUT) {
-    if (service->throttle_delay > delay) {
-      if (WaitForSingleObject(service->process_handle, service->throttle_delay - delay) == WAIT_TIMEOUT) service->throttle = 0;
-    }
-    else service->throttle = 0;
+  /* Post-start hook. */
+  if (! service->throttle) {
+    (void) nssm_hook(&hook_threads, service, NSSM_HOOK_EVENT_START, NSSM_HOOK_ACTION_POST, &control);
   }
 
   /* Ensure the restart delay is always applied. */
@@ -1756,6 +1820,8 @@ int stop_service(nssm_service_t *service, unsigned long exitcode, bool graceful,
 
   /* Signal we stopped */
   if (graceful) {
+    service->status.dwCurrentState = SERVICE_STOP_PENDING;
+    wait_for_hooks(service, true);
     service->status.dwCurrentState = SERVICE_STOPPED;
     if (exitcode) {
       service->status.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
@@ -1789,6 +1855,7 @@ void CALLBACK end_service(void *arg, unsigned char why) {
   TCHAR code[16];
   if (service->process_handle) {
     GetExitCodeProcess(service->process_handle, &exitcode);
+    service->exitcode = exitcode;
     /* Check real exit time. */
     if (exitcode != STILL_ACTIVE) get_process_exit_time(service->process_handle, &service->exit_time);
     CloseHandle(service->process_handle);
@@ -1814,6 +1881,10 @@ void CALLBACK end_service(void *arg, unsigned char why) {
   }
   service->pid = 0;
 
+  /* Exit hook. */
+  service->exit_count++;
+  (void) nssm_hook(&hook_threads, service, NSSM_HOOK_EVENT_EXIT, NSSM_HOOK_ACTION_POST, NULL, NSSM_HOOK_DEADLINE, true);
+
   /*
     The why argument is true if our wait timed out or false otherwise.
     Our wait is infinite so why will never be true when called by the system.
@@ -1849,6 +1920,7 @@ void CALLBACK end_service(void *arg, unsigned char why) {
     /* Do nothing, just like srvany would */
     case NSSM_EXIT_IGNORE:
       log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_EXIT_IGNORE, service->name, code, exit_action_strings[action], service->exe, 0);
+      wait_for_hooks(service, false);
       Sleep(INFINITE);
     break;
 
@@ -1862,6 +1934,7 @@ void CALLBACK end_service(void *arg, unsigned char why) {
     case NSSM_EXIT_UNCLEAN:
       log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_EXIT_UNCLEAN, service->name, code, exit_action_strings[action], 0);
       stop_service(service, exitcode, false, default_action);
+      wait_for_hooks(service, false);
       free_imports();
       exit(exitcode);
     break;
@@ -1879,8 +1952,6 @@ void throttle_restart(nssm_service_t *service) {
   if (service->restart_delay > throttle_ms) ms = service->restart_delay;
   else ms = throttle_ms;
 
-  if (service->throttle > 7) service->throttle = 8;
-
   _sntprintf_s(milliseconds, _countof(milliseconds), _TRUNCATE, _T("%lu"), ms);
 
   if (service->throttle == 1 && service->restart_delay > throttle_ms) log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_RESTART_DELAY, service->name, milliseconds, 0);

+ 8 - 0
service.h

@@ -94,17 +94,25 @@ typedef struct {
   HANDLE process_handle;
   unsigned long pid;
   HANDLE wait_handle;
+  unsigned long exitcode;
   bool stopping;
   bool allow_restart;
   unsigned long throttle;
   CRITICAL_SECTION throttle_section;
   bool throttle_section_initialised;
+  CRITICAL_SECTION hook_section;
+  bool hook_section_initialised;
   CONDITION_VARIABLE throttle_condition;
   HANDLE throttle_timer;
   LARGE_INTEGER throttle_duetime;
+  FILETIME nssm_creation_time;
   FILETIME creation_time;
   FILETIME exit_time;
   TCHAR *initial_env;
+  unsigned long last_control;
+  unsigned long start_requested_count;
+  unsigned long start_count;
+  unsigned long exit_count;
 } nssm_service_t;
 
 void WINAPI service_main(unsigned long, TCHAR **);

+ 45 - 0
settings.cpp

@@ -186,6 +186,50 @@ static int setting_get_exit_action(const TCHAR *service_name, void *param, const
   return 1;
 }
 
+static inline bool split_hook_name(const TCHAR *hook_name, TCHAR *hook_event, TCHAR *hook_action) {
+  TCHAR *s;
+
+  for (s = (TCHAR *) hook_name; *s; s++) {
+    if (*s == _T('/')) {
+      *s = _T('\0');
+      _sntprintf_s(hook_event, HOOK_NAME_LENGTH, _TRUNCATE, _T("%s"), hook_name);
+      _sntprintf_s(hook_action, HOOK_NAME_LENGTH, _TRUNCATE, _T("%s"), ++s);
+      return valid_hook_name(hook_event, hook_action, false);
+    }
+  }
+
+  print_message(stderr, NSSM_MESSAGE_INVALID_HOOK_NAME, hook_name);
+  return false;
+}
+
+static int setting_set_hook(const TCHAR *service_name, void *param, const TCHAR *name, void *default_value, value_t *value, const TCHAR *additional) {
+  TCHAR hook_event[HOOK_NAME_LENGTH];
+  TCHAR hook_action[HOOK_NAME_LENGTH];
+  if (! split_hook_name(additional, hook_event, hook_action)) return -1;
+
+  TCHAR *cmd;
+  if (value && value->string) cmd = value->string;
+  else cmd = _T("");
+
+  if (set_hook(service_name, hook_event, hook_action, cmd)) return -1;
+  if (! _tcslen(cmd)) return 0;
+  return 1;
+}
+
+static int setting_get_hook(const TCHAR *service_name, void *param, const TCHAR *name, void *default_value, value_t *value, const TCHAR *additional) {
+  TCHAR hook_event[HOOK_NAME_LENGTH];
+  TCHAR hook_action[HOOK_NAME_LENGTH];
+  if (! split_hook_name(additional, hook_event, hook_action)) return -1;
+
+  TCHAR cmd[CMD_LENGTH];
+  if (get_hook(service_name, hook_event, hook_action, cmd, sizeof(cmd))) return -1;
+
+  value_from_string(name, value, cmd);
+
+  if (! _tcslen(cmd)) return 0;
+  return 1;
+}
+
 static int setting_set_affinity(const TCHAR *service_name, void *param, const TCHAR *name, void *default_value, value_t *value, const TCHAR *additional) {
   HKEY key = (HKEY) param;
   if (! key) return -1;
@@ -1020,6 +1064,7 @@ settings_t settings[] = {
   { NSSM_REG_FLAGS, REG_EXPAND_SZ, (void *) _T(""), false, 0, setting_set_string, setting_get_string },
   { NSSM_REG_DIR, REG_EXPAND_SZ, (void *) _T(""), false, 0, setting_set_string, setting_get_string },
   { NSSM_REG_EXIT, REG_SZ, (void *) exit_action_strings[NSSM_EXIT_RESTART], false, ADDITIONAL_MANDATORY, setting_set_exit_action, setting_get_exit_action },
+  { NSSM_REG_HOOK, REG_SZ, (void *) _T(""), false, ADDITIONAL_MANDATORY, setting_set_hook, setting_get_hook },
   { NSSM_REG_AFFINITY, REG_SZ, 0, false, 0, setting_set_affinity, setting_get_affinity },
   { NSSM_REG_ENV, REG_MULTI_SZ, NULL, false, ADDITIONAL_CRLF, setting_set_environment, setting_get_environment },
   { NSSM_REG_ENV_EXTRA, REG_MULTI_SZ, NULL, false, ADDITIONAL_CRLF, setting_set_environment, setting_get_environment },