From 23b8173ce06a843f18a9269fc6f7d4d0d224cd4c Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Thu, 31 Oct 2013 13:14:31 +0000 Subject: [PATCH] Skip some or all methods of stopping the application. Use the AppStopMethodSkip bit field to specify any method(s) which should not be employed when stopping the application. --- README.txt | 43 +++++++++++++++++++++++++++++++++++++++++++ messages.mc | 32 ++++++++++++++++++++++++++++++++ nssm.h | 6 ++++++ process.cpp | 39 ++++++++++++++++++++++++++------------- process.h | 4 ++-- registry.cpp | 22 +++++++++++++++++++++- registry.h | 3 ++- service.cpp | 7 ++++--- 8 files changed, 136 insertions(+), 20 deletions(-) diff --git a/README.txt b/README.txt index 904468a..3559245 100644 --- a/README.txt +++ b/README.txt @@ -140,6 +140,49 @@ request to suicide if you explicitly configure a registry key for exit code 0. If only the default action is set to Suicide NSSM will instead exit gracefully. +Stopping the service +-------------------- +When stopping a service NSSM will attempt several different methods of killing +the monitored application, each of which can be disabled if necessary. + +First NSSM will attempt to generate a Control-C event and send it to the +application's console. Batch scripts or console applications may intercept +the event and shut themselves down gracefully. GUI applications do not have +consoles and will not respond to this method. + +Secondly NSSM will enumerate all windows created by the application and send +them a WM_CLOSE message, requesting a graceful exit. + +Thirdly NSSM will enumerate all threads created by the application and send +them a WM_QUIT message, requesting a graceful exit. Not all applications' +threads have message queues; those which do not will not respond to this +method. + +Finally NSSM will call TerminateProcess() to request that the operating +system forcibly terminate the application. TerminateProcess() cannot be +trapped or ignored, so in most circumstances the application will be killed. +However, there is no guarantee that it will have a chance to perform any +tidyup operations before it exits. + +Any or all of the methods above may be disabled. NSSM will look for the +HKLM\SYSTEM\CurrentControlSet\Services\\Parameters\AppStopMethodSkip +registry value which should be of type REG_DWORD set to a bit field describing +which methods should not be applied. + + If AppStopMethodSkip includes 1, Control-C events will not be generated. + If AppStopMethodSkip includes 2, WM_CLOSE messages will not be posted. + If AppStopMethodSkip includes 4, WM_QUIT messages will not be posted. + If AppStopMethodSkip includes 8, TerminateProcess() will not be called. + +If, for example, you knew that an application did not respond to Control-C +events and did not have a thread message queue, you could set AppStopMethodSkip +to 5 and NSSM would not attempt to use those methods to stop the application. + +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. + + I/O redirection --------------- NSSM can redirect the managed application's I/O to any path capable of being diff --git a/messages.mc b/messages.mc index 21eb640..0aea1ae 100644 --- a/messages.mc +++ b/messages.mc @@ -1238,3 +1238,35 @@ Error setting up one or more I/O filehandles. Service %1 will not be started. Language = Italian Error setting up one or more I/O filehandles. Service %1 will not be started. . + +MessageId = +1 +SymbolicName = NSSM_EVENT_BOGUS_STOP_METHOD_SKIP +Severity = Warning +Language = English +The registry value %2, used to specify the method(s) by which %3 will skip when attempting to stop service %1, was not of type REG_DWORD. All available methods will be used. +. +Language = French +The registry value %2, used to specify the method(s) by which %3 will skip when attempting to stop service %1, was not of type REG_DWORD. All available methods will be used. +. +Language = Italian +The registry value %2, used to specify the method(s) by which %3 will skip when attempting to stop service %1, was not of type REG_DWORD. All available methods will be used. +. + +MessageId = +1 +SymbolicName = NSSM_EVENT_PROCESS_STILL_ACTIVE +Severity = Warning +Language = English +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. +. +Language = French +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. +. +Language = Italian +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. +. diff --git a/nssm.h b/nssm.h index 0e918c3..afff8c4 100644 --- a/nssm.h +++ b/nssm.h @@ -59,4 +59,10 @@ int str_equiv(const char *, const char *); /* Margin of error for service status wait hints in milliseconds. */ #define NSSM_WAITHINT_MARGIN 2000 +/* Methods used to try to stop the application. */ +#define NSSM_STOP_METHOD_CONSOLE (1 << 0) +#define NSSM_STOP_METHOD_WINDOW (1 << 1) +#define NSSM_STOP_METHOD_THREADS (1 << 2) +#define NSSM_STOP_METHOD_TERMINATE (1 << 3) + #endif diff --git a/process.cpp b/process.cpp index 67bc978..e654811 100644 --- a/process.cpp +++ b/process.cpp @@ -134,7 +134,7 @@ int kill_threads(char *service_name, kill_t *k) { } /* Give the process a chance to die gracefully. */ -int kill_process(char *service_name, HANDLE process_handle, unsigned long pid, unsigned long exitcode) { +int kill_process(char *service_name, 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; @@ -147,16 +147,20 @@ int kill_process(char *service_name, HANDLE process_handle, unsigned long pid, u kill_t k = { pid, exitcode, 0 }; /* Try to send a Control-C event to the console. */ - if (! kill_console(service_name, process_handle, pid)) return 1; + if (stop_method & NSSM_STOP_METHOD_CONSOLE) { + if (! kill_console(service_name, process_handle, pid)) return 1; + } /* Try to post messages to the windows belonging to the given process ID. If the process is a console application it won't have any windows so there's no guarantee of success. */ - EnumWindows((WNDENUMPROC) kill_window, (LPARAM) &k); - if (k.signalled) { - if (! WaitForSingleObject(process_handle, NSSM_KILL_WINDOW_GRACE_PERIOD)) return 1; + 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; + } } /* @@ -164,12 +168,18 @@ int kill_process(char *service_name, HANDLE process_handle, unsigned long pid, u process. Console applications might have them (but probably won't) so there's still no guarantee of success. */ - if (kill_threads(service_name, &k)) { - if (! WaitForSingleObject(process_handle, NSSM_KILL_THREADS_GRACE_PERIOD)) return 1; + if (stop_method & NSSM_STOP_METHOD_THREADS) { + if (kill_threads(service_name, &k)) { + if (! WaitForSingleObject(process_handle, NSSM_KILL_THREADS_GRACE_PERIOD)) return 1; + } } /* We tried being nice. Time for extreme prejudice. */ - return TerminateProcess(process_handle, exitcode); + if (stop_method & NSSM_STOP_METHOD_TERMINATE) { + return TerminateProcess(process_handle, exitcode); + } + + return 0; } /* Simulate a Control-C event to our console (shared with the app). */ @@ -223,7 +233,7 @@ int kill_console(char *service_name, HANDLE process_handle, unsigned long pid) { return ret; } -void kill_process_tree(char *service_name, unsigned long pid, unsigned long exitcode, unsigned long ppid, FILETIME *parent_creation_time, FILETIME *parent_exit_time) { +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) { /* Shouldn't happen unless the service failed to start. */ if (! pid) return; @@ -250,7 +260,7 @@ void kill_process_tree(char *service_name, unsigned long pid, unsigned long exit } /* 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, 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, stop_method, pe.th32ProcessID, exitcode, ppid, parent_creation_time, parent_exit_time); while (true) { /* Try to get the next process. */ @@ -262,7 +272,7 @@ void kill_process_tree(char *service_name, unsigned long pid, unsigned long exit return; } - if (! check_parent(service_name, &pe, pid, parent_creation_time, parent_exit_time)) kill_process_tree(service_name, pe.th32ProcessID, exitcode, ppid, parent_creation_time, parent_exit_time); + 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); } CloseHandle(snapshot); @@ -277,10 +287,13 @@ void kill_process_tree(char *service_name, unsigned long pid, unsigned long exit char ppid_string[16]; _snprintf(ppid_string, sizeof(ppid_string), "%d", ppid); log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_KILL_PROCESS_TREE, pid_string, ppid_string, service_name, 0); - if (! kill_process(service_name, process_handle, pid, exitcode)) { + if (! kill_process(service_name, stop_method, process_handle, pid, exitcode)) { /* Maybe it already died. */ unsigned long ret; - if (! GetExitCodeProcess(process_handle, &ret) || ret == STILL_ACTIVE) log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_TERMINATEPROCESS_FAILED, pid_string, service_name, error_string(GetLastError()), 0); + if (! GetExitCodeProcess(process_handle, &ret) || ret == STILL_ACTIVE) { + if (stop_method & NSSM_STOP_METHOD_TERMINATE) log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_TERMINATEPROCESS_FAILED, pid_string, service_name, error_string(GetLastError()), 0); + else log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_PROCESS_STILL_ACTIVE, service_name, pid_string, NSSM, NSSM_REG_STOP_METHOD_SKIP, 0); + } } CloseHandle(process_handle); diff --git a/process.h b/process.h index 2560621..4a1547b 100644 --- a/process.h +++ b/process.h @@ -14,8 +14,8 @@ 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_process(char *, HANDLE, unsigned long, unsigned long); int kill_console(char *, HANDLE, unsigned long); -void kill_process_tree(char *, unsigned long, unsigned long, unsigned long, FILETIME *, FILETIME *); +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 *); #endif diff --git a/registry.cpp b/registry.cpp index eacde54..aa16ac7 100644 --- a/registry.cpp +++ b/registry.cpp @@ -219,7 +219,7 @@ int get_number(HKEY key, char *value, unsigned long *number) { return get_number(key, value, number, true); } -int get_parameters(char *service_name, char *exe, int exelen, char *flags, int flagslen, char *dir, int dirlen, char **env, unsigned long *throttle_delay, STARTUPINFO *si) { +int get_parameters(char *service_name, char *exe, int exelen, char *flags, int flagslen, char *dir, int dirlen, char **env, unsigned long *throttle_delay, unsigned long *stop_method, STARTUPINFO *si) { unsigned long ret; /* Get registry */ @@ -298,6 +298,26 @@ int get_parameters(char *service_name, char *exe, int exelen, char *flags, int f if (! throttle_ok) *throttle_delay = NSSM_RESET_THROTTLE_RESTART; + /* Try to get service stop flags. */ + type = REG_DWORD; + unsigned long stop_method_skip; + 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) { + if (ret != ERROR_FILE_NOT_FOUND) { + if (type != REG_DWORD) { + log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_BOGUS_STOP_METHOD_SKIP, service_name, NSSM_REG_STOP_METHOD_SKIP, NSSM, 0); + } + else log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, NSSM_REG_STOP_METHOD_SKIP, error_string(GetLastError()), 0); + } + } + else stop_ok = true; + + /* Try all methods except those requested to be skipped. */ + *stop_method = ~0; + if (stop_ok) *stop_method &= ~stop_method_skip; + /* Close registry */ RegCloseKey(key); diff --git a/registry.h b/registry.h index 811ac75..2f45d81 100644 --- a/registry.h +++ b/registry.h @@ -8,6 +8,7 @@ #define NSSM_REG_ENV "AppEnvironment" #define NSSM_REG_EXIT "AppExit" #define NSSM_REG_THROTTLE "AppThrottle" +#define NSSM_REG_STOP_METHOD_SKIP "AppStopMethodSkip" #define NSSM_REG_STDIN "AppStdin" #define NSSM_REG_STDOUT "AppStdout" #define NSSM_REG_STDERR "AppStderr" @@ -24,7 +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 *); -int get_parameters(char *, char *, int, char *, int, char *, int, char **, unsigned long *, STARTUPINFO *); +int get_parameters(char *, char *, int, char *, int, char *, int, char **, 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 21726df..873b8a8 100644 --- a/service.cpp +++ b/service.cpp @@ -12,6 +12,7 @@ char flags[CMD_LENGTH]; char dir[MAX_PATH]; bool stopping; unsigned long throttle_delay; +unsigned long stop_method; HANDLE throttle_timer; LARGE_INTEGER throttle_duetime; FILETIME creation_time; @@ -372,7 +373,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, &si); + int ret = get_parameters(service_name, exe, sizeof(exe), flags, sizeof(flags), dir, sizeof(dir), &env, &throttle_delay, &stop_method, &si); if (ret) { log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GET_PARAMETERS_FAILED, service_name, 0); return stop_service(2, true, true); @@ -431,7 +432,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, process_handle, pid, 0); + kill_process(service_name, stop_method, process_handle, pid, 0); } else log_event(EVENTLOG_INFORMATION_TYPE, NSSM_EVENT_PROCESS_ALREADY_STOPPED, service_name, exe, 0); @@ -480,7 +481,7 @@ void CALLBACK end_service(void *arg, unsigned char why) { } /* Clean up. */ - kill_process_tree(service_name, pid, exitcode, pid, &creation_time, &exit_time); + kill_process_tree(service_name, 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