/** * @file src/main.cpp * @brief Definitions for the main entry point for Sunshine. */ // standard includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "confighttp.h" #include "entry_handler.h" #include "globals.h" #include "host_stats.h" #include "httpcommon.h" #include "logging.h" #include "main.h" #include "nvhttp.h" #include "process.h" #include "rtsp.h" #include "system_tray.h" #include "update.h" #include "upnp.h" #include "version_compare.h" #include "uuid.h" #include "video.h" #include "session_history.h" #include "state_storage.h" #include "steam_auto_sync.h" #ifdef __linux__ #include "lutris_auto_sync.h" #endif #include "webrtc_stream.h" #ifdef _WIN32 #include #include "src/display_helper_integration.h" #include "src/platform/windows/frame_limiter_nvcp.h" #include "src/platform/windows/misc.h" #include "src/platform/windows/playnite_integration.h" #include "src/platform/windows/rtss_integration.h" #include "src/platform/windows/startup_encoder_probe_policy.h" #include "src/platform/windows/virtual_display.h" #include "src/platform/windows/virtual_display_cleanup.h" #elif defined(__linux__) #include "src/platform/linux/capability_sanitizer.h" #include "src/platform/linux/maintenance_cli.h" #include "src/platform/linux/private_display.h" #include "src/platform/linux/display_backend.h" #include #endif #ifdef _WIN32 #include "platform/windows/misc.h" #include "platform/windows/display_helper_integration.h" #include "platform/windows/virtual_display.h" #endif #define PROBE_DISPLAY_UUID "38F72B96-B00C-4F21-8B6C-E1BFF1602B0E" extern "C" { #include "rswrapper.h" } using namespace std::literals; #ifndef __linux__ std::map> signal_handlers; #ifdef _WIN32 #define WIDEN_STRING_LITERAL_IMPL(value) L##value #define WIDEN_STRING_LITERAL(value) WIDEN_STRING_LITERAL_IMPL(value) #endif void on_signal_forwarder(int sig) { signal_handlers.at(sig)(); } template void on_signal(int sig, FN &&fn) { signal_handlers.emplace(sig, std::forward(fn)); std::signal(sig, on_signal_forwarder); } #endif namespace { static_assert(std::atomic_bool::is_always_lock_free, "shutdown signal flag must be lock-free in a signal handler"); class shutdown_deadline_t { public: explicit shutdown_deadline_t(std::atomic_bool *signal_requested, bool supervised_machine_host): signal_requested_ {signal_requested}, supervised_machine_host_ {supervised_machine_host} { // The machine host runs under systemd with SendSIGKILL=no so that the // display topology survives a stop. That makes this watchdog the only // thing that can end a shutdown whose joins never return: without it a // hung host outlives its unit, keeps the ports, and blocks every // upgrade and restart until someone kills it by hand. The preserve // request was already made when the signal arrived, so a forced exit // after the deadline loses nothing that a graceful exit would keep. try { worker_ = std::jthread([this](std::stop_token) { run(); }); } catch (const std::system_error &e) { BOOST_LOG(error) << "Unable to create the shutdown deadline watchdog: " << e.what(); } } shutdown_deadline_t(const shutdown_deadline_t &) = delete; shutdown_deadline_t &operator=(const shutdown_deadline_t &) = delete; ~shutdown_deadline_t() { complete(); } void arm() { if (!worker_.joinable()) { return; } std::lock_guard lock {mutex_}; if (state_ == state_e::idle) { state_ = state_e::armed; cv_.notify_one(); } } void complete() { { std::lock_guard lock {mutex_}; if (state_ != state_e::firing) { state_ = state_e::completed; } cv_.notify_one(); } // This object is owned by main(), never by its worker. Joining here // prevents a deadline thread from escaping into CRT/static teardown. if (worker_.joinable()) { worker_.join(); } } private: enum class state_e { idle, armed, completed, firing, }; void run() { std::unique_lock lock {mutex_}; while (state_ == state_e::idle && (!signal_requested_ || !signal_requested_->load(std::memory_order_relaxed))) { // The Linux signal-wait thread may publish shutdown before main reaches // shutdown_event->view(). Poll the flag so that early startup remains // covered without doing lock-taking work in asynchronous signal context. cv_.wait_for(lock, std::chrono::milliseconds(50)); } if (state_ == state_e::idle) { state_ = state_e::armed; } if (state_ != state_e::armed) { return; } // The machine host's unit allows 20 seconds for a stop; leave a margin so // the forced exit lands before systemd gives up on the unit. const auto deadline = supervised_machine_host_ ? std::chrono::seconds(15) : std::chrono::seconds(10); if (cv_.wait_until(lock, std::chrono::steady_clock::now() + deadline, [this] { return state_ != state_e::armed; })) { return; } // Completion and expiry serialize through mutex_. A recovered shutdown // therefore cannot leave a stale timer behind to trap later. state_ = state_e::firing; lock.unlock(); if (supervised_machine_host_) { // The deadline must be unconditional: even an asynchronous log sink // can be blocked by the shutdown path that this watchdog protects. std::_Exit(lifetime::desired_exit_code); } BOOST_LOG(fatal) << deadline.count() << " seconds passed, yet ArtLight's still running: Forcing shutdown"sv; lifetime::debug_trap(); } std::mutex mutex_; std::condition_variable cv_; state_e state_ {state_e::idle}; std::atomic_bool *signal_requested_ = nullptr; bool supervised_machine_host_ = false; std::jthread worker_; }; } // namespace std::map> cmd_to_func { {"creds"sv, [](const char *name, int argc, char **argv) { return args::creds(name, argc, argv); }}, {"help"sv, [](const char *name, int argc, char **argv) { return args::help(name); }}, {"version"sv, [](const char *name, int argc, char **argv) { return args::version(); }}, #ifdef _WIN32 {"restore-nvprefs-undo"sv, [](const char *name, int argc, char **argv) { return args::restore_nvprefs_undo(); }}, #endif }; #ifdef _WIN32 LRESULT CALLBACK SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CLOSE: DestroyWindow(hwnd); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; case WM_ENDSESSION: { if (!wParam) { return 0; } // Trigger async shutdown so the message loop keeps pumping. // When the main thread's cleanup guard runs, it will // PostMessage(WM_CLOSE) to this window, which dispatches // through the outer GetMessage loop → WM_CLOSE → DestroyWindow // → WM_DESTROY → PostQuitMessage → GetMessage returns 0 → // thread exits. // // Previously this called exit_sunshine(0, false) which blocked // the thread in a sleep loop, starving the message pump. That // prevented WM_CLOSE from ever being processed, the join // timed out, the thread was detached, and the still-running // thread crashed during CRT atexit cleanup (abort/FAST_FAIL). BOOST_LOG(info) << "Received WM_ENDSESSION"sv; lifetime::exit_sunshine(0, true); return 0; } default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } } WINAPI BOOL ConsoleCtrlHandler(DWORD type) { if (type == CTRL_CLOSE_EVENT) { BOOST_LOG(info) << "Console closed handler called"; lifetime::exit_sunshine(0, false); } return FALSE; } #endif namespace { // Startup diagnostics. // // An uncaught exception during startup used to kill the process with only // "terminate called after throwing an instance of 'std::system_error'" on // stderr. Launched from Control or as a service there is nobody reading // stderr, so the crash looked like the server simply never starting and left // no log at all -- the log is only opened further down. These markers make // both halves visible: how far startup got, and what was thrown. void startup_marker(const char *step) { std::fprintf(stderr, "[ArtLightServer] startup: %s\n", step); std::fflush(stderr); } void startup_failure(const char *what, const char *detail) { std::fprintf(stderr, "[ArtLightServer] FATAL: %s: %s\n", what, detail); std::fflush(stderr); // Safe to attempt once logging is up; a no-op before it. try { BOOST_LOG(fatal) << "FATAL during startup: "sv << what << ": "sv << detail; } catch (...) { // Logging is not up yet, or is being torn down. stderr above is the record. } } // A guarded startup step that failed but is NOT fatal. // // Deliberately a separate function with separate wording: startup continues, // so this must never be recorded as a crash. If it were, a field report would // read as a server that died when it is in fact up and serving. void startup_warning(const char *what, const char *detail) { std::fprintf(stderr, "[ArtLightServer] WARNING: startup step failed, continuing: %s: %s\n", what, detail); std::fflush(stderr); // Safe to attempt once logging is up; a no-op before it. try { BOOST_LOG(warning) << "Startup step failed, continuing: "sv << what << ": "sv << detail; } catch (...) { // Logging is not up yet, or is being torn down. stderr above is the record. } } // Run a startup step that the server does not need in order to serve. // // A throw from any of these used to escape main entirely: the process aborted // before it could listen, and because logging is barely up the crash left no // log and only a stderr line nobody reads. A failed permissions repair, or a // stale NVCP/RTSS recovery file left behind by an earlier version, was enough // to take the whole server down with it. Report it and carry on. template void startup_step(const char *name, FN &&fn) { startup_marker(name); try { fn(); } catch (const std::exception &e) { startup_warning(name, e.what()); } catch (...) { startup_warning(name, "(non-std exception)"); } } } // namespace static int real_main(int argc, char *argv[]); int main(int argc, char *argv[]) { try { return real_main(argc, argv); } catch (const std::exception &e) { startup_failure("uncaught exception escaped startup", e.what()); } catch (...) { startup_failure("uncaught non-std exception escaped startup", "(no what())"); } return 1; } static int real_main(int argc, char *argv[]) { #ifdef __linux__ #ifdef SUNSHINE_BUILD_STEAMOS if (platf::linux_cli::command(argc, argv)) { std::fputs("ArtLight Server: native Linux maintenance commands are unavailable in the SteamOS user bundle.\n", stderr); return 2; } #else // Maintenance never enters host initialization. In particular, sudo must // reach the root-owned administrative helper before the host-only capability // policy rejects a root process. No configuration or logging is parsed here. if (const auto result = platf::linux_cli::dispatch(argc, argv)) { return *result; } #endif if (!platf::linux_security::sanitize_startup_capabilities()) { const int error_number = errno ? errno : EPERM; std::fprintf(stderr, "ArtLight Server: failed to sanitize Linux startup capabilities: %s\n", std::strerror(error_number)); return 1; } // Block termination before any worker or GPU resource can exist. Every // subsequently created thread inherits this mask; one dedicated sigwait() // thread consumes the signal synchronously in ordinary thread context. sigset_t termination_signal_set; sigemptyset(&termination_signal_set); sigaddset(&termination_signal_set, SIGINT); sigaddset(&termination_signal_set, SIGTERM); if (const int error_number = pthread_sigmask(SIG_BLOCK, &termination_signal_set, nullptr); error_number != 0) { std::fprintf(stderr, "ArtLight Server: failed to block termination signals: %s\n", std::strerror(error_number)); return 1; } const char *machine_host_environment = std::getenv("ARTLIGHT_MACHINE_HOST"); const bool supervised_machine_host = machine_host_environment && machine_host_environment[0] == '1' && machine_host_environment[1] == '\0'; #else constexpr bool supervised_machine_host = false; #endif lifetime::argv = argv; startup_marker("enter real_main"); // Staged packages must report their version before assets or a user profile // exist. Keep this query independent of configuration and host startup. if (argc == 2 && argv[1] == "--version"sv) { std::printf("%s version: %s commit: %s\n", PROJECT_NAME, PROJECT_VERSION, PROJECT_VERSION_COMMIT); return 0; } #ifdef SUNSHINE_BUILD_STEAMOS // Resolve assets from the executable's release, including launches outside // the wrapper and upgrades which switch the "current" symlink underneath us. std::error_code bundle_error; const auto bundle_executable = std::filesystem::read_symlink("/proc/self/exe", bundle_error); if (!bundle_error) { std::filesystem::current_path(bundle_executable.parent_path().parent_path(), bundle_error); } if (bundle_error) { std::fprintf(stderr, "ArtLight: cannot resolve SteamOS bundle: %s\n", bundle_error.message().c_str()); return 1; } #endif #ifdef _WIN32 // Avoid searching the PATH in case a user has configured their system insecurely // by placing a user-writable directory in the system-wide PATH variable. SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); setlocale(LC_ALL, "C"); #endif #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" // Use UTF-8 conversion for the default C++ locale (used by boost::log) startup_marker("locale setup"); std::locale utf8_locale(std::locale(), new std::codecvt_utf8); std::locale::global(utf8_locale); boost::filesystem::path::imbue(utf8_locale); #pragma GCC diagnostic pop startup_marker("create mail manager"); mail::man = std::make_shared(); // parse config file startup_marker("config::parse"); if (config::parse(argc, argv)) { return 0; } startup_marker("config::parse returned"); auto log_deinit_guard = logging::init(config::sunshine.min_log_level, config::sunshine.log_file); if (!log_deinit_guard) { BOOST_LOG(error) << "Logging failed to initialize"sv; } startup_marker("logging initialised"); #ifdef _WIN32 const auto app_user_model_id_status = SetCurrentProcessExplicitAppUserModelID(WIDEN_STRING_LITERAL(PROJECT_APP_USER_MODEL_ID)); if (FAILED(app_user_model_id_status)) { BOOST_LOG(warning) << "Failed to set explicit AppUserModelID; Windows may reuse legacy notification branding"sv; } #endif #ifndef SUNSHINE_EXTERNAL_PROCESS // Setup third-party library logging logging::setup_av_logging(config::sunshine.min_log_level); logging::setup_libdisplaydevice_logging(config::sunshine.min_log_level); #endif #ifdef __ANDROID__ // Setup Android-specific logging logging::setup_android_logging(); #endif // logging can begin at this point // if anything is logged prior to this point, it will appear in stdout, but not in the log viewer in the UI // the version should be printed to the log before anything else BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VERSION << " commit: " << PROJECT_VERSION_COMMIT; #ifdef _WIN32 const auto windows_version = platf::query_windows_version(); BOOST_LOG(info) << "Windows version: product=" << windows_version.product_name << ", display_version=" << windows_version.display_version << ", build=" << windows_version.current_build; if (windows_version.build_number.has_value() && *windows_version.build_number < 22000) { BOOST_LOG(warning) << "Windows 10 detected; HDR will not work on the ArtLight Server Virtual Display."; } #endif if (version_compare::is_prerelease_channel(PROJECT_VERSION)) { BOOST_LOG(info) << "Prerelease build detected; default min_log_level is debug unless overridden."; } BOOST_LOG(info) << "Effective min_log_level=" << config::sunshine.min_log_level; // Log publisher metadata log_publisher_data(); // Log modified_config_settings config::log_config_settings(config::modified_config_settings, false); config::modified_config_settings.clear(); #ifdef _WIN32 // None of these are required to serve a stream, so a failure in any of them // must not take the server down with it. startup_step("statefile::repair_config_permissions", [] { statefile::repair_config_permissions(); }); startup_step("frame_limiter_nvcp::restore_pending_overrides", [] { platf::frame_limiter_nvcp::restore_pending_overrides(); }); startup_step("rtss_restore_pending_overrides", [] { platf::rtss_restore_pending_overrides(); }); startup_marker("display recovery restores done"); #endif if (!config::sunshine.cmd.name.empty()) { auto fn = cmd_to_func.find(config::sunshine.cmd.name); if (fn == std::end(cmd_to_func)) { BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name; BOOST_LOG(info) << "Possible commands:"sv; for (auto &[key, _] : cmd_to_func) { BOOST_LOG(info) << '\t' << key; } return 7; } return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv); } // Display configuration is managed by the external Windows helper; no in-process init. // Construct the process-owned shutdown deadline before the session monitor. // Its cleanup guard also runs on early startup returns, so it must be able // to bound that join as well as the ordinary shutdown path below. auto shutdown_event = mail::man->event(mail::shutdown); std::atomic_bool shutdown_signal_requested {false}; shutdown_deadline_t shutdown_deadline {&shutdown_signal_requested, supervised_machine_host}; #ifdef __linux__ std::atomic_bool termination_signal_monitor_stopping {false}; std::atomic_bool termination_signal_monitor_finished {false}; std::thread termination_signal_monitor; try { termination_signal_monitor = std::thread([&]() { int signal_number = 0; const int wait_error = sigwait(&termination_signal_set, &signal_number); if (wait_error != 0) { BOOST_LOG(error) << "Termination signal wait failed: " << std::strerror(wait_error); shutdown_event->raise(true); termination_signal_monitor_finished.store(true, std::memory_order_release); return; } if (termination_signal_monitor_stopping.load(std::memory_order_acquire)) { termination_signal_monitor_finished.store(true, std::memory_order_release); return; } shutdown_signal_requested.store(true, std::memory_order_relaxed); if (supervised_machine_host) { platf::linux_private_display::request_process_shutdown_preserve(); } BOOST_LOG(info) << (signal_number == SIGINT ? "Interrupt handler called"sv : "Terminate handler called"sv); shutdown_event->raise(true); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 if (config::sunshine.system_tray) { system_tray::end_tray(); } #endif termination_signal_monitor_finished.store(true, std::memory_order_release); }); } catch (const std::system_error &exception) { BOOST_LOG(error) << "Unable to create the termination signal monitor: " << exception.what(); return 1; } auto stop_termination_signal_monitor = [&]() { if (!termination_signal_monitor.joinable()) { return; } termination_signal_monitor_stopping.store(true, std::memory_order_release); if (!termination_signal_monitor_finished.load(std::memory_order_acquire)) { const int wake_error = pthread_kill(termination_signal_monitor.native_handle(), SIGTERM); if (wake_error != 0 && wake_error != ESRCH) { BOOST_LOG(error) << "Unable to wake the termination signal monitor: " << std::strerror(wake_error); } } termination_signal_monitor.join(); }; auto termination_signal_monitor_guard = util::fail_guard(stop_termination_signal_monitor); #endif #ifdef WIN32 // Modify relevant NVIDIA control panel settings if the system has corresponding gpu if (nvprefs_instance.load()) { // Restore global settings to the undo file left by improper termination of sunshine.exe nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); // Modify application settings for sunshine.exe nvprefs_instance.modify_application_profile(); // Modify global settings, undo file is produced in the process to restore after improper termination nvprefs_instance.modify_global_profile(); // Unload dynamic library to survive driver re-installation nvprefs_instance.unload(); } // 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::promise session_monitor_hwnd_promise; auto session_monitor_hwnd_future = session_monitor_hwnd_promise.get_future().share(); std::promise session_monitor_thread_id_promise; auto session_monitor_thread_id_future = session_monitor_thread_id_promise.get_future().share(); std::promise session_monitor_join_thread_promise; auto session_monitor_join_thread_future = session_monitor_join_thread_promise.get_future(); std::thread session_monitor_thread([&]() { platf::set_thread_name("session_monitor"); session_monitor_join_thread_promise.set_value_at_thread_exit(); // Create a message queue immediately so shutdown can always fall back // to PostThreadMessage(WM_QUIT), even if window creation fails. MSG msg {}; PeekMessage(&msg, nullptr, 0, 0, PM_NOREMOVE); session_monitor_thread_id_promise.set_value(GetCurrentThreadId()); WNDCLASSA wnd_class {}; wnd_class.lpszClassName = "SunshineSessionMonitorClass"; wnd_class.lpfnWndProc = SessionMonitorWindowProc; if (!RegisterClassA(&wnd_class)) { session_monitor_hwnd_promise.set_value(nullptr); BOOST_LOG(error) << "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 ); session_monitor_hwnd_promise.set_value(wnd); if (!wnd) { BOOST_LOG(error) << "Failed to create session monitor window"sv << std::endl; return; } ShowWindow(wnd, SW_HIDE); // Run the message loop for our window while (GetMessage(&msg, nullptr, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessage(&msg); } }); auto shutdown_session_monitor = [&]() { if (!session_monitor_thread.joinable()) { return; } // This cleanup guard covers early returns before shutdown_event->view(). // Arm the owned deadline before any potentially unbounded join so that a // pathological message loop cannot hang process teardown indefinitely. shutdown_deadline.arm(); auto request_session_monitor_shutdown = [&](bool force_quit_only) { if (!force_quit_only) { if (session_monitor_hwnd_future.wait_for(1s) == std::future_status::ready) { if (HWND session_monitor_hwnd = session_monitor_hwnd_future.get()) { if (PostMessage(session_monitor_hwnd, WM_CLOSE, 0, 0)) { return true; } BOOST_LOG(warning) << "Failed to post WM_CLOSE to session monitor window: "sv << GetLastError(); } else { BOOST_LOG(warning) << "Session monitor window was not created"sv; } } else { BOOST_LOG(warning) << "session_monitor_hwnd_future reached timeout"; } } if (session_monitor_thread_id_future.wait_for(1s) != std::future_status::ready) { BOOST_LOG(warning) << "session_monitor_thread_id_future reached timeout"; return false; } const DWORD session_monitor_thread_id = session_monitor_thread_id_future.get(); if (!session_monitor_thread_id) { BOOST_LOG(warning) << "Session monitor thread id was not set"sv; return false; } if (PostThreadMessage(session_monitor_thread_id, WM_QUIT, 0, 0)) { return true; } BOOST_LOG(warning) << "Failed to post WM_QUIT to session monitor thread: "sv << GetLastError(); return false; }; request_session_monitor_shutdown(false); if (session_monitor_join_thread_future.wait_for(3s) == std::future_status::ready) { session_monitor_thread.join(); return; } BOOST_LOG(warning) << "session_monitor_join_thread_future reached timeout"; request_session_monitor_shutdown(true); // This thread owns a window/message queue and may access logging and // process globals. It must never escape into CRT teardown. The shutdown // deadline is armed before the normal call site below, so an unexpected // stuck message loop is diagnosed instead of being detached unsafely. if (session_monitor_join_thread_future.wait_for(5s) == std::future_status::ready) { session_monitor_thread.join(); } else { BOOST_LOG(error) << "session_monitor_thread still running after forced WM_QUIT; waiting for owned thread exit"; session_monitor_thread.join(); } }; auto session_monitor_join_thread_guard = util::fail_guard([&]() { shutdown_session_monitor(); }); #endif task_pool.start(1); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 // create tray thread and detach it if enabled in config if (config::sunshine.system_tray) { system_tray::run_tray(); } // Schedule periodic update checks if configured if (config::sunshine.update_check_interval_seconds > 0) { // Trigger an immediate update check on startup so users don't wait // a full interval before the first detection occurs. update::trigger_check(true); auto schedule_periodic = std::make_shared>(); *schedule_periodic = [schedule_periodic]() { update::periodic(); if (config::sunshine.update_check_interval_seconds > 0) { task_pool.pushDelayed(*schedule_periodic, std::chrono::seconds(config::sunshine.update_check_interval_seconds)); } }; task_pool.pushDelayed(*schedule_periodic, std::chrono::seconds(config::sunshine.update_check_interval_seconds)); } #endif #ifndef __linux__ // Other platforms retain their native signal/control handlers. Linux has // blocked these signals process-wide and consumes them synchronously above. on_signal(SIGINT, [&shutdown_signal_requested, shutdown_event]() { shutdown_signal_requested.store(true, std::memory_order_relaxed); BOOST_LOG(info) << "Interrupt handler called"sv; // Preserve ArtLight Server's eager application cleanup; the owned deadline above // replaces the detached task-pool watchdog that used to follow it. proc::proc.terminate(); // Break out of the main loop shutdown_event->raise(true); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 if (config::sunshine.system_tray) { system_tray::end_tray(); } #endif }); on_signal(SIGTERM, [&shutdown_signal_requested, shutdown_event]() { shutdown_signal_requested.store(true, std::memory_order_relaxed); BOOST_LOG(info) << "Terminate handler called"sv; // Break out of the main loop shutdown_event->raise(true); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 if (config::sunshine.system_tray) { system_tray::end_tray(); } #endif }); #endif #ifdef _WIN32 // Terminate gracefully on Windows when console window is closed SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE); #endif proc::refresh(config::stream.file_apps); // If any of the following fail, we log an error and continue event though sunshine will not function correctly. // This allows access to the UI to fix configuration problems or view the logs. auto platf_deinit_guard = platf::init(); if (!platf_deinit_guard) { BOOST_LOG(error) << "Platform failed to initialize"sv; } #ifdef __linux__ (void) platf::linux_display::backend().initialize(); auto linux_private_display_guard = util::fail_guard([supervised_machine_host]() { if (supervised_machine_host) { platf::linux_private_display::request_process_shutdown_preserve(); return; } (void) platf::linux_display::backend().revert(); }); #endif // Steam's catalog can change while the server is running. The watcher is // started after logging/platform initialization and owns its shutdown // thread independently of the task pool. platf::steam::autosync::start(); auto steam_autosync_guard = util::fail_guard([]() { platf::steam::autosync::stop(); }); #ifdef __linux__ platf::lutris::autosync::start(); auto lutris_autosync_guard = util::fail_guard([]() { platf::lutris::autosync::stop(); }); #endif #ifdef _WIN32 // Reconcile the Vulkan HDR implicit-layer registration with the configured preference. This makes // the Web UI toggle authoritative over the installer's unconditional registration and self-heals // when the installer's (now best-effort) registration was skipped or failed. Only attempt when // running as SYSTEM, since it writes HKLM; the call short-circuits when already in the desired state. if (platf::is_running_as_system()) { platf::set_vulkan_hdr_layer_enabled(config::video.dd.vulkan_hdr_layer); } #endif auto host_stats_deinit_guard = host_stats::start(); if (shutdown_event->peek()) { return lifetime::desired_exit_code; } auto proc_deinit_guard = proc::init(); if (!proc_deinit_guard) { BOOST_LOG(error) << "Proc failed to initialize"sv; } if (shutdown_event->peek()) { return lifetime::desired_exit_code; } reed_solomon_init(); auto input_deinit_guard = input::init(); if (input::probe_gamepads()) { BOOST_LOG(warning) << "No gamepad input is available"sv; } #ifdef _WIN32 const auto has_startup_stream_activity = [] { return rtsp_stream::has_pending_launch_or_startup() || rtsp_stream::session_count() != 0 || webrtc_stream::has_active_or_pending_sessions(); }; #endif #ifdef _WIN32 bool startup_probe_succeeded = false; #endif auto startup_probe = [&shutdown_event #ifdef _WIN32 , &startup_probe_succeeded , &has_startup_stream_activity #endif ]() { #ifdef _WIN32 constexpr unsigned int max_startup_probe_attempts = 2; unsigned int attempts = 0; std::optional required_adapter_luid; std::optional adapter_hint_lease; if (config::video.virtual_display_mode != config::video_t::virtual_display_mode_e::disabled) { const auto preferred_adapter = platf::resolve_preferred_render_adapter( config::video.adapter_name, config::video.adapter_pnp_id ); if (preferred_adapter) { required_adapter_luid = *preferred_adapter.luid; adapter_hint_lease = video::set_pending_virtual_display_adapter_hint(*preferred_adapter.luid); } } auto clear_adapter_hint = util::fail_guard([&adapter_hint_lease]() { if (adapter_hint_lease) { (void) video::clear_pending_virtual_display_adapter_hint(*adapter_hint_lease); } }); while (true) { const video::startup_encoder_probe_policy::state state { .cache_successful = video::has_successful_encoder_probe(), .stream_active = has_startup_stream_activity(), .shutting_down = shutdown_event->peek(), .attempts = attempts, .max_attempts = max_startup_probe_attempts, }; if (state.cache_successful) { startup_probe_succeeded = true; BOOST_LOG(debug) << "Startup encoder probe skipped; a successful adapter-scoped cache already exists."; return; } if (!video::startup_encoder_probe_policy::should_probe(state)) { if (state.stream_active) { BOOST_LOG(debug) << "Startup encoder probe stopped; a streaming session owns the runtime lifecycle."; } else if (video::startup_encoder_probe_policy::terminal_failure(state)) { BOOST_LOG(error) << "Startup encoder probing reached its terminal retry limit without a successful cache."; } return; } VDISPLAY::ensure_display_result encoder_probe_display_result {}; if (config::video.virtual_display_mode != config::video_t::virtual_display_mode_e::disabled) { encoder_probe_display_result = VDISPLAY::ensure_display(required_adapter_luid); } auto cleanup_encoder_probe_display = util::fail_guard([&encoder_probe_display_result]() { VDISPLAY::cleanup_ensure_display(encoder_probe_display_result); }); if (required_adapter_luid && !encoder_probe_display_result.owns_temporary_probe_request()) { BOOST_LOG(warning) << "Startup could not acquire the temporary virtual-display lease; continuing with exact-adapter synthetic encoder validation."; } else if (encoder_probe_display_result.owns_temporary_probe_request() && !encoder_probe_display_result.ready_for_capture()) { BOOST_LOG(info) << "Startup temporary display is not published to capture yet; synthetic encoder validation does not wait for it."; } ++attempts; if (!video::probe_encoders()) { startup_probe_succeeded = true; BOOST_LOG(info) << "Startup encoder probe produced a successful adapter-scoped cache."; return; } const video::startup_encoder_probe_policy::state failed_state { .cache_successful = video::has_successful_encoder_probe(), .stream_active = has_startup_stream_activity(), .shutting_down = shutdown_event->peek(), .attempts = attempts, .max_attempts = max_startup_probe_attempts, }; if (video::startup_encoder_probe_policy::should_probe(failed_state)) { BOOST_LOG(warning) << "Startup encoder probe failed; retrying without waiting for desktop publication (attempt " << (attempts + 1) << '/' << max_startup_probe_attempts << ")."; std::this_thread::sleep_for(250ms); } } #else if (video::has_successful_encoder_probe()) { return; } if (video::probe_encoders()) { BOOST_LOG(error) << "Failed to probe encoders during startup."; } #endif }; #ifdef _WIN32 auto startup_display_recovery = [&shutdown_event, &has_startup_stream_activity]() { if (shutdown_event->peek() || has_startup_stream_activity() || VDISPLAY::has_retained_ensure_display()) { return; } const auto virtual_displays = VDISPLAY::enumerateVirtualDisplays(); const bool has_active_virtual_display = std::any_of( virtual_displays.begin(), virtual_displays.end(), [](const VDISPLAY::VirtualDisplayInfo &info) { return info.is_active; } ); if (!has_active_virtual_display || shutdown_event->peek() || has_startup_stream_activity() || VDISPLAY::has_retained_ensure_display()) { return; } BOOST_LOG(warning) << "Startup detected active virtual display(s) with no active stream session; running cleanup."; (void) platf::virtual_display_cleanup::run("startup_recovery", config::video.dd.config_revert_on_disconnect); }; #endif // Initialize session history in its own directory so database hardening never // touches the shared config root that also contains credentials/pairing state. { std::filesystem::path state_path {config::nvhttp.file_state}; const auto config_dir = state_path.parent_path(); const auto history_dir = config_dir / "session_history"; const auto history_db = history_dir / "session_history.db"; // Best-effort alpha.1/alpha.2 migration: preserve any database that was // created in the shared config root before the storage was isolated. const auto legacy_history_db = config_dir / "session_history.db"; auto move_if_needed = [](const std::filesystem::path &from, const std::filesystem::path &to) -> bool { std::error_code ec; const bool from_exists = std::filesystem::exists(from, ec); if (!from_exists || ec) { return false; } ec.clear(); const bool to_exists = std::filesystem::exists(to, ec); if (to_exists || ec) { return false; } ec.clear(); std::filesystem::create_directories(to.parent_path(), ec); if (ec) { BOOST_LOG(warning) << "session_history: failed to create isolated history directory " << to.parent_path().string() << ": " << ec.message(); return false; } std::filesystem::rename(from, to, ec); if (ec) { BOOST_LOG(warning) << "session_history: failed to move legacy database file " << from.string() << " to " << to.string() << ": " << ec.message(); return false; } return true; }; const bool migrated_main_db = move_if_needed(legacy_history_db, history_db); if (migrated_main_db) { move_if_needed(legacy_history_db.string() + "-wal", history_db.string() + "-wal"); move_if_needed(legacy_history_db.string() + "-shm", history_db.string() + "-shm"); } session_history::init(history_db.string()); } auto session_history_shutdown_guard = util::fail_guard([]() { session_history::shutdown(); }); if (http::init()) { BOOST_LOG(fatal) << "HTTP interface failed to initialize"sv; #ifdef _WIN32 BOOST_LOG(fatal) << "To relaunch Apollo successfully, use the shortcut in the Start Menu. Do not run sunshine.exe manually."sv; std::this_thread::sleep_for(10s); #endif return -1; } #ifdef _WIN32 // Start Playnite integration (IPC + handlers) std::unique_ptr playnite_integration_guard; if (config::playnite.enabled) { playnite_integration_guard = platf::playnite::start(); } #endif std::thread configThread {confighttp::start}; // Produce the capability cache before discovery can advertise the service // or GameStream discovery/launch can observe it. The config UI remains // available while the bounded synthetic probe runs. startup_probe(); if (shutdown_event->peek()) { configThread.join(); return lifetime::desired_exit_code; } std::unique_ptr mDNS; auto sync_mDNS = std::async(std::launch::async, [&mDNS]() { if (config::sunshine.enable_discovery) { mDNS = platf::publish::start(); } }); std::unique_ptr upnp_unmap; auto sync_upnp = std::async(std::launch::async, [&upnp_unmap]() { upnp_unmap = upnp::start(); }); // FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced if (shutdown_event->peek()) { configThread.join(); return lifetime::desired_exit_code; } std::thread httpThread {nvhttp::start}; std::thread rtspThread {rtsp_stream::start}; #ifdef _WIN32 // Stale-display cleanup is separate from encoder validation and therefore // runs only after the network listeners are available. if (startup_probe_succeeded) { startup_display_recovery(); } else { BOOST_LOG(warning) << "Startup stale-display cleanup skipped because encoder validation did not produce a successful cache."; } #endif #ifdef _WIN32 // If we're using the default port and GameStream is enabled, warn the user if (config::sunshine.port == 47989 && is_gamestream_enabled()) { BOOST_LOG(fatal) << "GameStream is still enabled in GeForce Experience! This *will* cause streaming problems with Apollo!"sv; BOOST_LOG(fatal) << "Disable GameStream on the SHIELD tab in GeForce Experience or change the Port setting on the Advanced tab in the Apollo Web UI."sv; } #endif // Wait for shutdown shutdown_event->view(); #ifdef __linux__ if (supervised_machine_host) { platf::linux_private_display::request_process_shutdown_preserve(); } stop_termination_signal_monitor(); termination_signal_monitor_guard.disable(); #endif // The signal handler only wakes main; start the owned watchdog here so it // never constructs threads or queues work from signal context. shutdown_deadline.arm(); #ifdef WIN32 // Join the hidden shutdown-notification window while the deadline watchdog // is still armed. The guard remains for early-return paths only. shutdown_session_monitor(); session_monitor_join_thread_guard.disable(); #endif #ifdef _WIN32 // Stop the owned lock-screen virtual-output worker before recovery workers // can publish more overrides. Both use configuration, the display helper, // and mail, all of which remain live until these joins complete. config::request_deferred_virtual_output_reapply_shutdown(); VDISPLAY::request_virtual_display_recovery_shutdown(); config::join_deferred_virtual_output_reapply_worker(); VDISPLAY::join_virtual_display_recovery_monitors(); #endif httpThread.join(); configThread.join(); rtspThread.join(); #ifdef __linux__ platf::lutris::autosync::stop(); lutris_autosync_guard.disable(); #endif platf::steam::autosync::stop(); steam_autosync_guard.disable(); #ifdef _WIN32 // Full process shutdown cannot leave the paused-session watchdog running. // If it survives past main(), CRT teardown can fast-fail while the helper // watchdog thread is still unwinding. display_helper_integration::stop_watchdog(true); // The virtual display watchdog thread also lives in static storage. // Ensure it is joined before CRT on-exit handlers destroy the thread object. VDISPLAY::cleanup_retained_ensure_display(); VDISPLAY::closeVDisplayDevice(); #endif task_pool.stop(); task_pool.join(); // stop system tray #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::end_tray(); #endif #ifdef WIN32 // Restore global NVIDIA control panel settings if (nvprefs_instance.owning_undo_file() && nvprefs_instance.load()) { nvprefs_instance.restore_global_profile(); nvprefs_instance.unload(); } #endif session_history::shutdown(); session_history_shutdown_guard.disable(); return lifetime::desired_exit_code; }