diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 8b9468e3482..67e510ce123 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -33,6 +33,7 @@ #include "confighttp.h" #include "crypto.h" #include "display_device.h" +#include "entry_handler.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -1833,6 +1834,9 @@ namespace confighttp { platf::set_thread_name("confighttp::tcp"); server->start([&display_addr](const unsigned short port) { BOOST_LOG(info) << "Configuration UI available at [https://"sv << display_addr << ":" << port << "]"; +#ifdef _WIN32 + service_ctrl::signal_ready(); +#endif }); } catch (boost::system::system_error &err) { // It's possible the exception gets thrown after calling server->stop() from a different thread diff --git a/src/entry_handler.cpp b/src/entry_handler.cpp index e5e18920b6b..40152e7c2cd 100644 --- a/src/entry_handler.cpp +++ b/src/entry_handler.cpp @@ -3,11 +3,18 @@ * @brief Definitions for entry handling functions. */ // standard includes +#include +#include #include +#include #include #include +#include #include +// lib includes +#include + // local includes #include "config.h" #include "confighttp.h" @@ -121,6 +128,33 @@ bool is_gamestream_enabled() { } namespace service_ctrl { + /// Environment variable used to pass the inherited service readiness event handle. + constexpr auto SERVICE_READY_EVENT_ENV = "SUNSHINE_SERVICE_READY_EVENT"; + + void signal_ready() { + std::string event_handle_text; + if (!lizardbyte::common::get_env(SERVICE_READY_EVENT_ENV, event_handle_text)) { + return; + } + + std::uintptr_t ready_event_value {}; + const auto [end, error] = std::from_chars(event_handle_text.data(), event_handle_text.data() + event_handle_text.size(), ready_event_value); + if (error != std::errc {} || end != event_handle_text.data() + event_handle_text.size() || ready_event_value == 0) { + BOOST_LOG(warning) << "Ignoring invalid service ready event handle"; + static_cast(lizardbyte::common::unset_env(SERVICE_READY_EVENT_ENV)); + return; + } + + auto ready_event = std::bit_cast(ready_event_value); + if (!SetEvent(ready_event)) { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "Failed to signal service ready event: "sv << winerr; + } + + CloseHandle(ready_event); + static_cast(lizardbyte::common::unset_env(SERVICE_READY_EVENT_ENV)); + } + /** * @brief Owns Windows service-manager handles for the Sunshine service. */ diff --git a/src/entry_handler.h b/src/entry_handler.h index 32aba38e601..b3f9c6e95f0 100644 --- a/src/entry_handler.h +++ b/src/entry_handler.h @@ -119,6 +119,14 @@ bool is_gamestream_enabled(); * @brief Namespace for controlling the Sunshine service model on Windows. */ namespace service_ctrl { + /** + * @brief Signal the Windows service wrapper that Sunshine is ready. + * @examples + * signal_ready(); + * @examples_end + */ + void signal_ready(); + /** * @brief Check if the service is running. * @examples diff --git a/tests/unit/test_entry_handler.cpp b/tests/unit/test_entry_handler.cpp index c5016b3cbd5..84482928d6f 100644 --- a/tests/unit/test_entry_handler.cpp +++ b/tests/unit/test_entry_handler.cpp @@ -5,8 +5,42 @@ #include "../tests_common.h" #include "../tests_log_checker.h" +#ifdef _WIN32 + #include + #include + #include + #include + #include +#endif + #include +#ifdef _WIN32 +namespace { + constexpr auto SERVICE_READY_EVENT_ENV = "SUNSHINE_SERVICE_READY_EVENT"; + + class ServiceReadySignalTest: public testing::Test { + protected: + void SetUp() override { + static_cast(lizardbyte::common::unset_env(SERVICE_READY_EVENT_ENV)); + } + + void TearDown() override { + static_cast(lizardbyte::common::unset_env(SERVICE_READY_EVENT_ENV)); + } + }; + + std::string handle_to_string(HANDLE handle) { + return std::to_string(std::bit_cast(handle)); + } + + bool is_handle_open(HANDLE handle) { + DWORD flags {}; + return GetHandleInformation(handle, &flags) != FALSE; + } +} // namespace +#endif + TEST(EntryHandlerTests, LogPublisherDataTest) { // call log_publisher_data log_publisher_data(); @@ -16,3 +50,53 @@ TEST(EntryHandlerTests, LogPublisherDataTest) { ASSERT_TRUE(log_checker::line_starts_with("test_sunshine.log", "Info: Publisher Website: ")); ASSERT_TRUE(log_checker::line_starts_with("test_sunshine.log", "Info: Get support: ")); } + +#ifdef _WIN32 +TEST_F(ServiceReadySignalTest, SignalsAndClosesEventHandle) { + const auto source_event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + ASSERT_NE(source_event, nullptr); + + HANDLE inherited_event {}; + ASSERT_TRUE(DuplicateHandle(GetCurrentProcess(), source_event, GetCurrentProcess(), &inherited_event, 0, TRUE, DUPLICATE_SAME_ACCESS)); + ASSERT_EQ(lizardbyte::common::set_env(SERVICE_READY_EVENT_ENV, handle_to_string(inherited_event)), 0); + + service_ctrl::signal_ready(); + + EXPECT_EQ(WaitForSingleObject(source_event, 0), WAIT_OBJECT_0); + const auto inherited_event_is_open = is_handle_open(inherited_event); + EXPECT_FALSE(inherited_event_is_open); + if (inherited_event_is_open) { + CloseHandle(inherited_event); + } + + std::string env_value; + EXPECT_FALSE(lizardbyte::common::get_env(SERVICE_READY_EVENT_ENV, env_value)); + EXPECT_TRUE(CloseHandle(source_event)); +} + +TEST_F(ServiceReadySignalTest, ClearsInvalidHandleText) { + ASSERT_EQ(lizardbyte::common::set_env(SERVICE_READY_EVENT_ENV, "not-a-handle"), 0); + + service_ctrl::signal_ready(); + + std::string env_value; + EXPECT_FALSE(lizardbyte::common::get_env(SERVICE_READY_EVENT_ENV, env_value)); +} + +TEST_F(ServiceReadySignalTest, ClosesHandleWhenSetEventFails) { + const auto mutex = CreateMutexW(nullptr, FALSE, nullptr); + ASSERT_NE(mutex, nullptr); + ASSERT_EQ(lizardbyte::common::set_env(SERVICE_READY_EVENT_ENV, handle_to_string(mutex)), 0); + + service_ctrl::signal_ready(); + + const auto mutex_is_open = is_handle_open(mutex); + EXPECT_FALSE(mutex_is_open); + if (mutex_is_open) { + CloseHandle(mutex); + } + + std::string env_value; + EXPECT_FALSE(lizardbyte::common::get_env(SERVICE_READY_EVENT_ENV, env_value)); +} +#endif diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 5a2c9e178d0..4dd90f07f22 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -44,6 +44,7 @@ target_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) add_executable(sunshinesvc sunshinesvc.cpp) target_link_libraries(sunshinesvc ${CMAKE_THREAD_LIBS_INIT} + lizardbyte::common wtsapi32 ${PLATFORM_LIBRARIES}) target_compile_options(sunshinesvc PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) diff --git a/tools/sunshinesvc.cpp b/tools/sunshinesvc.cpp index 73d871b86cf..c1bf9e57cf2 100644 --- a/tools/sunshinesvc.cpp +++ b/tools/sunshinesvc.cpp @@ -3,7 +3,11 @@ * @brief Handles launching Sunshine.exe into user sessions as SYSTEM */ #define WIN32_LEAN_AND_MEAN +#include +#include +#include #include +#include #include #include #include @@ -19,6 +23,59 @@ HANDLE stop_event; HANDLE session_change_event; constexpr auto SERVICE_NAME = "SunshineService"; +constexpr auto SERVICE_READY_EVENT_ENV = "SUNSHINE_SERVICE_READY_EVENT"; +constexpr DWORD SERVICE_START_WAIT_HINT = 30 * 1000; +constexpr DWORD SUNSHINE_READY_TIMEOUT = 60 * 1000; +constexpr DWORD SUNSHINE_STARTUP_RETRY_LIMIT = 6; + +/** + * @brief Report to the Windows Service Control Manager that startup is still in progress. + */ +void ReportServiceStartPending() { + service_status.dwCurrentState = SERVICE_START_PENDING; + service_status.dwControlsAccepted = 0; + service_status.dwWin32ExitCode = NO_ERROR; + service_status.dwWaitHint = SERVICE_START_WAIT_HINT; + ++service_status.dwCheckPoint; + SetServiceStatus(service_status_handle, &service_status); +} + +/** + * @brief Report to the Windows Service Control Manager that the service is running. + */ +void ReportServiceRunning() { + service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_ACCEPT_SESSIONCHANGE; + service_status.dwCurrentState = SERVICE_RUNNING; + service_status.dwWin32ExitCode = NO_ERROR; + service_status.dwWaitHint = 0; + service_status.dwCheckPoint = 0; + SetServiceStatus(service_status_handle, &service_status); +} + +/** + * @brief Report to the Windows Service Control Manager that the service has stopped. + * + * @param win32_exit_code Exit code reported for the stopped service. + */ +void ReportServiceStopped(DWORD win32_exit_code = NO_ERROR) { + service_status.dwControlsAccepted = 0; + service_status.dwCurrentState = SERVICE_STOPPED; + service_status.dwWin32ExitCode = win32_exit_code; + service_status.dwWaitHint = 0; + service_status.dwCheckPoint = 0; + SetServiceStatus(service_status_handle, &service_status); +} + +/** + * @brief State observed while waiting for Sunshine.exe to signal readiness. + */ +enum class SunshineReadyResult : std::uint8_t { + ready, ///< Sunshine.exe signaled that its Web UI is accepting connections. + stop, ///< The service received a stop request. + exited, ///< Sunshine.exe exited before signaling readiness. + session_changed, ///< The active console session changed before readiness. + timed_out ///< Sunshine.exe did not signal readiness before the timeout. +}; DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) { switch (dwControl) { @@ -28,7 +85,7 @@ DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, L 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) { + if (dwEventType == WTS_CONSOLE_CONNECT && session_change_event != nullptr) { SetEvent(session_change_event); } return NO_ERROR; @@ -43,7 +100,9 @@ DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, L SetServiceStatus(service_status_handle, &service_status); // Trigger ServiceMain() to start cleanup - SetEvent(stop_event); + if (stop_event != nullptr) { + SetEvent(stop_event); + } return NO_ERROR; default: @@ -166,6 +225,134 @@ bool RunTerminationHelper(HANDLE console_token, DWORD pid) { return exit_code == 0; } +/** + * @brief Gracefully terminate Sunshine.exe, falling back to forced termination. + * + * @param console_token Token for the console session where Sunshine.exe is running. + * @param process_info Process information for the Sunshine.exe child process. + */ +void TerminateSunshineProcess(HANDLE console_token, PROCESS_INFORMATION &process_info) { + // Try to gracefully terminate Sunshine.exe. If it doesn't terminate in 20 seconds, forcefully terminate it. + if (!RunTerminationHelper(console_token, process_info.dwProcessId) || WaitForSingleObject(process_info.hProcess, 20000) != WAIT_OBJECT_0) { + TerminateProcess(process_info.hProcess, ERROR_PROCESS_ABORTED); + } +} + +/** + * @brief Wait for Sunshine.exe to signal that it has completed startup. + * + * @param process_handle Process handle for the Sunshine.exe child process. + * @param ready_event Event signaled by Sunshine.exe after the Web UI starts listening. + * @param console_session_id Console session ID where Sunshine.exe was launched. + * @param report_start_pending Whether to keep reporting SERVICE_START_PENDING while waiting. + * @return Observed child startup state. + */ +SunshineReadyResult WaitForSunshineReady(HANDLE process_handle, HANDLE ready_event, DWORD console_session_id, bool report_start_pending) { + const auto started_at = GetTickCount64(); + using enum SunshineReadyResult; + + while (true) { + if (report_start_pending) { + ReportServiceStartPending(); + } + + const auto elapsed = GetTickCount64() - started_at; + if (elapsed >= SUNSHINE_READY_TIMEOUT) { + return SunshineReadyResult::timed_out; + } + + const auto remaining = SUNSHINE_READY_TIMEOUT - elapsed; + const auto wait_time = static_cast(remaining > 3000 ? 3000 : remaining); + const std::array wait_objects {stop_event, process_handle, session_change_event, ready_event}; + + switch (WaitForMultipleObjects(static_cast(wait_objects.size()), wait_objects.data(), FALSE, wait_time)) { + case WAIT_OBJECT_0: + return stop; + + case WAIT_OBJECT_0 + 1: + return exited; + + case WAIT_OBJECT_0 + 2: + if (WTSGetActiveConsoleSessionId() == console_session_id) { + continue; + } + return session_changed; + + case WAIT_OBJECT_0 + 3: + return ready; + + case WAIT_TIMEOUT: + continue; + + default: + return timed_out; + } + } +} + +/** + * @brief Release service-wide handles and the process attribute list. + * + * @param startup_info Startup info containing the optional process attribute list. + * @param ready_event Event handle passed to Sunshine.exe for readiness signaling. + * @param log_file_handle Log file handle inherited by Sunshine.exe. + */ +void CleanupServiceResources(STARTUPINFOEXW &startup_info, HANDLE &ready_event, HANDLE &log_file_handle) { + if (startup_info.lpAttributeList != nullptr) { + DeleteProcThreadAttributeList(startup_info.lpAttributeList); + HeapFree(GetProcessHeap(), 0, startup_info.lpAttributeList); + startup_info.lpAttributeList = nullptr; + } + + if (ready_event != nullptr) { + CloseHandle(ready_event); + ready_event = nullptr; + } + + if (log_file_handle != INVALID_HANDLE_VALUE) { + CloseHandle(log_file_handle); + log_file_handle = INVALID_HANDLE_VALUE; + } + + if (session_change_event != nullptr) { + CloseHandle(session_change_event); + session_change_event = nullptr; + } + + if (stop_event != nullptr) { + CloseHandle(stop_event); + stop_event = nullptr; + } +} + +/** + * @brief Clean up service resources and report startup failure to the Service Control Manager. + * + * @param startup_info Startup info containing the optional process attribute list. + * @param ready_event Event handle passed to Sunshine.exe for readiness signaling. + * @param log_file_handle Log file handle inherited by Sunshine.exe. + * @param win32_exit_code Exit code reported for the stopped service. + */ +void FailServiceStart(STARTUPINFOEXW &startup_info, HANDLE &ready_event, HANDLE &log_file_handle, DWORD win32_exit_code) { + CleanupServiceResources(startup_info, ready_event, log_file_handle); + ReportServiceStopped(win32_exit_code); +} + +/** + * @brief Convert a child process startup exit state into a service failure code. + * + * @param process_handle Process handle for the Sunshine.exe child process. + * @return Exit code to report for the failed startup attempt. + */ +DWORD GetSunshineStartupFailureCode(HANDLE process_handle) { + DWORD exit_code = ERROR_PROCESS_ABORTED; + if (GetExitCodeProcess(process_handle, &exit_code) && exit_code == ERROR_SHUTDOWN_IN_PROGRESS) { + SetEvent(stop_event); + } + + return exit_code == NO_ERROR ? ERROR_PROCESS_ABORTED : exit_code; +} + VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { service_status_handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, nullptr); if (service_status_handle == nullptr) { @@ -174,47 +361,49 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { return; } + auto log_file_handle = INVALID_HANDLE_VALUE; + auto ready_event = static_cast(nullptr); + std::array inherited_handles {}; + + STARTUPINFOEXW startup_info = {}; + // 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.dwWaitHint = SERVICE_START_WAIT_HINT; service_status.dwControlsAccepted = 0; service_status.dwCheckPoint = 0; - service_status.dwCurrentState = SERVICE_START_PENDING; - SetServiceStatus(service_status_handle, &service_status); + ReportServiceStartPending(); // 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); + FailServiceStart(startup_info, ready_event, log_file_handle, GetLastError()); 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); + FailServiceStart(startup_info, ready_event, log_file_handle, GetLastError()); return; } - auto log_file_handle = OpenLogFileHandle(); + 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); + FailServiceStart(startup_info, ready_event, log_file_handle, GetLastError()); return; } - // We can use a single STARTUPINFOEXW for all the processes that we launch - STARTUPINFOEXW startup_info = {}; + SECURITY_ATTRIBUTES ready_event_security_attributes = {sizeof(ready_event_security_attributes), nullptr, TRUE}; + ready_event = CreateEventW(&ready_event_security_attributes, TRUE, FALSE, nullptr); + if (ready_event == nullptr) { + FailServiceStart(startup_info, ready_event, log_file_handle, GetLastError()); + return; + } + + // We can use a single STARTUPINFOEXW for all the processes that we launch. startup_info.StartupInfo.cb = sizeof(startup_info); startup_info.StartupInfo.lpDesktop = (LPWSTR) L"winsta0\\default"; startup_info.StartupInfo.dwFlags = STARTF_USESTDHANDLES; @@ -225,23 +414,29 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { // 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); + FailServiceStart(startup_info, ready_event, log_file_handle, GetLastError()); 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); + // Only allow Sunshine.exe to inherit the log file and ready event handles, not all inheritable handles. + inherited_handles = {log_file_handle, ready_event}; + if (!UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, static_cast(inherited_handles.data()), inherited_handles.size() * sizeof(HANDLE), nullptr, nullptr)) { + FailServiceStart(startup_info, ready_event, log_file_handle, GetLastError()); + return; + } - // 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); + bool service_running = false; + bool service_should_stop = false; + DWORD startup_failures = 0; + DWORD startup_failure_code = NO_ERROR; + using enum SunshineReadyResult; + + // Loop every 3 seconds until the stop event is set or Sunshine.exe is running. + while (!service_should_stop && WaitForSingleObject(stop_event, 3000) != WAIT_OBJECT_0) { + if (!service_running) { + ReportServiceStartPending(); + } - // 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 @@ -261,16 +456,90 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { } // Start Sunshine.exe inside our job object - UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_JOB_LIST, &job_handle, sizeof(job_handle), nullptr, nullptr); + if (!UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_JOB_LIST, static_cast(&job_handle), sizeof(job_handle), nullptr, nullptr)) { + startup_failure_code = GetLastError(); + CloseHandle(console_token); + CloseHandle(job_handle); + service_should_stop = true; + continue; + } + + ResetEvent(ready_event); - 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)) { + if (const auto ready_event_handle_text = std::format("{}", std::bit_cast(ready_event)); lizardbyte::common::set_env(SERVICE_READY_EVENT_ENV, ready_event_handle_text) != 0) { + startup_failure_code = ERROR_BAD_ENVIRONMENT; CloseHandle(console_token); CloseHandle(job_handle); + service_should_stop = true; continue; } - bool still_running; + PROCESS_INFORMATION process_info = {}; + const auto process_created = CreateProcessAsUserW(console_token, L"Sunshine.exe", nullptr, nullptr, nullptr, TRUE, CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &startup_info.StartupInfo, &process_info); + const auto create_process_error = process_created ? NO_ERROR : GetLastError(); + static_cast(lizardbyte::common::unset_env(SERVICE_READY_EVENT_ENV)); + if (!process_created) { + startup_failure_code = create_process_error; + CloseHandle(console_token); + CloseHandle(job_handle); + if (++startup_failures >= SUNSHINE_STARTUP_RETRY_LIMIT) { + service_should_stop = true; + } + continue; + } + + if (auto ready_result = WaitForSunshineReady(process_info.hProcess, ready_event, console_session_id, !service_running); ready_result != ready) { + if (ready_result == stop || ready_result == session_changed || ready_result == timed_out) { + TerminateSunshineProcess(console_token, process_info); + } + + if (ready_result == stop) { + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + CloseHandle(console_token); + CloseHandle(job_handle); + service_should_stop = true; + continue; + } + + if (ready_result == session_changed) { + startup_failures = 0; + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + CloseHandle(console_token); + CloseHandle(job_handle); + continue; + } + + startup_failure_code = ready_result == timed_out ? ERROR_TIMEOUT : GetSunshineStartupFailureCode(process_info.hProcess); + + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + CloseHandle(console_token); + CloseHandle(job_handle); + + if (WaitForSingleObject(stop_event, 0) == WAIT_OBJECT_0) { + service_should_stop = true; + } + + if (!service_should_stop) { + ++startup_failures; + } + + if (startup_failures >= SUNSHINE_STARTUP_RETRY_LIMIT) { + service_should_stop = true; + } + continue; + } + + startup_failures = 0; + startup_failure_code = NO_ERROR; + if (!service_running) { + ReportServiceRunning(); + service_running = true; + } + + bool still_running = false; 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}; @@ -284,12 +553,7 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { // 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); - } + TerminateSunshineProcess(console_token, process_info); still_running = false; break; @@ -305,6 +569,13 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { still_running = false; break; } + + default: + startup_failure_code = GetLastError(); + service_should_stop = true; + TerminateSunshineProcess(console_token, process_info); + still_running = false; + break; } } while (still_running); @@ -314,9 +585,10 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { CloseHandle(job_handle); } - // Let SCM know we've stopped - service_status.dwCurrentState = SERVICE_STOPPED; - SetServiceStatus(service_status_handle, &service_status); + // Let SCM know we've stopped. + const auto service_exit_code = WaitForSingleObject(stop_event, 0) == WAIT_OBJECT_0 ? NO_ERROR : startup_failure_code; + CleanupServiceResources(startup_info, ready_event, log_file_handle); + ReportServiceStopped(service_exit_code); } // This will run in a child process in the user session