Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmake/compile_definitions/common.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ set(SUNSHINE_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/audio.cpp"
"${CMAKE_SOURCE_DIR}/src/audio.h"
"${CMAKE_SOURCE_DIR}/src/platform/common.h"
"${CMAKE_SOURCE_DIR}/src/platform/permissions.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/permissions.h"
"${CMAKE_SOURCE_DIR}/src/process.cpp"
"${CMAKE_SOURCE_DIR}/src/process.h"
"${CMAKE_SOURCE_DIR}/src/network.cpp"
Expand Down
1 change: 1 addition & 0 deletions cmake/compile_definitions/macos.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
${CORE_VIDEO_LIBRARY}
${FOUNDATION_LIBRARY}
${IOKIT_LIBRARY}
${USER_NOTIFICATIONS_LIBRARY}
${VIDEO_TOOLBOX_LIBRARY})

set(APPLE_PLIST_TEMPLATE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/build/Info.plist.in")
Expand Down
1 change: 1 addition & 0 deletions cmake/dependencies/macos.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia)
FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo)
FIND_LIBRARY(FOUNDATION_LIBRARY Foundation)
FIND_LIBRARY(IOKIT_LIBRARY IOKit)
FIND_LIBRARY(USER_NOTIFICATIONS_LIBRARY UserNotifications)
FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox)

if(SUNSHINE_ENABLE_TRAY)
Expand Down
3 changes: 3 additions & 0 deletions cmake/targets/macos.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ else()
COMMENT "Copying bundle resources to build tree"
COMMAND "${CMAKE_COMMAND}" -E make_directory "${_bundle_resources_dir}"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_BINARY_DIR}/assets" "${_bundle_resources_dir}/assets"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${PROJECT_SOURCE_DIR}/src_assets/macos/build/sunshine.icns"
"${_bundle_resources_dir}/sunshine.icns"
VERBATIM)
endif()

Expand Down
12 changes: 12 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ editing the `conf` file in a text editor. Use the examples as reference.
The web UI groups these settings into the sidebar categories documented below. Encoder categories are shown only when
supported on the current platform.

## Permissions on every platform

Open **Troubleshooting > Permissions** in the Web UI to see required and optional access for the current platform.
The Home page flags verifiable required access that is missing. Sunshine checks for access granted while it is running
and restarts once after all verifiable required access is available. On Unix, adding a user to a group takes effect only
after a new login session; Sunshine cannot detect the new group membership in the existing process.

On Linux and FreeBSD, virtual keyboard, mouse, and gamepad input need read and write access to `/dev/uinput` (Linux
also checks `/dev/input/uinput`). The Web UI shows setup steps if that access is missing. On Windows, Sunshine checks
whether its account can list and create files in the `config` directory beside the executable. Windows provides no
consent prompt for directory ACLs, so the Web UI shows setup steps for correcting access.

## General

### locale
Expand Down
21 changes: 20 additions & 1 deletion docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,26 @@ systemctl --user --now enable app-dev.lizardbyte.app.Sunshine
> XDG Desktop Portal, but it is also aliased to "sunshine.service" for convenience.

### macOS
The first time you start Sunshine, you will be asked to grant access to screen recording and your microphone.
On first launch, Sunshine requests Screen Recording and keyboard and mouse control when those features are enabled.
It also requests Microphone access if you configured a custom **Audio Sink**, and optional Notifications access when
the system tray is enabled. Open **Troubleshooting > Permissions** in the Web UI to review these permissions or
open their System Settings pages. Sunshine restarts once after missing required permissions are granted, including
when a permission was removed and later restored.
If macOS offers **Quit & Reopen** after you allow Screen Recording, choose it so the running process receives the new
access. Sunshine exits its tray and server threads before macOS reopens it.

On macOS 15 and newer, macOS requests Local Network access when Sunshine advertises itself with Bonjour. System Audio
Recording access is requested once at startup with a brief audio tap, when the first stream captures audio if startup
access was unavailable, or when you use its Web UI button. The button also opens Screen & System Audio Recording
settings. macOS does not offer Sunshine a passive status check for Local Network
or System Audio Recording, so the Web UI identifies them as permissions handled when used. The Local Network button
opens **Privacy & Security**; choose **Local Network** there to manage app access.
Sunshine uses libvirtualhid to send keyboard and mouse events through CoreGraphics. Virtual gamepads use the separately
installed Virtual HID Broker described in the macOS gamepad setup above. Sunshine shows the broker's availability and
license under **Troubleshooting > Virtual Input**.
If a macOS privacy switch is enabled but Sunshine still reports access denied, remove that Sunshine entry and add the
installed app again. Development builds should use a consistent Apple-issued signing identity so macOS can recognize
the app across updates.

Sunshine supports native system audio capture on macOS 14.0 (Sonoma) and newer via Apple’s Audio Tap API.
To use it, simply leave the **Audio Sink** setting blank.
Expand Down
84 changes: 84 additions & 0 deletions src/confighttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@

#include <Windows.h>
#elif defined(__APPLE__)
#include "platform/macos/misc.h"

#include <CoreFoundation/CoreFoundation.h>
#endif

Expand All @@ -54,6 +56,7 @@
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "platform/permissions.h"
#include "process.h"
#include "rtsp.h"
#include "system_tray.h"
Expand Down Expand Up @@ -89,6 +92,9 @@ namespace confighttp {

namespace {
using license_status_provider_t = std::function<lvh::LicenseResult()>; ///< Provider for the current libvirtualhid license status.
#ifdef SUNSHINE_TESTS
std::optional<nlohmann::json> permission_status_override; ///< Deterministic permission statuses for HTTP tests.
#endif
#if defined(linux) || defined(__FreeBSD__) || defined(SUNSHINE_TESTS)
using portal_token_path_provider_t = std::function<fs::path()>; ///< Provider for the XDG Portal token path.
#endif
Expand Down Expand Up @@ -2005,6 +2011,82 @@ namespace confighttp {
platf::restart();
}

/**
* @brief Return permission status for the current host platform.
*
* @api_examples{/api/permissions|:| GET|:| null}
*/
void getPermissions(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}

nlohmann::json output_tree;
output_tree["permissions"] = nlohmann::json::array();
#ifdef SUNSHINE_TESTS
if (permission_status_override) {
output_tree["permissions"] = *permission_status_override;
send_response(response, output_tree);
return;
}
#endif
for (const auto &permission : platf::get_permission_statuses()) {
output_tree["permissions"].push_back({
{"id", permission.id},
{"status", permission.status},
{"required", permission.required},
{"verifiable", permission.verifiable},
{"requestable", permission.requestable},
});
}
send_response(response, output_tree);
}

#ifdef SUNSHINE_TESTS
void set_permission_statuses_for_testing(nlohmann::json permissions) {
permission_status_override = std::move(permissions);
}

void reset_permission_statuses_for_testing() {
permission_status_override.reset();
}
#endif

/**
* @brief Start a native permission request or open its settings pane.
*
* @api_examples{/api/permissions/request|:| POST|:| {"id":"screen_recording"}}
*/
void requestPermission(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
const auto client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
if (!check_content_type(response, request, "application/json")) {
return;
}

try {
const auto input = nlohmann::json::parse(request->content.string());
if (!input.is_object() || !input.contains("id") || !input["id"].is_string()) {
bad_request(response, request, "A permission ID is required");
return;
}

const bool requested = platf::request_permission(input["id"].get<std::string>());
if (!requested) {
bad_request(response, request, "Unknown or unavailable permission");
return;
}
send_response(response, {{"status", true}});
} catch (const nlohmann::json::exception &) {
bad_request(response, request, "Invalid permission request");
}
}

/**
* @brief Build Virtual HID Broker version and installation status.
*
Expand Down Expand Up @@ -2424,6 +2506,8 @@ namespace confighttp {
server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence;
server.resource["^/api/reset-portal-token$"]["POST"] = resetPortalToken;
server.resource["^/api/restart$"]["POST"] = restart;
server.resource["^/api/permissions$"]["GET"] = getPermissions;
server.resource["^/api/permissions/request$"]["POST"] = requestPermission;
server.resource["^/api/virtual-input/license$"]["GET"] = getVirtualInputLicense;
server.resource["^/api/virtual-input/license$"]["POST"] = updateVirtualInputLicense;
server.resource["^/api/virtual-input/status$"]["GET"] = getVirtualInputStatus;
Expand Down
28 changes: 28 additions & 0 deletions src/confighttp.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,41 @@ namespace confighttp {

void getVirtualInputStatus(const resp_https_t &response, const req_https_t &request);

/**
* @brief Return the host's permission statuses to the Web UI.
*
* @param response HTTPS response receiving the status JSON.
* @param request Authenticated HTTPS request.
*/
void getPermissions(const resp_https_t &response, const req_https_t &request);

/**
* @brief Initiate a native permission request from the Web UI.
*
* @param response HTTPS response receiving the request result.
* @param request Authenticated, CSRF protected request.
*/
void requestPermission(const resp_https_t &response, const req_https_t &request);

void getVirtualInputLicense(const resp_https_t &response, const req_https_t &request);

void updateVirtualInputLicense(const resp_https_t &response, const req_https_t &request);

void resetPortalToken(const resp_https_t &response, const req_https_t &request);

#ifdef SUNSHINE_TESTS
/**
* @brief Replace native permission statuses for deterministic HTTP tests.
*
* @param permissions JSON array returned by the status endpoint.
*/
void set_permission_statuses_for_testing(nlohmann::json permissions);

/**
* @brief Restore native permission status queries after HTTP tests.
*/
void reset_permission_statuses_for_testing();

using virtual_input_license_status_provider_t = std::function<lvh::LicenseResult()>; ///< Test provider for current libvirtualhid license status.
using portal_token_path_provider_t = std::function<std::filesystem::path()>; ///< Test provider for the XDG Portal token path.

Expand Down
45 changes: 44 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

// platform includes
#ifdef __APPLE__
#include "platform/macos/misc.h"

#include <mach-o/dyld.h>
#endif
#ifdef __linux__
Expand All @@ -38,6 +40,7 @@
#include "logging.h"
#include "main.h"
#include "nvhttp.h"
#include "platform/permissions.h"
#include "process.h"
#include "system_tray.h"
#include "upnp.h"
Expand Down Expand Up @@ -163,7 +166,7 @@ void mainThreadLoop(const std::shared_ptr<safe::event_t<bool>> &shutdown_event)
// Main thread event loop
BOOST_LOG(info) << "Starting main loop"sv;
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
while (system_tray::process_tray_events() == 0);
system_tray::run_tray_until_exit(shutdown_event);
#endif
BOOST_LOG(info) << "Main loop has exited"sv;
}
Expand Down Expand Up @@ -440,6 +443,13 @@ int main(int argc, char *argv[]) {
BOOST_LOG(error) << "Platform failed to initialize"sv;
}

// Capture the pre-request state so access granted during this launch causes
// one clean restart after every verifiable required permission is available.
const bool permission_restart_needed = !platf::required_permissions_granted(platf::get_permission_statuses());
#ifdef __APPLE__
platf::request_startup_permissions(tray_is_enabled && config::sunshine.system_tray);
#endif

auto proc_deinit_guard = proc::init();
if (!proc_deinit_guard) {
BOOST_LOG(error) << "Proc failed to initialize"sv;
Expand Down Expand Up @@ -512,8 +522,41 @@ int main(int argc, char *argv[]) {
#endif
}

#ifdef __APPLE__
std::jthread macos_audio_permission_requester;
if (!permission_restart_needed) {
macos_audio_permission_requester = std::jthread([]() {
platf::request_startup_system_audio_permission();
});
}
#endif

std::jthread permission_watcher;
if (permission_restart_needed) {
permission_watcher = std::jthread([](std::stop_token stop) {
while (!stop.stop_requested()) {
if (platf::required_permissions_granted(platf::get_permission_statuses())) {
BOOST_LOG(info) << "Required permissions granted; restarting Sunshine"sv;
platf::restart();
return;
}
std::this_thread::sleep_for(2s);
}
});
}

mainThreadLoop(shutdown_event);

permission_watcher.request_stop();
if (permission_watcher.joinable()) {
permission_watcher.join();
}
#ifdef __APPLE__
if (macos_audio_permission_requester.joinable()) {
macos_audio_permission_requester.join();
}
#endif

httpThread.join();
configThread.join();
rtspThread.join();
Expand Down
21 changes: 19 additions & 2 deletions src/platform/linux/misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
// platform includes
#include <arpa/inet.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <gio/gio.h> // For RTKit
#include <ifaddrs.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <pwd.h>
#include <sys/resource.h> // For setpriority
#include <sys/socket.h>
#include <unistd.h>

#if !defined(__FreeBSD__)
#include <sys/capability.h>
Expand All @@ -45,9 +47,7 @@
// lib includes
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/host_name.hpp>
#include <fcntl.h>
#include <lizardbyte/common/env.h>
#include <unistd.h>

#ifdef SUNSHINE_BUILD_DRM
#include <dirent.h>
Expand All @@ -64,6 +64,7 @@
#include "src/globals.h"
#include "src/logging.h"
#include "src/platform/common.h"
#include "src/platform/permissions.h"
#include "vaapi.h"

#ifdef __GNUC__
Expand Down Expand Up @@ -153,6 +154,22 @@ namespace dyn {
} // namespace dyn

namespace platf {
std::vector<permission_status_t> get_permission_statuses() {
// Match libvirtualhid's device paths and its read/write access check.
bool input_access = access("/dev/uinput", R_OK | W_OK) == 0;
#ifndef __FreeBSD__
input_access = input_access || access("/dev/input/uinput", R_OK | W_OK) == 0;
#endif
return {{"input", input_access ? "granted" : "denied", config::input.keyboard || config::input.mouse || config::input.controller, true}};
}

bool request_permission(std::string_view id) {
// Unix device access has no process-local permission prompt. The Web UI
// presents the group/device setup steps for this known permission.
(void) id;
return false;
}

namespace {
constexpr std::array privileged_gui_environment_variables {
"GDK_PIXBUF_MODULEDIR",
Expand Down
Loading
Loading