server/src/main.cpp
Rama main · réplica de lectura del repositorio original.
/**
* @file src/main.cpp
* @brief Definitions for the main entry point for Sunshine.
*/
// standard includes
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <codecvt>
#include <condition_variable>
#include <cstdio>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <mutex>
#include <system_error>
#include <thread>
// 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 <shobjidl.h>
#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 <pthread.h>
#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<int, std::function<void()>> 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<class FN>
void on_signal(int sig, FN &&fn) {
signal_handlers.emplace(sig, std::forward<FN>(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<std::string_view, std::function<int(const char *name, int argc, char **argv)>> 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<class FN>
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<wchar_t>);
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<safe::mail_raw_t>();
// 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<bool>(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<HWND> session_monitor_hwnd_promise;
auto session_monitor_hwnd_future = session_monitor_hwnd_promise.get_future().share();
std::promise<DWORD> session_monitor_thread_id_promise;
auto session_monitor_thread_id_future = session_monitor_thread_id_promise.get_future().share();
std::promise<void> 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<std::function<void()>>();
*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<LUID> required_adapter_luid;
std::optional<video::encoder_probe_adapter_hint_lease_t> 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<platf::deinit_t> 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<platf::deinit_t> mDNS;
auto sync_mDNS = std::async(std::launch::async, [&mDNS]() {
if (config::sunshine.enable_discovery) {
mDNS = platf::publish::start();
}
});
std::unique_ptr<platf::deinit_t> 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;
}