server/src/nvhttp.cpp
Rama main · réplica de lectura del repositorio original.
/**
* @file src/nvhttp.cpp
* @brief Definitions for the nvhttp (GameStream) server.
*/
// macros
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
// standard includes
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <format>
#include <functional>
#include <fstream>
#include <future>
#include <limits>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
// lib includes
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/context_base.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <Simple-Web-Server/server_http.hpp>
// local includes
#include "app_display_policy.h"
#include "config.h"
#include "display_device.h"
#include "display_helper_integration.h"
#include "file_handler.h"
#include "globals.h"
#include "hdr_request_policy.h"
#include "httpcommon.h"
#include "http_pairing_policy.h"
#include "hdr_request_policy.h"
#include "logging.h"
#include "network.h"
#include "nvhttp.h"
#include "remote_session.h"
#include "remote_display_topology.h"
#include "platform/common.h"
#include "pyrowave_protocol.h"
#include "state_storage.h"
#include "paired_state_policy.h"
#include "state_storage_policy.h"
#include "update.h"
#include "single_flight.h"
#include "state_storage_policy.h"
#ifdef _WIN32
#include "platform/windows/display.h"
#include "platform/windows/display_helper_request_policy.h"
#include "platform/windows/display_helper_request_helpers.h"
#include "platform/windows/misc.h"
#include "platform/windows/virtual_display.h"
#include "platform/windows/virtual_display_cleanup.h"
#elif defined(__linux__)
#include "platform/linux/private_display.h"
#include "platform/linux/routed_link.h"
#include "platform/linux/mangohud_policy.h"
#include "src/platform/linux/display_backend.h"
#include "platform/linux/display_power.h"
#include "platform/linux/private_display_resume_policy.h"
#endif
#include "process.h"
#include "rtsp.h"
#include "rtsp_pending_policy.h"
#include "stream.h"
#include "system_tray.h"
#include "video.h"
#include "webrtc_stream.h"
#include "zwpad.h"
using namespace std::literals;
namespace nvhttp {
namespace {
struct remote_role_owner_t {
remote_session::role_e role {remote_session::role_e::none};
std::uint64_t generation {};
};
std::mutex remote_role_owners_mutex;
std::unordered_map<std::string, remote_role_owner_t> remote_role_owners;
std::uint64_t active_session_generation(const proc::active_session_guard_t &session) {
if (!session.has_active_app) return 0;
const auto ticks = std::chrono::duration_cast<std::chrono::nanoseconds>(session.launch_started_at.time_since_epoch()).count();
return ticks > 0 ? static_cast<std::uint64_t>(ticks) : 0;
}
// Display creation/topology apply is deliberately synchronous and can
// outlive Moonlight's patience for a launch response. Serialize synthetic
// state transitions so a retry cannot make its dispatch decision against
// half-published ownership or race an explicit disconnect.
std::mutex remote_http_control_transition_mutex;
std::string remote_role_owner_key(std::string_view uuid, remote_session::role_e role) {
return std::string {uuid} + ':' + std::to_string(static_cast<unsigned int>(role));
}
struct remote_role_gate_snapshot_t {
remote_session::owner_t owner;
bool active {};
};
remote_role_gate_snapshot_t remote_role_gate_snapshot_for_client(std::string_view uuid) {
remote_role_gate_snapshot_t result;
std::optional<remote_role_owner_t> caller_owner;
{
std::lock_guard lock {remote_role_owners_mutex};
result.active = !remote_role_owners.empty();
for (const auto role : {remote_session::role_e::monitor, remote_session::role_e::input}) {
const auto it = remote_role_owners.find(remote_role_owner_key(uuid, role));
if (it == remote_role_owners.end()) continue;
caller_owner = it->second;
break;
}
}
if (!caller_owner) return result;
result.owner.role = caller_owner->role;
if (caller_owner->role == remote_session::role_e::monitor) {
const auto state = remote_session::monitor_runtime_snapshot(uuid, caller_owner->generation);
result.owner.retained = state.accepted;
result.owner.ready = state.ready;
result.owner.retryable = state.retryable;
if (!state.output.empty()) result.owner.output = state.output;
}
return result;
}
void remember_remote_owner(std::string_view uuid, remote_session::role_e role, std::uint64_t generation) {
std::lock_guard lock {remote_role_owners_mutex};
remote_role_owners.insert_or_assign(remote_role_owner_key(uuid, role), remote_role_owner_t {role, generation});
}
std::optional<std::uint64_t> remote_owner_generation(std::string_view uuid, remote_session::role_e role) {
std::lock_guard lock {remote_role_owners_mutex};
const auto it = remote_role_owners.find(remote_role_owner_key(uuid, role));
return it == remote_role_owners.end() ? std::nullopt : std::make_optional(it->second.generation);
}
void forget_remote_owner(std::string_view uuid, remote_session::role_e role, std::uint64_t generation) {
std::lock_guard lock {remote_role_owners_mutex};
const auto key = remote_role_owner_key(uuid, role);
const auto it = remote_role_owners.find(key);
if (it != remote_role_owners.end() && it->second.generation == generation) remote_role_owners.erase(it);
}
void forget_remote_client(std::string_view uuid) {
{
std::lock_guard lock {remote_role_owners_mutex};
remote_role_owners.erase(remote_role_owner_key(uuid, remote_session::role_e::monitor));
remote_role_owners.erase(remote_role_owner_key(uuid, remote_session::role_e::input));
}
remote_session::clear_app_replacement_confirmation(uuid);
}
void forget_all_remote_clients() {
std::lock_guard lock {remote_role_owners_mutex};
remote_role_owners.clear();
}
bool has_stream_session_activity() {
// RTSP removes STOPPING sessions from session_count() before stream::session::join()
// returns; pending launches/creations reserve the process-wide runtime layer
// before either protocol publishes an active session.
return rtsp_stream::has_pending_launch_or_startup() ||
rtsp_stream::session_count_no_cleanup() > 0 ||
stream::session::running_sessions.load(std::memory_order_acquire) != 0 ||
stream::session::teardown_sessions.load(std::memory_order_acquire) != 0 ||
webrtc_stream::has_active_or_pending_sessions() ||
webrtc_stream::has_capture_active() ||
webrtc_stream::has_teardown_in_progress();
}
} // namespace
static constexpr std::string_view EMPTY_PROPERTY_TREE_ERROR_MSG = "Property tree is empty. Probably, control flow got interrupted by an unexpected C++ exception. This is a bug in Sunshine. Moonlight-qt will report Malformed XML (missing root element)."sv;
void notify_remote_input_transport_lost(const std::string_view client_uuid, const std::uint64_t generation) {
forget_remote_owner(client_uuid, remote_session::role_e::input, generation);
if (!has_stream_session_activity() && remote_display_topology::instance().managed_client_identity_count() == 0) {
config::clear_runtime_config_overrides();
config::apply_config_now();
}
}
void notify_remote_monitor_released(const std::string_view client_uuid, const std::uint64_t generation) {
forget_remote_owner(client_uuid, remote_session::role_e::monitor, generation);
}
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
using p_named_cert_t = crypto::p_named_cert_t;
using PERM = crypto::PERM;
using verified_client_t = std::optional<crypto::named_cert_t>;
namespace {
std::int64_t now_seconds() {
return std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
)
.count();
}
} // namespace
struct client_t {
std::vector<p_named_cert_t> named_devices;
std::string remote_display_layout_json {R"({"version":1,"placements":{}})"};
};
struct pair_session_t;
crypto::cert_chain_t cert_chain;
static std::shared_ptr<safe::queue_t<crypto::x509_t>> pending_cert_queue =
std::make_shared<safe::queue_t<crypto::x509_t>>(30);
static std::string one_time_pin;
static std::string otp_passphrase;
static std::string otp_device_name;
static std::chrono::time_point<std::chrono::steady_clock> otp_creation_time;
namespace {
/*
* launch_session->fps is millihertz — the unit the client speaks, 60000 for 60 Hz — while
* mode_t is hertz. Passing it through unconverted asked the host to create a virtual display
* at 138000 Hz, which SetDisplayConfig rejects as ERROR_INVALID_PARAMETER during forced mode
* enumeration, so every topology containing that display failed validation.
*
* Platform-free arithmetic, and deliberately outside the _WIN32 block: the Linux identity
* reservation below asks for a mode in the same units, and while this lived in the Windows
* namespace the Linux build could not compile at all. Nothing noticed until the first Linux
* build after that commit — the Windows job compiles only its own half.
*/
int refresh_hz_from_session_fps(int fps) {
return fps >= 1000 ? static_cast<int>(std::lround(fps / 1000.0)) : fps;
}
} // namespace
#ifdef _WIN32
namespace {
bool remote_device_id_equals(const std::string &lhs, const std::string &rhs) {
return !lhs.empty() && !rhs.empty() && boost::iequals(lhs, rhs);
}
int remote_refresh_hz(const display_device::FloatingPoint &refresh) {
if (const auto *ratio = std::get_if<display_device::Rational>(&refresh)) {
return ratio->m_denominator == 0 ? 0 : static_cast<int>(std::lround(static_cast<double>(ratio->m_numerator) / ratio->m_denominator));
}
if (const auto *value = std::get_if<double>(&refresh)) {
return static_cast<int>(std::lround(*value));
}
return 0;
}
void refresh_remote_monitor_baseline(const bool extend_active_stream) {
const auto devices = display_helper_integration::enumerate_devices(display_device::DeviceEnumerationDetail::Minimal);
if (!devices) return;
const auto active_stream_output = extend_active_stream ? config::get_active_output_name() : std::string {};
const bool active_stream_uses_virtual =
!active_stream_output.empty() && VDISPLAY::is_virtual_display_output(active_stream_output);
const auto managed_client_ids = remote_display_topology::instance().managed_client_identity_ids();
std::vector<std::string> managed_device_ids;
if (active_stream_uses_virtual) {
for (const auto &client_id : managed_client_ids) {
if (const auto resolved = VDISPLAY::resolveActiveVirtualDisplayDeviceIdForStableId(client_id, active_stream_output, {}, false)) {
managed_device_ids.push_back(*resolved);
}
}
}
const auto active_game = proc::proc.active_session_guard();
std::vector<remote_display_topology::node_t> baseline;
for (const auto &device : *devices) {
if (device.m_device_id.empty() || device.m_display_name.empty() || !device.m_info) continue;
const bool is_virtual = VDISPLAY::is_virtual_display_output(device.m_device_id);
if (is_virtual) {
// A virtual output is a baseline anchor only when another stream is
// actively capturing that exact output. First-connection Remote
// Monitor deliberately ignores global virtual-display preferences.
if (!active_stream_uses_virtual ||
(!remote_device_id_equals(device.m_device_id, active_stream_output) &&
!boost::iequals(device.m_display_name, active_stream_output))) continue;
if (std::any_of(managed_device_ids.begin(), managed_device_ids.end(), [&](const auto &managed_id) {
return remote_device_id_equals(managed_id, device.m_device_id);
})) continue;
} else if (active_stream_uses_virtual) {
// An existing virtual stream defines the desktop being extended;
// do not reintroduce physical outputs that it intentionally replaced.
continue;
}
remote_display_topology::node_t node;
const bool owner_already_managed = std::find(managed_client_ids.begin(), managed_client_ids.end(), active_game.client_uuid) != managed_client_ids.end();
node.id = is_virtual && !active_game.client_uuid.empty() ?
active_game.client_uuid + (owner_already_managed ? ":streamed" : "") :
device.m_device_id;
node.device_id = device.m_device_id;
node.label = device.m_friendly_name.empty() ? device.m_display_name : device.m_friendly_name;
node.preexisting = true;
node.physical = !is_virtual;
node.active = true;
node.primary = device.m_info->m_primary;
node.x = device.m_info->m_origin_point.m_x;
node.y = device.m_info->m_origin_point.m_y;
node.configured_mode = {
.width = static_cast<int>(device.m_info->m_resolution.m_width),
.height = static_cast<int>(device.m_info->m_resolution.m_height),
.refresh_hz = remote_refresh_hz(device.m_info->m_refresh_rate),
.hdr = device.m_info->m_hdr_state.value_or(display_device::HdrState::Disabled) == display_device::HdrState::Enabled,
};
baseline.push_back(std::move(node));
}
remote_display_topology::instance().set_physical_baseline(std::move(baseline));
}
bool apply_remote_monitor_composition(const std::vector<remote_display_topology::node_t> &nodes) {
display_helper_integration::DisplayTopologyDefinition topology;
const auto physical = display_helper_integration::capture_physical_topology();
if (!physical) {
BOOST_LOG(warning) << "Remote monitor composition: could not capture the physical topology; composition abandoned.";
return false;
}
topology.topology = *physical;
auto has_device = [&topology](const std::string &id) {
return std::any_of(topology.topology.begin(), topology.topology.end(), [&id](const auto &group) {
return std::any_of(group.begin(), group.end(), [&id](const auto &candidate) { return remote_device_id_equals(candidate, id); });
});
};
for (const auto &node : nodes) {
std::string device_id = node.device_id.empty() ? node.id : node.device_id;
if (!node.physical && !node.preexisting) {
const auto resolved = VDISPLAY::resolveActiveVirtualDisplayDeviceIdForStableId(node.id, {}, {}, false);
if (!resolved) {
// Silent before now, and a silent false here is indistinguishable from a Windows refusal
// three layers down. Name the node that could not be resolved.
BOOST_LOG(warning) << "Remote monitor composition: no active virtual display for stable id '"
<< node.id << "' (label '" << node.label << "', " << node.x << "," << node.y
<< ", " << node.configured_mode.width << "x" << node.configured_mode.height
<< "x" << node.configured_mode.refresh_hz << "); topology cannot be built.";
return false;
}
device_id = *resolved;
}
BOOST_LOG(info) << "Remote monitor composition node: id='" << node.id << "' device_id='" << device_id
<< "' physical=" << node.physical << " preexisting=" << node.preexisting
<< " mode=" << node.configured_mode.width << "x" << node.configured_mode.height
<< "x" << node.configured_mode.refresh_hz << " at " << node.x << "," << node.y
<< (node.primary ? " primary" : "");
if (!has_device(device_id)) topology.topology.push_back({device_id});
/*
* Where the user last left this display wins over where the layout says it goes.
*
* The composer places a new display edge-on to what is already there, and it has no
* idea the user dragged this panel somewhere else last session. Windows cannot help:
* the arrangement is stored against a monitor that stopped existing when the last
* stream ended, so every session starts from the computed position again.
*/
auto placed = display_device::Point {node.x, node.y};
if (!node.physical) {
if (const auto saved = statefile::load_virtual_display_position(device_id)) {
placed = display_device::Point {saved->m_x, saved->m_y};
BOOST_LOG(info) << "Remote monitor composition: placing '" << node.id << "' at its remembered "
<< placed.m_x << "," << placed.m_y << " instead of the composed "
<< node.x << "," << node.y << ".";
}
}
topology.monitor_positions.emplace(device_id, placed);
if (node.primary) topology.primary_device = device_id;
}
// A group with no device in it is not a topology, it is a rejection waiting to happen.
topology.topology.erase(
std::remove_if(topology.topology.begin(), topology.topology.end(), [](const auto &group) { return group.empty(); }),
topology.topology.end()
);
if (topology.topology.empty()) {
BOOST_LOG(warning) << "Remote monitor composition: nothing left to place after resolving " << nodes.size()
<< " node(s); keeping the current topology.";
return false;
}
return display_helper_integration::apply_remote_composed_topology(topology);
}
std::optional<std::string> remote_monitor_exact_capture_output(
const std::string &client_uuid,
const remote_display_topology::mode_t &mode
) {
// The monitor role's display is created under "<uuid>:remote-monitor" - never under the bare
// client uuid, which is the game role's identity. Looking it up under the client uuid finds
// either nothing (monitor-only client, i.e. a second device) or the game display, whose mode
// is not the monitor's, and either way this check answers "not capture-ready" and the launch
// is refused with a 503. Ask for the identity the display was actually created with.
const auto expected_device = VDISPLAY::resolveActiveVirtualDisplayDeviceIdForStableId(
remote_display_topology::remote_monitor_stable_id(client_uuid), {}, {}, false
);
if (!expected_device) {
BOOST_LOG(warning) << "Remote Monitor capture check: no active virtual display for client '" << client_uuid
<< "' under its monitor identity; the display was not created or was already released.";
return std::nullopt;
}
const auto devices = display_helper_integration::enumerate_devices(display_device::DeviceEnumerationDetail::Minimal);
if (!devices) return std::nullopt;
const auto capture_outputs = platf::display_names(platf::mem_type_e::dxgi);
for (const auto &device : *devices) {
if (!remote_device_id_equals(device.m_device_id, *expected_device) || !device.m_info || device.m_display_name.empty()) continue;
const auto refresh = remote_refresh_hz(device.m_info->m_refresh_rate);
const auto have_width = static_cast<int>(device.m_info->m_resolution.m_width);
const auto have_height = static_cast<int>(device.m_info->m_resolution.m_height);
// Size decides whether this is the right display; refresh does not. The virtual display
// driver reports the streamed panel as 552 Hz for a 138 Hz request - the encoder logs the
// same figure on every successful stream - so an exact-refresh comparison here rejects a
// display that is present, correctly sized and already capturing. Say both numbers out
// loud when they differ, and let capture prove itself.
if (have_width != static_cast<int>(mode.width) || have_height != static_cast<int>(mode.height)) {
BOOST_LOG(warning) << "Remote Monitor capture check: display '" << device.m_display_name << "' is "
<< have_width << "x" << have_height << "x" << refresh << " but the client asked for "
<< mode.width << "x" << mode.height << "x" << mode.refresh_hz << ".";
return std::nullopt;
}
if (refresh != static_cast<int>(mode.refresh_hz)) {
BOOST_LOG(info) << "Remote Monitor capture check: display '" << device.m_display_name << "' reports "
<< refresh << " Hz for a " << mode.refresh_hz
<< " Hz request; accepting it on size, because this driver does not report refresh back honestly.";
}
const auto output = std::find_if(capture_outputs.begin(), capture_outputs.end(), [&](const auto &candidate) {
return remote_device_id_equals(candidate, device.m_display_name);
});
if (output != capture_outputs.end()) return *output;
return std::nullopt;
}
return std::nullopt;
}
void register_remote_monitor_runtime() {
remote_display_topology::instance().set_runtime_callbacks({
.create_or_reclaim = [](const std::string &client_uuid, const std::string &client_label, const remote_display_topology::mode_t &mode) {
if (!VDISPLAY::ensure_driver_is_ready()) return false;
// Monitor role, not the bare client uuid: the same client may already own a game
// display under that uuid, and two roles must never share one display identity.
const auto stable_uuid = VDISPLAY::virtualDisplayUuidFromStableId(
remote_display_topology::remote_monitor_stable_id(client_uuid));
GUID guid {};
std::memcpy(&guid, stable_uuid.b8, sizeof(guid));
// HDR was hardcoded off here, so a client that asked for HDR got an SDR display and an
// SDR encode on its second screen while its main screen stayed HDR. The driver enables
// HDR itself when the request is true; the profile only supplies luminance metadata, so
// a null profile still yields a real HDR10 display.
return VDISPLAY::createVirtualDisplay(
client_uuid.c_str(), client_label.c_str(), nullptr,
static_cast<std::uint32_t>(mode.width), static_cast<std::uint32_t>(mode.height),
static_cast<std::uint32_t>(mode.refresh_hz * 1000), guid,
static_cast<std::uint32_t>(mode.refresh_hz * 1000), false, 1,
mode.hdr, false, true, true
).has_value();
},
.apply_composed_topology = apply_remote_monitor_composition,
.exact_target_has_current_mode_and_dxgi = remote_monitor_exact_capture_output,
.remove_owned_display = [](const std::string &client_uuid) {
const auto stable_uuid = VDISPLAY::virtualDisplayUuidFromStableId(
remote_display_topology::remote_monitor_stable_id(client_uuid));
GUID guid {};
std::memcpy(&guid, stable_uuid.b8, sizeof(guid));
return VDISPLAY::removeVirtualDisplay(guid);
},
});
remote_display_topology::instance().set_plaintext_rtsp_warning_provider([](const std::string &) {
return rtsp_stream::plaintext_route_warning();
});
remote_session::register_monitor_runtime_hooks({
.activate_or_resume = [](std::string_view uuid, std::string_view label, std::string_view requested_mode, const bool hdr_requested, std::uint64_t generation) -> remote_session::monitor_runtime_state_t {
remote_display_topology::mode_t mode;
if (std::sscanf(std::string {requested_mode}.c_str(), "%dx%d@%d", &mode.width, &mode.height, &mode.refresh_hz) != 3 ||
mode.width <= 0 || mode.height <= 0 || mode.refresh_hz <= 0) {
return remote_session::monitor_runtime_state_t {.retryable = true, .error = "Remote Monitor requested an invalid display mode."};
}
// The client's HDR request used to be dropped here (the parameter was unnamed), so the
// monitor display was always created SDR and the session was forced to SDR to match.
mode.hdr = hdr_requested;
refresh_remote_monitor_baseline(has_stream_session_activity());
const auto state = remote_display_topology::instance().activate_or_resume(std::string {uuid}, std::string {label}, mode, generation);
return {.accepted = state.accepted, .ready = state.ready, .retryable = state.retryable, .output = state.output, .error = state.error};
},
.snapshot = [](std::string_view uuid, std::uint64_t generation) {
const auto state = remote_display_topology::instance().snapshot(std::string {uuid}, generation);
return remote_session::monitor_runtime_state_t {.accepted = state.accepted, .ready = state.ready, .retryable = state.retryable, .output = state.output, .error = state.error};
},
.explicit_release = [](std::string_view uuid, std::uint64_t generation, std::string_view reason) {
remote_display_topology::instance().explicit_release(std::string {uuid}, generation, std::string {reason});
},
.transport_lost = [](std::string_view uuid, std::uint64_t generation) {
remote_display_topology::instance().transport_lost(std::string {uuid}, generation);
},
.unpair = [](std::string_view uuid) { remote_display_topology::instance().unpair_client(std::string {uuid}); },
.shutdown = [] { remote_display_topology::instance().shutdown(); },
});
}
} // namespace
#elif defined(__linux__)
namespace {
int linux_remote_refresh_hz(const display_device::FloatingPoint &refresh) {
if (const auto *ratio = std::get_if<display_device::Rational>(&refresh)) {
return ratio->m_denominator == 0 ? 0 : static_cast<int>(std::lround(static_cast<double>(ratio->m_numerator) / ratio->m_denominator));
}
if (const auto *value = std::get_if<double>(&refresh)) {
return static_cast<int>(std::lround(*value));
}
return 0;
}
int linux_remote_logical_dimension(const unsigned int pixels, const display_device::FloatingPoint &scale) {
double factor = 1.0;
if (const auto *ratio = std::get_if<display_device::Rational>(&scale)) {
if (ratio->m_denominator != 0) {
factor = static_cast<double>(ratio->m_numerator) / ratio->m_denominator;
}
} else if (const auto *value = std::get_if<double>(&scale)) {
factor = *value;
}
return static_cast<int>(std::lround(pixels / std::max(factor, 0.01)));
}
void refresh_remote_monitor_baseline(const bool extend_active_stream) {
const auto devices = display_helper_integration::enumerate_devices(
display_device::DeviceEnumerationDetail::Full
);
if (!devices) {
return;
}
const auto active_stream_output = extend_active_stream ? config::get_active_output_name() : std::string {};
const bool active_stream_uses_private =
!active_stream_output.empty() && platf::linux_private_display::is_private_output(active_stream_output);
const auto managed_client_ids = remote_display_topology::instance().managed_client_identity_ids();
std::vector<std::string> managed_outputs;
managed_outputs.reserve(managed_client_ids.size());
for (const auto &client_id : managed_client_ids) {
if (const auto output = platf::linux_private_display::output_for_client(client_id)) {
managed_outputs.push_back(*output);
}
}
const auto active_game = proc::proc.active_session_guard();
std::vector<remote_display_topology::node_t> baseline;
for (const auto &device : *devices) {
if (device.m_device_id.empty() || device.m_display_name.empty() || !device.m_info) {
continue;
}
const bool is_private = platf::linux_private_display::is_private_output(device.m_device_id);
if (is_private) {
// Coordinator-managed normal/Remote Monitor identities are emitted by
// compose_locked(). Only an older or shared private stream that is not
// in the coordinator remains a pre-existing baseline anchor.
if (!active_stream_uses_private || (device.m_device_id != active_stream_output && device.m_display_name != active_stream_output) || std::find(managed_outputs.begin(), managed_outputs.end(), device.m_device_id) != managed_outputs.end()) {
continue;
}
} else if (active_stream_uses_private) {
// An existing private stream defines the desktop being extended; do
// not expose physical outputs that its exclusive policy hid.
continue;
}
remote_display_topology::node_t node;
const bool owner_already_managed =
std::find(managed_client_ids.begin(), managed_client_ids.end(), active_game.client_uuid) != managed_client_ids.end();
node.id = is_private && !active_game.client_uuid.empty() ?
active_game.client_uuid + (owner_already_managed ? ":streamed" : "") :
device.m_device_id;
node.device_id = device.m_device_id;
node.label = device.m_friendly_name.empty() ? device.m_display_name : device.m_friendly_name;
node.preexisting = true;
node.physical = !is_private;
node.active = true;
node.primary = device.m_info->m_primary;
node.x = device.m_info->m_origin_point.m_x;
node.y = device.m_info->m_origin_point.m_y;
node.configured_mode = {
.width = static_cast<int>(device.m_info->m_resolution.m_width),
.height = static_cast<int>(device.m_info->m_resolution.m_height),
.refresh_hz = linux_remote_refresh_hz(device.m_info->m_refresh_rate),
.hdr = device.m_info->m_hdr_state.value_or(display_device::HdrState::Disabled) == display_device::HdrState::Enabled,
};
node.layout_width = linux_remote_logical_dimension(
device.m_info->m_resolution.m_width,
device.m_info->m_resolution_scale
);
node.layout_height = linux_remote_logical_dimension(
device.m_info->m_resolution.m_height,
device.m_info->m_resolution_scale
);
baseline.push_back(std::move(node));
}
remote_display_topology::instance().set_physical_baseline(std::move(baseline));
}
enum class linux_normal_identity_result_e {
not_needed,
ready,
capacity_rejected,
topology_failed,
};
linux_normal_identity_result_e reserve_linux_normal_display_identity(
const std::shared_ptr<rtsp_stream::launch_session_t> &launch_session
) {
const auto &owner_uuid = launch_session->normal_vdd_owner_uuid.empty() ?
launch_session->client_uuid : launch_session->normal_vdd_owner_uuid;
const auto mode = launch_session->virtual_display_mode_override.value_or(config::video.virtual_display_mode);
if (!launch_session->virtual_display || mode == config::video_t::virtual_display_mode_e::shared || launch_session->role != remote_session::role_e::game) {
return linux_normal_identity_result_e::not_needed;
}
if (launch_session->normal_vdd_identity_token != 0) {
return linux_normal_identity_result_e::ready;
}
const auto reservation = remote_display_topology::instance().reserve_normal_game_identity(
owner_uuid,
launch_session->client_name,
{
.width = launch_session->width,
.height = launch_session->height,
.refresh_hz = refresh_hz_from_session_fps(launch_session->fps),
.hdr = rtsp_stream::hdr_capture_required(*launch_session, config::video),
}
);
if (!reservation.accepted) {
launch_session->normal_vdd_capacity_rejected = true;
launch_session->virtual_display_failed = true;
return linux_normal_identity_result_e::capacity_rejected;
}
launch_session->normal_vdd_identity_token = reservation.token;
launch_session->normal_vdd_identity_newly_reserved = reservation.newly_reserved;
const bool reapply_topology = platf::linux_private_display::resume_policy::requires_topology_reapply(
reservation.newly_reserved,
launch_session->virtual_display_needs_resume_apply
);
if (reapply_topology) {
const bool stream_active = has_stream_session_activity();
refresh_remote_monitor_baseline(stream_active);
const auto managed_ids = remote_display_topology::instance().managed_client_identity_ids();
const auto monitor_ids = remote_display_topology::instance().protected_remote_monitor_client_ids();
if (platf::linux_private_display::resume_policy::can_use_session_apply_only(
stream_active,
managed_ids.size() == 1 && managed_ids.front() == owner_uuid,
!monitor_ids.empty()
)) {
// Launch/resume applies and verifies the parsed session configuration
// before admitting the stream. Composing its raw client mode first
// doubles KScreen/HDR setup and may immediately undo mode overrides.
launch_session->virtual_display_needs_resume_apply = true;
BOOST_LOG(debug) << "Linux private display: deferring the sole game output to the verified session apply.";
return linux_normal_identity_result_e::ready;
}
} else {
BOOST_LOG(debug) << "Linux private display: reusing the retained game topology without a resume modeset.";
}
if (reapply_topology && !remote_display_topology::instance().reapply_composed_topology()) {
if (reservation.newly_reserved) {
const bool can_retire = remote_display_topology::instance().rollback_normal_game_identity(
owner_uuid,
reservation.token
);
const auto protected_clients = remote_display_topology::instance().protected_remote_monitor_client_ids();
const bool topology_restored = can_retire && remote_display_topology::instance().reapply_composed_topology();
if (topology_restored && std::find(protected_clients.begin(), protected_clients.end(), owner_uuid) == protected_clients.end()) {
(void) platf::linux_private_display::remote_remove_owned_display(owner_uuid);
}
}
launch_session->normal_vdd_identity_token = 0;
launch_session->normal_vdd_identity_newly_reserved = false;
launch_session->virtual_display_failed = true;
return linux_normal_identity_result_e::topology_failed;
}
if (!platf::linux_private_display::publish_current_session_state(*launch_session)) {
BOOST_LOG(error) << "Linux private display: composed output did not publish verified session state.";
if (reservation.newly_reserved) {
const bool can_retire = remote_display_topology::instance().rollback_normal_game_identity(
owner_uuid,
reservation.token
);
if (can_retire && remote_display_topology::instance().reapply_composed_topology()) {
(void) platf::linux_private_display::remote_remove_owned_display(owner_uuid);
}
}
launch_session->normal_vdd_identity_token = 0;
launch_session->normal_vdd_identity_newly_reserved = false;
launch_session->virtual_display_failed = true;
return linux_normal_identity_result_e::topology_failed;
}
return linux_normal_identity_result_e::ready;
}
void rollback_linux_normal_display_identity(
const std::shared_ptr<rtsp_stream::launch_session_t> &launch_session
) {
const auto &owner_uuid = launch_session->normal_vdd_owner_uuid.empty() ?
launch_session->client_uuid : launch_session->normal_vdd_owner_uuid;
if (!launch_session->normal_vdd_identity_newly_reserved) {
return;
}
const bool can_retire = remote_display_topology::instance().rollback_normal_game_identity(
owner_uuid,
launch_session->normal_vdd_identity_token
);
const auto protected_clients = remote_display_topology::instance().protected_remote_monitor_client_ids();
const bool topology_restored = can_retire && remote_display_topology::instance().reapply_composed_topology();
if (topology_restored && std::find(protected_clients.begin(), protected_clients.end(), owner_uuid) == protected_clients.end()) {
(void) platf::linux_private_display::remote_remove_owned_display(owner_uuid);
}
}
void register_remote_monitor_runtime() {
remote_display_topology::instance().set_runtime_callbacks({
.create_or_reclaim = [](const std::string &client_uuid, const std::string &, const remote_display_topology::mode_t &mode) {
return platf::linux_private_display::remote_create_or_reclaim(client_uuid, mode);
},
.resolve_mode = platf::linux_private_display::remote_resolve_mode,
.apply_composed_topology = platf::linux_private_display::remote_apply_composed_topology,
.exact_target_has_current_mode_and_dxgi = platf::linux_private_display::remote_exact_capture_output,
.remove_owned_display = platf::linux_private_display::remote_remove_monitor_display,
.observe_shared_mode = platf::linux_private_display::remote_observe_shared_mode,
});
remote_display_topology::instance().set_plaintext_rtsp_warning_provider([](const std::string &) {
return rtsp_stream::plaintext_route_warning();
});
remote_session::register_monitor_runtime_hooks({
.activate_or_resume = [](std::string_view uuid, std::string_view label, std::string_view requested_mode, const bool hdr_requested, std::uint64_t generation) -> remote_session::monitor_runtime_state_t {
remote_display_topology::mode_t mode;
if (std::sscanf(std::string {requested_mode}.c_str(), "%dx%d@%d", &mode.width, &mode.height, &mode.refresh_hz) != 3 || mode.width <= 0 || mode.height <= 0 || mode.refresh_hz <= 0) {
return remote_session::monitor_runtime_state_t {.retryable = true, .error = "Remote Monitor requested an invalid display mode."};
}
mode.hdr = hdr_requested;
refresh_remote_monitor_baseline(has_stream_session_activity());
const auto state = remote_display_topology::instance().activate_or_resume(
std::string {uuid},
std::string {label},
mode,
generation
);
return {.accepted = state.accepted, .ready = state.ready, .retryable = state.retryable, .output = state.output, .error = state.error, .hdr_enabled = state.hdr_enabled};
},
.snapshot = [](std::string_view uuid, std::uint64_t generation) {
const auto state = remote_display_topology::instance().snapshot(std::string {uuid}, generation);
return remote_session::monitor_runtime_state_t {.accepted = state.accepted, .ready = state.ready, .retryable = state.retryable, .output = state.output, .error = state.error, .hdr_enabled = state.hdr_enabled};
},
.explicit_release = [](std::string_view uuid, std::uint64_t generation, std::string_view reason) {
remote_display_topology::instance().explicit_release(std::string {uuid}, generation, std::string {reason});
},
.transport_lost = [](std::string_view uuid, std::uint64_t generation) {
remote_display_topology::instance().transport_lost(std::string {uuid}, generation);
},
.unpair = [](std::string_view uuid) {
remote_display_topology::instance().unpair_client(std::string {uuid});
},
.shutdown = [] {
remote_display_topology::instance().shutdown(
platf::linux_private_display::process_shutdown_preserve_requested()
);
},
});
}
} // namespace
#endif
std::string cert_subject_name_for_log(const crypto::x509_t &cert) {
auto subject_name = crypto::subject_name(cert.get());
if (subject_name.empty()) {
return "unknown"s;
}
return subject_name;
}
class SunshineHTTPSServer: public SimpleWeb::ServerBase<SunshineHTTPS> {
public:
SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file):
ServerBase<SunshineHTTPS>::ServerBase(443),
context(boost::asio::ssl::context::tls_server) {
// Disabling TLS 1.0 and 1.1 (see RFC 8996)
context.set_options(boost::asio::ssl::context::no_tlsv1);
context.set_options(boost::asio::ssl::context::no_tlsv1_1);
context.use_certificate_chain_file(certification_file);
context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem);
}
std::function<bool(std::shared_ptr<Request>, SSL *)> verify;
std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed;
protected:
boost::asio::ssl::context context;
void after_bind() override {
if (verify) {
context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once);
context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {
// To respond with an error message, a connection must be established
return 1;
});
}
}
// This is Server<HTTPS>::accept() with SSL validation support added
void accept() override {
auto connection = create_connection(*io_service, context);
acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) {
auto lock = connection->handler_runner->continue_lock();
if (!lock) {
return;
}
if (ec != SimpleWeb::error::operation_aborted) {
this->accept();
}
auto session = std::make_shared<Session>(config.max_request_streambuf_size, connection);
if (!ec) {
boost::asio::ip::tcp::no_delay option(true);
SimpleWeb::error_code ec;
session->connection->socket->lowest_layer().set_option(option, ec);
session->connection->set_timeout(config.timeout_request);
session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) {
session->connection->cancel_timeout();
auto lock = session->connection->handler_runner->continue_lock();
if (!lock) {
return;
}
if (!ec) {
SimpleWeb::error_code remote_endpoint_ec;
session->connection->socket->lowest_layer().remote_endpoint(remote_endpoint_ec);
if (remote_endpoint_ec) {
if (this->on_error) {
this->on_error(session->request, remote_endpoint_ec);
}
return;
}
if (verify && !verify(session->request, session->connection->socket->native_handle())) {
this->write(session, on_verify_failed);
} else {
this->read(session);
}
} else if (this->on_error) {
this->on_error(session->request, ec);
}
});
} else if (this->on_error) {
this->on_error(session->request, ec);
}
});
}
};
using https_server_t = SunshineHTTPSServer;
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
struct conf_intern_t {
std::string servercert;
std::string pkey;
} conf_intern;
struct http_encoder_capabilities_t {
// Keep verification paired with the advertised values. On Windows, the
// helper may use a temporary adapter identity that is cleared on return.
video::advertised_encoder_capabilities_t advertised;
bool probe_complete;
};
#ifdef _WIN32
namespace {
bool display_helper_session_available() {
if (platf::is_running_as_system()) {
return true;
}
HANDLE user_token = platf::retrieve_users_token(false);
const bool available = (user_token != nullptr);
if (user_token) {
CloseHandle(user_token);
}
return available;
}
bool has_active_virtual_display() {
const auto virtual_displays = VDISPLAY::enumerateVirtualDisplays();
return std::any_of(
virtual_displays.begin(),
virtual_displays.end(),
[](const VDISPLAY::VirtualDisplayInfo &info) {
return info.is_active;
}
);
}
bool has_any_active_display() {
if (VDISPLAY::has_active_physical_display()) {
return true;
}
if (VDISPLAY::has_retained_ensure_display()) {
return true;
}
return has_active_virtual_display();
}
bool has_active_or_stopping_stream_session();
http_encoder_capabilities_t advertised_encoder_capabilities_for_http() {
std::optional<video::encoder_probe_adapter_hint_lease_t> idle_virtual_adapter_hint;
std::optional<LUID> idle_virtual_required_adapter;
if (config::video.virtual_display_mode != config::video_t::virtual_display_mode_e::disabled &&
!has_stream_session_activity()) {
const auto intended_adapter = platf::resolve_preferred_render_adapter(
config::video.adapter_name,
config::video.adapter_pnp_id
);
if (intended_adapter) {
idle_virtual_required_adapter = *intended_adapter.luid;
idle_virtual_adapter_hint =
video::set_pending_virtual_display_adapter_hint(*intended_adapter.luid);
}
}
auto idle_virtual_adapter_hint_guard = util::fail_guard([&] {
if (idle_virtual_adapter_hint) {
(void) video::clear_pending_virtual_display_adapter_hint(*idle_virtual_adapter_hint);
}
});
const auto publish = [](
video::advertised_encoder_capabilities_t caps,
const bool probe_complete,
const std::string_view reason
) {
BOOST_LOG(debug)
<< "HTTP encoder capabilities: probe_complete=" << probe_complete
<< ", hdr="
<< (caps.hevc_mode == 3 || caps.av1_mode == 3)
<< ", hevc_mode=" << caps.hevc_mode
<< ", av1_mode=" << caps.av1_mode
<< ", pyrowave_mode=" << caps.pyrowave_mode
<< ", source=" << reason << '.';
return http_encoder_capabilities_t {
.advertised = std::move(caps),
.probe_complete = probe_complete,
};
};
bool probe_complete = false;
auto caps = video::advertised_encoder_capabilities(false, &probe_complete);
if (probe_complete) {
return publish(std::move(caps), true, "matching-cache");
}
// Session starts publish their pending owner while holding this gate.
// Hold it through the idle decision and temporary-display probe so a
// new start cannot enter between the counter samples below.
std::unique_lock<std::mutex> lifecycle_lock(
stream_lifecycle_mutex(),
std::try_to_lock
);
if (!lifecycle_lock.owns_lock()) {
BOOST_LOG(debug) << "Skipping HTTP encoder capability probe while stream lifecycle work owns the gate.";
return publish(std::move(caps), false, "lifecycle-gate");
}
caps = video::advertised_encoder_capabilities(false, &probe_complete);
if (probe_complete) {
return publish(std::move(caps), true, "matching-cache-after-gate");
}
if (has_active_or_stopping_stream_session()) {
BOOST_LOG(debug) << "Skipping HTTP encoder capability probe while a streaming session is active or stopping.";
return publish(std::move(caps), false, "active-or-stopping-session");
}
auto ensure_result = VDISPLAY::ensure_display(idle_virtual_required_adapter);
auto cleanup_probe_display = util::fail_guard([&ensure_result]() {
VDISPLAY::cleanup_ensure_display(ensure_result);
});
if (idle_virtual_required_adapter && !ensure_result.owns_temporary_probe_request()) {
BOOST_LOG(warning)
<< "HTTP capability discovery could not acquire a temporary-display lease; continuing with exact-adapter synthetic validation.";
} else if (ensure_result.owns_temporary_probe_request() && !ensure_result.ready_for_capture()) {
BOOST_LOG(info)
<< "HTTP capability discovery is probing synthetic surfaces before the temporary display is published.";
}
caps = video::advertised_encoder_capabilities(true, &probe_complete);
return publish(std::move(caps), probe_complete, "idle-probe");
}
void cleanup_virtual_display_state(const bool force_display_restore = false) {
stream::session::cleanup_reservation_t cleanup_reservation;
const bool has_active_display = has_active_virtual_display();
const bool has_retained_probe_display = VDISPLAY::has_retained_ensure_display();
if (!has_active_display && !has_retained_probe_display) {
if (force_display_restore) {
(void) display_helper_integration::revert();
return;
}
BOOST_LOG(debug) << "Skipping virtual display cleanup after cancel because no active virtual display exists.";
return;
}
if (!has_active_display) {
BOOST_LOG(info) << "Removing retained encoder-probe virtual display after cancel.";
VDISPLAY::cleanup_retained_ensure_display();
return;
}
const auto cleanup = platf::virtual_display_cleanup::run(
"cancel",
force_display_restore || config::video.dd.config_revert_on_disconnect
);
if (cleanup.helper_revert_dispatched) {
display_helper_integration::stop_watchdog();
}
}
bool has_active_or_stopping_stream_session() {
// Sample the generic/VDD cleanup signals on both sides of the protocol
// activity snapshot. This closes both counter handoffs:
// launch: generic reservation -> pending protocol owner
// teardown: protocol owner -> generic cleanup reservation
// Each writer publishes the successor before releasing the predecessor.
if (platf::virtual_display_cleanup::in_progress() ||
stream::session::cleanup_reservations.load(std::memory_order_acquire) != 0) {
return true;
}
if (has_stream_session_activity()) {
return true;
}
return platf::virtual_display_cleanup::in_progress() ||
stream::session::cleanup_reservations.load(std::memory_order_acquire) != 0;
}
// Same idleness test as has_active_or_stopping_stream_session() minus the
// generic cleanup-reservation term. Teardown-path callers hold a reservation
// across their whole request, so that term reports their own frame as
// activity and makes the predicate unconditionally true; launch()/resume()
// teardown guards are reservation-insensitive for the same reason. A
// concurrent virtual-display cleanup is still honoured, double-sampled
// around the activity snapshot to close the same handoff.
bool has_stream_session_activity_or_display_cleanup() {
if (platf::virtual_display_cleanup::in_progress()) {
return true;
}
if (has_stream_session_activity()) {
return true;
}
return platf::virtual_display_cleanup::in_progress();
}
void cleanup_virtual_display_if_idle_locked() {
try {
if (has_stream_session_activity_or_display_cleanup()) {
BOOST_LOG(info) << "Skipping virtual display cleanup because a streaming session is active or stopping.";
return;
}
if (!remote_display_topology::instance().generic_virtual_display_cleanup_allowed()) {
BOOST_LOG(info) << "Deferring virtual display cleanup until the remaining managed client display sessions release ownership.";
return;
}
// The shared finalizer stays armed while managed display owners exist.
// Re-run it after each owner release so the final release consumes a
// queued app REVERT exactly once. If no shared finalization is armed,
// fall back to the bounded cancel cleanup path.
if (stream::session::finalize_shared_runtime_if_idle("managed_display_owner_release")) {
return;
}
cleanup_virtual_display_state(proc::consume_deferred_display_revert());
} catch (const std::exception &e) {
BOOST_LOG(warning) << "Virtual display cleanup failed: " << e.what();
} catch (...) {
BOOST_LOG(warning) << "Virtual display cleanup failed with an unknown exception.";
}
}
void cleanup_virtual_display_if_idle() {
// Serialize the final owner check through cleanup and exclude a
// concurrent RTSP or WebRTC start. Callers already holding the lifecycle
// gate must use cleanup_virtual_display_if_idle_locked().
std::unique_lock<std::mutex> lifecycle_lock(stream_lifecycle_mutex());
cleanup_virtual_display_if_idle_locked();
}
void prepare_virtual_display_for_session(
const std::shared_ptr<rtsp_stream::launch_session_t> &launch_session,
bool no_active_sessions,
bool allow_display_changes,
bool is_input_only,
std::optional<std::string> &pending_output_override,
std::optional<video::encoder_probe_adapter_hint_lease_t> &pending_adapter_hint,
const std::function<bool()> &display_startup_cancelled,
const std::chrono::steady_clock::time_point display_startup_deadline
) {
// This routine is the authoritative resolver for normal NVHTTP launch
// and resume requests. proc::execute must not later re-enable a virtual
// display after a per-app physical output override selected one.
launch_session->virtual_display_request_resolved = true;
auto disable_virtual_display_request = [&]() {
launch_session->virtual_display = false;
launch_session->virtual_display_failed = false;
launch_session->virtual_display_guid_bytes.fill(0);
launch_session->virtual_display_device_id.clear();
launch_session->virtual_display_ready_since.reset();
launch_session->virtual_display_hdr_enabled.reset();
};
std::optional<std::string> app_output_override;
auto app_display_override = proc::display_policy::app_override_e::inherit;
if (launch_session->output_name_override) {
app_output_override = boost::algorithm::trim_copy(*launch_session->output_name_override);
}
if (app_output_override && !app_output_override->empty() && VDISPLAY::is_virtual_display_selection(*app_output_override)) {
launch_session->virtual_display = true;
app_display_override = proc::display_policy::app_override_e::virtual_display;
app_output_override.reset();
} else if (app_output_override) {
app_display_override = proc::display_policy::app_override_e::physical;
}
launch_session->virtual_display_recreated_on_demand = false;
launch_session->virtual_display_needs_resume_apply = false;
bool config_requests_virtual = config::video.virtual_display_mode != config::video_t::virtual_display_mode_e::disabled;
if (launch_session->virtual_display_mode_override) {
config_requests_virtual =
*launch_session->virtual_display_mode_override != config::video_t::virtual_display_mode_e::disabled;
}
const bool forced_sudavda_virtual_display =
config::video.output_name == VDISPLAY::SUDOVDA_VIRTUAL_DISPLAY_SELECTION;
const bool client_requests_virtual = launch_session->client_virtual_display_override.value_or(
launch_session->client_requests_virtual_display
);
const bool session_requests_virtual = launch_session->app_metadata && launch_session->app_metadata->virtual_screen;
const bool launch_requests_physical = launch_session->client_virtual_display_override &&
!*launch_session->client_virtual_display_override;
bool request_virtual_display = proc::display_policy::resolve_virtual_display_request(
(config_requests_virtual && !launch_requests_physical) ||
launch_session->virtual_display || session_requests_virtual || forced_sudavda_virtual_display,
app_display_override
) || client_requests_virtual;
const auto requested_virtual_display_mode =
launch_session->virtual_display_mode_override.value_or(config::video.virtual_display_mode);
const bool shared_virtual_display_mode =
requested_virtual_display_mode == config::video_t::virtual_display_mode_e::shared;
auto shared_virtual_display_uuid = VDISPLAY::persistentVirtualDisplayUuid();
if (shared_virtual_display_mode && !http::shared_virtual_display_guid.empty()) {
try {
shared_virtual_display_uuid =
uuid_util::uuid_t::parse(http::shared_virtual_display_guid);
} catch (...) {
// Creation uses the same persistent fallback and repairs the stored value.
}
}
const std::string virtual_display_stable_id =
shared_virtual_display_mode ?
shared_virtual_display_uuid.string() :
(!launch_session->client_uuid.empty() ?
launch_session->client_uuid :
launch_session->unique_id);
const auto virtual_display_stable_uuid =
VDISPLAY::virtualDisplayUuidFromStableId(virtual_display_stable_id);
GUID virtual_display_stable_guid {};
std::memcpy(
&virtual_display_stable_guid,
virtual_display_stable_uuid.b8,
sizeof(virtual_display_stable_guid)
);
bool has_app_output_override = app_output_override.has_value();
auto make_framegen_policy = [&](bool uses_virtual_display) {
return framegen::make_stream_start_policy({
.fps = launch_session->fps,
.fps_scaled = launch_session->fps,
.display_refresh_millihz = launch_session->client_display_refresh_millihz,
.frame_generation_enabled = launch_session->frame_generation_enabled,
.gen1_framegen_fix = launch_session->gen1_framegen_fix,
.gen2_framegen_fix = launch_session->gen2_framegen_fix,
.lossless_scaling_framegen = launch_session->lossless_scaling_framegen,
.lossless_rtss_limit = launch_session->lossless_scaling_rtss_limit,
.frame_generation_provider = launch_session->frame_generation_provider,
.uses_virtual_display = uses_virtual_display,
.capture_mode = config::video.capture,
.auto_capture_uses_wgc = platf::dxgi::should_use_wgc_default(),
.auto_virtual_framegen_limiter = config::frame_limiter.virtual_display_limiter_enabled(),
.virtual_display_refresh_multiplier = config::frame_limiter.fixed_virtual_display_refresh_multiplier(),
.virtual_display_fixed_refresh_millihz = config::frame_limiter.fixed_virtual_display_refresh_millihz(launch_session->client_vrr_requested),
});
};
const auto requested_display_framegen_policy =
make_framegen_policy(has_app_output_override ? false : request_virtual_display);
const bool framegen_requires_virtual_display = requested_display_framegen_policy.requires_virtual_display;
if (framegen_requires_virtual_display) {
request_virtual_display = true;
app_output_override.reset();
has_app_output_override = false;
}
auto apply_framegen_refresh_policy = [&](bool uses_virtual_display) {
const auto framegen_policy = make_framegen_policy(uses_virtual_display);
launch_session->framegen_refresh_rate = framegen_policy.framegen_refresh_rate;
launch_session->framegen_refresh_millihz = framegen_policy.framegen_refresh_millihz;
launch_session->framegen_refresh_multiplier = framegen_policy.refresh_multiplier;
launch_session->framegen_fixed_refresh = framegen_policy.fixed_refresh;
};
auto reserve_normal_vdd_identity = [&]() {
if (shared_virtual_display_mode || launch_session->role != remote_session::role_e::game) return true;
if (launch_session->normal_vdd_identity_token != 0) return true;
const remote_display_topology::mode_t mode {
.width = launch_session->width,
.height = launch_session->height,
.refresh_hz = refresh_hz_from_session_fps(launch_session->fps),
.hdr = rtsp_stream::hdr_capture_required(*launch_session, config::video),
};
const auto reservation = remote_display_topology::instance().reserve_normal_game_identity(
launch_session->client_uuid,
launch_session->client_name,
mode
);
if (!reservation.accepted) {
launch_session->normal_vdd_capacity_rejected = true;
launch_session->virtual_display_failed = true;
BOOST_LOG(warning) << "Rejecting per-client virtual display for client '" << launch_session->client_uuid
<< "' because all four client identities are already reserved.";
return false;
}
launch_session->normal_vdd_identity_token = reservation.token;
launch_session->normal_vdd_identity_newly_reserved = reservation.newly_reserved;
return true;
};
BOOST_LOG(debug) << "Display helper: session prep client='" << launch_session->client_name
<< "' allow_display_changes=" << allow_display_changes
<< " no_active_sessions=" << no_active_sessions
<< " request_virtual_display=" << request_virtual_display
<< " framegen_requires_virtual_display=" << framegen_requires_virtual_display
<< " previous_virtual_device_id='" << launch_session->virtual_display_device_id
<< "' active_output='" << config::get_active_output_name()
<< "' app_output_override='" << (app_output_override ? *app_output_override : std::string {})
<< "'.";
if (!VDISPLAY::policy::should_prepare_display_for_new_session(no_active_sessions)) {
const auto previous_virtual_display_device_id = launch_session->virtual_display_device_id;
launch_session->virtual_display = false;
launch_session->virtual_display_failed = request_virtual_display;
launch_session->virtual_display_guid_bytes.fill(0);
launch_session->virtual_display_device_id.clear();
launch_session->virtual_display_ready_since.reset();
launch_session->virtual_display_hdr_enabled.reset();
launch_session->virtual_display_recreated_on_demand = false;
launch_session->virtual_display_needs_resume_apply = false;
if (request_virtual_display) {
if (!reserve_normal_vdd_identity()) return;
const auto existing_device =
VDISPLAY::resolveActiveVirtualDisplayDeviceIdForStableId(
virtual_display_stable_id,
previous_virtual_display_device_id,
launch_session->client_name,
VDISPLAY::policy::allow_generic_resume_fallback()
);
if (existing_device &&
VDISPLAY::configuredRenderAdapterMatchesVirtualDisplay(
virtual_display_stable_guid,
"active RTSP/shared virtual display reuse"
)) {
launch_session->virtual_display = true;
launch_session->virtual_display_failed = false;
launch_session->virtual_display_device_id = *existing_device;
launch_session->virtual_display_ready_since = std::chrono::steady_clock::now();
launch_session->virtual_display_hdr_enabled.reset();
apply_framegen_refresh_policy(true);
BOOST_LOG(info) << "Display helper: another session is active; joining its validated virtual capture target (device_id="
<< *existing_device << ").";
} else if (existing_device) {
BOOST_LOG(error) << "Existing virtual display does not match the configured capture adapter; refusing to claim shared-session virtual-display reuse.";
} else {
BOOST_LOG(warning) << "Another session is active, but no reusable virtual display was found for this request.";
}
} else {
apply_framegen_refresh_policy(false);
BOOST_LOG(info) << "Display helper: another session is active; joining its existing capture target without display changes.";
}
return;
}
bool pinned_virtual_display_mode_disabled = false;
if (has_app_output_override && !framegen_requires_virtual_display) {
request_virtual_display = false;
disable_virtual_display_request();
if (!launch_session->virtual_display_mode_override) {
launch_session->virtual_display_mode_override = config::video_t::virtual_display_mode_e::disabled;
pinned_virtual_display_mode_disabled = true;
}
}
if (!allow_display_changes) {
if (request_virtual_display) {
if (!reserve_normal_vdd_identity()) return;
if (auto existing_device =
VDISPLAY::resolveActiveVirtualDisplayDeviceIdForStableId(
virtual_display_stable_id,
launch_session->virtual_display_device_id,
launch_session->client_name,
VDISPLAY::policy::allow_generic_resume_fallback()
)) {
if (VDISPLAY::configuredRenderAdapterMatchesVirtualDisplay(
virtual_display_stable_guid,
"RTSP resume virtual display reuse"
)) {
launch_session->virtual_display = true;
launch_session->virtual_display_failed = false;
launch_session->virtual_display_device_id = *existing_device;
launch_session->virtual_display_ready_since = std::chrono::steady_clock::now();
launch_session->virtual_display_hdr_enabled.reset();
launch_session->virtual_display_needs_resume_apply = true;
config::set_runtime_output_name_override(*existing_device);
pending_output_override = *existing_device;
apply_framegen_refresh_policy(true);
BOOST_LOG(info) << "Display helper: preserving virtual display capture target for resume (device_id="
<< *existing_device << ").";
BOOST_LOG(debug) << "Display helper: preserving capture target and refreshing display state for resume.";
return;
}
launch_session->virtual_display = false;
launch_session->virtual_display_failed = true;
launch_session->virtual_display_device_id.clear();
launch_session->virtual_display_ready_since.reset();
launch_session->virtual_display_hdr_enabled.reset();
BOOST_LOG(warning) << "Display helper: existing resume virtual display is on a different or unknown adapter; recreating it on demand.";
}
BOOST_LOG(info) << "Display helper: resume requested virtual display capture but no reusable virtual display was found;"
<< " recreating one on demand.";
launch_session->virtual_display_recreated_on_demand = true;
} else {
if (app_output_override) {
config::set_runtime_output_name_override(*app_output_override);
pending_output_override = *app_output_override;
apply_framegen_refresh_policy(false);
BOOST_LOG(info) << "Display helper: preserving output override for resume: "
<< (app_output_override->empty() ? "primary display" : *app_output_override);
} else {
BOOST_LOG(debug) << "Display helper: skipping virtual display changes for resume.";
}
return;
}
}
// Snapshot current display state BEFORE any display enumeration.
// queryDisplayConfig(QueryType::All) in output_exists() and other calls can activate
// external dummy plugs, which would pollute the snapshot used for session restore.
if (no_active_sessions) {
if (!display_helper_integration::snapshot_current_display_state(
display_startup_cancelled,
display_startup_deadline)) {
BOOST_LOG(warning) << "Display helper snapshot before session start was not accepted.";
}
}
if (app_output_override) {
config::set_runtime_output_name_override(*app_output_override);
pending_output_override = *app_output_override;
BOOST_LOG(info) << "App-specific display override requested: output_name="
<< (app_output_override->empty() ? "primary display" : *app_output_override);
}
BOOST_LOG(debug) << "config_requests_virtual: " << config_requests_virtual;
BOOST_LOG(debug) << "client_requests_virtual: " << client_requests_virtual;
BOOST_LOG(debug) << "session_requests_virtual: " << session_requests_virtual;
BOOST_LOG(debug) << "framegen_requires_virtual_display: " << framegen_requires_virtual_display;
BOOST_LOG(debug) << "request_virtual_display: " << request_virtual_display;
const auto requested_output_name = app_output_override ? *app_output_override : config::get_active_output_name();
if (has_app_output_override) {
request_virtual_display = false;
disable_virtual_display_request();
}
if (!request_virtual_display && !requested_output_name.empty()) {
if (!display_device::output_exists(requested_output_name)) {
BOOST_LOG(warning) << "Requested display '" << requested_output_name
<< "' not found; initializing virtual display instead.";
if (!has_app_output_override) {
request_virtual_display = true;
}
}
}
if (is_input_only) {
disable_virtual_display_request();
} else {
auto apply_virtual_display_request = [&](bool should_request_virtual_display) {
if (!should_request_virtual_display) {
disable_virtual_display_request();
return;
}
if (!reserve_normal_vdd_identity()) return;
const auto intended_adapter = platf::resolve_preferred_render_adapter(
config::video.adapter_name,
config::video.adapter_pnp_id
);
if (intended_adapter) {
pending_adapter_hint =
video::set_pending_virtual_display_adapter_hint(*intended_adapter.luid);
} else {
BOOST_LOG(warning)
<< "Cannot publish the pending virtual-display adapter identity before creation (status="
<< platf::adapter_resolution_status_name(intended_adapter.status) << ").";
}
if (proc::vDisplayDriverStatus.load(std::memory_order_acquire) != VDISPLAY::DRIVER_STATUS::OK) {
proc::initVDisplayDriver();
const auto driver_status = proc::vDisplayDriverStatus.load(std::memory_order_acquire);
if (driver_status != VDISPLAY::DRIVER_STATUS::OK) {
BOOST_LOG(warning) << "SudaVDA driver unavailable (status=" << static_cast<int>(driver_status) << "). Continuing with best-effort virtual display creation.";
}
}
auto parse_uuid = [](const std::string &value) -> std::optional<uuid_util::uuid_t> {
if (value.empty()) {
return std::nullopt;
}
try {
return uuid_util::uuid_t::parse(value);
} catch (...) {
return std::nullopt;
}
};
auto ensure_shared_guid = [&]() -> uuid_util::uuid_t {
if (!http::shared_virtual_display_guid.empty()) {
if (auto parsed = parse_uuid(http::shared_virtual_display_guid)) {
return *parsed;
}
}
auto generated = VDISPLAY::persistentVirtualDisplayUuid();
http::shared_virtual_display_guid = generated.string();
nvhttp::save_state();
return generated;
};
const bool shared_mode = shared_virtual_display_mode;
uuid_util::uuid_t session_uuid;
if (shared_mode) {
session_uuid = ensure_shared_guid();
launch_session->unique_id = session_uuid.string();
} else if (auto parsed = parse_uuid(launch_session->unique_id)) {
session_uuid = *parsed;
} else {
session_uuid = VDISPLAY::persistentVirtualDisplayUuid();
launch_session->unique_id = session_uuid.string();
}
std::string display_uuid_source;
if (!shared_mode && !launch_session->client_uuid.empty()) {
display_uuid_source = launch_session->client_uuid;
BOOST_LOG(debug) << "Using client UUID for virtual display: " << display_uuid_source;
} else {
display_uuid_source = session_uuid.string();
BOOST_LOG(debug) << "Using session UUID for virtual display: " << display_uuid_source;
}
GUID virtual_display_guid {};
if (!shared_mode && !launch_session->client_uuid.empty()) {
const auto client_virtual_display_uuid = VDISPLAY::virtualDisplayUuidFromStableId(launch_session->client_uuid);
std::memcpy(&virtual_display_guid, client_virtual_display_uuid.b8, sizeof(virtual_display_guid));
std::copy_n(
std::cbegin(client_virtual_display_uuid.b8),
sizeof(client_virtual_display_uuid.b8),
launch_session->virtual_display_guid_bytes.begin()
);
} else {
std::memcpy(&virtual_display_guid, session_uuid.b8, sizeof(virtual_display_guid));
std::copy_n(std::cbegin(session_uuid.b8), sizeof(session_uuid.b8), launch_session->virtual_display_guid_bytes.begin());
}
uint32_t vd_width = launch_session->resolution_override ?
static_cast<uint32_t>(launch_session->resolution_override->width) :
(launch_session->width > 0 ? static_cast<uint32_t>(launch_session->width) : 1920u);
uint32_t vd_height = launch_session->resolution_override ?
static_cast<uint32_t>(launch_session->resolution_override->height) :
(launch_session->height > 0 ? static_cast<uint32_t>(launch_session->height) : 1080u);
// Virtual-display creation may eagerly enable HDR. Default to no state change so
// "Do not change HDR" preserves the retained Windows setting.
std::optional<bool> virtual_display_hdr_requested;
display_helper_integration::helpers::SessionDisplayConfigurationHelper initial_display_helper(config::video, *launch_session, true);
if (auto initial_configuration = initial_display_helper.initial_virtual_display_configuration()) {
if (initial_configuration->m_resolution &&
initial_configuration->m_resolution->m_width > 0 &&
initial_configuration->m_resolution->m_height > 0) {
vd_width = initial_configuration->m_resolution->m_width;
vd_height = initial_configuration->m_resolution->m_height;
BOOST_LOG(info) << "Virtual display initial resolution resolved from display configuration: "
<< vd_width << 'x' << vd_height;
}
if (initial_configuration->m_hdr_state) {
virtual_display_hdr_requested =
*initial_configuration->m_hdr_state == display_device::HdrState::Enabled;
}
}
// A fixed VRR mode must be advertised by the driver at its exact rate.
const bool fixed_vd_refresh = launch_session->framegen_fixed_refresh &&
launch_session->framegen_refresh_millihz &&
*launch_session->framegen_refresh_millihz > 0;
const uint32_t base_vd_fps_millihz = fixed_vd_refresh ?
*launch_session->framegen_refresh_millihz :
launch_session->client_display_refresh_millihz > 0 ?
launch_session->client_display_refresh_millihz :
framegen::normalize_refresh_millihz(launch_session->fps);
uint32_t vd_fps = rtsp_stream::effective_display_refresh_millihz(*launch_session);
if (vd_fps == 0) {
vd_fps = 60000u;
}
const bool framegen_refresh_active =
(launch_session->framegen_refresh_millihz && *launch_session->framegen_refresh_millihz > 0) ||
(launch_session->framegen_refresh_rate && *launch_session->framegen_refresh_rate > 0);
const int refresh_multiplier =
framegen_refresh_active ? rtsp_stream::framegen_refresh_multiplier(*launch_session) : 1;
if (base_vd_fps_millihz > 0 && refresh_multiplier > 1) {
const uint64_t minimum = static_cast<uint64_t>(base_vd_fps_millihz) * static_cast<uint64_t>(refresh_multiplier);
vd_fps = std::max(vd_fps, static_cast<uint32_t>(std::min<uint64_t>(minimum, std::numeric_limits<uint32_t>::max())));
}
std::string client_label;
if (shared_mode) {
client_label = config::nvhttp.sunshine_name.empty() ? "Sunshine Shared Display" : config::nvhttp.sunshine_name + " Shared";
} else {
if (!launch_session->client_name.empty()) {
client_label = launch_session->client_name;
} else if (!launch_session->device_name.empty()) {
client_label = launch_session->device_name;
} else {
client_label = config::nvhttp.sunshine_name;
}
if (client_label.empty()) {
client_label = "Sunshine";
}
}
const auto desired_layout = launch_session->virtual_display_layout_override.value_or(config::video.virtual_display_layout);
const bool wants_extended_layout = desired_layout != config::video_t::virtual_display_layout_e::exclusive;
if (wants_extended_layout) {
// HTTP capability discovery can retain a temporary virtual display. The
// stream creation path removes that probe before it creates the session
// display, so never carry any existing virtual identity into the stream
// baseline. The request helper adds exactly the new session display.
auto topology_snapshot = display_helper_integration::capture_physical_topology();
if (topology_snapshot) {
launch_session->virtual_display_topology_snapshot = *topology_snapshot;
} else {
launch_session->virtual_display_topology_snapshot.reset();
}
} else {
launch_session->virtual_display_topology_snapshot.reset();
}
// Capture physical monitor refresh rates before VD creation so they can be
// restored after the virtual display is configured (VD creation at (0,0) can
// cause Windows to reset other monitors' refresh rates).
if (auto pre_vd_devices = display_helper_integration::enumerate_devices()) {
std::map<std::string, std::pair<unsigned int, unsigned int>> rates;
for (const auto &device : *pre_vd_devices) {
if (device.m_device_id.empty() || !device.m_info ||
VDISPLAY::is_virtual_display_output(device.m_device_id)) {
continue;
}
if (const auto *rat = std::get_if<display_device::Rational>(&device.m_info->m_refresh_rate)) {
rates[device.m_device_id] = {rat->m_numerator, rat->m_denominator};
} else if (const auto *dbl = std::get_if<double>(&device.m_info->m_refresh_rate)) {
auto num = static_cast<unsigned int>(std::round(*dbl * 1000));
rates[device.m_device_id] = {num, 1000u};
}
}
if (!rates.empty()) {
launch_session->pre_virtual_display_refresh_rates = std::move(rates);
}
}
VDISPLAY::setWatchdogFeedingEnabled(true);
const char *hdr_profile = launch_session->hdr_profile ? launch_session->hdr_profile->c_str() : nullptr;
auto display_info = VDISPLAY::createVirtualDisplay(
display_uuid_source.c_str(),
client_label.c_str(),
hdr_profile,
vd_width,
vd_height,
vd_fps,
virtual_display_guid,
base_vd_fps_millihz,
framegen_refresh_active,
refresh_multiplier,
virtual_display_hdr_requested,
false,
!shared_mode,
!remote_display_topology::instance().protected_remote_monitor_client_ids().empty()
);
if (display_info) {
launch_session->virtual_display = true;
launch_session->virtual_display_failed = false;
if (display_info->device_id && !display_info->device_id->empty()) {
launch_session->virtual_display_device_id = *display_info->device_id;
} else if (auto resolved_device = VDISPLAY::resolveActiveVirtualDisplayDeviceIdForStableId(
display_uuid_source,
launch_session->virtual_display_device_id,
client_label,
VDISPLAY::policy::allow_generic_resume_fallback()
)) {
launch_session->virtual_display_device_id = *resolved_device;
} else {
launch_session->virtual_display_device_id.clear();
}
if (!launch_session->virtual_display_device_id.empty()) {
config::set_runtime_output_name_override(launch_session->virtual_display_device_id);
pending_output_override = launch_session->virtual_display_device_id;
if (pending_adapter_hint) {
(void) video::mark_pending_virtual_display_adapter_hint_ready_for_verification(
*pending_adapter_hint
);
}
}
launch_session->virtual_display_ready_since = display_info->ready_since;
launch_session->virtual_display_hdr_enabled = display_info->hdr_enabled;
if (display_info->display_name && !display_info->display_name->empty()) {
BOOST_LOG(info) << "Virtual display created at " << platf::to_utf8(*display_info->display_name);
} else {
BOOST_LOG(info) << "Virtual display created (device name pending enumeration).";
}
VDISPLAY::VirtualDisplayRecoveryParams recovery_params;
recovery_params.guid = virtual_display_guid;
recovery_params.width = vd_width;
recovery_params.height = vd_height;
recovery_params.fps = vd_fps;
recovery_params.base_fps_millihz = base_vd_fps_millihz;
recovery_params.framegen_refresh_active = framegen_refresh_active;
recovery_params.framegen_refresh_multiplier = refresh_multiplier;
recovery_params.hdr_requested = virtual_display_hdr_requested;
recovery_params.client_uid = display_uuid_source;
recovery_params.client_name = client_label;
recovery_params.hdr_profile = launch_session->hdr_profile;
recovery_params.display_name = display_info->display_name;
recovery_params.monitor_device_path = display_info->monitor_device_path;
if (display_info->device_id && !display_info->device_id->empty()) {
recovery_params.device_id = *display_info->device_id;
} else if (!launch_session->virtual_display_device_id.empty()) {
recovery_params.device_id = launch_session->virtual_display_device_id;
}
recovery_params.max_attempts = 3;
GUID recovery_guid = virtual_display_guid;
recovery_params.should_abort = [recovery_guid]() {
return !VDISPLAY::is_virtual_display_guid_tracked(recovery_guid);
};
auto recovery_session = std::make_shared<rtsp_stream::launch_session_t>(
display_helper_integration::helpers::make_display_request_session_snapshot(*launch_session)
);
recovery_params.confirmed_active_at_schedule = display_info->confirmed_active;
recovery_params.on_recovery_success = [recovery_session](const VDISPLAY::VirtualDisplayCreationResult &result, std::stop_token stop_token) -> std::function<void()> {
const auto cancelled = [&] {
return stop_token.stop_requested();
};
std::optional<config::runtime_output_override_lease_t> recovery_output_override_lease;
auto clear_recovery_output_override = util::fail_guard([&] {
if (recovery_output_override_lease) {
(void) config::clear_runtime_output_name_override_if_lease(*recovery_output_override_lease);
}
});
const auto wait_or_cancel = [&](std::chrono::milliseconds delay) {
const auto deadline = std::chrono::steady_clock::now() + delay;
while (!cancelled()) {
const auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
return false;
}
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now);
std::this_thread::sleep_for(std::min(std::max(remaining, std::chrono::milliseconds(1)), std::chrono::milliseconds(50)));
}
return true;
};
if (cancelled()) {
return {};
}
if (result.device_id && !result.device_id->empty()) {
recovery_session->virtual_display_device_id = *result.device_id;
if (cancelled()) {
return {};
}
recovery_output_override_lease = config::set_runtime_output_name_override_with_lease(
recovery_session->virtual_display_device_id
);
}
if (cancelled()) {
return {};
}
recovery_session->virtual_display_ready_since = result.ready_since;
recovery_session->virtual_display_hdr_enabled = result.hdr_enabled;
if (recovery_session->virtual_display) {
// Re-apply display configuration synchronously on the recovery monitor thread.
// Running this inline (blocking) prevents the recovery monitor from polling during
// topology churn caused by APPLY, which would otherwise cause transient CCD
// enumeration failures that look like the display disappeared.
constexpr int kMaxApplyAttempts = 5;
bool applied = false;
for (int attempt = 1; attempt <= kMaxApplyAttempts; ++attempt) {
if (cancelled()) {
return {};
}
(void) display_helper_integration::disarm_pending_restore(cancelled);
if (cancelled()) {
return {};
}
auto request = display_helper_integration::helpers::build_request_from_session(config::video, *recovery_session);
if (!request) {
BOOST_LOG(warning) << "Virtual display recovery: failed to rebuild display helper request after recreation (attempt "
<< attempt << "/" << kMaxApplyAttempts << ").";
if (wait_or_cancel(std::chrono::milliseconds(250 + (attempt - 1) * 250))) {
return {};
}
continue;
}
if (cancelled()) {
return {};
}
// This recovery worker is torn down with the session, so it
// keeps the short shutdown-class helper IPC timeouts.
if (display_helper_integration::apply(
*request,
nullptr,
cancelled,
display_helper_integration::ApplyRetryPolicy::Full,
{},
true)) {
BOOST_LOG(info) << "Virtual display recovery: re-applied session display configuration (including exclusivity) after recreation.";
applied = true;
break;
}
if (cancelled()) {
return {};
}
BOOST_LOG(warning) << "Virtual display recovery: display helper apply failed after recreation (attempt "
<< attempt << "/" << kMaxApplyAttempts << ").";
if (wait_or_cancel(std::chrono::milliseconds(250 + (attempt - 1) * 250))) {
return {};
}
}
if (!cancelled() && mail::man) {
mail::man->event<int>(mail::switch_display)->raise(-1);
}
if (cancelled()) {
return {};
}
BOOST_LOG(info) << "Virtual display recovery: requested capture reinit to pick up recreated display"
<< (applied ? "." : " (apply did not succeed).");
}
std::function<void()> rollback_output_override;
if (recovery_output_override_lease) {
const auto lease = *recovery_output_override_lease;
rollback_output_override = [lease] {
(void) config::clear_runtime_output_name_override_if_lease(lease);
};
}
clear_recovery_output_override.disable();
return rollback_output_override;
};
VDISPLAY::schedule_virtual_display_recovery_monitor(recovery_params);
} else {
disable_virtual_display_request();
launch_session->virtual_display = false;
launch_session->virtual_display_failed = true;
launch_session->virtual_display_guid_bytes.fill(0);
launch_session->virtual_display_device_id.clear();
launch_session->virtual_display_ready_since.reset();
launch_session->virtual_display_hdr_enabled.reset();
launch_session->framegen_refresh_rate.reset();
launch_session->framegen_refresh_millihz.reset();
launch_session->framegen_refresh_multiplier = 1;
launch_session->framegen_fixed_refresh = false;
BOOST_LOG(warning) << "Virtual display creation failed.";
}
};
// A per-app physical output override must never suppress this fallback:
// should_auto_enable_virtual_display() only returns true when no capturable
// physical display is active at all, so the pinned output cannot exist and the
// session would otherwise land on the generic temporary probe display.
if (!request_virtual_display && VDISPLAY::should_auto_enable_virtual_display()) {
BOOST_LOG(info) << "No physical monitors detected. Automatically enabling virtual display.";
request_virtual_display = true;
if (has_app_output_override) {
BOOST_LOG(info) << "Dropping unsatisfiable per-app display override '"
<< (app_output_override && !app_output_override->empty() ? *app_output_override : std::string {"primary display"})
<< "' because no physical display is available.";
// Release the pin so capture targets the virtual display created below
// instead of an output that does not exist.
config::set_runtime_output_name_override(std::nullopt);
pending_output_override.reset();
app_output_override.reset();
has_app_output_override = false;
launch_session->output_name_override.reset();
if (pinned_virtual_display_mode_disabled) {
// Restore the mode this session would have had without the override so
// shared/per-client identity stays consistent with virtual_display_stable_id.
launch_session->virtual_display_mode_override.reset();
pinned_virtual_display_mode_disabled = false;
}
}
}
if (allow_display_changes) {
apply_framegen_refresh_policy(request_virtual_display);
if (request_virtual_display) {
// A new virtual-display session supersedes the prior session's restore.
// Disarm it before any driver mutation; checking first used to return
// early and made the DISARM below unreachable in the exact race it was
// intended to prevent.
const bool virtual_display_mutation_allowed =
display_helper_integration::request_policy::supersede_restore_for_virtual_display(
[&] {
(void) display_helper_integration::disarm_pending_restore(
display_startup_cancelled,
display_startup_deadline
);
},
[&] {
return display_helper_integration::restore_in_progress(
display_startup_cancelled,
display_startup_deadline
);
}
);
if (!virtual_display_mutation_allowed) {
BOOST_LOG(warning) << "Display helper: virtual display creation deferred because physical display restoration is still in progress; using physical fallback for this session.";
launch_session->virtual_display = false;
launch_session->virtual_display_failed = true;
launch_session->virtual_display_guid_bytes.fill(0);
launch_session->virtual_display_device_id.clear();
launch_session->virtual_display_ready_since.reset();
launch_session->virtual_display_hdr_enabled.reset();
apply_framegen_refresh_policy(false);
return;
}
}
apply_virtual_display_request(request_virtual_display);
if (launch_session->virtual_display && !launch_session->virtual_display_device_id.empty()) {
config::set_runtime_output_name_override(launch_session->virtual_display_device_id);
pending_output_override = launch_session->virtual_display_device_id;
}
} else {
BOOST_LOG(debug) << "Display helper: skipping virtual display changes for resume.";
}
}
}
} // namespace
#endif
#ifdef __linux__
namespace {
void cleanup_virtual_display_if_idle_locked() {
try {
if (has_stream_session_activity()) {
BOOST_LOG(info) << "Skipping Linux private-display cleanup because a streaming session is active or stopping.";
return;
}
if (!remote_display_topology::instance().generic_virtual_display_cleanup_allowed()) {
BOOST_LOG(info) << "Deferring Linux private-display cleanup until the remaining managed client identities release ownership.";
return;
}
if (stream::session::finalize_shared_runtime_if_idle("managed_display_owner_release")) {
return;
}
(void) platf::linux_display::backend().revert();
} catch (const std::exception &error) {
BOOST_LOG(warning) << "Linux private-display cleanup failed: " << error.what();
} catch (...) {
BOOST_LOG(warning) << "Linux private-display cleanup failed with an unknown exception.";
}
}
void cleanup_virtual_display_if_idle() {
std::unique_lock<std::mutex> lifecycle_lock(stream_lifecycle_mutex());
cleanup_virtual_display_if_idle_locked();
}
} // namespace
#endif
#ifndef _WIN32
namespace {
http_encoder_capabilities_t advertised_encoder_capabilities_for_http() {
const auto publish = [](video::advertised_encoder_capabilities_t caps, const std::string_view reason) {
const bool probe_complete = video::has_successful_encoder_probe();
BOOST_LOG(debug)
<< "HTTP encoder capabilities: probe_complete=" << probe_complete
<< ", hdr="
<< (caps.hevc_mode == 3 || caps.av1_mode == 3)
<< ", hevc_mode=" << caps.hevc_mode
<< ", av1_mode=" << caps.av1_mode
<< ", pyrowave_mode=" << caps.pyrowave_mode
<< ", source=" << reason << '.';
return http_encoder_capabilities_t {
.advertised = std::move(caps),
.probe_complete = probe_complete,
};
};
if (video::has_successful_encoder_probe()) {
return publish(video::advertised_encoder_capabilities(false), "matching-cache");
}
std::unique_lock<std::mutex> lifecycle_lock(stream_lifecycle_mutex(), std::try_to_lock);
if (!lifecycle_lock.owns_lock()) {
BOOST_LOG(debug) << "Skipping HTTP encoder capability probe while stream lifecycle work owns the gate.";
return publish(video::advertised_encoder_capabilities(false), "lifecycle-gate");
}
if (video::has_successful_encoder_probe()) {
return publish(video::advertised_encoder_capabilities(false), "matching-cache-after-gate");
}
if (has_stream_session_activity()) {
BOOST_LOG(debug) << "Skipping HTTP encoder capability probe while a streaming session is active or stopping.";
return publish(video::advertised_encoder_capabilities(false), "active-or-stopping-session");
}
return publish(video::advertised_encoder_capabilities(true), "idle-probe");
}
} // namespace
#endif
web_stream_capabilities_t get_web_stream_capabilities() {
const auto snapshot = advertised_encoder_capabilities_for_http();
const auto &caps = snapshot.advertised;
const bool probe_complete = snapshot.probe_complete;
return {
.probe_complete = probe_complete,
.h264 = probe_complete,
.hevc = probe_complete && caps.hevc_mode >= 2,
.av1 = probe_complete && caps.av1_mode >= 2,
.hevc_hdr = probe_complete && caps.hevc_mode >= 3,
.av1_hdr = probe_complete && caps.av1_mode >= 3,
};
}
// uniqueID, session
std::unordered_map<std::string, pair_session_t> map_id_sess;
std::mutex pairing_sessions_mutex;
constexpr auto pairing_session_expiry = std::chrono::minutes(10);
client_t client_root;
std::mutex client_mutex;
std::atomic_bool authorization_state_ready {false};
std::atomic<uint32_t> session_id_counter;
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>;
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
enum class op_e {
ADD, ///< Add certificate
REMOVE ///< Remove certificate
};
client_t client_root_snapshot() {
std::lock_guard<std::mutex> lock(client_mutex);
client_t snapshot;
snapshot.named_devices.reserve(client_root.named_devices.size());
for (const auto &named_cert : client_root.named_devices) {
if (named_cert) {
snapshot.named_devices.emplace_back(std::make_shared<crypto::named_cert_t>(*named_cert));
}
}
snapshot.remote_display_layout_json = client_root.remote_display_layout_json;
return snapshot;
}
std::string get_arg(const args_t &args, const char *name, const char *default_value) {
auto it = args.find(name);
if (it == std::end(args)) {
if (default_value != nullptr) {
return std::string(default_value);
}
throw std::out_of_range(name);
}
return it->second;
}
// Helper function to extract command entries from a JSON object.
cmd_list_t extract_command_entries(const nlohmann::json &j, const std::string &key) {
cmd_list_t commands;
if (j.contains(key)) {
try {
for (const auto &item : j.at(key)) {
try {
std::string cmd = item.at("cmd").get<std::string>();
bool elevated = util::get_non_string_json_value<bool>(item, "elevated", false);
commands.push_back({cmd, elevated});
} catch (const std::exception &e) {
BOOST_LOG(warning) << "Error parsing command entry: " << e.what();
}
}
} catch (const std::exception &e) {
BOOST_LOG(warning) << "Error retrieving key \"" << key << "\": " << e.what();
}
} else {
BOOST_LOG(debug) << "Key \"" << key << "\" not found in the JSON.";
}
return commands;
}
std::optional<config::video_t::virtual_display_mode_e> parse_virtual_display_mode_override(const std::string &value) {
if (value.empty()) {
return std::nullopt;
}
const auto normalized = boost::algorithm::to_lower_copy(value);
if (normalized == "disabled") {
return config::video_t::virtual_display_mode_e::disabled;
}
if (normalized == "per_client") {
return config::video_t::virtual_display_mode_e::per_client;
}
if (normalized == "shared") {
return config::video_t::virtual_display_mode_e::shared;
}
return std::nullopt;
}
std::optional<config::video_t::virtual_display_layout_e> parse_virtual_display_layout_override(const std::string &value) {
if (value.empty()) {
return std::nullopt;
}
const auto normalized = boost::algorithm::to_lower_copy(value);
if (normalized == "exclusive") {
return config::video_t::virtual_display_layout_e::exclusive;
}
if (normalized == "extended") {
return config::video_t::virtual_display_layout_e::extended;
}
if (normalized == "extended_primary") {
return config::video_t::virtual_display_layout_e::extended_primary;
}
if (normalized == "extended_isolated") {
return config::video_t::virtual_display_layout_e::extended_isolated;
}
if (normalized == "extended_primary_isolated") {
return config::video_t::virtual_display_layout_e::extended_primary_isolated;
}
return std::nullopt;
}
std::optional<std::string> exact_certificate_identity(const crypto::x509_t &certificate) {
if (!certificate) {
return std::nullopt;
}
const auto encoded_length = i2d_X509(certificate.get(), nullptr);
if (encoded_length <= 0 || static_cast<std::size_t>(encoded_length) > pairing_policy::max_paired_certificate_length) {
return std::nullopt;
}
std::string identity(static_cast<std::size_t>(encoded_length), '\0');
auto *cursor = reinterpret_cast<unsigned char *>(identity.data());
if (i2d_X509(certificate.get(), &cursor) != encoded_length) {
return std::nullopt;
}
return identity;
}
bool build_paired_client_records(
const client_t &client,
std::vector<std::string> &certificate_identities,
std::vector<pairing_policy::paired_client_record_view_t> &records,
std::vector<crypto::x509_t> *parsed_certificates = nullptr
) {
certificate_identities.clear();
records.clear();
if (client.named_devices.size() > pairing_policy::max_paired_clients) {
return false;
}
certificate_identities.reserve(client.named_devices.size());
records.reserve(client.named_devices.size());
if (parsed_certificates) {
parsed_certificates->clear();
parsed_certificates->reserve(client.named_devices.size());
}
for (const auto &named_cert : client.named_devices) {
if (!named_cert || named_cert->name.size() > pairing_policy::max_paired_client_name_length || named_cert->cert.empty() || named_cert->cert.size() > pairing_policy::max_paired_certificate_length) {
return false;
}
auto certificate = crypto::x509(named_cert->cert);
auto identity = exact_certificate_identity(certificate);
if (!identity) {
return false;
}
certificate_identities.emplace_back(std::move(*identity));
records.push_back({named_cert->uuid, certificate_identities.back(), !!named_cert->perm});
if (parsed_certificates) {
parsed_certificates->emplace_back(std::move(certificate));
}
}
return pairing_policy::paired_client_state_valid(records);
}
bool primary_bootstrap(statefile::json_load_result_e status, const nlohmann::json &snapshot) {
if (status == statefile::json_load_result_e::missing) return true;
if (status != statefile::json_load_result_e::loaded || !snapshot.is_object() ||
(snapshot.contains("root") && !snapshot["root"].is_object())) return false;
pt::ptree tree;
std::istringstream input(snapshot.dump());
pt::read_json(input, tree);
return statefile::policy::valid_primary_state(tree, true) && !statefile::policy::valid_primary_state(tree, false);
}
bool save_state_snapshot_locked(const client_t &client, bool allow_missing_state = false) {
if (!authorization_state_ready.load(std::memory_order_acquire)) return false;
const auto &vibeshine_path = statefile::vibeshine_state_path();
const bool share_state_file = statefile::share_state_file();
nlohmann::json root;
// This selector is authoritative. A refusal must never be bypassed by
// independently choosing an older paired backup over changed credentials.
const auto primary = statefile::load_primary_state(root);
if (primary != statefile::json_load_result_e::loaded &&
!(allow_missing_state && primary == statefile::json_load_result_e::missing)) {
BOOST_LOG(error) << "Refusing to replace unavailable ArtLight pairing state.";
return false;
}
root["root"]["uniqueid"] = http::unique_id;
root["root"]["remote_display_layout"] = client.remote_display_layout_json;
if (share_state_file) {
root["root"]["last_notified_version"] = update::state.last_notified_version;
}
nlohmann::json named_cert_nodes = nlohmann::json::array();
std::unordered_set<std::string> unique_certs;
std::unordered_map<std::string, int> name_counts;
for (auto &named_cert_p : client.named_devices) {
if (unique_certs.insert(named_cert_p->cert).second) {
nlohmann::json named_cert_node = nlohmann::json::object();
std::string base_name = named_cert_p->name;
size_t pos = base_name.find(" (");
if (pos != std::string::npos) {
base_name = base_name.substr(0, pos);
}
int count = name_counts[base_name]++;
std::string final_name = base_name;
if (count > 0) {
final_name += " (" + std::to_string(count + 1) + ")";
}
named_cert_node["name"] = final_name;
named_cert_node["cert"] = named_cert_p->cert;
named_cert_node["uuid"] = named_cert_p->uuid;
named_cert_node["display_mode"] = named_cert_p->display_mode;
if (!named_cert_p->virtual_display_mode_override.empty()) {
named_cert_node["virtual_display_mode"] = named_cert_p->virtual_display_mode_override;
}
if (!named_cert_p->virtual_display_layout_override.empty()) {
named_cert_node["virtual_display_layout"] = named_cert_p->virtual_display_layout_override;
}
if (!named_cert_p->hdr_profile.empty()) {
named_cert_node["hdr_profile"] = named_cert_p->hdr_profile;
}
if (!named_cert_p->output_name_override.empty()) {
named_cert_node["output_name_override"] = named_cert_p->output_name_override;
}
named_cert_node["perm"] = static_cast<uint32_t>(named_cert_p->perm);
named_cert_node["enable_legacy_ordering"] = named_cert_p->enable_legacy_ordering;
named_cert_node["allow_client_commands"] = named_cert_p->allow_client_commands;
named_cert_node["always_use_virtual_display"] = named_cert_p->always_use_virtual_display;
if (named_cert_p->prefer_10bit_sdr) {
named_cert_node["prefer_10bit_sdr"] = true;
}
if (named_cert_p->last_seen.has_value()) {
named_cert_node["last_seen"] = *named_cert_p->last_seen;
}
if (!named_cert_p->config_overrides.empty()) {
named_cert_node["config_overrides"] = named_cert_p->config_overrides;
}
if (!named_cert_p->do_cmds.empty()) {
nlohmann::json do_cmds_node = nlohmann::json::array();
for (const auto &cmd : named_cert_p->do_cmds) {
do_cmds_node.push_back(crypto::command_entry_t::serialize(cmd));
}
named_cert_node["do"] = do_cmds_node;
}
if (!named_cert_p->undo_cmds.empty()) {
nlohmann::json undo_cmds_node = nlohmann::json::array();
for (const auto &cmd : named_cert_p->undo_cmds) {
undo_cmds_node.push_back(crypto::command_entry_t::serialize(cmd));
}
named_cert_node["undo"] = undo_cmds_node;
}
named_cert_nodes.push_back(named_cert_node);
}
}
root["root"]["named_devices"] = named_cert_nodes;
root["root"].erase("devices"); // Legacy certificates now have canonical records.
try {
if (!state_policy::normalize_snapshot(root)) return false;
statefile::write_sunshine_state_atomic(root);
} catch (const std::exception &e) {
BOOST_LOG(error) << "Couldn't write ArtLight pairing state: " << e.what();
return false;
}
if (!share_state_file) {
auto ensure_root = [](pt::ptree &tree) -> pt::ptree & {
auto it = tree.find("root");
if (it == tree.not_found()) {
auto inserted = tree.insert(tree.end(), std::make_pair(std::string("root"), pt::ptree {}));
return inserted->second;
}
return it->second;
};
pt::ptree vibeshine_tree;
if (!statefile::load_json_for_update(vibeshine_path, vibeshine_tree)) return true;
auto &vibe_root = ensure_root(vibeshine_tree);
vibe_root.put("last_notified_version", update::state.last_notified_version);
#ifdef _WIN32
if (!http::shared_virtual_display_guid.empty()) {
vibe_root.put("shared_virtual_display_guid", http::shared_virtual_display_guid);
}
#endif
try {
statefile::write_json_atomic(vibeshine_path, vibeshine_tree);
} catch (const std::exception &e) {
BOOST_LOG(error) << "Couldn't write "sv << vibeshine_path << ": "sv << e.what();
}
}
return true;
}
bool save_state() {
if (config::sunshine.flags[config::flag::FRESH_STATE]) return true;
statefile::migrate_recent_state_keys();
// Match load_state lock order and snapshot only after gaining the state lock.
std::lock_guard<std::mutex> state_lock(statefile::state_mutex());
return save_state_snapshot_locked(client_root_snapshot());
}
bool load_state() {
statefile::migrate_recent_state_keys();
const auto &vibeshine_path = statefile::vibeshine_state_path();
const bool share_state_file = statefile::share_state_file();
std::lock_guard<std::mutex> state_lock(statefile::state_mutex());
struct parsed_state_t {
client_t client;
nlohmann::json layout;
};
const auto parse_state = [](nlohmann::json &tree) -> std::optional<parsed_state_t> {
try {
if (!state_policy::normalize_snapshot(tree)) return std::nullopt;
const auto &root = tree["root"];
client_t client;
client.remote_display_layout_json = root.value("remote_display_layout", client.remote_display_layout_json);
if (root.contains("devices")) {
for (auto &device_node : root["devices"]) {
if (device_node.contains("certs")) {
for (auto &el : device_node["certs"]) {
auto named_cert_p = std::make_shared<crypto::named_cert_t>();
named_cert_p->name = "";
named_cert_p->cert = el.get<std::string>();
named_cert_p->uuid = uuid_util::uuid_t::generate().string();
named_cert_p->display_mode = "";
named_cert_p->output_name_override.clear();
named_cert_p->perm = PERM::_all;
named_cert_p->enable_legacy_ordering = true;
named_cert_p->allow_client_commands = true;
named_cert_p->always_use_virtual_display = false;
named_cert_p->prefer_10bit_sdr = false;
client.named_devices.emplace_back(named_cert_p);
}
}
}
}
if (root.contains("named_devices")) {
for (auto &el : root["named_devices"]) {
auto named_cert_p = std::make_shared<crypto::named_cert_t>();
named_cert_p->name = el.value("name", "");
named_cert_p->cert = el.value("cert", "");
named_cert_p->uuid = el.value("uuid", "");
named_cert_p->display_mode = el.value("display_mode", "");
named_cert_p->hdr_profile = el.value("hdr_profile", "");
named_cert_p->output_name_override = el.value("output_name_override", "");
named_cert_p->virtual_display_mode_override = el.value("virtual_display_mode", "");
named_cert_p->virtual_display_layout_override = el.value("virtual_display_layout", "");
named_cert_p->perm = (PERM) (util::get_non_string_json_value<uint32_t>(el, "perm", (uint32_t) PERM::_all)) & PERM::_all;
// Imported Vibeshine pairings use a boolean instead of Apollo's
// permission mask. Never reactivate a disabled imported client.
if (!util::get_non_string_json_value<bool>(el, "enabled", true)) {
named_cert_p->perm = PERM::_no;
}
named_cert_p->enable_legacy_ordering = util::get_non_string_json_value<bool>(el, "enable_legacy_ordering", true);
named_cert_p->allow_client_commands = util::get_non_string_json_value<bool>(el, "allow_client_commands", true);
named_cert_p->always_use_virtual_display = util::get_non_string_json_value<bool>(el, "always_use_virtual_display", false);
named_cert_p->prefer_10bit_sdr =
el.contains("prefer_10bit_sdr") && !el["prefer_10bit_sdr"].is_null() &&
util::get_non_string_json_value<bool>(el, "prefer_10bit_sdr", false);
if (el.contains("last_seen") && !el["last_seen"].is_null()) {
named_cert_p->last_seen = util::get_non_string_json_value<std::int64_t>(el, "last_seen", 0);
} else {
named_cert_p->last_seen.reset();
}
named_cert_p->config_overrides.clear();
if (el.contains("config_overrides") && el["config_overrides"].is_object()) {
for (const auto &entry : el["config_overrides"].items()) {
if (entry.key().empty()) {
continue;
}
named_cert_p->config_overrides[entry.key()] = entry.value().get<std::string>();
}
}
{
std::unordered_map<std::string, std::string> normalized_overrides;
config::merge_config_overrides(normalized_overrides, named_cert_p->config_overrides);
named_cert_p->config_overrides = std::move(normalized_overrides);
}
named_cert_p->do_cmds = extract_command_entries(el, "do");
named_cert_p->undo_cmds = extract_command_entries(el, "undo");
client.named_devices.emplace_back(named_cert_p);
}
}
nlohmann::json remote_display_layout;
try {
remote_display_layout = remote_display_topology::normalize_layout(
nlohmann::json::parse(client.remote_display_layout_json)
);
} catch (...) {
remote_display_layout = remote_display_topology::normalize_layout(nlohmann::json {});
}
client.remote_display_layout_json = remote_display_layout.dump();
std::vector<std::string> identities;
std::vector<pairing_policy::paired_client_record_view_t> records;
if (!build_paired_client_records(client, identities, records)) return std::nullopt;
return parsed_state_t {std::move(client), std::move(remote_display_layout)};
} catch (...) {
BOOST_LOG(error) << "ArtLight pairing state is malformed.";
return std::nullopt;
}
};
nlohmann::json tree;
const auto primary = statefile::load_primary_state(tree);
if (primary == statefile::json_load_result_e::failed) return false;
auto parsed = primary == statefile::json_load_result_e::loaded ? parse_state(tree) : std::nullopt;
if (!parsed) {
if (primary_bootstrap(primary, tree) && http::credentials_created_this_run) {
http::uuid = uuid_util::uuid_t::generate();
http::unique_id = http::uuid.string();
authorization_state_ready.store(true, std::memory_order_release);
if (!save_state_snapshot_locked(client_t {}, true)) {
authorization_state_ready.store(false, std::memory_order_release);
return false;
}
update::state.last_notified_version.clear();
return true;
}
BOOST_LOG(error) << "No valid ArtLight pairing snapshot; refusing to create a replacement identity.";
return false;
}
// Selection has already restored and validated any recovered snapshot.
try {
statefile::write_json_atomic(statefile::sunshine_state_backup_path(), tree);
} catch (const std::exception &e) {
BOOST_LOG(warning) << "Could not persist ArtLight recovery snapshot: " << e.what();
}
const auto &root = tree["root"];
http::unique_id = root["uniqueid"].get<std::string>();
http::uuid = uuid_util::uuid_t::parse(http::unique_id);
if (share_state_file) {
update::state.last_notified_version = root.value("last_notified_version", "");
} else if (!vibeshine_path.empty()) {
try {
pt::ptree vibeshine_tree;
if (statefile::load_json(vibeshine_path, vibeshine_tree) != statefile::json_load_result_e::loaded) {
throw std::runtime_error("notification state is unavailable");
}
update::state.last_notified_version = vibeshine_tree.get("root.last_notified_version", "");
#ifdef _WIN32
http::shared_virtual_display_guid = vibeshine_tree.get("root.shared_virtual_display_guid", "");
#endif
} catch (const std::exception &e) {
BOOST_LOG(warning) << "Couldn't read "sv << vibeshine_path << " for notification state: "sv << e.what();
update::state.last_notified_version.clear();
#ifdef _WIN32
http::shared_virtual_display_guid.clear();
#endif
}
} else {
update::state.last_notified_version.clear();
#ifdef _WIN32
http::shared_virtual_display_guid.clear();
#endif
}
#ifdef _WIN32
if (share_state_file && !root.contains("shared_virtual_display_guid")) {
http::shared_virtual_display_guid.clear();
}
#endif
{
std::lock_guard<std::mutex> lock(client_mutex);
cert_chain.clear();
for (auto &named_cert : parsed->client.named_devices) cert_chain.add(named_cert);
client_root = std::move(parsed->client);
}
remote_display_topology::instance().set_layout(std::move(parsed->layout));
authorization_state_ready.store(true, std::memory_order_release);
return true;
}
bool add_authorized_client(const p_named_cert_t &named_cert_p) {
const bool transient = config::sunshine.flags[config::flag::FRESH_STATE];
if (!transient) statefile::migrate_recent_state_keys();
std::lock_guard<std::mutex> state_lock(statefile::state_mutex());
std::lock_guard<std::mutex> client_lock(client_mutex);
if (!transient && !authorization_state_ready.load(std::memory_order_acquire)) return false;
auto candidate = crypto::x509(named_cert_p->cert);
if (!candidate) return false;
auto existing = std::find_if(client_root.named_devices.begin(), client_root.named_devices.end(), [&](const auto &record) {
auto certificate = crypto::x509(record->cert);
return certificate && X509_cmp(candidate.get(), certificate.get()) == 0;
});
if (existing != client_root.named_devices.end()) {
// A verified re-pair restores a disabled client's default access while
// retaining its stable UUID, commands and display preferences.
if ((*existing)->perm == PERM::_no) {
(*existing)->perm = PERM::_default;
if (!transient && !save_state_snapshot_locked(client_root)) {
(*existing)->perm = PERM::_no;
return false;
}
}
*named_cert_p = **existing;
return true;
}
if (client_root.named_devices.size() >= pairing_policy::max_paired_clients) return false;
client_root.named_devices.push_back(named_cert_p);
std::vector<std::string> identities;
std::vector<pairing_policy::paired_client_record_view_t> records;
if (!build_paired_client_records(client_root, identities, records)) {
client_root.named_devices.pop_back();
return false;
}
if (!transient && !save_state_snapshot_locked(client_root)) {
client_root.named_devices.pop_back();
return false;
}
cert_chain.add(client_root.named_devices.back());
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_paired(named_cert_p->name);
#endif
return true;
}
struct resolved_client_identity_t {
std::string uuid;
std::string name;
};
std::mutex tls_client_identity_mutex;
std::unordered_map<std::string, resolved_client_identity_t> tls_client_identity_by_endpoint;
std::string endpoint_key(req_https_t request) {
if (!request) {
return {};
}
const auto endpoint = request->remote_endpoint();
if (endpoint.address().is_unspecified() || endpoint.port() == 0) {
return {};
}
return endpoint.address().to_string() + ":" + std::to_string(endpoint.port());
}
std::optional<resolved_client_identity_t> resolve_client_identity_from_peer_cert_locked(
const crypto::x509_t &client_cert
) {
auto presented_identity = exact_certificate_identity(client_cert);
if (!presented_identity) {
BOOST_LOG(debug) << "No valid client certificate identity available";
return std::nullopt;
}
std::vector<std::string> certificate_identities;
std::vector<pairing_policy::paired_client_record_view_t> paired_client_records;
if (!build_paired_client_records(client_root, certificate_identities, paired_client_records)) {
BOOST_LOG(error) << "Refusing client authentication because pairing state is invalid or ambiguous.";
return std::nullopt;
}
const auto resolution = pairing_policy::resolve_paired_client(
paired_client_records,
*presented_identity
);
switch (resolution.status) {
case pairing_policy::paired_client_resolution_e::authorized:
{
const auto &named_cert = client_root.named_devices[resolution.index];
BOOST_LOG(debug) << "Found exact enabled client UUID: " << named_cert->uuid
<< " for client: " << named_cert->name;
return resolved_client_identity_t {named_cert->uuid, named_cert->name};
}
case pairing_policy::paired_client_resolution_e::disabled:
BOOST_LOG(warning) << "Client certificate belongs to a disabled paired device.";
break;
case pairing_policy::paired_client_resolution_e::invalid_state:
BOOST_LOG(error) << "Refusing client authentication because pairing state is invalid or ambiguous.";
break;
case pairing_policy::paired_client_resolution_e::unknown_certificate:
BOOST_LOG(debug) << "No exact paired client certificate found";
break;
}
return std::nullopt;
}
bool paired_client_uuid_enabled_locked(const std::string_view uuid) {
if (!pairing_policy::valid_paired_client_uuid(uuid) || client_root.named_devices.size() > pairing_policy::max_paired_clients) {
return false;
}
const crypto::named_cert_t *match = nullptr;
std::unordered_set<std::string_view> seen_uuids;
seen_uuids.reserve(client_root.named_devices.size());
for (const auto &client : client_root.named_devices) {
if (!client || !pairing_policy::valid_paired_client_uuid(client->uuid) || client->name.size() > pairing_policy::max_paired_client_name_length || client->cert.empty() || client->cert.size() > pairing_policy::max_paired_certificate_length || !seen_uuids.insert(client->uuid).second) {
return false;
}
if (client->uuid == uuid) {
match = client.get();
}
}
return match && !!match->perm;
}
bool paired_client_uuid_enabled(const std::string_view uuid, const PERM expected_permissions) {
std::lock_guard<std::mutex> lock(client_mutex);
if (!paired_client_uuid_enabled_locked(uuid)) return false;
const auto record = std::find_if(client_root.named_devices.begin(), client_root.named_devices.end(),
[&](const auto &client) { return client->uuid == uuid; });
// Reject a request whose authorization changed during launch preparation.
return record != client_root.named_devices.end() && (*record)->perm == expected_permissions;
}
void remember_tls_client_identity(req_https_t request, const resolved_client_identity_t &identity) {
const auto key = endpoint_key(request);
if (key.empty() || identity.uuid.empty()) {
return;
}
std::lock_guard<std::mutex> lock(tls_client_identity_mutex);
if (!tls_client_identity_by_endpoint.contains(key) &&
tls_client_identity_by_endpoint.size() >= pairing_policy::max_paired_clients * 8) {
tls_client_identity_by_endpoint.erase(tls_client_identity_by_endpoint.begin());
}
tls_client_identity_by_endpoint.insert_or_assign(key, identity);
}
void forget_tls_client_identity(req_https_t request) {
const auto key = endpoint_key(request);
if (key.empty()) {
return;
}
std::lock_guard<std::mutex> lock(tls_client_identity_mutex);
tls_client_identity_by_endpoint.erase(key);
}
void forget_tls_client_identities_for_uuid(const std::string_view uuid) {
std::lock_guard<std::mutex> lock(tls_client_identity_mutex);
for (auto it = tls_client_identity_by_endpoint.begin(); it != tls_client_identity_by_endpoint.end();) {
if (it->second.uuid == uuid) {
it = tls_client_identity_by_endpoint.erase(it);
} else {
++it;
}
}
}
void clear_tls_client_identities() {
std::lock_guard<std::mutex> lock(tls_client_identity_mutex);
tls_client_identity_by_endpoint.clear();
}
std::optional<resolved_client_identity_t> get_remembered_tls_client_identity(req_https_t request) {
const auto key = endpoint_key(request);
if (key.empty()) {
return std::nullopt;
}
std::lock_guard<std::mutex> client_lock(client_mutex);
std::lock_guard<std::mutex> lock(tls_client_identity_mutex);
const auto it = tls_client_identity_by_endpoint.find(key);
if (it == tls_client_identity_by_endpoint.end()) {
return std::nullopt;
}
if (!paired_client_uuid_enabled_locked(it->second.uuid)) {
tls_client_identity_by_endpoint.erase(it);
return std::nullopt;
}
return it->second;
}
std::mutex launch_request_mutex;
// Configured-app process transitions are serialized independently from
// RTSP admission. Synthetic remote controls use per-entry admission and
// do not take this mutex.
remote_session::normal_app_transition_gate_t normal_http_app_transition_mutex;
std::mutex stream_lifecycle_gate;
std::mutex &stream_lifecycle_mutex() {
return stream_lifecycle_gate;
}
std::unique_lock<std::mutex> acquire_stream_start_lifecycle_lock() {
bool waited_for_teardown = false;
for (;;) {
std::unique_lock<std::mutex> lifecycle_lock {stream_lifecycle_gate};
if (stream::session::teardown_sessions.load(std::memory_order_acquire) == 0) {
if (waited_for_teardown) {
BOOST_LOG(debug) << "Stream start: prior RTSP teardown completed; lifecycle gate acquired.";
}
return lifecycle_lock;
}
if (!waited_for_teardown) {
waited_for_teardown = true;
BOOST_LOG(debug) << "Stream start: yielding lifecycle gate to prior RTSP teardown.";
}
lifecycle_lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
std::string resolve_known_client_uuid_from_launch_id(const std::string &launch_unique_id) {
if (launch_unique_id.empty()) {
return {};
}
std::lock_guard<std::mutex> lock(client_mutex);
for (const auto &named_cert : client_root.named_devices) {
if (named_cert && named_cert->uuid == launch_unique_id) {
return launch_unique_id;
}
}
BOOST_LOG(debug) << "Ignoring unmatched launch uniqueid for per-client settings: " << launch_unique_id;
return {};
}
bool is_placeholder_client_name(const std::string &name) {
const auto trimmed = boost::algorithm::trim_copy(name);
return boost::iequals(trimmed, "self");
}
std::string display_client_name_for_session(const std::string &paired_name, const std::string &device_name, const std::string &host_name) {
const auto paired = boost::algorithm::trim_copy(paired_name);
const auto device = boost::algorithm::trim_copy(device_name);
const auto host = boost::algorithm::trim_copy(host_name);
if (!paired.empty() && !is_placeholder_client_name(paired)) {
return paired;
}
if (!device.empty() && !is_placeholder_client_name(device)) {
return device;
}
if (!host.empty() && !is_placeholder_client_name(host)) {
return host;
}
return "Sunshine"s;
}
std::string client_name_for_uuid(const std::string &uuid) {
if (uuid.empty()) {
return {};
}
std::lock_guard<std::mutex> lock(client_mutex);
for (const auto &named_cert : client_root.named_devices) {
if (named_cert && named_cert->uuid == uuid) {
return named_cert->name;
}
}
return {};
}
resolved_client_identity_t resolve_client_identity(req_https_t request, const verified_client_t &verified_client) {
if (auto remembered = get_remembered_tls_client_identity(request)) {
return *remembered;
}
resolved_client_identity_t identity;
if (verified_client) {
identity.uuid = verified_client->uuid;
identity.name = verified_client->name;
}
return identity;
}
static std::shared_ptr<rtsp_stream::launch_session_t> make_launch_session_from_snapshot(
bool host_audio,
bool input_only,
const args_t &args,
const verified_client_t &verified_client,
const resolved_client_identity_t *resolved_client_identity,
req_https_t request,
bool use_app_color_preference = true
) {
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
launch_session->id = ++session_id_counter;
launch_session->appid = 0;
launch_session->gen1_framegen_fix = false;
launch_session->gen2_framegen_fix = false;
launch_session->frame_generation_enabled = false;
launch_session->lossless_scaling_framegen = false;
launch_session->framegen_refresh_rate.reset();
launch_session->framegen_refresh_millihz.reset();
launch_session->framegen_refresh_multiplier = 1;
launch_session->framegen_fixed_refresh = false;
launch_session->lossless_scaling_target_fps.reset();
launch_session->lossless_scaling_rtss_limit.reset();
launch_session->frame_generation_provider = "lossless-scaling";
launch_session->client_vrr_requested = false;
#ifdef _WIN32
#endif
const auto identity_uuid = resolved_client_identity ? resolved_client_identity->uuid : (verified_client ? verified_client->uuid : std::string());
const auto identity_name = resolved_client_identity ? resolved_client_identity->name : (verified_client ? verified_client->name : std::string());
launch_session->device_name = identity_name.empty() ? config::nvhttp.sunshine_name : identity_name;
launch_session->virtual_display = false;
launch_session->virtual_display_guid_bytes.fill(0);
launch_session->virtual_display_device_id.clear();
launch_session->virtual_display_ready_since.reset();
launch_session->virtual_display_hdr_enabled.reset();
launch_session->app_metadata.reset();
launch_session->client_uuid = identity_uuid;
launch_session->client_name = identity_name;
launch_session->hdr_profile.reset();
launch_session->client_display_mode_override = false;
launch_session->client_display_refresh_millihz = 0;
launch_session->client_requests_virtual_display = false;
launch_session->client_virtual_display_override.reset();
launch_session->resolution_override.reset();
launch_session->scale_factor = 100;
launch_session->virtual_display_failed = false;
auto client_name_arg = get_arg(args, "clientName", "");
if (!client_name_arg.empty()) {
if (launch_session->client_name.empty()) {
launch_session->client_name = client_name_arg;
}
launch_session->device_name = client_name_arg;
}
// A launch uniqueid is client supplied and cannot authorize a caller.
// The paired TLS certificate is the only identity used for a session
// role, per-client settings, or monitor ownership.
const auto launch_client_uuid = resolve_known_client_uuid_from_launch_id(get_arg(args, "uniqueid", ""));
if (!launch_client_uuid.empty() && launch_client_uuid != launch_session->client_uuid) {
BOOST_LOG(warning) << "Ignoring launch uniqueid that conflicts with the authenticated TLS client identity.";
}
// If launched from client
if (launch_session->client_uuid != http::unique_id) {
auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true);
std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key));
launch_session->host_audio = host_audio;
// Encrypted RTSP is enabled with client reported corever >= 1
auto corever = util::from_view(get_arg(args, "corever", "0"));
if (corever >= 1) {
launch_session->rtsp_cipher = crypto::cipher::gcm_t {
launch_session->gcm_key,
false
};
launch_session->rtsp_iv_counter = 0;
}
launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s;
// Generate the unique identifiers for this connection that we will send later during RTSP handshake
unsigned char raw_payload[8];
RAND_bytes(raw_payload, sizeof(raw_payload));
launch_session->av_ping_payload = util::hex_vec(raw_payload);
RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data));
launch_session->iv.resize(16);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
auto prepend_iv_p = (uint8_t *) &prepend_iv;
std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));
}
struct parsed_display_mode_t {
int width = 0;
int height = 0;
std::uint32_t refresh_millihz = 0;
};
const auto parse_display_mode = [](const std::string &mode_text) -> std::optional<parsed_display_mode_t> {
constexpr std::uint32_t kMinRefreshMillihz = 10'000;
constexpr std::uint32_t kMaxRefreshMillihz = 1'000'000;
const auto parse_unsigned = [](std::string_view value, std::uint32_t maximum, std::uint32_t &result) {
if (value.empty()) {
return false;
}
std::uint64_t parsed = 0;
for (const char ch : value) {
if (ch < '0' || ch > '9') {
return false;
}
const auto digit = static_cast<std::uint32_t>(ch - '0');
if (parsed > (maximum - digit) / 10) {
return false;
}
parsed = parsed * 10 + digit;
}
result = static_cast<std::uint32_t>(parsed);
return true;
};
const auto first_separator = mode_text.find('x');
const auto second_separator = first_separator == std::string::npos ? std::string::npos : mode_text.find('x', first_separator + 1);
if (first_separator == std::string::npos || second_separator == std::string::npos ||
mode_text.find('x', second_separator + 1) != std::string::npos) {
return std::nullopt;
}
std::uint32_t width = 0;
std::uint32_t height = 0;
if (!parse_unsigned(std::string_view(mode_text).substr(0, first_separator), static_cast<std::uint32_t>(std::numeric_limits<int>::max()), width) ||
!parse_unsigned(std::string_view(mode_text).substr(first_separator + 1, second_separator - first_separator - 1), static_cast<std::uint32_t>(std::numeric_limits<int>::max()), height)) {
return std::nullopt;
}
const std::string_view refresh_text {mode_text.data() + second_separator + 1, mode_text.size() - second_separator - 1};
const auto decimal_point = refresh_text.find('.');
std::uint32_t refresh_millihz = 0;
if (decimal_point == std::string_view::npos) {
std::uint32_t raw_refresh = 0;
if (!parse_unsigned(refresh_text, kMaxRefreshMillihz, raw_refresh)) {
return std::nullopt;
}
// ArtLight Server's legacy mode values at or above 1000 are already millihertz.
refresh_millihz = raw_refresh >= 1000 ? raw_refresh : raw_refresh * 1000;
} else {
if (refresh_text.find('.', decimal_point + 1) != std::string_view::npos) {
return std::nullopt;
}
std::uint32_t whole_hertz = 0;
std::uint32_t fractional_millihz = 0;
const auto fractional = refresh_text.substr(decimal_point + 1);
if (fractional.empty() || fractional.size() > 3 ||
!parse_unsigned(refresh_text.substr(0, decimal_point), 1000, whole_hertz) ||
!parse_unsigned(fractional, 999, fractional_millihz)) {
return std::nullopt;
}
const auto scale = fractional.size() == 1 ? 100u : fractional.size() == 2 ? 10u : 1u;
refresh_millihz = whole_hertz * 1000 + fractional_millihz * scale;
}
if (width == 0 || height == 0 || refresh_millihz < kMinRefreshMillihz || refresh_millihz > kMaxRefreshMillihz) {
return std::nullopt;
}
return parsed_display_mode_t {
.width = static_cast<int>(width),
.height = static_cast<int>(height),
.refresh_millihz = refresh_millihz,
};
};
const auto requested_mode_text = get_arg(args, "mode", config::video.fallback_mode.c_str());
if (const auto requested_mode = parse_display_mode(requested_mode_text)) {
launch_session->width = requested_mode->width;
launch_session->height = requested_mode->height;
launch_session->fps = static_cast<int>(requested_mode->refresh_millihz);
} else {
BOOST_LOG(warning) << "Failed to parse requested display mode '" << requested_mode_text << "'; using 1920x1080x60.";
launch_session->width = 1920;
launch_session->height = 1080;
launch_session->fps = 60000;
}
BOOST_LOG(info) << "Display mode for client ["sv << verified_client->name << "] requested to ["sv << requested_mode_text << ']';
if (verified_client->display_mode.empty()) {
launch_session->client_display_mode_override = false;
} else if (const auto display_mode = parse_display_mode(verified_client->display_mode)) {
// The per-client display mode controls the presentation/RTSS rate, but
// does not overwrite the client-selected stream cadence.
launch_session->width = display_mode->width;
launch_session->height = display_mode->height;
launch_session->client_display_mode_override = true;
launch_session->client_display_refresh_millihz = display_mode->refresh_millihz;
BOOST_LOG(info) << "Display mode for client ["sv << verified_client->name << "] overridden to ["sv << verified_client->display_mode << ']';
} else {
BOOST_LOG(warning) << "Failed to parse client display mode override: " << verified_client->display_mode;
launch_session->client_display_mode_override = false;
}
if (!verified_client->virtual_display_mode_override.empty()) {
if (const auto parsed_mode = parse_virtual_display_mode_override(verified_client->virtual_display_mode_override)) {
launch_session->virtual_display_mode_override = *parsed_mode;
}
}
if (!verified_client->virtual_display_layout_override.empty()) {
if (const auto parsed_layout = parse_virtual_display_layout_override(verified_client->virtual_display_layout_override)) {
launch_session->virtual_display_layout_override = *parsed_layout;
}
}
launch_session->client_requests_virtual_display = verified_client->always_use_virtual_display;
if (!verified_client->hdr_profile.empty()) {
launch_session->hdr_profile = verified_client->hdr_profile;
}
launch_session->unique_id = get_arg(args, "uniqueid", "unknown");
launch_session->perm = verified_client->perm;
const auto launch_appid_arg = get_arg(args, "appid", "0");
const auto launch_appuuid_arg = get_arg(args, "appuuid", "");
auto launch_app_ctx = proc::proc.resolve_app(launch_appid_arg, launch_appuuid_arg);
int app_scale_factor = 100;
launch_session->appid = launch_app_ctx ? (int) util::from_view(launch_app_ctx->id) : (int) util::from_view(launch_appid_arg);
if (!verified_client->output_name_override.empty()) {
launch_session->output_name_override = verified_client->output_name_override;
}
const auto original_client_name = boost::algorithm::trim_copy(launch_session->client_name);
if (!original_client_name.empty() && is_placeholder_client_name(original_client_name)) {
const auto resolved_display_client_name =
display_client_name_for_session(launch_session->client_name, launch_session->device_name, config::nvhttp.sunshine_name);
BOOST_LOG(warning) << "Resolved paired client name '" << launch_session->client_name
<< "' is not safe for display identity; using '" << resolved_display_client_name << "' instead.";
launch_session->client_name = resolved_display_client_name;
} else {
launch_session->client_name = original_client_name;
}
if (launch_app_ctx || launch_session->appid > 0) {
try {
if (auto app_ctx = launch_app_ctx ? launch_app_ctx : proc::proc.resolve_app(launch_session->appid)) {
launch_session->appid = (int) util::from_view(app_ctx->id);
app_scale_factor = app_ctx->scale_factor;
launch_session->gen1_framegen_fix = app_ctx->gen1_framegen_fix;
launch_session->gen2_framegen_fix = app_ctx->gen2_framegen_fix;
launch_session->frame_generation_enabled = app_ctx->frame_generation_enabled;
launch_session->lossless_scaling_framegen = app_ctx->lossless_scaling_framegen;
launch_session->lossless_scaling_target_fps = app_ctx->lossless_scaling_target_fps;
launch_session->lossless_scaling_rtss_limit = app_ctx->lossless_scaling_rtss_limit;
launch_session->frame_generation_provider = app_ctx->frame_generation_provider;
rtsp_stream::launch_session_t::app_metadata_t metadata;
metadata.id = app_ctx->id;
metadata.name = app_ctx->name;
metadata.virtual_screen = app_ctx->virtual_screen || app_ctx->virtual_display;
metadata.has_command = !app_ctx->cmd.empty();
metadata.has_playnite = !app_ctx->playnite_id.empty();
metadata.playnite_fullscreen = app_ctx->playnite_fullscreen;
launch_session->virtual_display = app_ctx->virtual_screen;
if (!launch_session->virtual_display_mode_override && app_ctx->virtual_display_mode_override) {
launch_session->virtual_display_mode_override = app_ctx->virtual_display_mode_override;
}
if (!launch_session->virtual_display_layout_override && app_ctx->virtual_display_layout_override) {
launch_session->virtual_display_layout_override = app_ctx->virtual_display_layout_override;
}
if (!launch_session->dd_config_option_override && app_ctx->dd_config_option_override) {
launch_session->dd_config_option_override = app_ctx->dd_config_option_override;
}
if (app_ctx->output_name_override) {
launch_session->output_name_override = *app_ctx->output_name_override;
}
launch_session->app_metadata = std::move(metadata);
}
} catch (...) {
}
}
launch_session->framegen_refresh_rate.reset();
launch_session->framegen_refresh_millihz.reset();
launch_session->framegen_refresh_multiplier = 1;
launch_session->framegen_fixed_refresh = false;
launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0"));
launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610"));
launch_session->surround_params = (get_arg(args, "surroundParams", ""));
launch_session->gcmap = util::from_view(get_arg(args, "gcmap", "0"));
const auto playstation_gamepad_mask =
(int) util::from_view(get_arg(args, "psmap", "0"));
launch_session->playstation_gamepad_mask =
playstation_gamepad_mask > 0 ?
playstation_gamepad_mask & launch_session->gcmap & 0xFFFF :
0;
launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0"));
launch_session->client_vrr_requested = util::from_view(get_arg(args, "clientVrrRequested", "0"));
auto color_app_ctx = launch_app_ctx;
const auto current_color_app_id = proc::proc.current_app_id();
const auto color_control = remote_session::identify(launch_session->appid, launch_appuuid_arg, current_color_app_id);
if (!color_app_ctx && ((launch_session->appid <= 0 && launch_appuuid_arg.empty()) ||
color_control == remote_session::control_e::resume ||
color_control == remote_session::control_e::running_game)) {
color_app_ctx = proc::proc.resolve_app(current_color_app_id);
}
launch_session->prefer_sdr_10bit = rtsp_stream::hdr_request_policy::resolve_prefer_10bit_sdr(
verified_client->prefer_10bit_sdr,
use_app_color_preference && color_app_ctx ? color_app_ctx->prefer_10bit_sdr : std::nullopt
);
#if defined(_WIN32) || defined(__linux__)
{
const auto hdr_request = rtsp_stream::hdr_request_policy::apply(
{
launch_session->enable_hdr,
launch_session->prefer_sdr_10bit,
launch_session->force_sdr,
},
config::video.dd.hdr_request_override
);
launch_session->enable_hdr = hdr_request.enable_hdr;
launch_session->prefer_sdr_10bit = hdr_request.prefer_sdr_10bit;
launch_session->force_sdr = hdr_request.force_sdr;
}
#endif
if (const auto virtual_display_arg = args.find("virtualDisplay"); virtual_display_arg != std::end(args)) {
launch_session->client_virtual_display_override = util::from_view(virtual_display_arg->second) != 0;
if (!*launch_session->client_virtual_display_override) {
launch_session->virtual_display_mode_override = config::video_t::virtual_display_mode_e::disabled;
}
}
const auto client_scale_factor = util::from_view(get_arg(args, "scaleFactor", "100"));
launch_session->scale_factor = client_scale_factor > 0 &&
client_scale_factor <= std::numeric_limits<std::uint32_t>::max() ?
static_cast<std::uint32_t>(client_scale_factor) :
100u;
const auto effective_scale_factor = app_scale_factor != 100 ?
static_cast<std::int64_t>(app_scale_factor) :
client_scale_factor;
if (effective_scale_factor > 0 && effective_scale_factor != 100 && launch_session->width > 0 && launch_session->height > 0) {
const auto scale_dimension = [effective_scale_factor](const int dimension) -> std::optional<int> {
const auto unsigned_dimension = static_cast<std::uint64_t>(dimension);
const auto unsigned_scale_factor = static_cast<std::uint64_t>(effective_scale_factor);
if (unsigned_dimension > std::numeric_limits<std::uint64_t>::max() / unsigned_scale_factor) {
return std::nullopt;
}
const auto scaled_dimension = (unsigned_dimension * unsigned_scale_factor) / 100u;
const auto even_dimension = scaled_dimension & ~1ull;
if (even_dimension == 0 || even_dimension > static_cast<std::uint64_t>(std::numeric_limits<int>::max())) {
return std::nullopt;
}
return static_cast<int>(even_dimension);
};
if (const auto width = scale_dimension(launch_session->width), height = scale_dimension(launch_session->height); width && height) {
launch_session->resolution_override = rtsp_stream::launch_session_t::resolution_override_t {
.width = *width,
.height = *height,
};
BOOST_LOG(info) << "Using launch resolution override " << *width << "x" << *height
<< " for effective scale factor=" << effective_scale_factor << ".";
} else {
BOOST_LOG(warning) << "Ignoring invalid effective scale factor=" << effective_scale_factor
<< " for requested mode " << launch_session->width << "x" << launch_session->height << ".";
}
}
// Encrypted RTSP is enabled with client reported corever >= 1.
const auto corever = util::from_view(get_arg(args, "corever", "0"));
if (corever >= 1) {
launch_session->rtsp_cipher = crypto::cipher::gcm_t {
launch_session->gcm_key,
false
};
launch_session->rtsp_iv_counter = 0;
}
launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s;
if (request) {
launch_session->rtsp_source_address = request->remote_endpoint().address().to_string();
}
launch_session->client_do_cmds = verified_client->do_cmds;
launch_session->client_undo_cmds = verified_client->undo_cmds;
launch_session->input_only = input_only;
launch_session->iv.resize(16);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
auto prepend_iv_p = (uint8_t *) &prepend_iv;
std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));
#ifdef _WIN32
{
// Default the capture gate to "proceed"; launch/resume replace it when an
// APPLY is dispatched so capture can wait for the helper's verification.
std::promise<rtsp_stream::launch_session_t::display_helper_gate_status_e> gate_promise;
gate_promise.set_value(rtsp_stream::launch_session_t::display_helper_gate_status_e::proceed);
launch_session->display_helper_gate = gate_promise.get_future().share();
}
#endif
return launch_session;
}
std::shared_ptr<rtsp_stream::launch_session_t> make_launch_session(
bool host_audio,
bool input_only,
const args_t &args,
const crypto::named_cert_t *named_cert_p,
const resolved_client_identity_t *resolved_client_identity
) {
verified_client_t verified_client;
if (named_cert_p) {
verified_client = *named_cert_p;
}
return make_launch_session_from_snapshot(host_audio, input_only, args, verified_client, resolved_client_identity, nullptr);
}
void remove_session(const pair_session_t &sess) {
const std::string unique_id = sess.client.uniqueID;
map_id_sess.erase(unique_id);
}
/**
* @brief Answer the client still waiting on a pending pairing request and close that connection.
* @param session The pending session whose getservercert response is still open.
* @param status_code The HTTP-style status code to report.
* @param status_message The status message to report.
*/
void close_pending_pairing_response(pair_session_t &session, const int status_code, const std::string_view status_message) {
pt::ptree tree;
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", status_code);
tree.put("root.<xmlattr>.status_message", std::string(status_message));
std::ostringstream data;
pt::write_xml(data, tree);
auto &response = session.async_insert_pin.response;
try {
if (response.has_left() && response.left()) {
response.left()->close_connection_after_response = true;
response.left()->write(data.str());
} else if (response.has_right() && response.right()) {
response.right()->close_connection_after_response = true;
response.right()->write(data.str());
}
} catch (const std::exception &error) {
BOOST_LOG(debug) << "Closing a pending pairing response failed: " << error.what();
} catch (...) {
BOOST_LOG(debug) << "Closing a pending pairing response failed";
}
}
void expire_pairing_sessions_locked(const std::chrono::steady_clock::time_point now) {
for (auto session = map_id_sess.begin(); session != map_id_sess.end();) {
if (now - session->second.created_at <= pairing_session_expiry) {
++session;
continue;
}
close_pending_pairing_response(session->second, 408, "Pairing request expired"sv);
session = map_id_sess.erase(session);
}
}
void fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", status_msg);
remove_session(sess); // Security measure, delete the session when something went wrong and force a re-pair
BOOST_LOG(warning) << "Pair attempt failed due to " << status_msg;
}
void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) {
if (sess.last_phase != PAIR_PHASE::NONE) {
fail_pair(sess, tree, "Out of order call to getservercert");
return;
}
sess.last_phase = PAIR_PHASE::GETSERVERCERT;
if (sess.async_insert_pin.salt.size() < 32) {
fail_pair(sess, tree, "Salt too short");
return;
}
std::string_view salt_view {sess.async_insert_pin.salt.data(), 32};
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
auto key = crypto::gen_aes_key(salt, pin);
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
tree.put("root.paired", 1);
tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true));
tree.put("root.<xmlattr>.status_code", 200);
}
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const std::string &challenge) {
if (sess.last_phase != PAIR_PHASE::GETSERVERCERT) {
fail_pair(sess, tree, "Out of order call to clientchallenge");
return;
}
sess.last_phase = PAIR_PHASE::CLIENTCHALLENGE;
if (!sess.cipher_key) {
fail_pair(sess, tree, "Cipher key not set");
return;
}
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
std::vector<uint8_t> decrypted;
cipher.decrypt(challenge, decrypted);
auto x509 = crypto::x509(conf_intern.servercert);
auto sign = crypto::signature(x509);
auto serversecret = crypto::rand(16);
decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign));
decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret));
auto hash = crypto::hash({(char *) decrypted.data(), decrypted.size()});
auto serverchallenge = crypto::rand(16);
std::string plaintext;
plaintext.reserve(hash.size() + serverchallenge.size());
plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash));
plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge));
std::vector<uint8_t> encrypted;
cipher.encrypt(plaintext, encrypted);
sess.serversecret = std::move(serversecret);
sess.serverchallenge = std::move(serverchallenge);
tree.put("root.paired", 1);
tree.put("root.challengeresponse", util::hex_vec(encrypted, true));
tree.put("root.<xmlattr>.status_code", 200);
}
void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const std::string &encrypted_response) {
if (sess.last_phase != PAIR_PHASE::CLIENTCHALLENGE) {
fail_pair(sess, tree, "Out of order call to serverchallengeresp");
return;
}
sess.last_phase = PAIR_PHASE::SERVERCHALLENGERESP;
if (!sess.cipher_key || sess.serversecret.empty()) {
fail_pair(sess, tree, "Cipher key or serversecret not set");
return;
}
std::vector<uint8_t> decrypted;
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
cipher.decrypt(encrypted_response, decrypted);
sess.clienthash = std::move(decrypted);
auto serversecret = sess.serversecret;
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));
tree.put("root.pairingsecret", util::hex_vec(serversecret, true));
tree.put("root.paired", 1);
tree.put("root.<xmlattr>.status_code", 200);
}
void clientpairingsecret(
pair_session_t &sess,
const std::shared_ptr<safe::queue_t<crypto::x509_t>> &pending_certs,
pt::ptree &tree,
const std::string &client_pairing_secret
) {
if (sess.last_phase != PAIR_PHASE::SERVERCHALLENGERESP) {
fail_pair(sess, tree, "Out of order call to clientpairingsecret");
return;
}
sess.last_phase = PAIR_PHASE::CLIENTPAIRINGSECRET;
auto &client = sess.client;
if (client_pairing_secret.size() <= 16) {
fail_pair(sess, tree, "Client pairing secret too short");
return;
}
std::string_view secret {client_pairing_secret.data(), 16};
std::string_view sign {client_pairing_secret.data() + secret.size(), client_pairing_secret.size() - secret.size()};
auto x509 = crypto::x509(client.cert);
if (!x509) {
fail_pair(sess, tree, "Invalid client certificate");
return;
}
auto x509_sign = crypto::signature(x509);
std::string data;
data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size());
data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge));
data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign));
data.insert(std::end(data), std::begin(secret), std::end(secret));
auto hash = crypto::hash(data);
// if hash not correct, probably MITM
bool same_hash = hash.size() == sess.clienthash.size() && std::equal(hash.begin(), hash.end(), sess.clienthash.begin());
auto verify = crypto::verify256(crypto::x509(client.cert), secret, sign);
if (same_hash && verify) {
tree.put("root.paired", 1);
if (is_placeholder_client_name(client.name)) {
BOOST_LOG(warning) << "PIN submitted with reserved client name '" << client.name << "'; refusing to pair with placeholder identity.";
remove_session(sess);
return;
}
auto named_cert_p = std::make_shared<crypto::named_cert_t>();
named_cert_p->name = display_client_name_for_session(client.name, std::string {}, "Moonlight Client"s);
for (char &c : named_cert_p->name) {
if (c == '(') {
c = '[';
} else if (c == ')') {
c = ']';
}
}
named_cert_p->cert = std::move(client.cert);
named_cert_p->uuid = uuid_util::uuid_t::generate().string();
// If the device is the first one paired with the server, assign full permission.
bool first_client = false;
{
std::lock_guard<std::mutex> lock(client_mutex);
first_client = client_root.named_devices.empty();
}
if (first_client) {
named_cert_p->perm = PERM::_all;
} else {
named_cert_p->perm = PERM::_default;
}
named_cert_p->enable_legacy_ordering = true;
named_cert_p->allow_client_commands = true;
named_cert_p->always_use_virtual_display = false;
named_cert_p->output_name_override.clear();
if (!add_authorized_client(named_cert_p)) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 503);
remove_session(sess);
return;
}
if (pending_certs) {
pending_certs->raise(crypto::x509(named_cert_p->cert));
}
} else {
tree.put("root.paired", 0);
BOOST_LOG(warning) << "Pair attempt failed due to same_hash: " << same_hash << ", verify: " << verify;
}
remove_session(sess);
tree.put("root.<xmlattr>.status_code", 200);
}
void clientpairingsecret(pair_session_t &sess, pt::ptree &tree, const std::string &client_pairing_secret) {
clientpairingsecret(sess, pending_cert_queue, tree, client_pairing_secret);
}
template<class T>
struct tunnel;
template<>
struct tunnel<SunshineHTTPS> {
static auto constexpr to_string = "HTTPS"sv;
};
template<>
struct tunnel<SimpleWeb::HTTP> {
static auto constexpr to_string = "NONE"sv;
};
inline verified_client_t get_verified_cert(req_https_t request) {
if (auto remembered = get_remembered_tls_client_identity(request)) {
std::lock_guard<std::mutex> lock(client_mutex);
for (const auto &named_cert_p : client_root.named_devices) {
if (named_cert_p && !!named_cert_p->perm && named_cert_p->uuid == remembered->uuid) {
return *named_cert_p;
}
}
}
return std::nullopt;
}
inline PERM client_perm(const verified_client_t &verified_client) {
return verified_client ? verified_client->perm : PERM::_no;
}
inline bool has_client_perm(const verified_client_t &verified_client, PERM perm) {
return !!(client_perm(verified_client) & perm);
}
// `quiet` keeps high-frequency background polls (e.g. ServerCommand) at debug level
// so a client that legitimately lacks the permission doesn't flood the log.
inline void log_permission_denied(std::string_view action, std::string_view perm_label, const verified_client_t &verified_client, bool quiet = false) {
const auto perm = client_perm(verified_client);
auto &log_target = quiet ? debug : warning;
if (verified_client) {
BOOST_LOG(log_target) << "Permission " << action << " denied for client [" << verified_client->name << "]: it lacks the \"" << perm_label
<< "\" permission (current permission mask 0x" << std::hex << (uint32_t) perm << std::dec
<< "). Grant it in the Web UI under Client Management.";
} else {
BOOST_LOG(log_target) << "Permission " << action << " denied: HTTPS client certificate not recognized. The device may need to be re-paired.";
}
}
// Sent to Moonlight as the launch/resume/cancel status_message, which most clients
// show verbatim in their error dialog, so it has to explain the fix rather than just fail.
inline std::string permission_denied_status_message(const verified_client_t &verified_client, std::string_view perm_label) {
if (!verified_client) {
return "Permission denied: this device's certificate is not recognized by the host. Unpair and pair this device again.";
}
std::string msg;
msg.reserve(160);
msg += "Permission denied: this device lacks the \"";
msg += perm_label;
msg += "\" permission. Enable it on the host in the ArtLight Server Web UI under Client Management.";
return msg;
}
template<class T>
void print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
BOOST_LOG(verbose) << "HTTP "sv << request->method << ' ' << request->path << " tunnel="sv << tunnel<T>::to_string;
if (!request->header.empty()) {
BOOST_LOG(verbose) << "Headers:"sv;
for (auto &[name, val] : request->header) {
BOOST_LOG(verbose) << name << " -- " << val;
}
}
auto query = request->parse_query_string();
if (!query.empty()) {
BOOST_LOG(verbose) << "Query Params:"sv;
for (auto &[name, val] : query) {
BOOST_LOG(verbose) << name << " -- " << val;
}
}
}
template<class T>
void not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
pt::ptree tree;
tree.put("root.<xmlattr>.status_code", 404);
std::ostringstream data;
pt::write_xml(data, tree);
response->write(SimpleWeb::StatusCode::client_error_not_found, data.str());
response->close_connection_after_response = true;
}
template<class T>
void unpair(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
std::lock_guard pairing_lock {pairing_sessions_mutex};
pt::ptree tree;
auto fg = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto args = request->parse_query_string();
auto unique_id = get_arg(args, "uniqueid", "");
// An unauthenticated unpair request cannot replace the PIN approval target.
const bool cleaned_pending_pair = false;
bool removed = false;
if constexpr (std::is_same_v<T, SunshineHTTPS>) {
if (auto verified_client = get_verified_cert(request)) {
removed = unpair_client(verified_client->uuid);
}
}
tree.put("root.unpaired", removed ? 1 : 0);
tree.put("root.<xmlattr>.status_code", 200);
if (cleaned_pending_pair) {
BOOST_LOG(info) << "Cleaned pending pairing session during unpair request";
}
}
template<class T>
void pair(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
std::lock_guard pairing_lock {pairing_sessions_mutex};
pt::ptree tree;
auto fg = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
if (!config::sunshine.enable_pairing) {
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Pairing is disabled for this instance");
return;
}
auto args = request->parse_query_string();
if (args.find("uniqueid"s) == std::end(args)) {
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Missing uniqueid parameter");
return;
}
auto uniqID {get_arg(args, "uniqueid")};
if (!pairing_policy::valid_unique_id(uniqID)) {
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Invalid uniqueid");
return;
}
expire_pairing_sessions_locked(std::chrono::steady_clock::now());
args_t::const_iterator it;
if (it = args.find("phrase"); it != std::end(args)) {
if (it->second == "getservercert"sv) {
const auto client_certificate = get_arg(args, "clientcert", "");
const auto salt = get_arg(args, "salt", "");
const auto existing = map_id_sess.find(uniqID);
const bool replacing = existing != map_id_sess.end();
const bool same_identity = replacing && pairing_policy::valid_hex_field(client_certificate, 2) &&
existing->second.client.cert == util::from_hex_vec(client_certificate, true);
const auto admission = pairing_policy::admit_pending_session(uniqID, client_certificate, salt, map_id_sess.size(), replacing, same_identity);
if (!admission.accepted) {
tree.put("root.<xmlattr>.status_code", admission.failure_message == "Too many pending pairing sessions"sv ? 429 : 400);
tree.put("root.<xmlattr>.status_message", admission.failure_message);
return;
}
if (replacing) {
close_pending_pairing_response(existing->second, 409, "Pairing request replaced by a newer request from the same client"sv);
map_id_sess.erase(existing);
}
pair_session_t sess;
auto deviceName {get_arg(args, "devicename")};
if (deviceName == "roth"sv) {
deviceName = "Legacy Moonlight Client";
}
sess.client.uniqueID = std::move(uniqID);
sess.client.name = std::move(deviceName);
sess.client.cert = util::from_hex_vec(client_certificate, true);
BOOST_LOG(verbose) << sess.client.cert;
auto session_id = sess.client.uniqueID;
auto [ptr, inserted] = map_id_sess.emplace(std::move(session_id), std::move(sess));
if (!inserted) {
tree.put("root.<xmlattr>.status_code", 429);
tree.put("root.<xmlattr>.status_message", "Too many pending pairing sessions");
return;
}
ptr->second.async_insert_pin.salt = salt;
auto it = args.find("otpauth");
if (it != std::end(args)) {
if (one_time_pin.empty() || (std::chrono::steady_clock::now() - otp_creation_time > OTP_EXPIRE_DURATION)) {
one_time_pin.clear();
otp_passphrase.clear();
otp_device_name.clear();
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "OTP auth not available.");
} else {
auto hash = util::hex(crypto::hash(one_time_pin + ptr->second.async_insert_pin.salt + otp_passphrase), true);
if (hash.to_string_view() == it->second) {
if (!otp_device_name.empty()) {
ptr->second.client.name = std::move(otp_device_name);
}
getservercert(ptr->second, tree, one_time_pin);
one_time_pin.clear();
otp_passphrase.clear();
otp_device_name.clear();
return;
}
}
// Always return positive, attackers will fail in the next steps.
getservercert(ptr->second, tree, crypto::rand(16));
return;
}
if (config::sunshine.flags[config::flag::PIN_STDIN]) {
std::string pin;
std::cout << "Please insert pin: "sv;
std::getline(std::cin, pin);
getservercert(ptr->second, tree, pin);
} else {
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_require_pin();
#endif
ptr->second.async_insert_pin.response = std::move(response);
fg.disable();
return;
}
} else if (it->second == "pairchallenge"sv) {
tree.put("root.paired", 1);
tree.put("root.<xmlattr>.status_code", 200);
return;
}
}
auto sess_it = map_id_sess.find(uniqID);
if (sess_it == std::end(map_id_sess)) {
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Invalid uniqueid");
return;
}
if (it = args.find("clientchallenge"); it != std::end(args)) {
if (!pairing_policy::valid_hex_field(it->second, 2)) {
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Invalid clientchallenge");
return;
}
auto challenge = util::from_hex_vec(it->second, true);
clientchallenge(sess_it->second, tree, challenge);
} else if (it = args.find("serverchallengeresp"); it != std::end(args)) {
if (!pairing_policy::valid_hex_field(it->second, 2)) {
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Invalid serverchallengeresp");
return;
}
auto encrypted_response = util::from_hex_vec(it->second, true);
serverchallengeresp(sess_it->second, tree, encrypted_response);
} else if (it = args.find("clientpairingsecret"); it != std::end(args)) {
if (!pairing_policy::valid_hex_field(it->second, 34)) {
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Invalid clientpairingsecret");
return;
}
auto pairingsecret = util::from_hex_vec(it->second, true);
clientpairingsecret(sess_it->second, tree, pairingsecret);
} else {
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "Invalid pairing request");
}
}
bool pin(std::string pin, std::string name) {
std::lock_guard pairing_lock {pairing_sessions_mutex};
expire_pairing_sessions_locked(std::chrono::steady_clock::now());
pt::ptree tree;
if (map_id_sess.empty()) {
BOOST_LOG(warning) << "PIN submitted but no pending pairing session exists";
return false;
}
// ensure pin is 4 digits
if (pin.size() != 4) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put(
"root.<xmlattr>.status_message",
std::format("Pin must be 4 digits, {} provided", pin.size())
);
return false;
}
// ensure all pin characters are numeric
if (!std::all_of(pin.begin(), pin.end(), ::isdigit)) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Pin must be numeric");
return false;
}
if (map_id_sess.size() != 1 || map_id_sess.begin()->second.last_phase != PAIR_PHASE::NONE) {
BOOST_LOG(warning) << "PIN submitted but no active pending pairing session is ready";
return false;
}
auto sess_it = map_id_sess.begin();
auto &sess = sess_it->second;
if (sess.async_insert_pin.salt.size() < 32) {
BOOST_LOG(warning) << "PIN submitted but pending pairing session has an invalid salt";
remove_session(sess);
return false;
}
getservercert(sess, tree, pin);
if (!name.empty()) {
sess.client.name = name;
}
// response to the request for pin
std::ostringstream data;
pt::write_xml(data, tree);
auto &async_response = sess.async_insert_pin.response;
// Keep Content-Length on this delayed response; Moonlight waits for a complete body.
if (async_response.has_left() && async_response.left()) {
async_response.left()->write(data.str());
} else if (async_response.has_right() && async_response.right()) {
async_response.right()->write(data.str());
} else {
BOOST_LOG(warning) << "PIN submitted but pending pairing session has no response channel";
remove_session(sess);
return false;
}
// reset async_response
async_response = std::decay_t<decltype(async_response.left())>();
// response to the current request
return true;
}
template<class T>
void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
int pair_status = 0;
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
const auto verified_client = get_verified_cert(request);
pair_status = !resolve_client_identity(request, verified_client).uuid.empty();
}
auto local_endpoint = request->local_endpoint();
pt::ptree tree;
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.hostname", config::nvhttp.sunshine_name);
tree.put("root.appversion", VERSION);
tree.put("root.GfeVersion", GFE_VERSION);
tree.put("root.uniqueid", http::unique_id);
tree.put("root.HttpsPort", net::map_port(PORT_HTTPS));
tree.put("root.ExternalPort", net::map_port(PORT_HTTP));
// Optional Moonlight extension. These describe integration/configuration,
// not proof that a particular game's limiter was successfully applied.
// Virtual-display limiting is independent of the manual limiter switch.
// Report the configured default display path; app/client overrides and
// per-game provider success are resolved later at launch.
const bool automatic_virtual_limiter =
config::video.virtual_display_mode != config::video_t::virtual_display_mode_e::disabled &&
config::frame_limiter.virtual_display_limiter_enabled();
#ifdef _WIN32
tree.put("root.FrameLimiterSupported", 1);
tree.put("root.FrameLimiterEnabled", automatic_virtual_limiter || (config::frame_limiter.enable &&
!boost::iequals(config::frame_limiter.provider, "none") &&
!boost::iequals(config::frame_limiter.provider, "disabled")) ? 1 : 0);
tree.put("root.VirtualDisplayFrameLimiterEnabled", config::frame_limiter.virtual_display_limiter_enabled() ? 1 : 0);
tree.put("root.FrameLimiterFpsLimitMilliHz", config::frame_limiter.fps_limit_millihz);
#elif defined(__linux__)
const bool limiter_provider_selected = platf::mangohud::linux_provider_selected(config::frame_limiter.provider);
tree.put("root.FrameLimiterSupported", 1);
tree.put("root.FrameLimiterEnabled", limiter_provider_selected && (config::frame_limiter.enable || automatic_virtual_limiter) ? 1 : 0);
tree.put("root.VirtualDisplayFrameLimiterEnabled", limiter_provider_selected && config::frame_limiter.virtual_display_limiter_enabled() ? 1 : 0);
tree.put("root.FrameLimiterFpsLimitMilliHz", config::frame_limiter.fps_limit_millihz);
#else
tree.put("root.FrameLimiterSupported", 0);
tree.put("root.FrameLimiterEnabled", 0);
tree.put("root.VirtualDisplayFrameLimiterEnabled", 0);
tree.put("root.FrameLimiterFpsLimitMilliHz", 0);
#endif
#ifdef _WIN32
// Artemis checks /serverinfo before it offers virtual-display launches.
// Publish the Windows capability and its current driver state for both
// paired and unpaired discovery requests.
tree.put("root.VirtualDisplayCapable", true);
tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus.load(std::memory_order_acquire) == VDISPLAY::DRIVER_STATUS::OK);
tree.put("root.VirtualDisplayHDRCapable", true);
#elif defined(__linux__)
// Independent monitor support is separate from compositor HDR capture.
const auto display_capabilities = platf::linux_display::backend().capabilities();
tree.put("root.VirtualDisplayCapable", display_capabilities.independent_outputs);
tree.put("root.VirtualDisplayDriverReady", display_capabilities.independent_outputs_ready);
tree.put("root.VirtualDisplayHDRCapable", display_capabilities.independent_outputs_hdr);
#else
tree.put("root.VirtualDisplayCapable", false);
tree.put("root.VirtualDisplayDriverReady", false);
tree.put("root.VirtualDisplayHDRCapable", false);
#endif
// Only include the MAC address for requests sent from paired clients over HTTPS.
// For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore.
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address())));
auto verified_client = get_verified_cert(request);
const auto perm = client_perm(verified_client);
if (!!(perm & PERM::server_cmd)) {
pt::ptree &root_node = tree.get_child("root");
if (config::sunshine.server_cmds.size() > 0) {
// Broadcast server_cmds
for (const auto &cmd : config::sunshine.server_cmds) {
pt::ptree cmd_node;
cmd_node.put_value(cmd.cmd_name);
root_node.push_back(std::make_pair("ServerCommand", cmd_node));
}
}
} else {
log_permission_denied("Get ServerCommand"sv, "Run server commands"sv, verified_client, true);
}
tree.put("root.Permission", std::to_string((uint32_t) perm));
} else {
tree.put("root.mac", "00:00:00:00:00:00");
tree.put("root.Permission", "0");
}
// Moonlight clients track LAN IPv6 addresses separately from LocalIP which is expected to
// always be an IPv4 address. If we return that same IPv6 address here, it will clobber the
// stored LAN IPv4 address. To avoid this, we need to return an IPv4 address in this field
// when we get a request over IPv6.
//
// HACK: We should return the IPv4 address of local interface here, but we don't currently
// have that implemented. For now, we will emulate the behavior of GFE+GS-IPv6-Forwarder,
// which returns 127.0.0.1 as LocalIP for IPv6 connections. Moonlight clients with IPv6
// support know to ignore this bogus address.
if (local_endpoint.address().is_v6() && !local_endpoint.address().to_v6().is_v4_mapped()) {
tree.put("root.LocalIP", "127.0.0.1");
} else {
tree.put("root.LocalIP", net::addr_to_normalized_string(local_endpoint.address()));
}
#ifdef _WIN32
const auto advertised_video = advertised_encoder_capabilities_for_http().advertised;
#else
const auto advertised_video = video::advertised_encoder_capabilities(true);
#endif
tree.put("root.MaxLumaPixelsHEVC", advertised_video.hevc_mode > 1 ? "1869449984" : "0");
uint32_t codec_mode_flags = SCM_H264;
if (advertised_video.yuv444_for_codec[0]) {
codec_mode_flags |= SCM_H264_HIGH8_444;
}
if (advertised_video.hevc_mode >= 2) {
codec_mode_flags |= SCM_HEVC;
if (advertised_video.yuv444_for_codec[1]) {
codec_mode_flags |= SCM_HEVC_REXT8_444;
}
}
if (advertised_video.hevc_mode >= 3) {
codec_mode_flags |= SCM_HEVC_MAIN10;
if (advertised_video.yuv444_for_codec[1]) {
codec_mode_flags |= SCM_HEVC_REXT10_444;
}
}
if (advertised_video.av1_mode >= 2) {
codec_mode_flags |= SCM_AV1_MAIN8;
if (advertised_video.yuv444_for_codec[2]) {
codec_mode_flags |= SCM_AV1_HIGH8_444;
}
}
if (advertised_video.av1_mode >= 3) {
codec_mode_flags |= SCM_AV1_MAIN10;
if (advertised_video.yuv444_for_codec[2]) {
codec_mode_flags |= SCM_AV1_HIGH10_444;
}
}
if (advertised_video.pyrowave_mode >= 2) {
codec_mode_flags |= pyrowave::protocol::SCM_MASK_PYROWAVE;
}
tree.put("root.ServerCodecModeSupport", codec_mode_flags);
tree.put("root.PairStatus", pair_status);
// This is the host's outbound physical link, not measured end-to-end
// throughput. Unknown, wireless, and virtual routes report zero.
if (pair_status && advertised_video.pyrowave_mode >= 2) {
std::uint64_t link_bps = 0;
#if defined(_WIN32) || defined(__linux__)
link_bps = platf::routed_link_bps(local_endpoint.address(), request->remote_endpoint().address());
#endif
tree.put("root.PyroWaveHostLinkMbps", link_bps / 1'000'000);
tree.put("root.PyroWaveBandwidthProbeBytes", 32U * 1024U * 1024U);
}
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
const int current_appid = proc::proc.running();
const auto active_session = proc::proc.active_session_guard();
const auto verified_client = get_verified_cert(request);
const auto identity = resolve_client_identity(request, verified_client);
const auto remote_gate = remote_role_gate_snapshot_for_client(identity.uuid);
const auto current_app = proc::proc.resolve_app(current_appid);
// The view permission is what exposes_active_game() gates on, so it has to be
// filled in here as well. It was left at its default (false) on this path, which
// read as "this caller may not view" for every client — so the active game was
// never reported and every other device was told the host was free mid-stream.
// Same fields as the /launch and /resume callers further down.
const remote_session::caller_t caller {
.uuid = identity.uuid,
.paired = !identity.uuid.empty(),
.may_view = has_client_perm(verified_client, PERM::view),
};
const remote_session::game_t game {
.running = current_appid > 0 && current_appid != proc::input_only_app_id,
.owner_uuid = active_session.client_uuid,
.generation = active_session_generation(active_session),
.app = current_app ? remote_session::app_t {static_cast<std::int32_t>(util::from_view(current_app->id)), current_app->uuid, current_app->name, false} : remote_session::app_t {},
};
bool replacement_confirmation_active = false;
if (caller.paired) {
if (remote_gate.active) {
remote_session::clear_app_replacement_confirmation(caller.uuid);
} else if (config::video.remote_monitor_confirm_app_replacement) {
replacement_confirmation_active = remote_session::app_replacement_confirmation_active(caller.uuid, game.generation);
}
}
// Input-only sessions stay on their existing resume path. Other active
// games are visible only to their owner so another paired caller gets
// the remote-session controls instead of a globally busy host.
const bool expose_active_game =
remote_session::exposes_active_game(caller, game, remote_gate.owner, remote_gate.active, replacement_confirmation_active) &&
!(config::input.enable_input_only_mode && current_appid != proc::input_only_app_id);
tree.put("root.currentgame", expose_active_game ? current_appid : 0);
tree.put("root.currentgameuuid", expose_active_game ? proc::proc.get_running_app_uuid() : "");
tree.put("root.state", expose_active_game ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE");
} else {
tree.put("root.currentgame", 0);
tree.put("root.currentgameuuid", "");
tree.put("root.state", "SUNSHINE_SERVER_FREE");
}
std::ostringstream data;
pt::write_xml(data, tree);
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Cache-Control", "no-store, no-cache, must-revalidate");
headers.emplace("Pragma", "no-cache");
response->write(SimpleWeb::StatusCode::success_ok, data.str(), headers);
response->close_connection_after_response = true;
}
bool get_client_prefer_10bit_sdr(const std::string &uuid) {
std::lock_guard<std::mutex> lock(client_mutex);
for (auto &named_cert : client_root.named_devices) {
if (named_cert->uuid == uuid) {
return named_cert->prefer_10bit_sdr;
}
}
return false;
}
verified_client_t get_client_snapshot_by_uuid(const std::string &uuid) {
if (uuid.empty()) {
return std::nullopt;
}
std::lock_guard<std::mutex> lock(client_mutex);
for (auto &named_cert : client_root.named_devices) {
if (named_cert->uuid == uuid) {
return *named_cert;
}
}
return std::nullopt;
}
nlohmann::json get_all_clients() {
nlohmann::json named_cert_nodes = nlohmann::json::array();
const client_t client = client_root_snapshot();
std::list<std::string> connected_uuids = rtsp_stream::get_all_session_uuids();
for (auto &named_cert : client.named_devices) {
nlohmann::json named_cert_node;
named_cert_node["name"] = named_cert->name;
named_cert_node["uuid"] = named_cert->uuid;
named_cert_node["display_mode"] = named_cert->display_mode;
if (!named_cert->hdr_profile.empty()) {
named_cert_node["hdr_profile"] = named_cert->hdr_profile;
}
named_cert_node["output_name_override"] = named_cert->output_name_override;
named_cert_node["virtual_display_mode"] = named_cert->virtual_display_mode_override;
named_cert_node["virtual_display_layout"] = named_cert->virtual_display_layout_override;
named_cert_node["perm"] = static_cast<uint32_t>(named_cert->perm);
named_cert_node["enable_legacy_ordering"] = named_cert->enable_legacy_ordering;
named_cert_node["allow_client_commands"] = named_cert->allow_client_commands;
named_cert_node["always_use_virtual_display"] = named_cert->always_use_virtual_display;
named_cert_node["prefer_10bit_sdr"] = named_cert->prefer_10bit_sdr;
if (named_cert->last_seen.has_value()) {
named_cert_node["last_seen"] = *named_cert->last_seen;
}
if (!named_cert->config_overrides.empty()) {
named_cert_node["config_overrides"] = named_cert->config_overrides;
}
// Add "do" commands if available
if (!named_cert->do_cmds.empty()) {
nlohmann::json do_cmds_node = nlohmann::json::array();
for (const auto &cmd : named_cert->do_cmds) {
do_cmds_node.push_back(crypto::command_entry_t::serialize(cmd));
}
named_cert_node["do"] = do_cmds_node;
}
// Add "undo" commands if available
if (!named_cert->undo_cmds.empty()) {
nlohmann::json undo_cmds_node = nlohmann::json::array();
for (const auto &cmd : named_cert->undo_cmds) {
undo_cmds_node.push_back(crypto::command_entry_t::serialize(cmd));
}
named_cert_node["undo"] = undo_cmds_node;
}
// Determine connection status
bool connected = false;
if (connected_uuids.empty()) {
connected = false;
} else {
for (auto it = connected_uuids.begin(); it != connected_uuids.end(); ++it) {
if (*it == named_cert->uuid) {
connected = true;
connected_uuids.erase(it);
break;
}
}
}
named_cert_node["connected"] = connected;
named_cert_nodes.push_back(named_cert_node);
}
return named_cert_nodes;
}
nlohmann::json get_remote_display_layout() {
const auto client = client_root_snapshot();
try {
return remote_display_topology::normalize_layout(nlohmann::json::parse(client.remote_display_layout_json));
} catch (...) {
return {
{"version", remote_display_topology::layout_version},
{"placements", nlohmann::json::object()},
};
}
}
bool set_remote_display_layout(const nlohmann::json &layout, std::string &error) {
std::vector<std::string> known_clients;
{
std::lock_guard<std::mutex> lock(client_mutex);
for (const auto &client : client_root.named_devices) {
if (client) {
known_clients.push_back(client->uuid);
}
}
}
if (!remote_display_topology::validate_layout(
layout,
known_clients,
remote_display_topology::instance().physical_node_ids(),
error)) {
return false;
}
const auto canonical_layout = remote_display_topology::normalize_layout(layout);
{
std::lock_guard<std::mutex> lock(client_mutex);
client_root.remote_display_layout_json = canonical_layout.dump();
}
save_state();
remote_display_topology::instance().set_layout(canonical_layout);
return true;
}
void mark_client_last_seen(const std::string &uuid) {
if (uuid.empty()) {
return;
}
bool changed = false;
{
std::lock_guard<std::mutex> lock(client_mutex);
for (auto &named_cert : client_root.named_devices) {
if (named_cert->uuid != uuid) {
continue;
}
const auto now = now_seconds();
if (named_cert->last_seen.has_value() && *named_cert->last_seen == now) {
return;
}
named_cert->last_seen = now;
changed = true;
break;
}
}
if (changed && !config::sunshine.flags[config::flag::FRESH_STATE]) {
save_state();
}
}
void applist(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Cache-Control", "no-store, no-cache, must-revalidate");
headers.emplace("Pragma", "no-cache");
response->write(SimpleWeb::StatusCode::success_ok, data.str(), headers);
response->close_connection_after_response = true;
});
auto &apps = tree.add_child("root", pt::ptree {});
apps.put("<xmlattr>.status_code", 200);
auto verified_client = get_verified_cert(request);
if (has_client_perm(verified_client, PERM::_all_actions)) {
auto current_appid = proc::proc.running();
// Only expose the special "Terminate" entry (and the "busy minimal list" behavior)
// when input-only mode is enabled. Otherwise, Moonlight handles terminate/resume UI
// without needing a fake app entry in the list.
const bool show_terminate_entry =
config::input.enable_input_only_mode && current_appid > 0 && current_appid != proc::input_only_app_id;
const bool should_hide_inactive_apps = show_terminate_entry;
auto app_list = proc::proc.get_apps();
std::vector<const proc::ctx_t *> visible_apps;
visible_apps.reserve(app_list.size());
for (const auto &app : app_list) {
auto appid = util::from_view(app.id);
bool include = true;
if (should_hide_inactive_apps) {
if (
appid != current_appid && appid != proc::input_only_app_id && appid != proc::terminate_app_id
) {
include = false;
}
} else if (appid == proc::terminate_app_id) {
include = show_terminate_entry;
}
if (!include) {
continue;
}
visible_apps.push_back(&app);
}
std::vector<remote_session::app_t> configured_apps;
configured_apps.reserve(visible_apps.size());
for (const auto *app : visible_apps) {
const auto appid = util::from_view(app->id);
// The source remote-session controls replace ArtLight's legacy
// synthetic input/terminate entries; do not advertise both sets.
if (appid == proc::input_only_app_id || appid == proc::terminate_app_id) {
continue;
}
configured_apps.push_back({static_cast<std::int32_t>(appid), app->uuid, app->name, false});
}
const auto current_app = proc::proc.resolve_app(current_appid);
const auto active_session = proc::proc.active_session_guard();
const auto identity = resolve_client_identity(request, verified_client);
const remote_session::caller_t caller {
.uuid = identity.uuid,
.paired = !identity.uuid.empty(),
.may_view = has_client_perm(verified_client, PERM::view),
.may_launch = has_client_perm(verified_client, PERM::launch),
// ArtLight has no separate termination permission; normal app
// termination is authorized by the launch permission too.
.may_terminate = has_client_perm(verified_client, PERM::launch),
};
const remote_session::game_t game {
.running = current_appid > 0 && current_appid != proc::input_only_app_id,
.owner_uuid = active_session.client_uuid,
.generation = active_session_generation(active_session),
.app = current_app ? remote_session::app_t {static_cast<std::int32_t>(util::from_view(current_app->id)), current_app->uuid, current_app->name, false} : remote_session::app_t {},
};
const auto remote_gate = remote_role_gate_snapshot_for_client(identity.uuid);
const auto projection = remote_session::project(
caller,
game,
remote_gate.owner,
configured_apps,
remote_gate.active
);
const bool enable_legacy_ordering = config::sunshine.legacy_ordering && verified_client->enable_legacy_ordering;
size_t bits = 0;
if (enable_legacy_ordering && !projection.catalogue.empty()) {
bits = zwpad::pad_width_for_count(projection.catalogue.size());
}
#ifdef _WIN32
const auto advertised_video = advertised_encoder_capabilities_for_http().advertised;
#else
const auto advertised_video = video::advertised_encoder_capabilities(true);
#endif
const bool is_hdr_supported = advertised_video.hevc_mode == 3 || advertised_video.av1_mode == 3;
for (size_t i = 0; i < projection.catalogue.size(); ++i) {
const auto &entry = projection.catalogue[i];
const auto configured = std::find_if(visible_apps.begin(), visible_apps.end(), [&entry](const auto *candidate) {
return candidate->uuid == entry.uuid;
});
std::string app_name = entry.title;
if (enable_legacy_ordering && bits > 0) {
app_name = zwpad::pad_for_ordering(app_name, bits, i);
}
pt::ptree app_node;
app_node.put("IsHdrSupported"s, is_hdr_supported ? 1 : 0);
app_node.put("AppTitle"s, app_name);
app_node.put("UUID", entry.uuid);
app_node.put("IDX", entry.synthetic ? std::to_string(entry.id) : (configured == visible_apps.end() ? "" : (*configured)->idx));
app_node.put("ID", entry.id);
app_node.put(
"ArtVersion",
remote_session::identify(entry.id, entry.uuid) == remote_session::control_e::running_game && current_app ? current_app->art_version :
entry.synthetic ? (configured == visible_apps.end() ? "remote-session-v6" : (*configured)->art_version) :
(configured == visible_apps.end() ? "" : (*configured)->art_version)
);
apps.push_back(std::make_pair("App", std::move(app_node)));
}
} else {
log_permission_denied("ListApp"sv, "List applications"sv, verified_client);
pt::ptree app_node;
app_node.put("IsHdrSupported"s, 0);
app_node.put("AppTitle"s, "Permission denied - enable \"List applications\" for this device in the host's Web UI");
app_node.put("UUID", "");
app_node.put("IDX", "0");
app_node.put("ID", "114514");
apps.push_back(std::make_pair("App", std::move(app_node)));
return;
}
}
void resume(bool &host_audio, resp_https_t response, req_https_t request, int current_appid, bool normal_app_transition = true, bool launched_from_applist = false);
void cancel(resp_https_t response, req_https_t request);
void launch(bool &host_audio, resp_https_t response, req_https_t request, int current_appid) {
print_req<SunshineHTTPS>(request);
#ifdef _WIN32
// Keep encoder probes blocked across the complete failure unwind: virtual
// display removal, response publication, and any final helper restore.
stream::session::cleanup_reservation_t cleanup_reservation;
#endif
pt::ptree tree;
bool revert_display_configuration = false;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
if (revert_display_configuration) {
display_helper_integration::revert();
}
});
auto args = request->parse_query_string();
if (
args.find("rikey"s) == std::end(args) ||
args.find("rikeyid"s) == std::end(args) ||
args.find("localAudioPlayMode"s) == std::end(args) ||
(args.find("appid"s) == std::end(args) && args.find("appuuid"s) == std::end(args))
) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Missing a required launch parameter");
return;
}
const auto verified_client = get_verified_cert(request);
const auto request_client_identity = resolve_client_identity(request, verified_client);
if (!verified_client || request_client_identity.uuid.empty()) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "A paired TLS client identity is required");
return;
}
auto appid_str = get_arg(args, "appid", "0");
auto appuuid_str = get_arg(args, "appuuid", "");
// Synthetic controls are host actions, never configured applications.
// Identify them before resolve_app() so they cannot collide with an
// apps.json id and accidentally launch a real process.
const auto synthetic_control = remote_session::identify(util::from_view(appid_str), appuuid_str, current_appid);
// A secondary client sees the active game in its projected catalogue
// while serverinfo is deliberately presented as free. Selecting it is a
// Resume request, not a second process launch.
if (synthetic_control == remote_session::control_e::none && current_appid > 0) {
if (const auto active_app = proc::proc.resolve_app(current_appid)) {
const bool requests_active_app =
!appuuid_str.empty() ? appuuid_str == active_app->uuid :
util::from_view(appid_str) == util::from_view(active_app->id);
if (requests_active_app) {
remote_session::clear_app_replacement_confirmation(request_client_identity.uuid);
g.disable();
resume(host_audio, std::move(response), std::move(request), current_appid, true, true);
return;
}
}
}
if (synthetic_control != remote_session::control_e::none) {
std::unique_lock remote_transition_lock {remote_http_control_transition_mutex};
const auto active_session = proc::proc.active_session_guard();
const auto active_app = proc::proc.resolve_app(current_appid);
const remote_session::game_t game {
.running = current_appid > 0,
.owner_uuid = active_session.client_uuid,
.generation = active_session_generation(active_session),
.app = active_app ? remote_session::app_t {static_cast<std::int32_t>(util::from_view(active_app->id)), active_app->uuid, active_app->name, false} : remote_session::app_t {},
};
const remote_session::caller_t caller {
.uuid = request_client_identity.uuid,
.paired = true,
.may_view = has_client_perm(verified_client, PERM::view),
.may_launch = has_client_perm(verified_client, PERM::launch),
.may_terminate = has_client_perm(verified_client, PERM::launch),
};
const auto owner = remote_role_gate_snapshot_for_client(request_client_identity.uuid).owner;
const auto decision = remote_session::dispatch(caller, game, owner, synthetic_control);
if (!decision.allowed) {
tree.put("root.resume", 0);
// Authentication and paired-client capabilities were already checked
// above. A denial here is a state/role conflict, not an authorization
// failure, so do not show Moonlight a false permission error.
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "Remote session action conflicts with this client's current session state");
return;
}
remote_session::clear_app_replacement_confirmation(request_client_identity.uuid);
if (decision.resume && decision.resume_role == remote_session::role_e::game && current_appid > 0) {
g.disable();
resume(host_audio, std::move(response), std::move(request), current_appid, true, true);
return;
}
if (decision.terminate) {
const bool caller_owns_active_game = !game.owner_uuid.empty() && request_client_identity.uuid == game.owner_uuid;
if (remote_session::requires_termination_confirmation(
config::video.remote_monitor_terminate_on_first_request,
caller_owns_active_game
)) {
const auto confirmation = remote_session::arm_or_confirm_termination(request_client_identity.uuid, game.generation, game.app.id);
if (confirmation == remote_session::terminate_confirmation_e::prompt) {
BOOST_LOG(info) << "Terminate confirmation armed for client " << request_client_identity.uuid
<< " (app=" << game.app.id << ", generation=" << game.generation << ").";
tree.put("root.resume", 0);
tree.put("root.gamesession", 0);
tree.put("root.<xmlattr>.status_code", 410);
tree.put("root.<xmlattr>.status_message", std::string {remote_session::termination_confirmation_message()});
return;
}
BOOST_LOG(info) << "Terminate confirmation accepted for client " << request_client_identity.uuid
<< " (app=" << game.app.id << ", generation=" << game.generation << ").";
} else {
// Do not let a previous guarded request survive a configuration
// change or an owner request and confirm a later extra-client launch.
remote_session::clear_termination_confirmation(request_client_identity.uuid);
BOOST_LOG(info) << "Terminate accepted on the first request for client " << request_client_identity.uuid
<< (caller_owns_active_game ? " (active-game owner)." : " (configured first-request mode).");
}
const bool disconnected = rtsp_stream::disconnect_game_sessions(false);
stream::session::release_terminated_game_displays();
proc::proc.terminate(false, true);
tree.put("root.resume", 0);
tree.put("root.gamesession", 0);
if (!disconnected) {
BOOST_LOG(info) << "Terminate found no active game transport; closed the paused configured application lifecycle.";
}
const auto completion = *remote_session::successful_control_completion(synthetic_control);
tree.put("root.<xmlattr>.status_code", completion.status_code);
tree.put("root.<xmlattr>.status_message", std::string {completion.status_message});
return;
}
if (synthetic_control == remote_session::control_e::disconnect_input ||
synthetic_control == remote_session::control_e::disconnect_monitor) {
if (decision.already_complete) {
const auto completion = *remote_session::successful_control_completion(synthetic_control);
tree.put("root.resume", 0);
tree.put("root.gamesession", 0);
tree.put("root.<xmlattr>.status_code", completion.status_code);
tree.put("root.<xmlattr>.status_message", std::string {completion.status_message});
return;
}
const auto role = synthetic_control == remote_session::control_e::disconnect_input ?
remote_session::role_e::input :
remote_session::role_e::monitor;
const auto generation = remote_owner_generation(request_client_identity.uuid, role);
if (!generation) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "Remote session generation is no longer owned by this caller");
return;
}
(void) rtsp_stream::disconnect_remote_role_session(
request_client_identity.uuid,
role,
*generation,
false
);
if (role == remote_session::role_e::monitor) {
remote_session::release_monitor(
request_client_identity.uuid,
*generation,
"Disconnect Monitor"
);
}
forget_remote_owner(request_client_identity.uuid, role, *generation);
#if defined(_WIN32) || defined(__linux__)
if (role == remote_session::role_e::monitor) {
cleanup_virtual_display_if_idle();
}
#endif
const auto completion = *remote_session::successful_control_completion(synthetic_control);
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", completion.status_code);
tree.put("root.<xmlattr>.status_message", std::string {completion.status_message});
tree.put("root.gamesession", 0);
return;
}
if (synthetic_control == remote_session::control_e::resume ||
synthetic_control == remote_session::control_e::running_game) {
if (decision.resume_role == remote_session::role_e::game && current_appid > 0) {
g.disable();
resume(host_audio, std::move(response), std::move(request), current_appid, true, true);
return;
}
if (decision.resume_role != remote_session::role_e::monitor ||
!remote_owner_generation(request_client_identity.uuid, remote_session::role_e::monitor)) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "No running game or retained Remote Monitor belongs to this caller");
return;
}
}
if (synthetic_control != remote_session::control_e::input &&
synthetic_control != remote_session::control_e::monitor &&
synthetic_control != remote_session::control_e::resume &&
synthetic_control != remote_session::control_e::running_game) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Unsupported remote session control");
return;
}
const bool no_active_sessions = !has_stream_session_activity();
const auto runtime_app = proc::proc.resolve_app(
"0",
remote_session::synthetic(
synthetic_control == remote_session::control_e::input ? remote_session::control_e::input : remote_session::control_e::monitor
).uuid
);
auto client_settings = verified_client;
if (!client_settings && !request_client_identity.uuid.empty()) {
client_settings = get_client_snapshot_by_uuid(request_client_identity.uuid);
}
std::unordered_map<std::string, std::string> requested_runtime_overrides;
if (runtime_app) {
config::merge_config_overrides(requested_runtime_overrides, runtime_app->config_overrides);
}
if (client_settings) {
config::merge_config_overrides(requested_runtime_overrides, client_settings->config_overrides);
}
if (!no_active_sessions &&
!config::adapter_config_overrides_compatible_with_active(requested_runtime_overrides)) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Another stream is active with a different capture adapter selection");
return;
}
auto previous_runtime_overrides = config::runtime_config_overrides_snapshot();
bool runtime_overrides_applied = false;
bool keep_runtime_overrides = false;
auto runtime_overrides_guard = util::fail_guard([&]() {
if (!runtime_overrides_applied || keep_runtime_overrides) {
return;
}
config::set_runtime_config_overrides(std::move(previous_runtime_overrides));
if (!has_stream_session_activity()) {
config::apply_config_now();
} else {
config::mark_deferred_reload();
}
});
if (no_active_sessions) {
try {
auto overrides = requested_runtime_overrides;
#ifdef _WIN32
if (client_settings &&
!client_settings->hdr_profile.empty() &&
!overrides.contains("rtx_hdr_peak_brightness")) {
if (const auto profile_peak = VDISPLAY::hdr_profile_peak_luminance_nits(client_settings->hdr_profile)) {
overrides.insert_or_assign(
"rtx_hdr_peak_brightness",
std::to_string(std::clamp<std::uint32_t>(*profile_peak, 400, 2000))
);
}
}
#endif
config::set_runtime_config_overrides(std::move(overrides));
runtime_overrides_applied = true;
config::apply_config_now();
} catch (...) {
config::set_runtime_config_overrides(previous_runtime_overrides);
config::apply_config_now();
runtime_overrides_applied = false;
throw;
}
}
auto _hot_apply_gate = config::acquire_apply_read_gate();
if (no_active_sessions) {
config::record_active_adapter_config();
}
const bool is_remote_input = synthetic_control == remote_session::control_e::input;
auto launch_session = make_launch_session_from_snapshot(
false,
is_remote_input,
args,
verified_client,
&request_client_identity,
request,
false
);
launch_session->role_generation = launch_session->id;
launch_session->role = is_remote_input ?
remote_session::role_e::input :
remote_session::role_e::monitor;
launch_session->host_audio = remote_session::uses_host_audio(launch_session->role);
launch_session->continuous_audio = false;
if (launch_session->role == remote_session::role_e::monitor) {
#ifdef __linux__
launch_session->display_power_guard = platf::display_power::acquire();
if (!launch_session->display_power_guard) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "The desktop display could not be woken for streaming");
return;
}
#endif
const auto monitor_fps_hz = launch_session->fps >= 1000 ?
static_cast<int>(std::lround(launch_session->fps / 1000.0)) :
launch_session->fps;
const auto mode = std::format(
"{}x{}@{}",
launch_session->width,
launch_session->height,
monitor_fps_hz
);
const auto monitor = remote_session::activate_or_resume_monitor(
request_client_identity.uuid,
request_client_identity.name,
mode,
rtsp_stream::hdr_capture_required(*launch_session, config::video),
launch_session->role_generation
);
if (monitor.accepted) {
// Publish retryable ownership as well as ready ownership. This makes
// the reduced Resume/Disconnect Monitor catalogue reachable after a
// failed apply, and the next Resume retries with a new generation.
remember_remote_owner(
request_client_identity.uuid,
launch_session->role,
launch_session->role_generation
);
}
if (!monitor.ready || monitor.output.empty()) {
const auto refusal = monitor.error.empty() ? std::string {"Remote Monitor exact capture target is not ready"} : monitor.error;
// A refusal that exists only in the HTTP response leaves nothing behind on the host: the
// client shows a 503 and the log reads clean, which is exactly how a failed test gets
// mistaken for a quiet one. Say it here, with the role and the stage that refused.
BOOST_LOG(warning) << "Remote Monitor refused for client '" << request_client_identity.uuid
<< "' (role=" << (is_remote_input ? "input" : "monitor")
<< ", retryable=" << monitor.retryable << "): " << refusal;
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", monitor.retryable ? 503 : 500);
tree.put("root.<xmlattr>.status_message", refusal);
return;
}
launch_session->virtual_display_hdr_enabled = monitor.hdr_enabled;
if (rtsp_stream::hdr_capture_required(*launch_session, config::video) && !monitor.hdr_enabled) {
launch_session->force_sdr = true;
}
launch_session->remote_capture_output = monitor.output;
BOOST_LOG(info) << "Remote Monitor exact capture target for client '"
<< request_client_identity.uuid << "' is '" << monitor.output << "'.";
}
stream::session::arm_shared_runtime_cleanup(launch_session->virtual_display_guid_bytes);
if (!paired_client_uuid_enabled(launch_session->client_uuid, verified_client->perm)) {
if (launch_session->role == remote_session::role_e::monitor) {
remote_session::release_monitor(request_client_identity.uuid, launch_session->role_generation, "Paired client authorization revoked");
forget_remote_owner(request_client_identity.uuid, launch_session->role, launch_session->role_generation);
#if defined(_WIN32) || defined(__linux__)
cleanup_virtual_display_if_idle_locked();
#endif
}
tree.put("root.resume", 0);
tree.put("root.gamesession", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Paired client authorization was revoked before stream admission");
return;
}
if (!rtsp_stream::launch_session_raise(launch_session)) {
if (launch_session->role == remote_session::role_e::monitor) {
remote_session::release_monitor(
request_client_identity.uuid,
launch_session->role_generation,
"RTSP admission rejected"
);
forget_remote_owner(
request_client_identity.uuid,
launch_session->role,
launch_session->role_generation
);
#if defined(_WIN32) || defined(__linux__)
cleanup_virtual_display_if_idle();
#endif
}
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "RTSP pending session admission was rejected");
return;
}
keep_runtime_overrides = true;
if (launch_session->role != remote_session::role_e::monitor) {
remember_remote_owner(
request_client_identity.uuid,
launch_session->role,
launch_session->role_generation
);
}
tree.put("root.<xmlattr>.status_code", 200);
tree.put(
"root.sessionUrl0",
std::format(
"{}{}:{}",
launch_session->rtsp_url_scheme,
net::addr_to_url_escaped_string(request->local_endpoint().address()),
static_cast<int>(net::map_port(rtsp_stream::RTSP_SETUP_PORT))
)
);
tree.put("root.gamesession", 1);
return;
}
std::unique_lock normal_transition_lock {normal_http_app_transition_mutex};
auto requested_app = proc::proc.resolve_app(appid_str, appuuid_str);
auto appid = requested_app ? util::from_view(requested_app->id) : util::from_view(appid_str);
auto current_app_uuid = proc::proc.get_running_app_uuid();
bool is_input_only = config::input.enable_input_only_mode && (appid == proc::input_only_app_id || (appuuid_str == REMOTE_INPUT_UUID));
auto required_perm = PERM::launch;
BOOST_LOG(verbose) << "Launching app [" << appid_str << "] with UUID [" << appuuid_str << "]";
// BOOST_LOG(verbose) << "QS: " << request->query_string;
// If we have already launched an app, we should allow clients with view permission to join the input only or current app's session.
if (
current_appid > 0 && (appuuid_str != TERMINATE_APP_UUID || appid != proc::terminate_app_id) && (is_input_only || appid == current_appid || (!appuuid_str.empty() && appuuid_str == current_app_uuid))
) {
required_perm = PERM::_allow_view;
}
if (!has_client_perm(verified_client, required_perm)) {
const auto perm_label = required_perm == PERM::launch ? "Launch applications"sv : "View stream"sv;
log_permission_denied("LaunchApp"sv, perm_label, verified_client);
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", permission_denied_status_message(verified_client, perm_label));
return;
}
if (!is_input_only) {
// Special handling for the "terminate" app
if (
(appid == proc::terminate_app_id && proc::terminate_app_id > 0) || appuuid_str == TERMINATE_APP_UUID
) {
proc::proc.terminate(false, true, false, true);
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 410);
tree.put("root.<xmlattr>.status_message", "App terminated.");
return;
}
if (
current_appid > 0 && current_appid != proc::input_only_app_id && ((appid > 0 && appid != current_appid) || (!appuuid_str.empty() && appuuid_str != current_app_uuid))
) {
if (remote_role_gate_snapshot_for_client(request_client_identity.uuid).active) {
remote_session::clear_app_replacement_confirmation(request_client_identity.uuid);
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "Remote Input or Remote Monitor is active; launch Terminate before starting a different app");
return;
}
if (!requested_app || appid <= 0) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "The requested replacement app was not found");
return;
}
if (config::video.remote_monitor_confirm_app_replacement) {
const auto active_session = proc::proc.active_session_guard();
const auto confirmation = remote_session::arm_or_confirm_app_replacement(
request_client_identity.uuid,
active_session_generation(active_session),
static_cast<std::int32_t>(appid)
);
if (confirmation == remote_session::app_replacement_confirmation_e::prompt) {
BOOST_LOG(info) << "App replacement confirmation armed for client " << request_client_identity.uuid
<< " (running_app=" << current_appid << ", requested_app=" << appid << ").";
tree.put("root.resume", 0);
tree.put("root.gamesession", 0);
tree.put("root.<xmlattr>.status_code", 410);
tree.put("root.<xmlattr>.status_message", std::string {remote_session::app_replacement_confirmation_message()});
return;
}
BOOST_LOG(info) << "App replacement confirmation accepted for client " << request_client_identity.uuid
<< " (running_app=" << current_appid << ", requested_app=" << appid << ").";
} else {
remote_session::clear_app_replacement_confirmation(request_client_identity.uuid);
}
BOOST_LOG(info) << "Replacing running app " << current_appid << " with app " << appid
<< " at the request of paired client " << request_client_identity.uuid << ".";
(void) rtsp_stream::disconnect_game_sessions(true);
proc::proc.terminate(false, true, false, true);
}
}
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
bool no_active_sessions =
!has_stream_session_activity() && proc::proc.current_app_id() <= 0;
// Runtime overrides are global process state. Do not reapply them while
// another RTSP/WebRTC session is active, otherwise a second client can mutate
// active stream limits (e.g. fps/encoding-related settings) mid-session.
const bool update_runtime_overrides = no_active_sessions;
// Build the requested layer even when another stream owns the process-wide
// runtime config. A shared capture may only be joined when its adapter pair
// is compatible with the one that is already active.
std::unordered_map<std::string, std::string> requested_runtime_overrides;
if (requested_app) {
config::merge_config_overrides(requested_runtime_overrides, requested_app->config_overrides);
}
auto client_settings = verified_client;
std::string client_uuid = request_client_identity.uuid;
const auto launch_client_uuid = resolve_known_client_uuid_from_launch_id(get_arg(args, "uniqueid", ""));
if (!launch_client_uuid.empty() && launch_client_uuid != client_uuid) {
BOOST_LOG(warning) << "Ignoring launch uniqueid for runtime overrides; TLS caller identity is authoritative.";
}
if (!client_settings && !client_uuid.empty()) {
client_settings = get_client_snapshot_by_uuid(client_uuid);
}
if (client_settings) {
config::merge_config_overrides(requested_runtime_overrides, client_settings->config_overrides);
}
if (!update_runtime_overrides &&
!config::adapter_config_overrides_compatible_with_active(requested_runtime_overrides)) {
BOOST_LOG(warning) << "Rejecting shared launch with a capture adapter selection that differs from the active stream.";
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put(
"root.<xmlattr>.status_message",
"Another stream is active with a different capture adapter selection"
);
return;
}
// Apply per-application runtime config overrides before we build session metadata or
// prepare display/capture so the effective config is used everywhere.
bool runtime_overrides_applied = false;
bool keep_runtime_overrides = false;
auto runtime_overrides_guard = util::fail_guard([&]() {
if (!runtime_overrides_applied || keep_runtime_overrides) {
return;
}
config::clear_runtime_config_overrides();
// Restore global config immediately when safe; otherwise defer.
if (!has_stream_session_activity()) {
config::apply_config_now();
} else {
config::mark_deferred_reload();
}
});
if (update_runtime_overrides) {
try {
auto overrides = requested_runtime_overrides;
#ifdef _WIN32
// "Auto" client peak brightness follows the selected Windows HDR calibration
// profile's MHC2 peak. An explicit app/client override remains authoritative.
if (client_settings &&
!client_settings->hdr_profile.empty() &&
!overrides.contains("rtx_hdr_peak_brightness")) {
if (const auto profile_peak = VDISPLAY::hdr_profile_peak_luminance_nits(client_settings->hdr_profile)) {
const auto effective_peak = std::clamp<std::uint32_t>(*profile_peak, 400, 2000);
overrides.insert_or_assign("rtx_hdr_peak_brightness", std::to_string(effective_peak));
BOOST_LOG(info) << "HDR peak: using " << effective_peak << " nits from MHC2 profile '"
<< client_settings->hdr_profile << "'"
<< (*profile_peak == effective_peak ? "." : " (clamped to supported range).");
} else {
BOOST_LOG(warning) << "HDR peak: profile '" << client_settings->hdr_profile
<< "' has no readable MHC2 peak; using the configured default.";
}
}
#endif
config::set_runtime_config_overrides(std::move(overrides));
runtime_overrides_applied = true;
// Re-apply config so overrides take effect in config::video/config::input/etc.
config::apply_config_now();
} catch (...) {
// If something goes wrong, fall back to global config only.
config::clear_runtime_config_overrides();
config::apply_config_now();
runtime_overrides_applied = true;
}
} else {
BOOST_LOG(debug) << "Launch while an RTSP/WebRTC session is already active; preserving current runtime overrides.";
}
// Prevent interleaving with hot-apply while we prep/start a session.
auto _hot_apply_gate = config::acquire_apply_read_gate();
if (no_active_sessions) {
config::record_active_adapter_config();
}
#ifdef _WIN32
const auto display_startup_deadline =
std::chrono::steady_clock::now() +
display_helper_integration::kStreamStartApplyVerificationTimeout;
const auto display_startup_cancelled = [display_startup_deadline] {
return std::chrono::steady_clock::now() >= display_startup_deadline;
};
// First step on stream start: stop any in-flight helper restore loop immediately.
// This must happen before any other display helper work to prevent restore/crash loops on virtual displays.
(void) display_helper_integration::disarm_pending_restore(
display_startup_cancelled,
display_startup_deadline
);
#endif
const bool allow_display_changes = true;
auto launch_session = make_launch_session_from_snapshot(host_audio, is_input_only, args, verified_client, &request_client_identity, request);
std::optional<std::string> pending_output_override;
auto output_override_guard = util::fail_guard([&]() {
if (pending_output_override) {
config::set_runtime_output_name_override(std::nullopt);
}
});
no_active_sessions = !has_stream_session_activity();
if (no_active_sessions) {
config::set_runtime_output_name_override(std::nullopt);
#if defined(_WIN32) || defined(__linux__)
stream::cancel_paused_display_cleanup();
#endif
}
#ifdef _WIN32
std::optional<video::encoder_probe_adapter_hint_lease_t> pending_adapter_hint;
auto pending_adapter_hint_guard = util::fail_guard([&] {
if (pending_adapter_hint) {
(void) video::clear_pending_virtual_display_adapter_hint(*pending_adapter_hint);
}
});
// Declare teardown before the identity guard so failure unwinds the
// normal-role reservation first. Generic cleanup can then proceed if this
// was the final managed identity, or remain pending for real peers.
auto virtual_display_teardown_guard = util::fail_guard([&]() {
stream::session::cleanup_reservation_t cleanup_reservation;
if (has_stream_session_activity()) {
return;
}
if (!launch_session->virtual_display) {
return;
}
BOOST_LOG(info) << "Launch aborted before session start; removing virtual displays.";
(void) platf::virtual_display_cleanup::run(
"launch_aborted",
config::video.dd.config_revert_on_disconnect
);
});
auto normal_vdd_identity_guard = util::fail_guard([&] {
if (launch_session->normal_vdd_identity_newly_reserved) {
remote_display_topology::instance().rollback_normal_game_identity(
launch_session->client_uuid,
launch_session->normal_vdd_identity_token
);
}
});
prepare_virtual_display_for_session(
launch_session,
no_active_sessions,
allow_display_changes,
is_input_only,
pending_output_override,
pending_adapter_hint,
display_startup_cancelled,
display_startup_deadline
);
if (launch_session->normal_vdd_capacity_rejected) {
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "Remote display capacity is four paired-client identities");
tree.put("root.gamesession", 0);
return;
}
#elif defined(__linux__)
platf::linux_display::prepared_display_t prepared;
if (!launch_session->input_only) {
prepared = platf::linux_display::backend().prepare_session(
*launch_session, no_active_sessions, allow_display_changes
);
if (!prepared.error.empty()) {
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", prepared.error);
tree.put("root.gamesession", 0);
return;
}
}
auto virtual_display_teardown_guard = util::fail_guard([&]() {
stream::session::cleanup_reservation_t cleanup_reservation;
if (!has_stream_session_activity() && launch_session->virtual_display) {
if (remote_display_topology::instance().generic_virtual_display_cleanup_allowed()) {
BOOST_LOG(info) << "Launch aborted before session start; restoring Linux private display state.";
(void) platf::linux_display::backend().revert();
} else {
BOOST_LOG(info) << "Launch aborted while another managed display identity remains; preserving its composed topology.";
}
}
});
auto normal_vdd_identity_guard = util::fail_guard([&] {
rollback_linux_normal_display_identity(launch_session);
});
const auto normal_identity = !launch_session->input_only ?
reserve_linux_normal_display_identity(launch_session) : linux_normal_identity_result_e::not_needed;
if (normal_identity == linux_normal_identity_result_e::capacity_rejected ||
normal_identity == linux_normal_identity_result_e::topology_failed) {
const bool capacity_rejected = normal_identity == linux_normal_identity_result_e::capacity_rejected;
tree.put("root.<xmlattr>.status_code", capacity_rejected ? 409 : 503);
tree.put("root.<xmlattr>.status_message", capacity_rejected ?
"Remote display capacity is four paired-client identities" :
"Failed to compose the Linux private streaming displays");
tree.put("root.gamesession", 0);
return;
}
if (!prepared.output_name.empty()) {
config::set_runtime_output_name_override(prepared.output_name);
pending_output_override = prepared.output_name;
}
#endif
// The display should be restored in case something fails as there are no other sessions.
if (no_active_sessions && !launch_session->input_only) {
revert_display_configuration = true;
#ifdef _WIN32
const bool helper_session_available = display_helper_session_available();
(void) display_helper_integration::disarm_pending_restore(
display_startup_cancelled,
display_startup_deadline
);
auto request = display_helper_integration::helpers::build_request_from_session(config::video, *launch_session);
if (!request) {
BOOST_LOG(warning) << "Display helper: failed to build display configuration request; continuing with existing display.";
}
if (request) {
display_helper_integration::ApplyVerificationTicket verification_ticket;
const bool applied = display_helper_integration::apply(
*request,
&verification_ticket,
display_startup_cancelled,
display_helper_integration::ApplyRetryPolicy::StreamStart,
display_startup_deadline);
launch_session->display_config_preapplied = applied;
if (!applied) {
if (helper_session_available) {
BOOST_LOG(warning) << "Display helper: failed to apply display configuration; continuing with existing display.";
}
} else {
auto gate_promise = std::make_shared<std::promise<rtsp_stream::launch_session_t::display_helper_gate_status_e>>();
launch_session->display_helper_gate = gate_promise->get_future().share();
BOOST_LOG(debug) << "Display helper: gating capture start on helper verification (non-blocking session start).";
std::thread([gate_promise, verification_ticket]() {
const auto status = display_helper_integration::wait_for_apply_verification(
verification_ticket,
display_helper_integration::kStreamStartApplyVerificationTimeout);
rtsp_stream::launch_session_t::display_helper_gate_status_e gate_status =
rtsp_stream::launch_session_t::display_helper_gate_status_e::proceed_gaveup;
if (status == display_helper_integration::ApplyVerificationStatus::Verified) {
gate_status = rtsp_stream::launch_session_t::display_helper_gate_status_e::proceed;
} else if (status == display_helper_integration::ApplyVerificationStatus::Failed) {
gate_status = rtsp_stream::launch_session_t::display_helper_gate_status_e::abort_failed;
}
try {
gate_promise->set_value(gate_status);
} catch (...) {
// best-effort: ignore double-satisfaction
}
}).detach();
}
}
// Apply a per-client HDR profile to physical displays (virtual displays are handled at creation time).
const auto physical_hdr_profile_policy = display_helper_integration::request_policy::evaluate({
.virtual_display = launch_session->virtual_display,
.virtual_display_failed = launch_session->virtual_display_failed,
.hdr_profile_selected = launch_session->hdr_profile && !launch_session->hdr_profile->empty(),
});
if (physical_hdr_profile_policy.apply_hdr_profile_to_physical) {
const auto active_output = config::get_active_output_name();
VDISPLAY::applyHdrProfileToOutput(
launch_session->client_name.c_str(),
launch_session->hdr_profile ? launch_session->hdr_profile->c_str() : nullptr,
active_output.empty() ? nullptr : active_output.c_str()
);
}
#else
display_helper_integration::DisplayApplyBuilder noop_builder;
noop_builder.set_session(*launch_session);
if (!display_helper_integration::apply(noop_builder.build())) {
if (launch_session->virtual_display) {
const std::string status_message = "Failed to activate the Linux private streaming display.";
BOOST_LOG(error) << status_message;
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", status_message);
tree.put("root.gamesession", 0);
return;
}
BOOST_LOG(warning) << "Display helper: failed to apply display configuration; continuing with existing display.";
}
#endif
// Probe encoders again before streaming to ensure our chosen
// encoder matches the active GPU (which could have changed
// due to hotplugging, driver crash, primary monitor change,
// or any number of other factors).
#ifdef _WIN32
bool encoder_probe_failed = false;
if (!video::has_successful_encoder_probe()) {
{
VDISPLAY::ensure_display_result ensure_result {};
auto cleanup_probe_display = util::fail_guard([&ensure_result]() {
VDISPLAY::cleanup_ensure_display(ensure_result);
});
if (VDISPLAY::policy::should_ensure_probe_display(launch_session->virtual_display)) {
ensure_result = VDISPLAY::ensure_display();
if (!ensure_result.owns_temporary_probe_request() && !ensure_result.ready_for_capture()) {
BOOST_LOG(warning)
<< "Launch could not acquire a temporary-display lease; continuing with synthetic encoder validation.";
}
}
encoder_probe_failed = video::probe_encoders();
}
} else {
BOOST_LOG(debug) << "Launch encoder probe skipped (matching selected-GPU cache).";
}
#else
bool encoder_probe_failed = video::probe_encoders();
#endif
if (encoder_probe_failed && !is_input_only) {
const std::string status_message = "Failed to initialize a video encoder on the selected adapter.";
BOOST_LOG(error) << status_message;
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", status_message);
tree.put("root.gamesession", 0);
return;
}
}
no_active_sessions = !has_stream_session_activity();
#ifdef _WIN32
auto pending_vulkan_hdr_layer_guard = util::fail_guard([]() {
rtsp_stream::set_vulkan_hdr_layer_pending_stream(false);
});
#endif
if (is_input_only) {
BOOST_LOG(info) << "Launching input only session..."sv;
launch_session->client_do_cmds.clear();
launch_session->client_undo_cmds.clear();
// Still probe encoders once, if input only session is launched first
// But we're ignoring if it's successful or not
if (no_active_sessions && !proc::proc.virtual_display) {
#ifdef _WIN32
if (has_any_active_display()) {
video::probe_encoders();
}
#else
video::probe_encoders();
#endif
// proc_t::terminate() leaves the app id at -1, so an idle host reports a
// non-positive id rather than 0 once anything has ever run. Testing for 0
// alone stopped input-only sessions from launching after the first app exit.
if (current_appid <= 0) {
proc::proc.launch_input_only();
}
}
} else if (appid > 0 || !appuuid_str.empty()) {
if (appid == current_appid || (!appuuid_str.empty() && appuuid_str == current_app_uuid)) {
// We're basically resuming the same app
BOOST_LOG(debug) << "Resuming app [" << proc::proc.get_last_run_app_name() << "] from launch app path...";
if (!proc::proc.allow_client_commands || !verified_client->allow_client_commands) {
launch_session->client_do_cmds.clear();
launch_session->client_undo_cmds.clear();
}
if (current_appid == proc::input_only_app_id) {
launch_session->input_only = true;
}
} else {
// Resolve once at the request boundary, then carry that canonical app
// through launch. An old artwork-versioned ID is an alias, so looking
// it up again by the raw request value would reject valid legacy clients.
if (!requested_app) {
BOOST_LOG(error) << "Couldn't find app with ID ["sv << appid_str << "] or UUID ["sv << appuuid_str << ']';
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "Cannot find requested application");
tree.put("root.gamesession", 0);
return;
}
if (!requested_app->allow_client_commands || !verified_client->allow_client_commands) {
launch_session->client_do_cmds.clear();
launch_session->client_undo_cmds.clear();
}
#ifdef _WIN32
rtsp_stream::set_vulkan_hdr_layer_pending_stream(rtsp_stream::effective_hdr_requested(*launch_session));
#endif
auto err = proc::proc.execute(*requested_app, launch_session);
if (err) {
tree.put("root.<xmlattr>.status_code", err);
tree.put(
"root.<xmlattr>.status_message",
err == 503 ? "Failed to initialize video capture/encoding. Is a display connected and turned on?" : "Failed to start the specified application"
);
tree.put("root.gamesession", 0);
return;
}
}
} else {
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "How did you get here?");
tree.put("root.gamesession", 0);
}
tree.put("root.<xmlattr>.status_code", 200);
tree.put(
"root.sessionUrl0",
std::format(
"{}{}:{}",
launch_session->rtsp_url_scheme,
net::addr_to_url_escaped_string(request->local_endpoint().address()),
static_cast<int>(net::map_port(rtsp_stream::RTSP_SETUP_PORT))
)
);
keep_runtime_overrides = true;
tree.put("root.gamesession", 1);
#ifdef _WIN32
tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus.load(std::memory_order_acquire) == VDISPLAY::DRIVER_STATUS::OK);
#elif defined(__linux__)
tree.put("root.VirtualDisplayDriverReady", platf::linux_display::backend().capabilities().independent_outputs_ready);
#else
tree.put("root.VirtualDisplayDriverReady", false);
#endif
#ifdef _WIN32
pending_vulkan_hdr_layer_guard.disable();
#endif
stream::session::arm_shared_runtime_cleanup(
launch_session->virtual_display_guid_bytes
);
if (!paired_client_uuid_enabled(launch_session->client_uuid, verified_client->perm)) {
if (appid > 0) proc::proc.terminate(false, true, false, true);
tree.put("root.resume", 0);
tree.put("root.gamesession", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Paired client authorization was revoked before stream admission");
return;
}
if (!rtsp_stream::launch_session_raise(launch_session)) {
if (appid > 0) proc::proc.terminate(false, true, false, true);
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "RTSP pending session admission was rejected");
tree.put("root.gamesession", 0);
return;
}
#if defined(_WIN32) || defined(__linux__)
virtual_display_teardown_guard.disable();
#endif
#if defined(_WIN32) || defined(__linux__)
normal_vdd_identity_guard.disable();
#endif
revert_display_configuration = false;
output_override_guard.disable();
runtime_overrides_guard.disable();
}
void resume(bool &host_audio, resp_https_t response, req_https_t request, int current_appid, const bool normal_app_transition, const bool launched_from_applist) {
print_req<SunshineHTTPS>(request);
#ifdef _WIN32
// See launch(): the response fail guard can restore through the helper
// after the virtual-display teardown guard has already completed.
stream::session::cleanup_reservation_t cleanup_reservation;
#endif
pt::ptree tree;
bool revert_display_configuration {false};
auto g = util::fail_guard([&]() {
std::ostringstream data;
if (tree.empty()) {
BOOST_LOG(error) << EMPTY_PROPERTY_TREE_ERROR_MSG;
}
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
if (revert_display_configuration) {
display_helper_integration::revert();
}
});
auto verified_client = get_verified_cert(request);
const auto request_client_identity = resolve_client_identity(request, verified_client);
if (!verified_client || request_client_identity.uuid.empty()) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "A paired TLS client identity is required");
return;
}
if (!has_client_perm(verified_client, PERM::_allow_view)) {
log_permission_denied("ViewApp"sv, "View stream"sv, verified_client);
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", permission_denied_status_message(verified_client, "View stream"sv));
return;
}
// proc_t::terminate() leaves the app id at -1 and nothing resets it to 0, so any
// non-positive id means nothing is running. Comparing against 0 alone would let a
// stale /resume run the whole resume path for an app that no longer exists.
if (current_appid <= 0) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "No running app to resume");
return;
}
auto args = request->parse_query_string();
if (
args.find("rikey"s) == std::end(args) ||
args.find("rikeyid"s) == std::end(args)
) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Missing a required resume parameter");
return;
}
// Keep the resume gate explicit at this point as well: runtime overrides
// below must remain bound to the paired TLS identity established above.
if (!verified_client || request_client_identity.uuid.empty()) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "A paired TLS client identity is required");
return;
}
std::unique_lock normal_transition_lock {normal_http_app_transition_mutex, std::defer_lock};
if (normal_app_transition) {
normal_transition_lock.lock();
}
// Newer Moonlight clients send localAudioPlayMode on /resume too,
// so we should use it if it's present in the args and there are
// no active sessions we could be interfering with.
// A pending launch must not reject this request: /resume is how a second viewer joins
// an app another client already started, and that client's launch stays pending for
// seconds (display apply, verification, encoder probe). has_stream_session_activity()
// already counts pending launches, so every mutating decision below degrades to a
// plain join on its own.
const bool no_active_sessions = !has_stream_session_activity();
const auto active_game = proc::proc.active_session_guard();
const bool secondary_game_client = remote_session::is_secondary_game_client(
active_game.client_uuid, request_client_identity.uuid
);
bool retained_game_output_ready = false;
if (no_active_sessions) {
if (const auto retained_output = config::runtime_output_name_override(); retained_output && !retained_output->empty()) {
const auto capture_outputs = platf::display_names();
retained_game_output_ready =
std::find(capture_outputs.begin(), capture_outputs.end(), *retained_output) != capture_outputs.end();
if (retained_game_output_ready) {
BOOST_LOG(info) << "Resume will join the capture-ready retained game output '"
<< *retained_output << "'.";
}
}
}
const bool joining_existing_game_output =
secondary_game_client || remote_session::joins_existing_game_output(
remote_session::role_e::game,
!no_active_sessions,
retained_game_output_ready
);
std::unordered_map<std::string, std::string> requested_runtime_overrides;
if (auto running_app = proc::proc.resolve_app(current_appid)) {
config::merge_config_overrides(requested_runtime_overrides, running_app->config_overrides);
}
auto client_settings = verified_client;
std::string client_uuid = request_client_identity.uuid;
const auto resume_client_uuid = resolve_known_client_uuid_from_launch_id(get_arg(args, "uniqueid", ""));
if (!resume_client_uuid.empty() && resume_client_uuid != client_uuid) {
BOOST_LOG(warning) << "Ignoring resume uniqueid for runtime overrides; TLS caller identity is authoritative.";
}
if (!client_settings && !client_uuid.empty()) {
client_settings = get_client_snapshot_by_uuid(client_uuid);
}
if (client_settings) {
config::merge_config_overrides(requested_runtime_overrides, client_settings->config_overrides);
}
#ifdef _WIN32
if (client_settings &&
!client_settings->hdr_profile.empty() &&
!requested_runtime_overrides.contains("rtx_hdr_peak_brightness")) {
if (const auto profile_peak = VDISPLAY::hdr_profile_peak_luminance_nits(client_settings->hdr_profile)) {
const auto effective_peak = std::clamp<std::uint32_t>(*profile_peak, 400, 2000);
requested_runtime_overrides.insert_or_assign("rtx_hdr_peak_brightness", std::to_string(effective_peak));
}
}
#endif
if (!no_active_sessions &&
!config::adapter_config_overrides_compatible_with_active(requested_runtime_overrides)) {
BOOST_LOG(warning) << "Rejecting shared resume with a capture adapter selection that differs from the active stream.";
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put(
"root.<xmlattr>.status_message",
"Another stream is active with a different capture adapter selection"
);
return;
}
bool runtime_overrides_reapplied = false;
auto previous_runtime_overrides = config::runtime_config_overrides_snapshot();
auto runtime_overrides_guard = util::fail_guard([&]() {
if (!runtime_overrides_reapplied) {
return;
}
config::set_runtime_config_overrides(std::move(previous_runtime_overrides));
if (!has_stream_session_activity()) {
config::apply_config_now();
} else {
config::mark_deferred_reload();
}
});
if (no_active_sessions && !secondary_game_client) {
config::set_runtime_config_overrides(std::move(requested_runtime_overrides));
config::apply_config_now();
runtime_overrides_reapplied = true;
}
const bool is_input_only = config::input.enable_input_only_mode && current_appid == proc::input_only_app_id;
const bool allow_display_changes = config::video.dd.config_revert_on_disconnect && !is_input_only;
const bool allow_session_display_changes = allow_display_changes && !joining_existing_game_output;
if (no_active_sessions && allow_session_display_changes) {
config::set_runtime_output_name_override(std::nullopt);
}
if (no_active_sessions && args.find("localAudioPlayMode"s) != std::end(args)) {
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
}
#if defined(_WIN32) || defined(__linux__)
if (no_active_sessions) {
stream::cancel_paused_display_cleanup();
}
#endif
// Prevent interleaving with hot-apply while we prep/resume a session
auto _hot_apply_gate = config::acquire_apply_read_gate();
if (no_active_sessions) {
config::record_active_adapter_config();
}
auto launch_session = make_launch_session_from_snapshot(host_audio, is_input_only, args, verified_client, &request_client_identity, request);
if (!proc::proc.allow_client_commands || !verified_client->allow_client_commands) {
launch_session->client_do_cmds.clear();
launch_session->client_undo_cmds.clear();
}
// A resume request carries no appid or appuuid, so make_launch_session() cannot resolve the
// app and leaves every per-app setting at its default. Seed them from the app that is still
// running, before the display is prepared below. Without this the session silently falls
// back to the global output setting on reconnect and ignores the app's display policy.
if (launch_session->appid <= 0 && current_appid > 0) {
try {
if (auto app_ctx = proc::proc.resolve_app(current_appid)) {
launch_session->appid = (int) util::from_view(app_ctx->id);
launch_session->gen1_framegen_fix = app_ctx->gen1_framegen_fix;
launch_session->gen2_framegen_fix = app_ctx->gen2_framegen_fix;
launch_session->frame_generation_enabled = app_ctx->frame_generation_enabled;
launch_session->lossless_scaling_framegen = app_ctx->lossless_scaling_framegen;
launch_session->lossless_scaling_target_fps = app_ctx->lossless_scaling_target_fps;
launch_session->lossless_scaling_rtss_limit = app_ctx->lossless_scaling_rtss_limit;
launch_session->frame_generation_provider = app_ctx->frame_generation_provider;
rtsp_stream::launch_session_t::app_metadata_t metadata;
metadata.id = app_ctx->id;
metadata.name = app_ctx->name;
metadata.virtual_screen = app_ctx->virtual_screen || app_ctx->virtual_display;
metadata.has_command = !app_ctx->cmd.empty();
metadata.has_playnite = !app_ctx->playnite_id.empty();
metadata.playnite_fullscreen = app_ctx->playnite_fullscreen;
// launch_session->virtual_display is deliberately left alone: the launch path assigns
// it from the app and then immediately overwrites it with the request's virtualDisplay
// argument and the per-client setting, both of which are already resolved here.
if (!launch_session->virtual_display_mode_override && app_ctx->virtual_display_mode_override) {
launch_session->virtual_display_mode_override = app_ctx->virtual_display_mode_override;
}
if (!launch_session->virtual_display_layout_override && app_ctx->virtual_display_layout_override) {
launch_session->virtual_display_layout_override = app_ctx->virtual_display_layout_override;
}
if (!launch_session->dd_config_option_override && app_ctx->dd_config_option_override) {
launch_session->dd_config_option_override = app_ctx->dd_config_option_override;
}
if (app_ctx->output_name_override) {
launch_session->output_name_override = *app_ctx->output_name_override;
}
launch_session->app_metadata = std::move(metadata);
BOOST_LOG(debug) << "Resume request carried no app id; applying settings from running app ["sv
<< app_ctx->name << "] (id " << app_ctx->id << ")."sv;
}
} catch (...) {
}
}
if (joining_existing_game_output) {
// A secondary game transport attaches to the display already owned by
// the running app. Its per-client virtual-display preferences must not
// create, reclaim, or retarget a display while another transport owns
// the active capture source.
launch_session->virtual_display = false;
launch_session->client_requests_virtual_display = false;
launch_session->client_virtual_display_override.reset();
launch_session->virtual_display_mode_override = config::video_t::virtual_display_mode_e::disabled;
launch_session->virtual_display_layout_override.reset();
launch_session->dd_config_option_override.reset();
launch_session->output_name_override.reset();
launch_session->virtual_display_guid_bytes.fill(0);
launch_session->virtual_display_device_id.clear();
BOOST_LOG(info) << "Joining the running game's active capture output without preparing a per-client display.";
}
#ifdef _WIN32
const auto display_startup_deadline =
std::chrono::steady_clock::now() +
display_helper_integration::kStreamStartApplyVerificationTimeout;
const auto display_startup_cancelled = [display_startup_deadline] {
return std::chrono::steady_clock::now() >= display_startup_deadline;
};
if (allow_session_display_changes) {
// Stop any in-flight helper restore loop before resuming display changes.
(void) display_helper_integration::disarm_pending_restore(
display_startup_cancelled,
display_startup_deadline
);
}
#endif
launch_session->secondary_game_client = secondary_game_client;
#ifdef __linux__
// The application retains its normal display lease while paused. A new
// TLS client resuming it must not create a second normal-game identity.
const auto display_owner = proc::proc.active_session_guard();
launch_session->normal_vdd_owner_uuid = platf::linux_private_display::resume_policy::reservation_owner(
launch_session->client_uuid, display_owner.client_uuid, display_owner.normal_vdd_identity_token
);
#endif
std::optional<std::string> pending_output_override;
auto output_override_guard = util::fail_guard([&]() {
if (pending_output_override) {
config::set_runtime_output_name_override(std::nullopt);
}
});
#ifdef _WIN32
std::optional<video::encoder_probe_adapter_hint_lease_t> pending_adapter_hint;
auto pending_adapter_hint_guard = util::fail_guard([&] {
if (pending_adapter_hint) {
(void) video::clear_pending_virtual_display_adapter_hint(*pending_adapter_hint);
}
});
auto virtual_display_teardown_guard = util::fail_guard([&]() {
stream::session::cleanup_reservation_t cleanup_reservation;
if (has_stream_session_activity()) {
return;
}
if (!launch_session->virtual_display) {
return;
}
BOOST_LOG(info) << "Resume aborted before session start; removing virtual displays.";
(void) platf::virtual_display_cleanup::run(
"resume_aborted",
config::video.dd.config_revert_on_disconnect
);
});
auto normal_vdd_identity_guard = util::fail_guard([&] {
if (launch_session->normal_vdd_identity_newly_reserved) {
remote_display_topology::instance().rollback_normal_game_identity(
launch_session->client_uuid,
launch_session->normal_vdd_identity_token
);
}
});
if (!joining_existing_game_output) {
prepare_virtual_display_for_session(
launch_session,
no_active_sessions,
allow_session_display_changes,
is_input_only,
pending_output_override,
pending_adapter_hint,
display_startup_cancelled,
display_startup_deadline
);
}
if (launch_session->normal_vdd_capacity_rejected) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "Remote display capacity is four paired-client identities");
return;
}
#elif defined(__linux__)
platf::linux_display::prepared_display_t prepared;
if (!joining_existing_game_output) {
prepared = platf::linux_display::backend().prepare_session(
*launch_session, no_active_sessions, allow_session_display_changes
);
if (!prepared.error.empty()) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", prepared.error);
return;
}
}
auto virtual_display_teardown_guard = util::fail_guard([&]() {
stream::session::cleanup_reservation_t cleanup_reservation;
if (!has_stream_session_activity() && launch_session->virtual_display) {
if (remote_display_topology::instance().generic_virtual_display_cleanup_allowed()) {
BOOST_LOG(info) << "Resume aborted before session start; restoring Linux private display state.";
(void) platf::linux_display::backend().revert();
} else {
BOOST_LOG(info) << "Resume aborted while another managed display identity remains; preserving its composed topology.";
}
}
});
auto normal_vdd_identity_guard = util::fail_guard([&] {
rollback_linux_normal_display_identity(launch_session);
});
const auto normal_identity = reserve_linux_normal_display_identity(launch_session);
if (normal_identity == linux_normal_identity_result_e::capacity_rejected ||
normal_identity == linux_normal_identity_result_e::topology_failed) {
const bool capacity_rejected = normal_identity == linux_normal_identity_result_e::capacity_rejected;
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", capacity_rejected ? 409 : 503);
tree.put("root.<xmlattr>.status_message", capacity_rejected ?
"Remote display capacity is four paired-client identities" :
"Failed to compose the Linux private streaming displays");
return;
}
if (!prepared.output_name.empty()) {
config::set_runtime_output_name_override(prepared.output_name);
pending_output_override = prepared.output_name;
}
#endif
if (no_active_sessions) {
// We want to prepare display only if there are no active sessions at
// the moment. This should be done before probing encoders as it could
// change the active displays.
const bool should_apply_display_request =
#ifdef __linux__
platf::linux_private_display::resume_policy::requires_session_apply(
launch_session->virtual_display,
allow_session_display_changes,
launch_session->normal_vdd_identity_newly_reserved,
launch_session->virtual_display_recreated_on_demand || launch_session->virtual_display_needs_resume_apply
);
#else
allow_session_display_changes ||
launch_session->virtual_display_recreated_on_demand ||
launch_session->virtual_display_needs_resume_apply;
#endif
if (should_apply_display_request) {
BOOST_LOG(debug) << "Display helper: applying session display request on "
<< (allow_session_display_changes ? "normal start/resume" :
(launch_session->virtual_display_recreated_on_demand ?
"resume virtual-display recreation" :
"resume virtual-display refresh"))
<< " for client '" << launch_session->client_name << "'.";
revert_display_configuration = allow_session_display_changes || launch_session->virtual_display_failed;
#ifdef _WIN32
const bool helper_session_available = display_helper_session_available();
(void) display_helper_integration::disarm_pending_restore(
display_startup_cancelled,
display_startup_deadline
);
auto request = display_helper_integration::helpers::build_request_from_session(config::video, *launch_session);
if (!request) {
BOOST_LOG(warning) << "Display helper: failed to build display configuration request; continuing with existing display.";
}
if (request) {
display_helper_integration::ApplyVerificationTicket verification_ticket;
const bool applied = display_helper_integration::apply(
*request,
&verification_ticket,
display_startup_cancelled,
display_helper_integration::ApplyRetryPolicy::StreamStart,
display_startup_deadline);
if (!applied) {
if (helper_session_available) {
BOOST_LOG(warning) << "Display helper: failed to apply display configuration; continuing with existing display.";
}
} else {
auto gate_promise = std::make_shared<std::promise<rtsp_stream::launch_session_t::display_helper_gate_status_e>>();
launch_session->display_helper_gate = gate_promise->get_future().share();
BOOST_LOG(debug) << "Display helper: gating capture start on helper verification (non-blocking session resume).";
std::thread([gate_promise, verification_ticket]() {
const auto status = display_helper_integration::wait_for_apply_verification(
verification_ticket,
display_helper_integration::kStreamStartApplyVerificationTimeout);
rtsp_stream::launch_session_t::display_helper_gate_status_e gate_status =
rtsp_stream::launch_session_t::display_helper_gate_status_e::proceed_gaveup;
if (status == display_helper_integration::ApplyVerificationStatus::Verified) {
gate_status = rtsp_stream::launch_session_t::display_helper_gate_status_e::proceed;
} else if (status == display_helper_integration::ApplyVerificationStatus::Failed) {
gate_status = rtsp_stream::launch_session_t::display_helper_gate_status_e::abort_failed;
}
try {
gate_promise->set_value(gate_status);
} catch (...) {
// best-effort: ignore double-satisfaction
}
}).detach();
}
}
// Apply a per-client HDR profile to physical displays (virtual displays are handled at creation time).
const auto physical_hdr_profile_policy = display_helper_integration::request_policy::evaluate({
.virtual_display = launch_session->virtual_display,
.virtual_display_failed = launch_session->virtual_display_failed,
.hdr_profile_selected = launch_session->hdr_profile && !launch_session->hdr_profile->empty(),
});
if (physical_hdr_profile_policy.apply_hdr_profile_to_physical) {
const auto active_output = config::get_active_output_name();
VDISPLAY::applyHdrProfileToOutput(
launch_session->client_name.c_str(),
launch_session->hdr_profile ? launch_session->hdr_profile->c_str() : nullptr,
active_output.empty() ? nullptr : active_output.c_str()
);
}
#else
display_helper_integration::DisplayApplyBuilder noop_builder;
noop_builder.set_session(*launch_session);
if (!display_helper_integration::apply(noop_builder.build())) {
BOOST_LOG(warning) << "Display helper: failed to apply display configuration; continuing with existing display.";
}
#endif
}
else {
#ifdef _WIN32
BOOST_LOG(debug) << "Display helper: skipping resume apply; only deferrals are allowed.";
#else
display_helper_integration::DisplayApplyBuilder noop_builder;
noop_builder.set_session(*launch_session);
if (!display_helper_integration::apply(noop_builder.build())) {
if (launch_session->virtual_display) {
const std::string status_message = "Failed to activate the Linux private streaming display.";
BOOST_LOG(error) << status_message;
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", status_message);
return;
}
BOOST_LOG(warning) << "Display helper: failed to apply display configuration; continuing with existing display.";
}
#endif
}
// Probe encoders again before streaming to ensure our chosen
// encoder matches the active GPU (which could have changed
// due to hotplugging, driver crash, primary monitor change,
// or any number of other factors).
#ifdef _WIN32
bool encoder_probe_failed = false;
if (!video::has_successful_encoder_probe()) {
{
VDISPLAY::ensure_display_result ensure_result {};
auto cleanup_probe_display = util::fail_guard([&ensure_result]() {
VDISPLAY::cleanup_ensure_display(ensure_result);
});
if (VDISPLAY::policy::should_ensure_probe_display(launch_session->virtual_display)) {
ensure_result = VDISPLAY::ensure_display();
if (!ensure_result.owns_temporary_probe_request() && !ensure_result.ready_for_capture()) {
BOOST_LOG(warning)
<< "Resume could not acquire a temporary-display lease; continuing with synthetic encoder validation.";
}
}
encoder_probe_failed = video::probe_encoders();
}
} else {
BOOST_LOG(debug) << "Resume encoder probe skipped (matching selected-GPU cache).";
}
#else
bool encoder_probe_failed = video::probe_encoders();
#endif
if (encoder_probe_failed && !launch_session->input_only) {
const std::string status_message = "Failed to initialize a video encoder on the selected adapter.";
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", status_message);
return;
}
}
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv;
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Encryption is mandatory for this host but unsupported by the client");
tree.put("root.gamesession", 0);
return;
}
tree.put("root.<xmlattr>.status_code", 200);
tree.put(
"root.sessionUrl0",
std::format(
"{}{}:{}",
launch_session->rtsp_url_scheme,
net::addr_to_url_escaped_string(request->local_endpoint().address()),
static_cast<int>(net::map_port(rtsp_stream::RTSP_SETUP_PORT))
)
);
tree.put(std::string {"root."} + std::string {remote_session::stream_start_response_key(launched_from_applist)}, 1);
#ifdef _WIN32
tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus.load(std::memory_order_acquire) == VDISPLAY::DRIVER_STATUS::OK);
#elif defined(__linux__)
tree.put("root.VirtualDisplayDriverReady", platf::linux_display::backend().capabilities().independent_outputs_ready);
#else
tree.put("root.VirtualDisplayDriverReady", false);
#endif
stream::session::arm_shared_runtime_cleanup(
launch_session->virtual_display_guid_bytes
);
if (!paired_client_uuid_enabled(launch_session->client_uuid, verified_client->perm)) {
tree.put("root.resume", 0);
tree.put("root.gamesession", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Paired client authorization was revoked before stream admission");
return;
}
if (!rtsp_stream::launch_session_raise(launch_session)) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 409);
tree.put("root.<xmlattr>.status_message", "RTSP pending session admission was rejected");
return;
}
#if defined(_WIN32) || defined(__linux__)
virtual_display_teardown_guard.disable();
#endif
#if defined(_WIN32) || defined(__linux__)
normal_vdd_identity_guard.disable();
#endif
output_override_guard.disable();
runtime_overrides_guard.disable();
revert_display_configuration = false;
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_client_connected(verified_client->name);
#endif
}
void cancel(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
#ifdef _WIN32
// Reserve cleanup before sampling process/session state and retain it
// through session teardown, app termination, and final display cleanup.
stream::session::cleanup_reservation_t cleanup_reservation;
#endif
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto verified_client = get_verified_cert(request);
const auto request_client_identity = resolve_client_identity(request, verified_client);
if (!verified_client || request_client_identity.uuid.empty()) {
tree.put("root.cancel", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "A paired TLS client identity is required");
return;
}
if (!has_client_perm(verified_client, PERM::launch)) {
log_permission_denied("CancelApp"sv, "Launch applications"sv, verified_client);
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", permission_denied_status_message(verified_client, "Launch applications"sv));
return;
}
const bool has_running_app = proc::proc.running() > 0;
if (!has_running_app) {
// Natural app exit clears its owner before the client sends the final
// cancel request. Acknowledge the completed operation without touching
// another client's transports or a concurrently admitted application.
tree.put("root.cancel", 1);
tree.put("root.<xmlattr>.status_code", 200);
return;
}
const auto active_session = proc::proc.active_session_guard();
const remote_session::caller_t caller {.uuid = request_client_identity.uuid, .paired = true, .may_terminate = true};
const remote_session::game_t game {
.running = has_running_app,
.owner_uuid = active_session.client_uuid,
.generation = active_session_generation(active_session),
};
const bool remote_sessions_active = remote_role_gate_snapshot_for_client(request_client_identity.uuid).active;
if (!remote_session::allows_normal_game_cancel(caller, game, remote_sessions_active)) {
tree.put("root.cancel", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put(
"root.<xmlattr>.status_message",
remote_sessions_active ?
"Only the configured running-game owner may cancel this game while Remote Input or Remote Monitor is active" :
"No running app is available to cancel"
);
return;
}
remote_session::clear_app_replacement_confirmation(request_client_identity.uuid);
tree.put("root.cancel", 1);
tree.put("root.<xmlattr>.status_code", 200);
#ifdef _WIN32
const bool preserve_deferred_launch =
has_running_app &&
proc::proc.is_launch_deferred() &&
rtsp_stream::session_count_no_cleanup() == 0;
if (preserve_deferred_launch) {
BOOST_LOG(info) << "Cancel requested while app launch is deferred; preserving deferred app and virtual display state.";
}
#else
constexpr bool preserve_deferred_launch = false;
#endif
rtsp_stream::terminate_sessions(preserve_deferred_launch);
if (has_running_app && !preserve_deferred_launch) {
proc::proc.terminate();
}
// The config needs to be reverted regardless of whether "proc::proc.terminate()" was called or not.
#if defined(_WIN32) || defined(__linux__)
// RTSP session termination above is synchronous, so by the time we reach
// this point the old session threads have already completed their joins.
cleanup_virtual_display_if_idle();
#endif
}
void appasset(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
auto fg = util::fail_guard([&]() {
response->write(SimpleWeb::StatusCode::server_error_internal_server_error);
response->close_connection_after_response = true;
});
auto verified_client = get_verified_cert(request);
if (!has_client_perm(verified_client, PERM::_all_actions)) {
log_permission_denied("Get AppAsset"sv, "List applications"sv, verified_client);
fg.disable();
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
response->close_connection_after_response = true;
return;
}
auto args = request->parse_query_string();
const auto appid = get_arg(args, "appid", "0");
const auto appuuid = get_arg(args, "appuuid", "");
const auto current_appid = proc::proc.running();
const auto synthetic_control = remote_session::identify(util::from_view(appid), appuuid, current_appid);
auto app_ctx = proc::proc.resolve_app(appid, appuuid);
std::string app_image;
if (app_ctx) {
app_image = proc::validate_app_image_path(app_ctx->image_path);
} else if (synthetic_control == remote_session::control_e::running_game) {
if (const auto running_app = proc::proc.resolve_app(current_appid)) {
app_image = proc::validate_app_image_path(running_app->image_path);
}
} else if (const auto artwork = remote_session::synthetic_artwork_filename(synthetic_control)) {
app_image = (fs::path {SUNSHINE_ASSETS_DIR} / "remote-session" / std::string {*artwork}).string();
} else {
app_image = proc::proc.get_app_image((int) util::from_view(appid));
}
fg.disable();
auto image = proc::read_validated_app_image(app_image);
if (!image) image = proc::read_validated_app_image(DEFAULT_APP_IMAGE_PATH);
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "image/png");
response->write(SimpleWeb::StatusCode::success_ok, image.value_or(std::string {}), headers);
response->close_connection_after_response = true;
}
void getClipboard(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
auto verified_client = get_verified_cert(request);
if (
!has_client_perm(verified_client, PERM::_allow_view) || !has_client_perm(verified_client, PERM::clipboard_read)
) {
log_permission_denied("Read Clipboard"sv, "Read clipboard"sv, verified_client);
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
response->close_connection_after_response = true;
return;
}
auto args = request->parse_query_string();
auto clipboard_type = get_arg(args, "type");
if (clipboard_type != "text"sv) {
BOOST_LOG(debug) << "Clipboard type [" << clipboard_type << "] is not supported!";
response->write(SimpleWeb::StatusCode::client_error_bad_request);
response->close_connection_after_response = true;
return;
}
std::list<std::string> connected_uuids = rtsp_stream::get_all_session_uuids();
bool found = !connected_uuids.empty();
if (found) {
found = (std::find(connected_uuids.begin(), connected_uuids.end(), verified_client->uuid) != connected_uuids.end());
}
if (!found) {
BOOST_LOG(debug) << "Client [" << verified_client->name << "] trying to get clipboard is not connected to a stream";
response->write(SimpleWeb::StatusCode::client_error_forbidden);
response->close_connection_after_response = true;
return;
}
std::string content = platf::get_clipboard();
response->write(content);
return;
}
void setClipboard(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
auto verified_client = get_verified_cert(request);
if (
!has_client_perm(verified_client, PERM::_allow_view) || !has_client_perm(verified_client, PERM::clipboard_set)
) {
log_permission_denied("Write Clipboard"sv, "Set clipboard"sv, verified_client);
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
response->close_connection_after_response = true;
return;
}
auto args = request->parse_query_string();
auto clipboard_type = get_arg(args, "type");
if (clipboard_type != "text"sv) {
BOOST_LOG(debug) << "Clipboard type [" << clipboard_type << "] is not supported!";
response->write(SimpleWeb::StatusCode::client_error_bad_request);
response->close_connection_after_response = true;
return;
}
std::list<std::string> connected_uuids = rtsp_stream::get_all_session_uuids();
bool found = !connected_uuids.empty();
if (found) {
found = (std::find(connected_uuids.begin(), connected_uuids.end(), verified_client->uuid) != connected_uuids.end());
}
if (!found) {
BOOST_LOG(debug) << "Client [" << verified_client->name << "] trying to set clipboard is not connected to a stream";
response->write(SimpleWeb::StatusCode::client_error_forbidden);
response->close_connection_after_response = true;
return;
}
std::string content = request->content.string();
bool success = platf::set_clipboard(content);
if (!success) {
BOOST_LOG(debug) << "Setting clipboard failed!";
response->write(SimpleWeb::StatusCode::server_error_internal_server_error);
response->close_connection_after_response = true;
}
response->write();
return;
}
void setBitrate(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto verified_client = get_verified_cert(request);
if (!has_client_perm(verified_client, PERM::_allow_view)) {
log_permission_denied("SetBitrate"sv, "View stream"sv, verified_client);
tree.put("root.bitrate", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", permission_denied_status_message(verified_client, "View stream"sv));
return;
}
auto args = request->parse_query_string();
const int requested = (int) util::from_view(get_arg(args, "bitrate", "0"));
if (requested <= 0) {
tree.put("root.bitrate", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Missing or invalid bitrate parameter");
return;
}
// Clamp to the host bitrate ceiling. max_bitrate == 0 means "unlimited" in config, so also
// enforce an absolute ceiling to keep the value sane and avoid overflow downstream
// (bitrate_kbps * 1000 must fit the encoder's 32-bit rate-control fields).
constexpr int absolute_max_bitrate_kbps = 500000; // 500 Mbps
int applied = requested;
if (config::video.max_bitrate > 0 && applied > config::video.max_bitrate) {
applied = config::video.max_bitrate;
}
if (applied > absolute_max_bitrate_kbps) {
applied = absolute_max_bitrate_kbps;
}
if (applied != requested) {
BOOST_LOG(info) << "Clamped requested bitrate "sv << requested << " kbps to "sv << applied << " kbps"sv;
}
const int updated = stream::set_bitrate_for_sessions(verified_client->uuid, applied);
if (updated <= 0) {
BOOST_LOG(warning) << "Bitrate change requested by ["sv << verified_client->name << "] but no matching active session was found"sv;
tree.put("root.bitrate", 0);
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "No active session for this client");
return;
}
BOOST_LOG(info) << "Client ["sv << verified_client->name << "] set runtime bitrate to "sv << applied << " kbps ("sv << updated << " session(s))"sv;
tree.put("root.bitrate", applied);
tree.put("root.<xmlattr>.status_code", 200);
}
void getAbrCapabilities(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
auto verified_client = get_verified_cert(request);
if (!has_client_perm(verified_client, PERM::_allow_view)) {
log_permission_denied("AbrCapabilities"sv, "View stream"sv, verified_client, true);
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
response->close_connection_after_response = true;
return;
}
// Server-side adaptive bitrate decisioning is not implemented. Reporting it unsupported makes
// Foundation-compatible clients (e.g. Moonlight V+) drive their own local ABR controller, which
// applies decisions through the runtime /bitrate endpoint above.
const std::string body = R"({"supported":false,"version":1,"features":["runtime_bitrate"]})";
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "application/json");
response->write(SimpleWeb::StatusCode::success_ok, body, headers);
response->close_connection_after_response = true;
}
struct pyrowave_probe_state_t {
resp_https_t response;
std::size_t remaining;
};
void sendPyroWaveProbeChunk(const std::shared_ptr<pyrowave_probe_state_t> &state) {
static const std::string chunk(1024U * 1024U, '\x5a');
const auto bytes = std::min(state->remaining, chunk.size());
state->response->write(chunk.data(), static_cast<std::streamsize>(bytes));
state->remaining -= bytes;
if (state->remaining > 0) {
state->response->send([state](const auto &error) {
if (!error) sendPyroWaveProbeChunk(state);
});
}
// The response destructor sends the final buffered chunk.
}
void getPyroWaveBandwidthProbe(resp_https_t response, req_https_t request) {
// Fixed-size download over the normal pinned, mutually authenticated HTTPS
// connection. The client measures actual host-to-client transfer time.
const auto verified_client = get_verified_cert(request);
const auto identity = resolve_client_identity(request, verified_client);
if (!verified_client || identity.uuid.empty()) {
response->write(SimpleWeb::StatusCode::client_error_forbidden, "Paired client required");
response->close_connection_after_response = true;
return;
}
static std::mutex quota_mutex;
static std::unordered_map<std::string, std::pair<std::chrono::steady_clock::time_point, unsigned>> quotas;
{
std::lock_guard lock {quota_mutex};
auto "a = quotas[identity.uuid];
const auto now = std::chrono::steady_clock::now();
if (quota.first == std::chrono::steady_clock::time_point {} || now - quota.first >= 1min) {
quota = {now, 0};
}
if (quota.second >= 8) {
response->write(SimpleWeb::StatusCode::client_error_too_many_requests, "Probe rate limit reached");
response->close_connection_after_response = true;
return;
}
++quota.second;
}
constexpr std::size_t probe_bytes = 32U * 1024U * 1024U;
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "application/octet-stream");
headers.emplace("Cache-Control", "no-store");
headers.emplace("Content-Length", std::to_string(probe_bytes));
response->write(SimpleWeb::StatusCode::success_ok, headers);
response->close_connection_after_response = true;
sendPyroWaveProbeChunk(std::make_shared<pyrowave_probe_state_t>(pyrowave_probe_state_t {response, probe_bytes}));
}
void setup(const std::string &pkey, const std::string &cert) {
conf_intern.pkey = pkey;
conf_intern.servercert = cert;
}
void start() {
platf::set_thread_name("nvhttp");
#if defined(_WIN32) || defined(__linux__)
// The listeners below can accept /launch as soon as they are started.
// Install the concrete coordinator callbacks before exposing that route.
register_remote_monitor_runtime();
#endif
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto port_http = net::map_port(PORT_HTTP);
auto port_https = net::map_port(PORT_HTTPS);
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
if (!clean_slate) {
if (!load_state()) {
// Do not expose a newly generated uniqueid when durable pairing state
// is unavailable. The controller will retry after the filesystem or
// profile issue is repaired, while the recovery copy remains intact.
BOOST_LOG(fatal) << "HTTP interface is stopping because durable pairing state could not be loaded."sv;
shutdown_event->raise(true);
return;
}
} else {
// FRESH_STATE is an explicit, non-persistent test/reset mode. It is the
// only path allowed to operate without a durable state snapshot.
authorization_state_ready.store(true, std::memory_order_release);
}
auto pkey = file_handler::read_file(config::nvhttp.pkey.c_str());
auto cert = file_handler::read_file(config::nvhttp.cert.c_str());
setup(pkey, cert);
// resume doesn't always get the parameter "localAudioPlayMode"
// launch will store it in host_audio
bool host_audio {};
https_server_t https_server {config::nvhttp.cert, config::nvhttp.pkey};
http_server_t http_server;
thread_pool_util::ThreadPool blocking_route_pool;
blocking_route_pool.start(1);
// Discovery routes are observation-only, so they must not queue behind the mutating
// routes. A launch/resume/cancel handler can hold the lifecycle gate across unbounded
// work, and on a single FIFO worker that made the host undiscoverable until restart.
thread_pool_util::ThreadPool discovery_route_pool;
discovery_route_pool.start(1);
// Verify certificates after establishing connection
https_server.verify = [](req_https_t req, SSL *ssl) {
forget_tls_client_identity(req);
crypto::x509_t x509_verify {
#if OPENSSL_VERSION_MAJOR >= 3
SSL_get1_peer_certificate(ssl)
#else
SSL_get_peer_certificate(ssl)
#endif
};
if (!x509_verify) {
BOOST_LOG(info) << "unknown -- denied"sv;
return false;
}
bool verified = false;
p_named_cert_t named_cert_p;
auto fg = util::fail_guard([&]() {
const auto subject_name = cert_subject_name_for_log(x509_verify);
BOOST_LOG(verbose) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv);
});
const char *err_str = nullptr;
{
std::lock_guard<std::mutex> lock(client_mutex);
if (pending_cert_queue) {
while (pending_cert_queue->peek()) {
auto cert = pending_cert_queue->pop();
if (!cert) {
continue;
}
const auto subject_name = cert_subject_name_for_log(cert);
BOOST_LOG(verbose) << "Added cert ["sv << subject_name << ']';
const auto pem = crypto::pem(cert);
auto named_it = std::find_if(
client_root.named_devices.begin(),
client_root.named_devices.end(),
[&pem](const p_named_cert_t &named_cert) {
return named_cert && named_cert->cert == pem;
}
);
if (named_it != client_root.named_devices.end()) {
cert_chain.add(*named_it);
} else {
BOOST_LOG(warning) << "Pending certificate not found in client registry: "sv << subject_name;
}
}
}
err_str = cert_chain.verify(x509_verify.get(), named_cert_p);
if (!err_str) {
const auto identity = resolve_client_identity_from_peer_cert_locked(x509_verify);
if (!identity) {
err_str = "Client certificate is not one exact enabled pairing record";
} else {
remember_tls_client_identity(req, *identity);
}
}
}
if (err_str) {
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
return verified;
}
verified = true;
return true;
};
https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) {
pt::ptree t