Graceful termination on shutdown, logoff, and service stop (#647)
This commit is contained in:
parent
dc5571ba98
commit
8ad7af86c0
2 changed files with 168 additions and 26 deletions
70
src/main.cpp
70
src/main.cpp
|
|
@ -136,16 +136,86 @@ std::map<std::string_view, std::function<int(const char *name, int argc, char **
|
||||||
{ "version"sv, version::entry }
|
{ "version"sv, version::entry }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
LRESULT CALLBACK SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
||||||
|
switch(uMsg) {
|
||||||
|
case WM_ENDSESSION: {
|
||||||
|
// Raise a SIGINT to trigger our cleanup logic and terminate ourselves
|
||||||
|
std::cout << "Received WM_ENDSESSION"sv << std::endl;
|
||||||
|
std::raise(SIGINT);
|
||||||
|
|
||||||
|
// The signal handling is asynchronous, so we will wait here to be terminated.
|
||||||
|
// If for some reason we don't terminate in a few seconds, Windows will kill us.
|
||||||
|
SuspendThread(GetCurrentThread());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
util::TaskPool::task_id_t force_shutdown = nullptr;
|
util::TaskPool::task_id_t force_shutdown = nullptr;
|
||||||
|
|
||||||
bool shutdown_by_interrupt = false;
|
bool shutdown_by_interrupt = false;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Wait as long as possible to terminate Sunshine.exe during logoff/shutdown
|
||||||
|
SetProcessShutdownParameters(0x100, SHUTDOWN_NORETRY);
|
||||||
|
|
||||||
|
// We must create a hidden window to receive shutdown notifications since we load gdi32.dll
|
||||||
|
std::thread window_thread([]() {
|
||||||
|
WNDCLASSA wnd_class {};
|
||||||
|
wnd_class.lpszClassName = "SunshineSessionMonitorClass";
|
||||||
|
wnd_class.lpfnWndProc = SessionMonitorWindowProc;
|
||||||
|
if(!RegisterClassA(&wnd_class)) {
|
||||||
|
std::cout << "Failed to register session monitor window class"sv << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto wnd = CreateWindowExA(
|
||||||
|
0,
|
||||||
|
wnd_class.lpszClassName,
|
||||||
|
"Sunshine Session Monitor Window",
|
||||||
|
0,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
nullptr);
|
||||||
|
if(!wnd) {
|
||||||
|
std::cout << "Failed to create session monitor window"sv << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowWindow(wnd, SW_HIDE);
|
||||||
|
|
||||||
|
// Run the message loop for our window
|
||||||
|
MSG msg {};
|
||||||
|
while(GetMessage(&msg, nullptr, 0, 0) > 0) {
|
||||||
|
TranslateMessage(&msg);
|
||||||
|
DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window_thread.detach();
|
||||||
|
#endif
|
||||||
|
|
||||||
auto exit_guard = util::fail_guard([&shutdown_by_interrupt, &force_shutdown]() {
|
auto exit_guard = util::fail_guard([&shutdown_by_interrupt, &force_shutdown]() {
|
||||||
if(!shutdown_by_interrupt) {
|
if(!shutdown_by_interrupt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// If this is running from a service with no console window, don't wait for user input to exit
|
||||||
|
if(GetConsoleWindow() == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
task_pool.cancel(force_shutdown);
|
task_pool.cancel(force_shutdown);
|
||||||
|
|
||||||
std::cout << "Sunshine exited: Press enter to continue"sv << std::endl;
|
std::cout << "Sunshine exited: Press enter to continue"sv << std::endl;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
#include <wtsapi32.h>
|
#include <wtsapi32.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers
|
// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers
|
||||||
#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST
|
#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST
|
||||||
#define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)
|
#define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)
|
||||||
|
|
@ -18,6 +20,8 @@ DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, L
|
||||||
case SERVICE_CONTROL_INTERROGATE:
|
case SERVICE_CONTROL_INTERROGATE:
|
||||||
return NO_ERROR;
|
return NO_ERROR;
|
||||||
|
|
||||||
|
case SERVICE_CONTROL_PRESHUTDOWN:
|
||||||
|
// The system is shutting down
|
||||||
case SERVICE_CONTROL_STOP:
|
case SERVICE_CONTROL_STOP:
|
||||||
// Let SCM know we're stopping in up to 30 seconds
|
// Let SCM know we're stopping in up to 30 seconds
|
||||||
service_status.dwCurrentState = SERVICE_STOP_PENDING;
|
service_status.dwCurrentState = SERVICE_STOP_PENDING;
|
||||||
|
|
@ -125,6 +129,49 @@ HANDLE OpenLogFileHandle() {
|
||||||
NULL);
|
NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RunTerminationHelper(HANDLE console_token, DWORD pid) {
|
||||||
|
WCHAR module_path[MAX_PATH];
|
||||||
|
GetModuleFileNameW(NULL, module_path, _countof(module_path));
|
||||||
|
std::wstring command { module_path };
|
||||||
|
|
||||||
|
command += L" --terminate " + std::to_wstring(pid);
|
||||||
|
|
||||||
|
STARTUPINFOW startup_info = {};
|
||||||
|
startup_info.cb = sizeof(startup_info);
|
||||||
|
startup_info.lpDesktop = (LPWSTR)L"winsta0\\default";
|
||||||
|
|
||||||
|
// Execute ourselves as a detached process in the user session with the --terminate argument.
|
||||||
|
// This will allow us to attach to Sunshine's console and send it a Ctrl-C event.
|
||||||
|
PROCESS_INFORMATION process_info;
|
||||||
|
if(!CreateProcessAsUserW(console_token,
|
||||||
|
NULL,
|
||||||
|
(LPWSTR)command.c_str(),
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
FALSE,
|
||||||
|
CREATE_UNICODE_ENVIRONMENT | DETACHED_PROCESS,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&startup_info,
|
||||||
|
&process_info)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the termination helper to complete
|
||||||
|
WaitForSingleObject(process_info.hProcess, INFINITE);
|
||||||
|
|
||||||
|
// Check the exit status of the helper process
|
||||||
|
DWORD exit_code;
|
||||||
|
GetExitCodeProcess(process_info.hProcess, &exit_code);
|
||||||
|
|
||||||
|
// Cleanup handles
|
||||||
|
CloseHandle(process_info.hProcess);
|
||||||
|
CloseHandle(process_info.hThread);
|
||||||
|
|
||||||
|
// If the helper process returned 0, it succeeded
|
||||||
|
return exit_code == 0;
|
||||||
|
}
|
||||||
|
|
||||||
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
|
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
|
||||||
service_status_handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, NULL);
|
service_status_handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, NULL);
|
||||||
if(service_status_handle == NULL) {
|
if(service_status_handle == NULL) {
|
||||||
|
|
@ -161,15 +208,6 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto job_handle = CreateJobObjectForChildProcess();
|
|
||||||
if(job_handle == NULL) {
|
|
||||||
// Tell SCM we failed to start
|
|
||||||
service_status.dwWin32ExitCode = GetLastError();
|
|
||||||
service_status.dwCurrentState = SERVICE_STOPPED;
|
|
||||||
SetServiceStatus(service_status_handle, &service_status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can use a single STARTUPINFOEXW for all the processes that we launch
|
// We can use a single STARTUPINFOEXW for all the processes that we launch
|
||||||
STARTUPINFOEXW startup_info = {};
|
STARTUPINFOEXW startup_info = {};
|
||||||
startup_info.StartupInfo.cb = sizeof(startup_info);
|
startup_info.StartupInfo.cb = sizeof(startup_info);
|
||||||
|
|
@ -198,17 +236,8 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
|
||||||
NULL,
|
NULL,
|
||||||
NULL);
|
NULL);
|
||||||
|
|
||||||
// Start Sunshine.exe inside our job object
|
|
||||||
UpdateProcThreadAttribute(startup_info.lpAttributeList,
|
|
||||||
0,
|
|
||||||
PROC_THREAD_ATTRIBUTE_JOB_LIST,
|
|
||||||
&job_handle,
|
|
||||||
sizeof(job_handle),
|
|
||||||
NULL,
|
|
||||||
NULL);
|
|
||||||
|
|
||||||
// Tell SCM we're running (and stoppable now)
|
// Tell SCM we're running (and stoppable now)
|
||||||
service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
|
service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;
|
||||||
service_status.dwCurrentState = SERVICE_RUNNING;
|
service_status.dwCurrentState = SERVICE_RUNNING;
|
||||||
SetServiceStatus(service_status_handle, &service_status);
|
SetServiceStatus(service_status_handle, &service_status);
|
||||||
|
|
||||||
|
|
@ -219,6 +248,22 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Job objects cannot span sessions, so we must create one for each process
|
||||||
|
auto job_handle = CreateJobObjectForChildProcess();
|
||||||
|
if(job_handle == NULL) {
|
||||||
|
CloseHandle(console_token);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Sunshine.exe inside our job object
|
||||||
|
UpdateProcThreadAttribute(startup_info.lpAttributeList,
|
||||||
|
0,
|
||||||
|
PROC_THREAD_ATTRIBUTE_JOB_LIST,
|
||||||
|
&job_handle,
|
||||||
|
sizeof(job_handle),
|
||||||
|
NULL,
|
||||||
|
NULL);
|
||||||
|
|
||||||
PROCESS_INFORMATION process_info;
|
PROCESS_INFORMATION process_info;
|
||||||
if(!CreateProcessAsUserW(console_token,
|
if(!CreateProcessAsUserW(console_token,
|
||||||
L"Sunshine.exe",
|
L"Sunshine.exe",
|
||||||
|
|
@ -232,20 +277,21 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
|
||||||
(LPSTARTUPINFOW)&startup_info,
|
(LPSTARTUPINFOW)&startup_info,
|
||||||
&process_info)) {
|
&process_info)) {
|
||||||
CloseHandle(console_token);
|
CloseHandle(console_token);
|
||||||
|
CloseHandle(job_handle);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close handles that are no longer needed
|
|
||||||
CloseHandle(console_token);
|
|
||||||
CloseHandle(process_info.hThread);
|
|
||||||
|
|
||||||
// Wait for either the stop event to be set or Sunshine.exe to terminate
|
// Wait for either the stop event to be set or Sunshine.exe to terminate
|
||||||
const HANDLE wait_objects[] = { stop_event, process_info.hProcess };
|
const HANDLE wait_objects[] = { stop_event, process_info.hProcess };
|
||||||
switch(WaitForMultipleObjects(_countof(wait_objects), wait_objects, FALSE, INFINITE)) {
|
switch(WaitForMultipleObjects(_countof(wait_objects), wait_objects, FALSE, INFINITE)) {
|
||||||
case WAIT_OBJECT_0:
|
case WAIT_OBJECT_0:
|
||||||
// The service is shutting down, so terminate Sunshine.exe.
|
// The service is shutting down, so try to gracefully terminate Sunshine.exe.
|
||||||
// TODO: Send a graceful exit request and only terminate forcefully as a last resort.
|
// If it doesn't terminate in 20 seconds, we will forcefully terminate it.
|
||||||
TerminateProcess(process_info.hProcess, ERROR_PROCESS_ABORTED);
|
if(!RunTerminationHelper(console_token, process_info.dwProcessId) ||
|
||||||
|
WaitForSingleObject(process_info.hProcess, 20000) != WAIT_OBJECT_0) {
|
||||||
|
// If it won't terminate gracefully, kill it now
|
||||||
|
TerminateProcess(process_info.hProcess, ERROR_PROCESS_ABORTED);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WAIT_OBJECT_0 + 1:
|
case WAIT_OBJECT_0 + 1:
|
||||||
|
|
@ -253,7 +299,10 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CloseHandle(process_info.hThread);
|
||||||
CloseHandle(process_info.hProcess);
|
CloseHandle(process_info.hProcess);
|
||||||
|
CloseHandle(console_token);
|
||||||
|
CloseHandle(job_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let SCM know we've stopped
|
// Let SCM know we've stopped
|
||||||
|
|
@ -261,12 +310,35 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
|
||||||
SetServiceStatus(service_status_handle, &service_status);
|
SetServiceStatus(service_status_handle, &service_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This will run in a child process in the user session
|
||||||
|
int DoGracefulTermination(DWORD pid) {
|
||||||
|
// Attach to Sunshine's console
|
||||||
|
if(!AttachConsole(pid)) {
|
||||||
|
return GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable our own Ctrl-C handling
|
||||||
|
SetConsoleCtrlHandler(NULL, TRUE);
|
||||||
|
|
||||||
|
// Send a Ctrl-C event to Sunshine
|
||||||
|
if(!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) {
|
||||||
|
return GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
static const SERVICE_TABLE_ENTRY service_table[] = {
|
static const SERVICE_TABLE_ENTRY service_table[] = {
|
||||||
{ (LPSTR)SERVICE_NAME, ServiceMain },
|
{ (LPSTR)SERVICE_NAME, ServiceMain },
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if this is a reinvocation of ourselves to send Ctrl-C to Sunshine.exe
|
||||||
|
if(argc == 3 && strcmp(argv[1], "--terminate") == 0) {
|
||||||
|
return DoGracefulTermination(atol(argv[2]));
|
||||||
|
}
|
||||||
|
|
||||||
// By default, services have their current directory set to %SYSTEMROOT%\System32.
|
// By default, services have their current directory set to %SYSTEMROOT%\System32.
|
||||||
// We want to use the directory where Sunshine.exe is located instead of system32.
|
// We want to use the directory where Sunshine.exe is located instead of system32.
|
||||||
// This requires stripping off 2 path components: the file name and the last folder
|
// This requires stripping off 2 path components: the file name and the last folder
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue