From e42e6900a5dad50b952d92c57344fdea2e13646e Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Tue, 12 Nov 2013 10:50:21 +0000 Subject: [PATCH 01/16] AttachConsole() isn't available in Windows 2000. Instead of calling AttachConsole() directory use a function pointer in the new global imports struct. It's safe to skip any attempt to attach to the console when the function isn't available. --- imports.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ imports.h | 16 ++++++++++++++++ messages.mc | 35 +++++++++++++++++++++++++++++++++++ nssm.cpp | 5 +++++ nssm.dsp | 8 ++++++++ nssm.h | 1 + nssm.vcproj | 8 ++++++++ process.cpp | 7 ++++++- service.cpp | 4 +++- 9 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 imports.cpp create mode 100644 imports.h diff --git a/imports.cpp b/imports.cpp new file mode 100644 index 0000000..49483d4 --- /dev/null +++ b/imports.cpp @@ -0,0 +1,56 @@ +#include "nssm.h" + +imports_t imports; + +/* + Try to set up function pointers. + In this first implementation it is not an error if we can't load them + because we aren't currently trying to load any functions which we + absolutely need. If we later add some indispensible imports we can + return non-zero here to force an application exit. +*/ +HMODULE get_dll(const char *dll, unsigned long *error) { + *error = 0; + + HMODULE ret = LoadLibrary(dll); + if (! ret) { + *error = GetLastError(); + log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_LOADLIBRARY_FAILED, dll, error_string(*error)); + } + + return ret; +} + +FARPROC get_import(HMODULE library, const char *function, unsigned long *error) { + *error = 0; + + FARPROC ret = GetProcAddress(library, function); + if (! ret) { + *error = GetLastError(); + log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_GETPROCADDRESS_FAILED, function, error_string(*error)); + } + + return ret; +} + +int get_imports() { + unsigned long error; + + ZeroMemory(&imports, sizeof(imports)); + + imports.kernel32 = get_dll("kernel32.dll", &error); + if (imports.kernel32) { + imports.AttachConsole = (AttachConsole_ptr) get_import(imports.kernel32, "AttachConsole", &error); + if (! imports.AttachConsole) { + if (error != ERROR_PROC_NOT_FOUND) return 2; + } + } + else if (error != ERROR_MOD_NOT_FOUND) return 1; + + return 0; +} + +void free_imports() { + if (imports.kernel32) FreeLibrary(imports.kernel32); + ZeroMemory(&imports, sizeof(imports)); +} diff --git a/imports.h b/imports.h new file mode 100644 index 0000000..f731ad6 --- /dev/null +++ b/imports.h @@ -0,0 +1,16 @@ +#ifndef IMPORTS_H +#define IMPORTS_H + +typedef BOOL (WINAPI *AttachConsole_ptr)(DWORD); + +typedef struct { + HMODULE kernel32; + AttachConsole_ptr AttachConsole; +} imports_t; + +HMODULE get_dll(const char *, unsigned long *); +FARPROC get_import(HMODULE, const char *, unsigned long *); +int get_imports(); +void free_imports(); + +#endif diff --git a/messages.mc b/messages.mc index 0aea1ae..8b6c984 100644 --- a/messages.mc +++ b/messages.mc @@ -1270,3 +1270,38 @@ The service %1 is stopping but PID %2 is still running. Usually %3 will call TerminateProcess() as a last resort to ensure that the process is stopped but the registry value %4 has been set and not all process termination methods have been attempted. It will no longer be possible to attempt to control the application and the service will report a stopped status. . + +MessageId = +1 +SymbolicName = NSSM_EVENT_LOADLIBRARY_FAILED +Severity = Warning +Language = English +Error loading the %1 DLL! +LoadLibrary() failed: +%2 +. +Language = French +Erreur à l'ouverture de la DLL %1! +LoadLibrary() a échoué: +%2 +. +Language = Italian +Errore apertura DLL %1! +Chiamata a LoadLibrary() fallita: +%2 +. + +MessageId = +1 +SymbolicName = NSSM_EVENT_GETPROCADDRESS_FAILED +Severity = Warning +Language = English +GetProcAddress(%1) failed: +%2 +. +Language = French +GetProcAddress(%1) a échoué: +%2 +. +Language = Italian +Chiamata a GetProcAddress(%1) fallita: +%2 +. diff --git a/nssm.cpp b/nssm.cpp index 1ee38bd..0ce8664 100644 --- a/nssm.cpp +++ b/nssm.cpp @@ -2,6 +2,7 @@ extern unsigned long tls_index; extern bool is_admin; +extern imports_t imports; /* String function */ int str_equiv(const char *a, const char *b) { @@ -69,6 +70,9 @@ int main(int argc, char **argv) { This will save time when running with no arguments from a command prompt. */ if (_fileno(stdin) < 0) { + /* Set up function pointers. */ + if (get_imports()) exit(111); + /* Start service magic */ SERVICE_TABLE_ENTRY table[] = { { NSSM, service_main }, { 0, 0 } }; if (! StartServiceCtrlDispatcher(table)) { @@ -76,6 +80,7 @@ int main(int argc, char **argv) { /* User probably ran nssm with no argument */ if (error == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) exit(usage(1)); log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_DISPATCHER_FAILED, error_string(error), 0); + free_imports(); exit(100); } } diff --git a/nssm.dsp b/nssm.dsp index a643e4d..2ac2b34 100644 --- a/nssm.dsp +++ b/nssm.dsp @@ -96,6 +96,10 @@ SOURCE=.\gui.cpp # End Source File # Begin Source File +SOURCE=.\imports.cpp +# End Source File +# Begin Source File + SOURCE=.\io.cpp # End Source File # Begin Source File @@ -128,6 +132,10 @@ SOURCE=.\gui.h # End Source File # Begin Source File +SOURCE=.\imports.h +# End Source File +# Begin Source File + SOURCE=.\io.h # End Source File # Begin Source File diff --git a/nssm.h b/nssm.h index afff8c4..d297527 100644 --- a/nssm.h +++ b/nssm.h @@ -7,6 +7,7 @@ #include #include #include "event.h" +#include "imports.h" #include "messages.h" #include "process.h" #include "registry.h" diff --git a/nssm.vcproj b/nssm.vcproj index 2b16220..040039d 100755 --- a/nssm.vcproj +++ b/nssm.vcproj @@ -467,6 +467,10 @@ + + @@ -628,6 +632,10 @@ > + + diff --git a/process.cpp b/process.cpp index 4c00e64..ee88af3 100644 --- a/process.cpp +++ b/process.cpp @@ -1,5 +1,7 @@ #include "nssm.h" +extern imports_t imports; + int get_process_creation_time(HANDLE process_handle, FILETIME *ft) { FILETIME creation_time, exit_time, kernel_time, user_time; @@ -186,8 +188,11 @@ int kill_process(char *service_name, unsigned long stop_method, HANDLE process_h int kill_console(char *service_name, HANDLE process_handle, unsigned long pid) { unsigned long ret; + /* Check we loaded AttachConsole(). */ + if (! imports.AttachConsole) return 4; + /* Try to attach to the process's console. */ - if (! AttachConsole(pid)) { + if (! imports.AttachConsole(pid)) { ret = GetLastError(); switch (ret) { diff --git a/service.cpp b/service.cpp index f82b9bd..fcb1c5a 100644 --- a/service.cpp +++ b/service.cpp @@ -538,7 +538,9 @@ void CALLBACK end_service(void *arg, unsigned char why) { /* Fake a crash so pre-Vista service managers will run recovery actions. */ case NSSM_EXIT_UNCLEAN: log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_EXIT_UNCLEAN, service_name, code, exit_action_strings[action], 0); - exit(stop_service(exitcode, false, default_action)); + stop_service(exitcode, false, default_action); + free_imports(); + exit(exitcode); break; } } -- 2.7.4 From 4550eb2281a16b698c6a68d8de3552fe13807590 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Tue, 12 Nov 2013 12:31:22 +0000 Subject: [PATCH 02/16] Try to throttle using a critical section. The first implementation of service restart throttling used a condition variable in a critical section to sleep for the required amount of time. The implementation was changed to use a waitable timer because Windows 2000 does not support SleepConditionVariableCS() or WakeConditionVariable(). Since we are now using LoadLibrary() and GetProcAddress() to use newer functions dynamically without having to build OS-specific binaries, we can now use a critical section where it's supported and fall back to a waitable timer when running on Windows 2000. --- imports.cpp | 10 ++++++++++ imports.h | 4 ++++ service.cpp | 40 +++++++++++++++++++++++++++++++--------- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/imports.cpp b/imports.cpp index 49483d4..99661be 100644 --- a/imports.cpp +++ b/imports.cpp @@ -44,6 +44,16 @@ int get_imports() { if (! imports.AttachConsole) { if (error != ERROR_PROC_NOT_FOUND) return 2; } + + imports.SleepConditionVariableCS = (SleepConditionVariableCS_ptr) get_import(imports.kernel32, "SleepConditionVariableCS", &error); + if (! imports.SleepConditionVariableCS) { + if (error != ERROR_PROC_NOT_FOUND) return 3; + } + + imports.WakeConditionVariable = (WakeConditionVariable_ptr) get_import(imports.kernel32, "WakeConditionVariable", &error); + if (! imports.WakeConditionVariable) { + if (error != ERROR_PROC_NOT_FOUND) return 4; + } } else if (error != ERROR_MOD_NOT_FOUND) return 1; diff --git a/imports.h b/imports.h index f731ad6..33dbc19 100644 --- a/imports.h +++ b/imports.h @@ -2,10 +2,14 @@ #define IMPORTS_H typedef BOOL (WINAPI *AttachConsole_ptr)(DWORD); +typedef BOOL (WINAPI *SleepConditionVariableCS_ptr)(PCONDITION_VARIABLE, PCRITICAL_SECTION, DWORD); +typedef void (WINAPI *WakeConditionVariable_ptr)(PCONDITION_VARIABLE); typedef struct { HMODULE kernel32; AttachConsole_ptr AttachConsole; + SleepConditionVariableCS_ptr SleepConditionVariableCS; + WakeConditionVariable_ptr WakeConditionVariable; } imports_t; HMODULE get_dll(const char *, unsigned long *); diff --git a/service.cpp b/service.cpp index fcb1c5a..d0cba9f 100644 --- a/service.cpp +++ b/service.cpp @@ -14,10 +14,15 @@ bool stopping; bool allow_restart; unsigned long throttle_delay; unsigned long stop_method; +CRITICAL_SECTION throttle_section; +CONDITION_VARIABLE throttle_condition; HANDLE throttle_timer; LARGE_INTEGER throttle_duetime; +bool use_critical_section; FILETIME creation_time; +extern imports_t imports; + static enum { NSSM_EXIT_RESTART, NSSM_EXIT_IGNORE, NSSM_EXIT_REALLY, NSSM_EXIT_UNCLEAN } exit_actions; static const char *exit_action_strings[] = { "Restart", "Ignore", "Exit", "Suicide", 0 }; @@ -184,6 +189,10 @@ void WINAPI service_main(unsigned long argc, char **argv) { return; } + /* We can use a condition variable in a critical section on Vista or later. */ + if (imports.SleepConditionVariableCS && imports.WakeConditionVariable) use_critical_section = true; + else use_critical_section = false; + /* Initialise status */ ZeroMemory(&service_status, sizeof(service_status)); service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS; @@ -218,9 +227,12 @@ void WINAPI service_main(unsigned long argc, char **argv) { } /* Used for signalling a resume if the service pauses when throttled. */ - throttle_timer = CreateWaitableTimer(0, 1, 0); - if (! throttle_timer) { - log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_CREATEWAITABLETIMER_FAILED, service_name, error_string(GetLastError()), 0); + if (use_critical_section) InitializeCriticalSection(&throttle_section); + else { + throttle_timer = CreateWaitableTimer(0, 1, 0); + if (! throttle_timer) { + log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_CREATEWAITABLETIMER_FAILED, service_name, error_string(GetLastError()), 0); + } } monitor_service(); @@ -333,10 +345,13 @@ unsigned long WINAPI service_control_handler(unsigned long control, unsigned lon case SERVICE_CONTROL_CONTINUE: log_service_control(service_name, control, true); - if (! throttle_timer) return ERROR_CALL_NOT_IMPLEMENTED; throttle = 0; - ZeroMemory(&throttle_duetime, sizeof(throttle_duetime)); - SetWaitableTimer(throttle_timer, &throttle_duetime, 0, 0, 0, 0); + if (use_critical_section) imports.WakeConditionVariable(&throttle_condition); + else { + if (! throttle_timer) return ERROR_CALL_NOT_IMPLEMENTED; + ZeroMemory(&throttle_duetime, sizeof(throttle_duetime)); + SetWaitableTimer(throttle_timer, &throttle_duetime, 0, 0, 0, 0); + } service_status.dwCurrentState = SERVICE_CONTINUE_PENDING; service_status.dwWaitHint = throttle_milliseconds() + NSSM_WAITHINT_MARGIN; log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_RESET_THROTTLE, service_name, 0); @@ -558,7 +573,8 @@ void throttle_restart() { _snprintf_s(milliseconds, sizeof(milliseconds), _TRUNCATE, "%d", ms); log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_THROTTLED, service_name, threshold, milliseconds, 0); - if (throttle_timer) { + if (use_critical_section) EnterCriticalSection(&throttle_section); + else if (throttle_timer) { ZeroMemory(&throttle_duetime, sizeof(throttle_duetime)); throttle_duetime.QuadPart = 0 - (ms * 10000LL); SetWaitableTimer(throttle_timer, &throttle_duetime, 0, 0, 0, 0); @@ -567,6 +583,12 @@ void throttle_restart() { service_status.dwCurrentState = SERVICE_PAUSED; SetServiceStatus(service_handle, &service_status); - if (throttle_timer) WaitForSingleObject(throttle_timer, INFINITE); - else Sleep(ms); + if (use_critical_section) { + imports.SleepConditionVariableCS(&throttle_condition, &throttle_section, ms); + LeaveCriticalSection(&throttle_section); + } + else { + if (throttle_timer) WaitForSingleObject(throttle_timer, INFINITE); + else Sleep(ms); + } } -- 2.7.4 From 3930cee5d4fdb4d451f4125a14e57caa60ee7b8f Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Tue, 12 Nov 2013 13:32:37 +0000 Subject: [PATCH 03/16] Fix tabbing between GUI fields. The tab key didn't switch between fields in the GUI because we didn't insert a call to IsDialogMessage() in the message loop. --- gui.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/gui.cpp b/gui.cpp index 84fd4c2..7955b1a 100644 --- a/gui.cpp +++ b/gui.cpp @@ -28,6 +28,7 @@ int nssm_gui(int resource, char *name) { /* Go! */ MSG message; while (GetMessage(&message, 0, 0, 0)) { + if (IsDialogMessage(dlg, &message)) continue; TranslateMessage(&message); DispatchMessage(&message); } -- 2.7.4 From 4142e6c279f302b6b43e4e4d41ea7afaa89293a0 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Tue, 12 Nov 2013 12:49:39 +0000 Subject: [PATCH 04/16] NSSM 2.17. --- README.txt | 2 +- nssm.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.txt b/README.txt index 3559245..2fbe5c8 100644 --- a/README.txt +++ b/README.txt @@ -1,5 +1,5 @@ NSSM: The Non-Sucking Service Manager -Version 2.16, 2012-12-01 +Version 2.17, 2013-11-12 NSSM is a service helper program similar to srvany and cygrunsrv. It can start any application as an NT service and will restart the service if it diff --git a/nssm.h b/nssm.h index d297527..b5d457e 100644 --- a/nssm.h +++ b/nssm.h @@ -18,9 +18,9 @@ int str_equiv(const char *, const char *); #define NSSM "nssm" -#define NSSM_VERSION "2.16" -#define NSSM_VERSIONINFO 2,16,0,0 -#define NSSM_DATE "2012-12-01" +#define NSSM_VERSION "2.17" +#define NSSM_VERSIONINFO 2,17,0,0 +#define NSSM_DATE "2013-11-12" /* MSDN says the commandline in CreateProcess() is limited to 32768 characters -- 2.7.4 From 64eea0aef7d23dc4cfdaa7d979473c629f41ab85 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 15 Nov 2013 15:43:46 +0000 Subject: [PATCH 05/16] Typos and formatting etc. --- ChangeLog.txt | 2 +- messages.mc | 2 +- process.cpp | 8 ++++---- service.cpp | 13 +++++++------ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/ChangeLog.txt b/ChangeLog.txt index 36a7ab3..1513a54 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,7 +1,7 @@ Changes since 2.16 ----------------- * NSSM can now redirect the service's I/O streams to any path - capable of being opened by CreateFile(). + capable of being opened by CreateFile(). * Allow building on Visual Studio Express. diff --git a/messages.mc b/messages.mc index 8b6c984..6f6ae40 100644 --- a/messages.mc +++ b/messages.mc @@ -990,7 +990,7 @@ n' . Language = Italian La chiave di registro %2, utilizzata per specificare il minimo numero di millisecondi che devono intercorrere prima che il servizio %1 sia considerato avviato correttamente, non è di tipo REG_DWORD. -Verrà usato il tempo di default pari a 3 ms. +Verrà usato il tempo di default pari a %3 ms. . MessageId = +1 diff --git a/process.cpp b/process.cpp index ee88af3..fc0056d 100644 --- a/process.cpp +++ b/process.cpp @@ -41,7 +41,7 @@ int check_parent(char *service_name, PROCESSENTRY32 *pe, unsigned long ppid, FIL HANDLE process_handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, pe->th32ProcessID); if (! process_handle) { char pid_string[16]; - _snprintf_s(pid_string, sizeof(pid_string), _TRUNCATE, "%d", pe->th32ProcessID); + _snprintf_s(pid_string, sizeof(pid_string), _TRUNCATE, "%lu", pe->th32ProcessID); log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENPROCESS_FAILED, pid_string, service_name, error_string(GetLastError()), 0); return 2; } @@ -243,8 +243,8 @@ void kill_process_tree(char *service_name, unsigned long stop_method, unsigned l if (! pid) return; char pid_string[16], code[16]; - _snprintf_s(pid_string, sizeof(pid_string), _TRUNCATE, "%d", pid); - _snprintf_s(code, sizeof(code), _TRUNCATE, "%d", exitcode); + _snprintf_s(pid_string, sizeof(pid_string), _TRUNCATE, "%lu", pid); + _snprintf_s(code, sizeof(code), _TRUNCATE, "%lu", exitcode); log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILLING, service_name, pid_string, code, 0); /* Get a snapshot of all processes in the system. */ @@ -290,7 +290,7 @@ void kill_process_tree(char *service_name, unsigned long stop_method, unsigned l } char ppid_string[16]; - _snprintf_s(ppid_string, sizeof(ppid_string), _TRUNCATE, "%d", ppid); + _snprintf_s(ppid_string, sizeof(ppid_string), _TRUNCATE, "%lu", ppid); log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILL_PROCESS_TREE, pid_string, ppid_string, service_name, 0); if (! kill_process(service_name, stop_method, process_handle, pid, exitcode)) { /* Maybe it already died. */ diff --git a/service.cpp b/service.cpp index d0cba9f..d8dd1aa 100644 --- a/service.cpp +++ b/service.cpp @@ -280,7 +280,7 @@ int monitor_service() { } log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_STARTED_SERVICE, exe, flags, service_name, dir, 0); - /* Monitor service service */ + /* Monitor service */ if (! RegisterWaitForSingleObject(&wait_handle, process_handle, end_service, (void *) pid, INFINITE, WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION)) { log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_REGISTERWAITFORSINGLEOBJECT_FAILED, service_name, exe, error_string(GetLastError()), 0); } @@ -309,11 +309,11 @@ void log_service_control(char *service_name, unsigned long control, bool handled /* "0x" + 8 x hex + NULL */ text = (char *) HeapAlloc(GetProcessHeap(), 0, 11); if (! text) { - log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, "control code", "log_service_control", 0); + log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, "control code", "log_service_control()", 0); return; } if (_snprintf_s(text, 11, _TRUNCATE, "0x%08x", control) < 0) { - log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, "control code", "log_service_control", 0); + log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, "control code", "log_service_control()", 0); HeapFree(GetProcessHeap(), 0, text); return; } @@ -497,11 +497,12 @@ void CALLBACK end_service(void *arg, unsigned char why) { tree. See below for the possible values of the why argument. */ if (! why) { - _snprintf_s(code, sizeof(code), _TRUNCATE, "%d", exitcode); + _snprintf_s(code, sizeof(code), _TRUNCATE, "%lu", exitcode); log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_ENDED_SERVICE, exe, service_name, code, 0); } /* Clean up. */ + if (exitcode == STILL_ACTIVE) exitcode = 0; kill_process_tree(service_name, stop_method, pid, exitcode, pid, &creation_time, &exit_time); /* @@ -569,8 +570,8 @@ void throttle_restart() { if (throttle > 7) throttle = 8; char threshold[8], milliseconds[8]; - _snprintf_s(threshold, sizeof(threshold), _TRUNCATE, "%d", throttle_delay); - _snprintf_s(milliseconds, sizeof(milliseconds), _TRUNCATE, "%d", ms); + _snprintf_s(threshold, sizeof(threshold), _TRUNCATE, "%lu", throttle_delay); + _snprintf_s(milliseconds, sizeof(milliseconds), _TRUNCATE, "%lu", ms); log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_THROTTLED, service_name, threshold, milliseconds, 0); if (use_critical_section) EnterCriticalSection(&throttle_section); -- 2.7.4 From ee52ab253b65c52ff039d8058c66b1a63a656b01 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Wed, 13 Nov 2013 15:03:47 +0000 Subject: [PATCH 06/16] Calculate service stop wait hint correctly. The wait hint when changing the service status to stopped failed to take into account the grace period after sending a Control-C event. It also failed to consider the case where one or more stop methods were disabled. --- service.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/service.cpp b/service.cpp index d8dd1aa..596aa7e 100644 --- a/service.cpp +++ b/service.cpp @@ -445,7 +445,10 @@ int stop_service(unsigned long exitcode, bool graceful, bool default_action) { /* Signal we are stopping */ if (graceful) { service_status.dwCurrentState = SERVICE_STOP_PENDING; - service_status.dwWaitHint = NSSM_KILL_WINDOW_GRACE_PERIOD + NSSM_KILL_THREADS_GRACE_PERIOD + NSSM_WAITHINT_MARGIN; + service_status.dwWaitHint = NSSM_WAITHINT_MARGIN; + if (stop_method & NSSM_STOP_METHOD_CONSOLE && imports.AttachConsole) service_status.dwWaitHint += NSSM_KILL_CONSOLE_GRACE_PERIOD; + if (stop_method & NSSM_STOP_METHOD_WINDOW) service_status.dwWaitHint += NSSM_KILL_WINDOW_GRACE_PERIOD; + if (stop_method & NSSM_STOP_METHOD_THREADS) service_status.dwWaitHint += NSSM_KILL_THREADS_GRACE_PERIOD; SetServiceStatus(service_handle, &service_status); } -- 2.7.4 From ea55c9d26cc6ea9473d4dd23f4a62109763873de Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Wed, 13 Nov 2013 15:32:40 +0000 Subject: [PATCH 07/16] Added override_milliseconds() helper. Helper function to retrieve a REG_DWORD value from the registry and assign it to a variable, substituting a default value if the registry entry is invalid or missing. The function is specifically tailored toward setting a value in milliseconds, hence the name. --- registry.cpp | 20 ++++++++++++++++++++ registry.h | 1 + 2 files changed, 21 insertions(+) diff --git a/registry.cpp b/registry.cpp index fbcc369..a1332b2 100644 --- a/registry.cpp +++ b/registry.cpp @@ -219,6 +219,26 @@ int get_number(HKEY key, char *value, unsigned long *number) { return get_number(key, value, number, true); } +void override_milliseconds(char *service_name, HKEY key, char *value, unsigned long *buffer, unsigned long default_value, unsigned long event) { + unsigned long type = REG_DWORD; + unsigned long buflen = sizeof(unsigned long); + bool ok = false; + unsigned long ret = RegQueryValueEx(key, value, 0, &type, (unsigned char *) buffer, &buflen); + if (ret != ERROR_SUCCESS) { + if (ret != ERROR_FILE_NOT_FOUND) { + if (type != REG_DWORD) { + char milliseconds[16]; + _snprintf_s(milliseconds, sizeof(milliseconds), _TRUNCATE, "%lu", default_value); + log_event(EVENTLOG_WARNING_TYPE, event, service_name, value, milliseconds, 0); + } + else log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, value, error_string(GetLastError()), 0); + } + } + else ok = true; + + if (! ok) *buffer = default_value; +} + int get_parameters(char *service_name, char *exe, unsigned long exelen, char *flags, unsigned long flagslen, char *dir, unsigned long dirlen, char **env, unsigned long *throttle_delay, unsigned long *stop_method, STARTUPINFO *si) { unsigned long ret; diff --git a/registry.h b/registry.h index 6fd5d9c..9129b64 100644 --- a/registry.h +++ b/registry.h @@ -25,6 +25,7 @@ int expand_parameter(HKEY, char *, char *, unsigned long, bool, bool); int expand_parameter(HKEY, char *, char *, unsigned long, bool); int get_number(HKEY, char *, unsigned long *, bool); int get_number(HKEY, char *, unsigned long *); +void override_milliseconds(char *, HKEY, char *, unsigned long *, unsigned long, unsigned long); int get_parameters(char *, char *, unsigned long, char *, unsigned long, char *, unsigned long, char **, unsigned long *, unsigned long *, STARTUPINFO *); int get_exit_action(char *, unsigned long *, unsigned char *, bool *); -- 2.7.4 From a9269d8370b339777c32e6e4a5e5be3ad7387a1c Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Wed, 13 Nov 2013 15:34:26 +0000 Subject: [PATCH 08/16] Use override_milliseconds() to find throttle restart delay. Use the new override_milliseconds() helper to find the override for the amount of time to wait when throttling restarts. --- registry.cpp | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/registry.cpp b/registry.cpp index a1332b2..7a7b784 100644 --- a/registry.cpp +++ b/registry.cpp @@ -300,28 +300,12 @@ int get_parameters(char *service_name, char *exe, unsigned long exelen, char *fl } /* Try to get throttle restart delay */ - unsigned long type = REG_DWORD; - unsigned long buflen = sizeof(*throttle_delay); - bool throttle_ok = false; - ret = RegQueryValueEx(key, NSSM_REG_THROTTLE, 0, &type, (unsigned char *) throttle_delay, &buflen); - if (ret != ERROR_SUCCESS) { - if (ret != ERROR_FILE_NOT_FOUND) { - if (type != REG_DWORD) { - char milliseconds[16]; - _snprintf_s(milliseconds, sizeof(milliseconds), _TRUNCATE, "%lu", NSSM_RESET_THROTTLE_RESTART); - log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_BOGUS_THROTTLE, service_name, NSSM_REG_THROTTLE, milliseconds, 0); - } - else log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, NSSM_REG_THROTTLE, error_string(GetLastError()), 0); - } - } - else throttle_ok = true; - - if (! throttle_ok) *throttle_delay = NSSM_RESET_THROTTLE_RESTART; + override_milliseconds(service_name, key, NSSM_REG_THROTTLE, throttle_delay, NSSM_RESET_THROTTLE_RESTART, NSSM_EVENT_BOGUS_THROTTLE); /* Try to get service stop flags. */ - type = REG_DWORD; + unsigned long type = REG_DWORD; unsigned long stop_method_skip; - buflen = sizeof(stop_method_skip); + unsigned long buflen = sizeof(stop_method_skip); bool stop_ok = false; ret = RegQueryValueEx(key, NSSM_REG_STOP_METHOD_SKIP, 0, &type, (unsigned char *) &stop_method_skip, &buflen); if (ret != ERROR_SUCCESS) { -- 2.7.4 From cb571db509c239c3c465907a718479612fe5bb15 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Wed, 13 Nov 2013 15:35:20 +0000 Subject: [PATCH 09/16] Allow overriding time to wait when trying to kill the application. Three new registry entries can be used to specify a wait time in milliseconds after attempting a stop method. AppStopMethodConsole AppStopMethodWindow AppStopMethodThreads The default for each remains the same, 1500ms. Thanks Russ Holmann. --- ChangeLog.txt | 5 +++++ README.txt | 18 ++++++++++++++++++ messages.mc | 39 +++++++++++++++++++++++++++++++++++++++ nssm.h | 6 +++--- process.cpp | 9 ++++++--- registry.cpp | 7 ++++++- registry.h | 5 ++++- service.cpp | 11 +++++++---- 8 files changed, 88 insertions(+), 12 deletions(-) diff --git a/ChangeLog.txt b/ChangeLog.txt index 1513a54..d4850a3 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,3 +1,8 @@ +Changes since 2.17 +----------------- + * Timeouts for each shutdown method can be configured in + the registry. + Changes since 2.16 ----------------- * NSSM can now redirect the service's I/O streams to any path diff --git a/README.txt b/README.txt index 2fbe5c8..f2fe712 100644 --- a/README.txt +++ b/README.txt @@ -43,6 +43,9 @@ they can clean up and shut down gracefully on receipt of the event. Since version 2.17, NSSM can redirect the managed application's I/O streams to an arbitrary path. +Since version 2.18, NSSM can be configured to wait a user-specified amount +of time for the application to exit when shutting down. + Usage ----- @@ -182,6 +185,20 @@ Take great care when including 8 in the value of AppStopMethodSkip. If NSSM does not call TerminateProcess() it is possible that the application will not exit when the service stops. +By default NSSM will allow processes 1500ms to respond to each of the methods +described above before proceeding to the next one. The timeout can be +configured on a per-method basis by creating REG_DWORD entries in the +registry under HKLM\SYSTEM\CurrentControlSet\Services\\Parameters. + + AppStopMethodConsole + AppStopMethodWindow + AppStopMethodThreads + +Each value should be set to the number of milliseconds to wait. Please note +that the timeout applies to each process in the application's process tree, +so the actual time to shutdown may be longer than the sum of all configured +timeouts if the application spawns multiple subprocesses. + I/O redirection --------------- @@ -282,6 +299,7 @@ Thanks to Riccardo Gusmeroli for Italian translation. Thanks to Eric Cheldelin for the inspiration to generate a Control-C event on shutdown. Thanks to Brian Baxter for suggesting how to escape quotes from the command prompt. +Thanks to Russ Holmann for suggesting that the shutdown timeout be configurable. Licence ------- diff --git a/messages.mc b/messages.mc index 6f6ae40..fdf295b 100644 --- a/messages.mc +++ b/messages.mc @@ -1305,3 +1305,42 @@ Language = Italian Chiamata a GetProcAddress(%1) fallita: %2 . + +MessageId = +1 +SymbolicName = NSSM_EVENT_BOGUS_KILL_CONSOLE_GRACE_PERIOD +Severity = Warning +Language = English +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after sending a Control-C event, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. +Language = French +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after sending a Control-C event, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. +Language = Italian +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after sending a Control-C event, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. + +MessageId = +1 +SymbolicName = NSSM_EVENT_BOGUS_KILL_WINDOW_GRACE_PERIOD +Severity = Warning +Language = English +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after posting a WM_CLOSE message to windows managed by the application, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. +Language = French +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after posting a WM_CLOSE message to windows managed by the application, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. +Language = Italian +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after posting a WM_CLOSE message to windows managed by the application, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. + +MessageId = +1 +SymbolicName = NSSM_EVENT_BOGUS_KILL_THREADS_GRACE_PERIOD +Severity = Warning +Language = English +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after posting a WM_QUIT message to the message queues of threads managed by the application, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. +Language = French +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after posting a WM_QUIT message to the message queues of threads managed by the application, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. +Language = Italian +The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after posting a WM_QUIT message to the message queues of threads managed by the application, was not of type REG_DWORD. The default time of %3 milliseconds will be used. +. diff --git a/nssm.h b/nssm.h index b5d457e..01228bd 100644 --- a/nssm.h +++ b/nssm.h @@ -43,17 +43,17 @@ int str_equiv(const char *, const char *); /* How many milliseconds to wait for the application to die after sending - a Control-C event to its console. + a Control-C event to its console. Override in registry. */ #define NSSM_KILL_CONSOLE_GRACE_PERIOD 1500 /* How many milliseconds to wait for the application to die after posting to - its windows' message queues. + its windows' message queues. Override in registry. */ #define NSSM_KILL_WINDOW_GRACE_PERIOD 1500 /* How many milliseconds to wait for the application to die after posting to - its threads' message queues. + its threads' message queues. Override in registry. */ #define NSSM_KILL_THREADS_GRACE_PERIOD 1500 diff --git a/process.cpp b/process.cpp index fc0056d..f060d72 100644 --- a/process.cpp +++ b/process.cpp @@ -1,6 +1,9 @@ #include "nssm.h" extern imports_t imports; +extern unsigned long kill_console_delay; +extern unsigned long kill_window_delay; +extern unsigned long kill_threads_delay; int get_process_creation_time(HANDLE process_handle, FILETIME *ft) { FILETIME creation_time, exit_time, kernel_time, user_time; @@ -161,7 +164,7 @@ int kill_process(char *service_name, unsigned long stop_method, HANDLE process_h if (stop_method & NSSM_STOP_METHOD_WINDOW) { EnumWindows((WNDENUMPROC) kill_window, (LPARAM) &k); if (k.signalled) { - if (! WaitForSingleObject(process_handle, NSSM_KILL_WINDOW_GRACE_PERIOD)) return 1; + if (! WaitForSingleObject(process_handle, kill_window_delay)) return 1; } } @@ -172,7 +175,7 @@ int kill_process(char *service_name, unsigned long stop_method, HANDLE process_h */ if (stop_method & NSSM_STOP_METHOD_THREADS) { if (kill_threads(service_name, &k)) { - if (! WaitForSingleObject(process_handle, NSSM_KILL_THREADS_GRACE_PERIOD)) return 1; + if (! WaitForSingleObject(process_handle, kill_threads_delay)) return 1; } } @@ -233,7 +236,7 @@ int kill_console(char *service_name, HANDLE process_handle, unsigned long pid) { } /* Wait for process to exit. */ - if (WaitForSingleObject(process_handle, NSSM_KILL_CONSOLE_GRACE_PERIOD)) return 6; + if (WaitForSingleObject(process_handle, kill_console_delay)) return 6; return ret; } diff --git a/registry.cpp b/registry.cpp index 7a7b784..7eb6112 100644 --- a/registry.cpp +++ b/registry.cpp @@ -239,7 +239,7 @@ void override_milliseconds(char *service_name, HKEY key, char *value, unsigned l if (! ok) *buffer = default_value; } -int get_parameters(char *service_name, char *exe, unsigned long exelen, char *flags, unsigned long flagslen, char *dir, unsigned long dirlen, char **env, unsigned long *throttle_delay, unsigned long *stop_method, STARTUPINFO *si) { +int get_parameters(char *service_name, char *exe, unsigned long exelen, char *flags, unsigned long flagslen, char *dir, unsigned long dirlen, char **env, unsigned long *throttle_delay, unsigned long *stop_method, unsigned long *kill_console_delay, unsigned long *kill_window_delay, unsigned long *kill_threads_delay, STARTUPINFO *si) { unsigned long ret; /* Get registry */ @@ -322,6 +322,11 @@ int get_parameters(char *service_name, char *exe, unsigned long exelen, char *fl *stop_method = ~0; if (stop_ok) *stop_method &= ~stop_method_skip; + /* Try to get kill delays - may fail. */ + override_milliseconds(service_name, key, NSSM_REG_KILL_CONSOLE_GRACE_PERIOD, kill_console_delay, NSSM_KILL_CONSOLE_GRACE_PERIOD, NSSM_EVENT_BOGUS_KILL_CONSOLE_GRACE_PERIOD); + override_milliseconds(service_name, key, NSSM_REG_KILL_WINDOW_GRACE_PERIOD, kill_window_delay, NSSM_KILL_WINDOW_GRACE_PERIOD, NSSM_EVENT_BOGUS_KILL_WINDOW_GRACE_PERIOD); + override_milliseconds(service_name, key, NSSM_REG_KILL_THREADS_GRACE_PERIOD, kill_threads_delay, NSSM_KILL_THREADS_GRACE_PERIOD, NSSM_EVENT_BOGUS_KILL_THREADS_GRACE_PERIOD); + /* Close registry */ RegCloseKey(key); diff --git a/registry.h b/registry.h index 9129b64..17f26d7 100644 --- a/registry.h +++ b/registry.h @@ -9,6 +9,9 @@ #define NSSM_REG_EXIT "AppExit" #define NSSM_REG_THROTTLE "AppThrottle" #define NSSM_REG_STOP_METHOD_SKIP "AppStopMethodSkip" +#define NSSM_REG_KILL_CONSOLE_GRACE_PERIOD "AppStopMethodConsole" +#define NSSM_REG_KILL_WINDOW_GRACE_PERIOD "AppStopMethodWindow" +#define NSSM_REG_KILL_THREADS_GRACE_PERIOD "AppStopMethodThreads" #define NSSM_REG_STDIN "AppStdin" #define NSSM_REG_STDOUT "AppStdout" #define NSSM_REG_STDERR "AppStderr" @@ -26,7 +29,7 @@ int expand_parameter(HKEY, char *, char *, unsigned long, bool); int get_number(HKEY, char *, unsigned long *, bool); int get_number(HKEY, char *, unsigned long *); void override_milliseconds(char *, HKEY, char *, unsigned long *, unsigned long, unsigned long); -int get_parameters(char *, char *, unsigned long, char *, unsigned long, char *, unsigned long, char **, unsigned long *, unsigned long *, STARTUPINFO *); +int get_parameters(char *, char *, unsigned long, char *, unsigned long, char *, unsigned long, char **, unsigned long *, unsigned long *, unsigned long *, unsigned long *, unsigned long *, STARTUPINFO *); int get_exit_action(char *, unsigned long *, unsigned char *, bool *); #endif diff --git a/service.cpp b/service.cpp index 596aa7e..f14390f 100644 --- a/service.cpp +++ b/service.cpp @@ -14,6 +14,9 @@ bool stopping; bool allow_restart; unsigned long throttle_delay; unsigned long stop_method; +unsigned long kill_console_delay; +unsigned long kill_window_delay; +unsigned long kill_threads_delay; CRITICAL_SECTION throttle_section; CONDITION_VARIABLE throttle_condition; HANDLE throttle_timer; @@ -390,7 +393,7 @@ int start_service() { /* Get startup parameters */ char *env = 0; - int ret = get_parameters(service_name, exe, sizeof(exe), flags, sizeof(flags), dir, sizeof(dir), &env, &throttle_delay, &stop_method, &si); + int ret = get_parameters(service_name, exe, sizeof(exe), flags, sizeof(flags), dir, sizeof(dir), &env, &throttle_delay, &stop_method, &kill_console_delay, &kill_window_delay, &kill_threads_delay, &si); if (ret) { log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GET_PARAMETERS_FAILED, service_name, 0); return stop_service(2, true, true); @@ -446,9 +449,9 @@ int stop_service(unsigned long exitcode, bool graceful, bool default_action) { if (graceful) { service_status.dwCurrentState = SERVICE_STOP_PENDING; service_status.dwWaitHint = NSSM_WAITHINT_MARGIN; - if (stop_method & NSSM_STOP_METHOD_CONSOLE && imports.AttachConsole) service_status.dwWaitHint += NSSM_KILL_CONSOLE_GRACE_PERIOD; - if (stop_method & NSSM_STOP_METHOD_WINDOW) service_status.dwWaitHint += NSSM_KILL_WINDOW_GRACE_PERIOD; - if (stop_method & NSSM_STOP_METHOD_THREADS) service_status.dwWaitHint += NSSM_KILL_THREADS_GRACE_PERIOD; + if (stop_method & NSSM_STOP_METHOD_CONSOLE && imports.AttachConsole) service_status.dwWaitHint += kill_console_delay; + if (stop_method & NSSM_STOP_METHOD_WINDOW) service_status.dwWaitHint += kill_window_delay; + if (stop_method & NSSM_STOP_METHOD_THREADS) service_status.dwWaitHint += kill_threads_delay; SetServiceStatus(service_handle, &service_status); } -- 2.7.4 From 636b16d3702049145143511c6461ff20bf1f245d Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 15 Nov 2013 11:21:58 +0000 Subject: [PATCH 10/16] Prevent buffer overrun in log_event(). We were only expecting six message strings but we could possible receive more. --- event.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/event.cpp b/event.cpp index 995b7af..32e4acf 100644 --- a/event.cpp +++ b/event.cpp @@ -1,6 +1,7 @@ #include "nssm.h" #define NSSM_ERROR_BUFSIZE 65535 +#define NSSM_NUM_EVENT_STRINGS 16 unsigned long tls_index; /* Convert error code to error string - must call LocalFree() on return value */ @@ -34,7 +35,7 @@ void log_event(unsigned short type, unsigned long id, ...) { va_list arg; int count; char *s; - char *strings[6]; + char *strings[NSSM_NUM_EVENT_STRINGS]; /* Open event log */ HANDLE handle = RegisterEventSource(0, TEXT(NSSM)); @@ -43,7 +44,8 @@ void log_event(unsigned short type, unsigned long id, ...) { /* Log it */ count = 0; va_start(arg, id); - while ((s = va_arg(arg, char *))) strings[count++] = s; + while ((s = va_arg(arg, char *)) && count < NSSM_NUM_EVENT_STRINGS - 1) strings[count++] = s; + strings[count] = 0; va_end(arg); ReportEvent(handle, type, 0, id, 0, count, 0, (const char **) strings, 0); -- 2.7.4 From f3d91adc48a7618fd9c94cbc45143f89b47b59e4 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 15 Nov 2013 11:24:45 +0000 Subject: [PATCH 11/16] Added await_shutdown() function. The system expects service STOP requests to be honoured promptly. If service shutdown will take longer than 30 seconds we must update the service status checkpoint variable before then. We also need to ensure that the service status wait hint time is strictly increasing every time we update the checkpoint, otherwise the service will be considered to be hung after 60 seconds. --- messages.mc | 16 ++++++++++++ nssm.h | 3 +++ service.cpp | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ service.h | 1 + 4 files changed, 106 insertions(+) diff --git a/messages.mc b/messages.mc index fdf295b..a2da26f 100644 --- a/messages.mc +++ b/messages.mc @@ -1344,3 +1344,19 @@ The registry value %2, used to specify the maximum number of milliseconds to wai Language = Italian The registry value %2, used to specify the maximum number of milliseconds to wait for service %1 to stop after posting a WM_QUIT message to the message queues of threads managed by the application, was not of type REG_DWORD. The default time of %3 milliseconds will be used. . + +MessageId = +1 +SymbolicName = NSSM_EVENT_AWAITING_SHUTDOWN +Severity = Informational +Language = English +%1 has waited %3 of %5 milliseconds for the %2 service to exit. +Next update in %4 milliseconds. +. +Language = French +%1 has waited %3 of %5 milliseconds for the %2 service to exit. +Next update in %4 milliseconds. +. +Language = Italian +%1 has waited %3 of %5 milliseconds for the %2 service to exit. +Next update in %4 milliseconds. +. diff --git a/nssm.h b/nssm.h index 01228bd..bfde164 100644 --- a/nssm.h +++ b/nssm.h @@ -66,4 +66,7 @@ int str_equiv(const char *, const char *); #define NSSM_STOP_METHOD_THREADS (1 << 2) #define NSSM_STOP_METHOD_TERMINATE (1 << 3) +/* How many milliseconds to wait before updating service status. */ +#define NSSM_SHUTDOWN_CHECKPOINT 20000 + #endif diff --git a/service.cpp b/service.cpp index f14390f..028b035 100644 --- a/service.cpp +++ b/service.cpp @@ -599,3 +599,89 @@ void throttle_restart() { else Sleep(ms); } } + +/* + When responding to a stop (or any other) request we need to set dwWaitHint to + the number of milliseconds we expect the operation to take, and optionally + increase dwCheckPoint. If dwWaitHint milliseconds elapses without the + operation completing or dwCheckPoint increasing, the system will consider the + service to be hung. + + However the system will consider the service to be hung after 30000 + milliseconds regardless of the value of dwWaitHint if dwCheckPoint has not + changed. Therefore if we want to wait longer than that we must periodically + increase dwCheckPoint. + + Furthermore, it will consider the service to be hung after 60000 milliseconds + regardless of the value of dwCheckPoint unless dwWaitHint is increased every + time dwCheckPoint is also increased. + + Our strategy then is to retrieve the initial dwWaitHint and wait for + NSSM_SHUTDOWN_CHECKPOINT milliseconds. If the process is still running and + we haven't finished waiting we increment dwCheckPoint and add whichever is + smaller of NSSM_SHUTDOWN_CHECKPOINT or the remaining timeout to dwWaitHint. + + Only doing both these things will prevent the system from killing the service. + + Returns: 1 if the wait timed out. + 0 if the wait completed. + -1 on error. +*/ +int await_shutdown(char *function_name, char *service_name, SERVICE_STATUS_HANDLE service_handle, SERVICE_STATUS *service_status, HANDLE process_handle, unsigned long timeout) { + unsigned long interval; + unsigned long waithint; + unsigned long ret; + unsigned long waited; + char interval_milliseconds[16]; + char timeout_milliseconds[16]; + char waited_milliseconds[16]; + char *function = function_name; + + /* Add brackets to function name. */ + size_t funclen = strlen(function_name) + 3; + char *func = (char *) HeapAlloc(GetProcessHeap(), 0, funclen); + if (func) { + if (_snprintf_s(func, funclen, _TRUNCATE, "%s()", function_name) > -1) function = func; + } + + _snprintf_s(timeout_milliseconds, sizeof(timeout_milliseconds), _TRUNCATE, "%lu", timeout); + + waithint = service_status->dwWaitHint; + waited = 0; + while (waited < timeout) { + interval = timeout - waited; + if (interval > NSSM_SHUTDOWN_CHECKPOINT) interval = NSSM_SHUTDOWN_CHECKPOINT; + + service_status->dwCurrentState = SERVICE_STOP_PENDING; + service_status->dwWaitHint += interval; + service_status->dwCheckPoint++; + SetServiceStatus(service_handle, service_status); + + if (waited) { + _snprintf_s(waited_milliseconds, sizeof(waited_milliseconds), _TRUNCATE, "%lu", waited); + _snprintf_s(interval_milliseconds, sizeof(interval_milliseconds), _TRUNCATE, "%lu", interval); + log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_AWAITING_SHUTDOWN, function, service_name, waited_milliseconds, interval_milliseconds, timeout_milliseconds, 0); + } + + switch (WaitForSingleObject(process_handle, interval)) { + case WAIT_OBJECT_0: + ret = 0; + goto awaited; + + case WAIT_TIMEOUT: + ret = 1; + break; + + default: + ret = -1; + goto awaited; + } + + waited += interval; + } + +awaited: + if (func) HeapFree(GetProcessHeap(), 0, func); + + return ret; +} diff --git a/service.h b/service.h index 2db92fd..49760ba 100644 --- a/service.h +++ b/service.h @@ -19,5 +19,6 @@ int start_service(); int stop_service(unsigned long, bool, bool); void CALLBACK end_service(void *, unsigned char); void throttle_restart(); +int await_shutdown(char *, char *, SERVICE_STATUS_HANDLE, SERVICE_STATUS *, HANDLE, unsigned long); #endif -- 2.7.4 From 99c5c2868f1d351d4d0569fda9199dda1aadad07 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 15 Nov 2013 15:17:39 +0000 Subject: [PATCH 12/16] Use await_shutdown(). Use the new await_shutdown() funtion to wait for the application to stop rather than calling WaitForSingleObject() directly, so we can update the service status periodically. --- process.cpp | 20 ++++++++++---------- process.h | 8 ++++---- service.cpp | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/process.cpp b/process.cpp index f060d72..7bb6dac 100644 --- a/process.cpp +++ b/process.cpp @@ -139,7 +139,7 @@ int kill_threads(char *service_name, kill_t *k) { } /* Give the process a chance to die gracefully. */ -int kill_process(char *service_name, unsigned long stop_method, HANDLE process_handle, unsigned long pid, unsigned long exitcode) { +int kill_process(char *service_name, SERVICE_STATUS_HANDLE service_handle, SERVICE_STATUS *service_status, unsigned long stop_method, HANDLE process_handle, unsigned long pid, unsigned long exitcode) { /* Shouldn't happen. */ if (! pid) return 1; if (! process_handle) return 1; @@ -153,7 +153,7 @@ int kill_process(char *service_name, unsigned long stop_method, HANDLE process_h /* Try to send a Control-C event to the console. */ if (stop_method & NSSM_STOP_METHOD_CONSOLE) { - if (! kill_console(service_name, process_handle, pid)) return 1; + if (! kill_console(service_name, service_handle, service_status, process_handle, pid)) return 1; } /* @@ -164,7 +164,7 @@ int kill_process(char *service_name, unsigned long stop_method, HANDLE process_h if (stop_method & NSSM_STOP_METHOD_WINDOW) { EnumWindows((WNDENUMPROC) kill_window, (LPARAM) &k); if (k.signalled) { - if (! WaitForSingleObject(process_handle, kill_window_delay)) return 1; + if (! await_shutdown(__FUNCTION__, service_name, service_handle, service_status, process_handle, kill_window_delay)) return 1; } } @@ -175,7 +175,7 @@ int kill_process(char *service_name, unsigned long stop_method, HANDLE process_h */ if (stop_method & NSSM_STOP_METHOD_THREADS) { if (kill_threads(service_name, &k)) { - if (! WaitForSingleObject(process_handle, kill_threads_delay)) return 1; + if (! await_shutdown(__FUNCTION__, service_name, service_handle, service_status, process_handle, kill_threads_delay)) return 1; } } @@ -188,7 +188,7 @@ int kill_process(char *service_name, unsigned long stop_method, HANDLE process_h } /* Simulate a Control-C event to our console (shared with the app). */ -int kill_console(char *service_name, HANDLE process_handle, unsigned long pid) { +int kill_console(char *service_name, SERVICE_STATUS_HANDLE service_handle, SERVICE_STATUS *service_status, HANDLE process_handle, unsigned long pid) { unsigned long ret; /* Check we loaded AttachConsole(). */ @@ -236,12 +236,12 @@ int kill_console(char *service_name, HANDLE process_handle, unsigned long pid) { } /* Wait for process to exit. */ - if (WaitForSingleObject(process_handle, kill_console_delay)) return 6; + if (await_shutdown(__FUNCTION__, service_name, service_handle, service_status, process_handle, kill_console_delay)) ret = 6; return ret; } -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) { +void kill_process_tree(char *service_name, SERVICE_STATUS_HANDLE service_handle, SERVICE_STATUS *service_status, unsigned long stop_method, unsigned long pid, unsigned long exitcode, unsigned long ppid, FILETIME *parent_creation_time, FILETIME *parent_exit_time) { /* Shouldn't happen unless the service failed to start. */ if (! pid) return; @@ -268,7 +268,7 @@ void kill_process_tree(char *service_name, unsigned long stop_method, unsigned l } /* This is a child of the doomed process so kill it. */ - 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); + if (! check_parent(service_name, &pe, pid, parent_creation_time, parent_exit_time)) kill_process_tree(service_name, service_handle, service_status, stop_method, pe.th32ProcessID, exitcode, ppid, parent_creation_time, parent_exit_time); while (true) { /* Try to get the next process. */ @@ -280,7 +280,7 @@ void kill_process_tree(char *service_name, unsigned long stop_method, unsigned l return; } - 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); + if (! check_parent(service_name, &pe, pid, parent_creation_time, parent_exit_time)) kill_process_tree(service_name, service_handle, service_status, stop_method, pe.th32ProcessID, exitcode, ppid, parent_creation_time, parent_exit_time); } CloseHandle(snapshot); @@ -295,7 +295,7 @@ void kill_process_tree(char *service_name, unsigned long stop_method, unsigned l char ppid_string[16]; _snprintf_s(ppid_string, sizeof(ppid_string), _TRUNCATE, "%lu", ppid); log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILL_PROCESS_TREE, pid_string, ppid_string, service_name, 0); - if (! kill_process(service_name, stop_method, process_handle, pid, exitcode)) { + if (! kill_process(service_name, service_handle, service_status, stop_method, process_handle, pid, exitcode)) { /* Maybe it already died. */ unsigned long ret; if (! GetExitCodeProcess(process_handle, &ret) || ret == STILL_ACTIVE) { diff --git a/process.h b/process.h index 4a1547b..21d86da 100644 --- a/process.h +++ b/process.h @@ -13,9 +13,9 @@ int get_process_creation_time(HANDLE, FILETIME *); int get_process_exit_time(HANDLE, FILETIME *); int check_parent(char *, PROCESSENTRY32 *, unsigned long, FILETIME *, FILETIME *); int CALLBACK kill_window(HWND, LPARAM); -int kill_threads(char *, kill_t *); -int kill_console(char *, HANDLE, unsigned long); -int kill_process(char *, unsigned long, HANDLE, unsigned long, unsigned long); -void kill_process_tree(char *, unsigned long, unsigned long, unsigned long, unsigned long, FILETIME *, FILETIME *); +int kill_threads(char *, SERVICE_STATUS_HANDLE, SERVICE_STATUS *, kill_t *); +int kill_console(char *, SERVICE_STATUS_HANDLE, SERVICE_STATUS *, HANDLE, unsigned long); +int kill_process(char *, SERVICE_STATUS_HANDLE, SERVICE_STATUS *, unsigned long, HANDLE, unsigned long, unsigned long); +void kill_process_tree(char *, SERVICE_STATUS_HANDLE, SERVICE_STATUS *, unsigned long, unsigned long, unsigned long, unsigned long, FILETIME *, FILETIME *); #endif diff --git a/service.cpp b/service.cpp index 028b035..5c6adaf 100644 --- a/service.cpp +++ b/service.cpp @@ -459,7 +459,7 @@ int stop_service(unsigned long exitcode, bool graceful, bool default_action) { if (pid) { /* Shut down service */ log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_TERMINATEPROCESS, service_name, exe, 0); - kill_process(service_name, stop_method, process_handle, pid, 0); + kill_process(service_name, service_handle, &service_status, stop_method, process_handle, pid, 0); } else log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_PROCESS_ALREADY_STOPPED, service_name, exe, 0); @@ -509,7 +509,7 @@ void CALLBACK end_service(void *arg, unsigned char why) { /* Clean up. */ if (exitcode == STILL_ACTIVE) exitcode = 0; - kill_process_tree(service_name, stop_method, pid, exitcode, pid, &creation_time, &exit_time); + kill_process_tree(service_name, service_handle, &service_status, stop_method, pid, exitcode, pid, &creation_time, &exit_time); /* The why argument is true if our wait timed out or false otherwise. -- 2.7.4 From a3ad2151ad143eb587958e61a93d2de405d490df Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 15 Nov 2013 15:44:46 +0000 Subject: [PATCH 13/16] Spawn a separate thread for stop_service(). We must acknowledge a STOP or SHUTDOWN control promptly but the termination of the application may take a significant amount of time if one of the AppStopMethod* registry values is set. We now spawn a separate thread to try to stop the process and to call await_shutdown() while the main thread immediately acknowledges receipt of the STOP request. Once the worker thread has updated the service status to say that application is really stopped we will be automatically cleaned up by the system. If for some reason we can't spawn a new thread we log an error and ignore user-supplied timeouts so as to ensure we tidy up promptly. --- messages.mc | 16 ++++++++++++++++ service.cpp | 30 ++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/messages.mc b/messages.mc index a2da26f..2ab7dc1 100644 --- a/messages.mc +++ b/messages.mc @@ -1360,3 +1360,19 @@ Language = Italian %1 has waited %3 of %5 milliseconds for the %2 service to exit. Next update in %4 milliseconds. . + +MessageId = +1 +SymbolicName = NSSM_EVENT_CREATETHREAD_FAILED +Severity = Error +Language = English +CreateThread() failed: +%1 +. +Language = French +CreateThread() a échoué: +%1 +. +Language = Italian +Chiamata a CreateThread() fallita: +%1 +. diff --git a/service.cpp b/service.cpp index 5c6adaf..c24263f 100644 --- a/service.cpp +++ b/service.cpp @@ -37,6 +37,14 @@ static inline int throttle_milliseconds() { return ret * 1000; } +/* + Wrapper to be called in a new thread so that we can acknowledge a STOP + control immediately. +*/ +static unsigned long WINAPI shutdown_service(void *arg) { + return stop_service(0, true, true); +} + /* Connect to the service manager */ SC_HANDLE open_service_manager() { SC_HANDLE ret = OpenSCManager(0, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS); @@ -343,7 +351,24 @@ unsigned long WINAPI service_control_handler(unsigned long control, unsigned lon case SERVICE_CONTROL_SHUTDOWN: case SERVICE_CONTROL_STOP: log_service_control(service_name, control, true); - stop_service(0, true, true); + /* + We MUST acknowledge the stop request promptly but we're committed to + waiting for the application to exit. Spawn a new thread to wait + while we acknowledge the request. + */ + if (! CreateThread(NULL, 0, shutdown_service, (void *) service_name, 0, NULL)) { + log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATETHREAD_FAILED, error_string(GetLastError()), 0); + + /* + We couldn't create a thread to tidy up so we'll have to force the tidyup + to complete in time in this thread. + */ + kill_console_delay = NSSM_KILL_CONSOLE_GRACE_PERIOD; + kill_window_delay = NSSM_KILL_WINDOW_GRACE_PERIOD; + kill_threads_delay = NSSM_KILL_THREADS_GRACE_PERIOD; + + stop_service(0, true, true); + } return NO_ERROR; case SERVICE_CONTROL_CONTINUE: @@ -449,9 +474,6 @@ int stop_service(unsigned long exitcode, bool graceful, bool default_action) { if (graceful) { service_status.dwCurrentState = SERVICE_STOP_PENDING; service_status.dwWaitHint = NSSM_WAITHINT_MARGIN; - if (stop_method & NSSM_STOP_METHOD_CONSOLE && imports.AttachConsole) service_status.dwWaitHint += kill_console_delay; - if (stop_method & NSSM_STOP_METHOD_WINDOW) service_status.dwWaitHint += kill_window_delay; - if (stop_method & NSSM_STOP_METHOD_THREADS) service_status.dwWaitHint += kill_threads_delay; SetServiceStatus(service_handle, &service_status); } -- 2.7.4 From 961ffcf2e9dba8795ad9019124822d9288442b6f Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 15 Nov 2013 15:48:56 +0000 Subject: [PATCH 14/16] Don't automatically close the GUI on error. If a parameter in the GUI was missing or invalid we were closing the dialogue and forcing the user to relaunch the installer/remover. Instead we now loop until the service has successfully been installed/removed or the user cancelled the operation. --- gui.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui.cpp b/gui.cpp index 7955b1a..c45347f 100644 --- a/gui.cpp +++ b/gui.cpp @@ -205,7 +205,7 @@ INT_PTR CALLBACK install_dlg(HWND window, UINT message, WPARAM w, LPARAM l) { switch (LOWORD(w)) { /* OK button */ case IDC_OK: - PostQuitMessage(install(window)); + if (! install(window)) PostQuitMessage(0); break; /* Cancel button */ @@ -220,7 +220,7 @@ INT_PTR CALLBACK install_dlg(HWND window, UINT message, WPARAM w, LPARAM l) { /* Remove button */ case IDC_REMOVE: - PostQuitMessage(remove(window)); + if (! remove(window)) PostQuitMessage(0); break; } return 1; -- 2.7.4 From 44d8d66023bbe08a0237f2b519f3332cee549d4b Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 15 Nov 2013 15:50:17 +0000 Subject: [PATCH 15/16] Allow the Escape key to close the GUI. Turns out IDCANCEL is a magic resource name. If a button has that name it automatically responds to Escape. --- ChangeLog.txt | 2 ++ gui.cpp | 2 +- nssm.rc | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ChangeLog.txt b/ChangeLog.txt index d4850a3..0317979 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -3,6 +3,8 @@ Changes since 2.17 * Timeouts for each shutdown method can be configured in the registry. + * The GUI is slightly less sucky. + Changes since 2.16 ----------------- * NSSM can now redirect the service's I/O streams to any path diff --git a/gui.cpp b/gui.cpp index c45347f..5aaf016 100644 --- a/gui.cpp +++ b/gui.cpp @@ -209,7 +209,7 @@ INT_PTR CALLBACK install_dlg(HWND window, UINT message, WPARAM w, LPARAM l) { break; /* Cancel button */ - case IDC_CANCEL: + case IDCANCEL: DestroyWindow(window); break; diff --git a/nssm.rc b/nssm.rc index d7c9d3b..8574f6c 100644 --- a/nssm.rc +++ b/nssm.rc @@ -70,7 +70,7 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "Install service",IDC_OK,55,69,50,14 - PUSHBUTTON "Cancel",IDC_CANCEL,111,69,50,14 + PUSHBUTTON "Cancel",IDCANCEL,111,69,50,14 EDITTEXT IDC_PATH,48,7,110,14,ES_AUTOHSCROLL PUSHBUTTON "Browse",IDC_BROWSE,163,7,50,14 EDITTEXT IDC_FLAGS,48,28,165,14,ES_AUTOHSCROLL @@ -161,7 +161,7 @@ LANGUAGE LANG_FRENCH, SUBLANG_FRENCH FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "Installer le service",IDC_OK,49,69,75,14 - PUSHBUTTON "Annuler",IDC_CANCEL,131,69,50,14 + PUSHBUTTON "Annuler",IDCANCEL,131,69,50,14 EDITTEXT IDC_PATH,48,7,110,14,ES_AUTOHSCROLL PUSHBUTTON "Parcourir",IDC_BROWSE,163,7,50,14 EDITTEXT IDC_FLAGS,48,28,165,14,ES_AUTOHSCROLL @@ -247,7 +247,7 @@ LANGUAGE LANG_ITALIAN, SUBLANG_ITALIAN FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "Installa servizio",IDC_OK,49,69,58,14 - PUSHBUTTON "Annulla",IDC_CANCEL,111,69,50,14 + PUSHBUTTON "Annulla",IDCANCEL,111,69,50,14 EDITTEXT IDC_PATH,48,7,110,14,ES_AUTOHSCROLL PUSHBUTTON "Sfoglia...",IDC_BROWSE,163,7,50,14 EDITTEXT IDC_FLAGS,48,28,165,14,ES_AUTOHSCROLL -- 2.7.4 From da1b8e1cb0ae3870405685a4d07f3d75d02e618a Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 15 Nov 2013 15:59:59 +0000 Subject: [PATCH 16/16] NSSM 2.18. --- README.txt | 2 +- nssm.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.txt b/README.txt index f2fe712..f5ddd2f 100644 --- a/README.txt +++ b/README.txt @@ -1,5 +1,5 @@ NSSM: The Non-Sucking Service Manager -Version 2.17, 2013-11-12 +Version 2.18, 2013-11-15 NSSM is a service helper program similar to srvany and cygrunsrv. It can start any application as an NT service and will restart the service if it diff --git a/nssm.h b/nssm.h index bfde164..1c1937c 100644 --- a/nssm.h +++ b/nssm.h @@ -18,9 +18,9 @@ int str_equiv(const char *, const char *); #define NSSM "nssm" -#define NSSM_VERSION "2.17" -#define NSSM_VERSIONINFO 2,17,0,0 -#define NSSM_DATE "2013-11-12" +#define NSSM_VERSION "2.18" +#define NSSM_VERSIONINFO 2,18,0,0 +#define NSSM_DATE "2013-11-15" /* MSDN says the commandline in CreateProcess() is limited to 32768 characters -- 2.7.4