mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-08-07 02:00:45 +00:00
965c91a428
* build(deps): bump third-party/lizardbyte-common Bumps [third-party/lizardbyte-common](https://github.com/LizardByte/lizardbyte-common) from `06cd442` to `011ad2b`. - [Release notes](https://github.com/LizardByte/lizardbyte-common/releases) - [Commits](https://github.com/LizardByte/lizardbyte-common/compare/06cd442b808f02f1674f3192a84d25b9a503c482...011ad2bb139083dcf4ec21f9c09f07470891a668) --- updated-dependencies: - dependency-name: third-party/lizardbyte-common dependency-version: 011ad2bb139083dcf4ec21f9c09f07470891a668 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * style: run clang-format across C++ sources Reformatted conditionals, loops, and long function calls across core, platform, NVENC, and test helper code to match updated formatting rules, including minor spacing normalization in type declarations. These are style-only edits with no intended behavior changes. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
366 lines
13 KiB
C++
366 lines
13 KiB
C++
/**
|
|
* @file tools/sunshinesvc.cpp
|
|
* @brief Handles launching Sunshine.exe into user sessions as SYSTEM
|
|
*/
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <format>
|
|
#include <string>
|
|
#include <Windows.h>
|
|
#include <WtsApi32.h>
|
|
|
|
// 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)
|
|
#endif
|
|
|
|
SERVICE_STATUS_HANDLE service_status_handle;
|
|
SERVICE_STATUS service_status;
|
|
HANDLE stop_event;
|
|
HANDLE session_change_event;
|
|
|
|
constexpr auto SERVICE_NAME = "SunshineService";
|
|
|
|
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) {
|
|
switch (dwControl) {
|
|
case SERVICE_CONTROL_INTERROGATE:
|
|
return NO_ERROR;
|
|
|
|
case SERVICE_CONTROL_SESSIONCHANGE:
|
|
// If a new session connects to the console, restart Sunshine
|
|
// to allow it to spawn inside the new console session.
|
|
if (dwEventType == WTS_CONSOLE_CONNECT) {
|
|
SetEvent(session_change_event);
|
|
}
|
|
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;
|
|
service_status.dwControlsAccepted = 0;
|
|
service_status.dwWaitHint = 30 * 1000;
|
|
SetServiceStatus(service_status_handle, &service_status);
|
|
|
|
// Trigger ServiceMain() to start cleanup
|
|
SetEvent(stop_event);
|
|
return NO_ERROR;
|
|
|
|
default:
|
|
return ERROR_CALL_NOT_IMPLEMENTED;
|
|
}
|
|
}
|
|
|
|
HANDLE CreateJobObjectForChildProcess() {
|
|
HANDLE job_handle = CreateJobObjectW(nullptr, nullptr);
|
|
if (!job_handle) {
|
|
return nullptr;
|
|
}
|
|
|
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limit_info = {};
|
|
|
|
// Kill Sunshine.exe when the final job object handle is closed (which will happen if we terminate unexpectedly).
|
|
// This ensures we don't leave an orphaned Sunshine.exe running with an inherited handle to our log file.
|
|
job_limit_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
|
|
// Allow Sunshine.exe to use CREATE_BREAKAWAY_FROM_JOB when spawning processes to ensure they can to live beyond
|
|
// the lifetime of SunshineSvc.exe. This avoids unexpected user data loss if we crash or are killed.
|
|
job_limit_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK;
|
|
|
|
if (!SetInformationJobObject(job_handle, JobObjectExtendedLimitInformation, &job_limit_info, sizeof(job_limit_info))) {
|
|
CloseHandle(job_handle);
|
|
return nullptr;
|
|
}
|
|
|
|
return job_handle;
|
|
}
|
|
|
|
LPPROC_THREAD_ATTRIBUTE_LIST AllocateProcThreadAttributeList(DWORD attribute_count) {
|
|
SIZE_T size;
|
|
InitializeProcThreadAttributeList(nullptr, attribute_count, 0, &size);
|
|
|
|
auto list = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, size);
|
|
if (list == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) {
|
|
HeapFree(GetProcessHeap(), 0, list);
|
|
return nullptr;
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
HANDLE DuplicateTokenForSession(DWORD console_session_id) {
|
|
HANDLE current_token;
|
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, ¤t_token)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Duplicate our own LocalSystem token
|
|
HANDLE new_token;
|
|
if (!DuplicateTokenEx(current_token, TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &new_token)) {
|
|
CloseHandle(current_token);
|
|
return nullptr;
|
|
}
|
|
|
|
CloseHandle(current_token);
|
|
|
|
// Change the duplicated token to the console session ID
|
|
if (!SetTokenInformation(new_token, TokenSessionId, &console_session_id, sizeof(console_session_id))) {
|
|
CloseHandle(new_token);
|
|
return nullptr;
|
|
}
|
|
|
|
return new_token;
|
|
}
|
|
|
|
HANDLE OpenLogFileHandle() {
|
|
WCHAR log_file_name[MAX_PATH];
|
|
|
|
// Create sunshine.log in the Temp folder (usually %SYSTEMROOT%\Temp)
|
|
GetTempPathW(_countof(log_file_name), log_file_name);
|
|
wcscat_s(log_file_name, L"sunshine.log");
|
|
|
|
// The file handle must be inheritable for our child process to use it
|
|
SECURITY_ATTRIBUTES security_attributes = {sizeof(security_attributes), nullptr, TRUE};
|
|
|
|
// Overwrite the old sunshine.log
|
|
return CreateFileW(log_file_name, GENERIC_WRITE, FILE_SHARE_READ, &security_attributes, CREATE_ALWAYS, 0, nullptr);
|
|
}
|
|
|
|
bool RunTerminationHelper(HANDLE console_token, DWORD pid) {
|
|
WCHAR module_path[MAX_PATH];
|
|
GetModuleFileNameW(nullptr, module_path, _countof(module_path));
|
|
std::wstring command;
|
|
|
|
command += L'"';
|
|
command += module_path;
|
|
command += L'"';
|
|
command += std::format(L" --terminate {}", 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, module_path, (LPWSTR) command.c_str(), nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT | DETACHED_PROCESS, nullptr, nullptr, &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, nullptr);
|
|
if (service_status_handle == nullptr) {
|
|
// Nothing we can really do here but terminate ourselves
|
|
ExitProcess(GetLastError());
|
|
return;
|
|
}
|
|
|
|
// Tell SCM we're starting
|
|
service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
|
|
service_status.dwServiceSpecificExitCode = 0;
|
|
service_status.dwWin32ExitCode = NO_ERROR;
|
|
service_status.dwWaitHint = 0;
|
|
service_status.dwControlsAccepted = 0;
|
|
service_status.dwCheckPoint = 0;
|
|
service_status.dwCurrentState = SERVICE_START_PENDING;
|
|
SetServiceStatus(service_status_handle, &service_status);
|
|
|
|
// Create a manual-reset stop event
|
|
stop_event = CreateEventA(nullptr, TRUE, FALSE, nullptr);
|
|
if (stop_event == nullptr) {
|
|
// Tell SCM we failed to start
|
|
service_status.dwWin32ExitCode = GetLastError();
|
|
service_status.dwCurrentState = SERVICE_STOPPED;
|
|
SetServiceStatus(service_status_handle, &service_status);
|
|
return;
|
|
}
|
|
|
|
// Create an auto-reset session change event
|
|
session_change_event = CreateEventA(nullptr, FALSE, FALSE, nullptr);
|
|
if (session_change_event == nullptr) {
|
|
// Tell SCM we failed to start
|
|
service_status.dwWin32ExitCode = GetLastError();
|
|
service_status.dwCurrentState = SERVICE_STOPPED;
|
|
SetServiceStatus(service_status_handle, &service_status);
|
|
return;
|
|
}
|
|
|
|
auto log_file_handle = OpenLogFileHandle();
|
|
if (log_file_handle == INVALID_HANDLE_VALUE) {
|
|
// 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);
|
|
startup_info.StartupInfo.lpDesktop = (LPWSTR) L"winsta0\\default";
|
|
startup_info.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
|
startup_info.StartupInfo.hStdInput = nullptr;
|
|
startup_info.StartupInfo.hStdOutput = log_file_handle;
|
|
startup_info.StartupInfo.hStdError = log_file_handle;
|
|
|
|
// Allocate an attribute list with space for 2 entries
|
|
startup_info.lpAttributeList = AllocateProcThreadAttributeList(2);
|
|
if (startup_info.lpAttributeList == nullptr) {
|
|
// Tell SCM we failed to start
|
|
service_status.dwWin32ExitCode = GetLastError();
|
|
service_status.dwCurrentState = SERVICE_STOPPED;
|
|
SetServiceStatus(service_status_handle, &service_status);
|
|
return;
|
|
}
|
|
|
|
// Only allow Sunshine.exe to inherit the log file handle, not all inheritable handles
|
|
UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &log_file_handle, sizeof(log_file_handle), nullptr, nullptr);
|
|
|
|
// Tell SCM we're running (and stoppable now)
|
|
service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_ACCEPT_SESSIONCHANGE;
|
|
service_status.dwCurrentState = SERVICE_RUNNING;
|
|
SetServiceStatus(service_status_handle, &service_status);
|
|
|
|
// Loop every 3 seconds until the stop event is set or Sunshine.exe is running
|
|
while (WaitForSingleObject(stop_event, 3000) != WAIT_OBJECT_0) {
|
|
auto console_session_id = WTSGetActiveConsoleSessionId();
|
|
if (console_session_id == 0xFFFFFFFF) {
|
|
// No console session yet
|
|
continue;
|
|
}
|
|
|
|
auto console_token = DuplicateTokenForSession(console_session_id);
|
|
if (console_token == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
// Job objects cannot span sessions, so we must create one for each process
|
|
auto job_handle = CreateJobObjectForChildProcess();
|
|
if (job_handle == nullptr) {
|
|
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), nullptr, nullptr);
|
|
|
|
PROCESS_INFORMATION process_info;
|
|
if (!CreateProcessAsUserW(console_token, L"Sunshine.exe", nullptr, nullptr, nullptr, TRUE, CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, (LPSTARTUPINFOW) &startup_info, &process_info)) {
|
|
CloseHandle(console_token);
|
|
CloseHandle(job_handle);
|
|
continue;
|
|
}
|
|
|
|
bool still_running;
|
|
do {
|
|
// Wait for the stop event to be set, Sunshine.exe to terminate, or the console session to change
|
|
const HANDLE wait_objects[] = {stop_event, process_info.hProcess, session_change_event};
|
|
switch (WaitForMultipleObjects(_countof(wait_objects), wait_objects, FALSE, INFINITE)) {
|
|
case WAIT_OBJECT_0 + 2:
|
|
if (WTSGetActiveConsoleSessionId() == console_session_id) {
|
|
// The active console session didn't actually change. Let Sunshine keep running.
|
|
still_running = true;
|
|
continue;
|
|
}
|
|
// Fall-through to terminate Sunshine.exe and start it again.
|
|
case WAIT_OBJECT_0:
|
|
// 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);
|
|
}
|
|
still_running = false;
|
|
break;
|
|
|
|
case WAIT_OBJECT_0 + 1:
|
|
{
|
|
// Sunshine terminated itself.
|
|
|
|
DWORD exit_code;
|
|
if (GetExitCodeProcess(process_info.hProcess, &exit_code) && exit_code == ERROR_SHUTDOWN_IN_PROGRESS) {
|
|
// Sunshine is asking for us to shut down, so gracefully stop ourselves.
|
|
SetEvent(stop_event);
|
|
}
|
|
still_running = false;
|
|
break;
|
|
}
|
|
}
|
|
} while (still_running);
|
|
|
|
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(nullptr, 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},
|
|
{nullptr, nullptr}
|
|
};
|
|
|
|
// 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
|
|
WCHAR module_path[MAX_PATH];
|
|
GetModuleFileNameW(nullptr, module_path, _countof(module_path));
|
|
for (auto i = 0; i < 2; i++) {
|
|
auto last_sep = wcsrchr(module_path, '\\');
|
|
if (last_sep) {
|
|
*last_sep = 0;
|
|
}
|
|
}
|
|
SetCurrentDirectoryW(module_path);
|
|
|
|
// Trigger our ServiceMain()
|
|
return StartServiceCtrlDispatcher(service_table);
|
|
}
|