Graceful termination on shutdown, logoff, and service stop (#647)

This commit is contained in:
Cameron Gutman 2022-12-29 08:32:23 -06:00 committed by GitHub
commit 8ad7af86c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 168 additions and 26 deletions

View file

@ -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;

View file

@ -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