Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,86 @@ std::map<std::string_view, std::function<int(const char *name, int argc, char **
{ "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[]) {
util::TaskPool::task_id_t force_shutdown = nullptr;

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]() {
if(!shutdown_by_interrupt) {
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);

std::cout << "Sunshine exited: Press enter to continue"sv << std::endl;
Expand Down
124 changes: 98 additions & 26 deletions tools/sunshinesvc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include <Windows.h>
#include <wtsapi32.h>

#include <string>

// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers
#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST
#define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)
Expand All @@ -18,6 +20,8 @@ DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, L
case SERVICE_CONTROL_INTERROGATE:
return NO_ERROR;

case SERVICE_CONTROL_PRESHUTDOWN:
// The system is shutting down
case SERVICE_CONTROL_STOP:
// Let SCM know we're stopping in up to 30 seconds
service_status.dwCurrentState = SERVICE_STOP_PENDING;
Expand Down Expand Up @@ -125,6 +129,49 @@ HANDLE OpenLogFileHandle() {
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) {
service_status_handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, NULL);
if(service_status_handle == NULL) {
Expand Down Expand Up @@ -161,15 +208,6 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
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
STARTUPINFOEXW startup_info = {};
startup_info.StartupInfo.cb = sizeof(startup_info);
Expand Down Expand Up @@ -198,17 +236,8 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
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)
service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;
service_status.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(service_status_handle, &service_status);

Expand All @@ -219,6 +248,22 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
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;
if(!CreateProcessAsUserW(console_token,
L"Sunshine.exe",
Expand All @@ -232,41 +277,68 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
(LPSTARTUPINFOW)&startup_info,
&process_info)) {
CloseHandle(console_token);
CloseHandle(job_handle);
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
const HANDLE wait_objects[] = { stop_event, process_info.hProcess };
switch(WaitForMultipleObjects(_countof(wait_objects), wait_objects, FALSE, INFINITE)) {
case WAIT_OBJECT_0:
// The service is shutting down, so terminate Sunshine.exe.
// TODO: Send a graceful exit request and only terminate forcefully as a last resort.
TerminateProcess(process_info.hProcess, ERROR_PROCESS_ABORTED);
// The service is shutting down, so try to gracefully terminate Sunshine.exe.
// If it doesn't terminate in 20 seconds, we will forcefully terminate it.
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;

case WAIT_OBJECT_0 + 1:
// Sunshine terminated itself.
break;
}

CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
CloseHandle(console_token);
CloseHandle(job_handle);
}

// Let SCM know we've stopped
service_status.dwCurrentState = SERVICE_STOPPED;
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[]) {
static const SERVICE_TABLE_ENTRY service_table[] = {
{ (LPSTR)SERVICE_NAME, ServiceMain },
{ 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.
// 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
Expand Down