[fix] scope sleep inhibition to active screen sharing, refs #123

This commit is contained in:
dijunkun
2026-10-24 01:24:38 +08:00
parent 1d465239cf
commit 182899d238
7 changed files with 323 additions and 7 deletions
@@ -1225,6 +1225,9 @@ bool GuiApplication::InitializeSDL() {
"windows while retaining Wayland portal capture and input");
}
#endif
// SDL disables the screensaver by default. A background remote desktop app
// should leave automatic display sleep under the user's system settings.
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
return false;
@@ -0,0 +1,245 @@
#include "features/devices/screen_sharing_power.h"
#include "rd_log.h"
#ifdef _WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <IOKit/pwr_mgt/IOPMLib.h>
#elif defined(__linux__) && !defined(__OHOS__)
#include <dbus/dbus.h>
#include <unistd.h>
#else
#include <SDL3/SDL.h>
#endif
namespace crossdesk {
#if defined(__linux__) && !defined(__OHOS__)
namespace {
constexpr const char* kApplication = "CrossDesk";
constexpr const char* kReason = "Sharing the desktop with a remote controller";
constexpr int kDbusTimeoutMs = 1000;
struct ConnectionCloser {
void operator()(DBusConnection* connection) const {
dbus_connection_close(connection);
dbus_connection_unref(connection);
}
};
using BusConnection = std::unique_ptr<DBusConnection, ConnectionCloser>;
using BusMessage = std::unique_ptr<DBusMessage, decltype(&dbus_message_unref)>;
BusConnection OpenBus(DBusBusType type) {
DBusError error;
dbus_error_init(&error);
BusConnection connection(dbus_bus_get_private(type, &error));
if (connection) {
dbus_connection_set_exit_on_disconnect(connection.get(), false);
} else {
LOG_WARN("Cannot connect to power management bus: {}",
error.message ? error.message : "unknown error");
}
dbus_error_free(&error);
return connection;
}
BusMessage Call(DBusConnection* connection, DBusMessage* message) {
return BusMessage(dbus_connection_send_with_reply_and_block(
connection, message, kDbusTimeoutMs, nullptr),
dbus_message_unref);
}
} // namespace
#endif
struct ScreenSharingPower::Impl {
#ifdef _WIN32
HANDLE request = INVALID_HANDLE_VALUE;
bool display = false;
bool system = false;
void Start() {
wchar_t reason_text[] = L"CrossDesk is sharing the desktop";
REASON_CONTEXT reason{};
reason.Version = POWER_REQUEST_CONTEXT_VERSION;
reason.Flags = POWER_REQUEST_CONTEXT_SIMPLE_STRING;
reason.Reason.SimpleReasonString = reason_text;
request = PowerCreateRequest(&reason);
if (request == INVALID_HANDLE_VALUE) {
LOG_WARN("Cannot create screen sharing power request: {}", GetLastError());
return;
}
// Handle-based requests also work in the unattended host, which has no
// SDL video subsystem, and do not depend on the calling thread's lifetime.
system = PowerSetRequest(request, PowerRequestSystemRequired) != 0;
if (!system)
LOG_WARN("Cannot prevent system idle sleep: {}", GetLastError());
display = PowerSetRequest(request, PowerRequestDisplayRequired) != 0;
if (!display)
LOG_WARN("Cannot prevent display idle sleep: {}", GetLastError());
}
void Stop() {
if (request == INVALID_HANDLE_VALUE) return;
if (display) PowerClearRequest(request, PowerRequestDisplayRequired);
if (system) PowerClearRequest(request, PowerRequestSystemRequired);
CloseHandle(request);
request = INVALID_HANDLE_VALUE;
display = system = false;
}
#elif defined(__APPLE__)
IOPMAssertionID display = kIOPMNullAssertionID;
IOPMAssertionID system = kIOPMNullAssertionID;
void Start() {
const auto reason = CFSTR("CrossDesk is sharing the desktop");
auto result = IOPMAssertionCreateWithName(
kIOPMAssertionTypePreventUserIdleSystemSleep, kIOPMAssertionLevelOn,
reason, &system);
if (result != kIOReturnSuccess)
LOG_WARN("Cannot prevent system idle sleep: {}", result);
result = IOPMAssertionCreateWithName(
kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn,
reason, &display);
if (result != kIOReturnSuccess)
LOG_WARN("Cannot prevent display idle sleep: {}", result);
}
void Stop() {
if (display != kIOPMNullAssertionID) IOPMAssertionRelease(display);
if (system != kIOPMNullAssertionID) IOPMAssertionRelease(system);
display = system = kIOPMNullAssertionID;
}
#elif defined(__linux__) && !defined(__OHOS__)
BusConnection session;
dbus_uint32_t cookie = 0;
bool desktop_inhibited = false;
bool gnome = false;
int sleep_fd = -1;
bool InhibitDesktop(bool use_gnome) {
const char* service = use_gnome ? "org.gnome.SessionManager"
: "org.freedesktop.ScreenSaver";
const char* path = use_gnome ? "/org/gnome/SessionManager"
: "/org/freedesktop/ScreenSaver";
BusMessage message(dbus_message_new_method_call(service, path, service,
"Inhibit"),
dbus_message_unref);
if (!message) return false;
dbus_bool_t appended;
if (use_gnome) {
const dbus_uint32_t window = 0;
const dbus_uint32_t flags = 4 | 8; // Suspend and session idle.
appended = dbus_message_append_args(
message.get(), DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_UINT32,
&window, DBUS_TYPE_STRING, &kReason, DBUS_TYPE_UINT32, &flags,
DBUS_TYPE_INVALID);
} else {
appended = dbus_message_append_args(
message.get(), DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_STRING,
&kReason, DBUS_TYPE_INVALID);
}
if (!appended) return false;
auto reply = Call(session.get(), message.get());
if (!reply || !dbus_message_get_args(reply.get(), nullptr, DBUS_TYPE_UINT32,
&cookie, DBUS_TYPE_INVALID))
return false;
gnome = use_gnome;
desktop_inhibited = true;
return true;
}
void Start() {
if (!dbus_threads_init_default()) {
LOG_WARN("Cannot initialize D-Bus for screen sharing power management");
return;
}
// These session APIs work without an SDL window, including native Wayland
// and a Slint window hidden in the tray. GNOME's API inhibits both kinds of
// idle sleep; other desktops use ScreenSaver plus logind below.
session = OpenBus(DBUS_BUS_SESSION);
if (session && InhibitDesktop(true)) return;
if (!session || !InhibitDesktop(false))
LOG_WARN("Cannot inhibit desktop idle while sharing the screen");
auto system_bus = OpenBus(DBUS_BUS_SYSTEM);
if (!system_bus) return;
BusMessage message(dbus_message_new_method_call(
"org.freedesktop.login1", "/org/freedesktop/login1",
"org.freedesktop.login1.Manager", "Inhibit"),
dbus_message_unref);
const char* what = "sleep:idle";
const char* mode = "block";
if (!message || !dbus_message_append_args(
message.get(), DBUS_TYPE_STRING, &what,
DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_STRING,
&kReason, DBUS_TYPE_STRING, &mode, DBUS_TYPE_INVALID)) {
LOG_WARN("Cannot create system sleep inhibition request");
return;
}
auto reply = Call(system_bus.get(), message.get());
// get_args duplicates the returned fd with CLOEXEC. Holding this fd keeps
// the inhibitor alive independently of the reply or D-Bus connection.
if (!reply || !dbus_message_get_args(reply.get(), nullptr, DBUS_TYPE_UNIX_FD,
&sleep_fd, DBUS_TYPE_INVALID))
LOG_WARN("Cannot inhibit system sleep while sharing the screen");
}
void Stop() {
if (sleep_fd >= 0) {
close(sleep_fd);
sleep_fd = -1;
}
if (session && desktop_inhibited &&
dbus_connection_get_is_connected(session.get())) {
const char* service = gnome ? "org.gnome.SessionManager"
: "org.freedesktop.ScreenSaver";
const char* path = gnome ? "/org/gnome/SessionManager"
: "/org/freedesktop/ScreenSaver";
BusMessage message(dbus_message_new_method_call(
service, path, service,
gnome ? "Uninhibit" : "UnInhibit"),
dbus_message_unref);
if (message && dbus_message_append_args(
message.get(), DBUS_TYPE_UINT32, &cookie,
DBUS_TYPE_INVALID)) {
dbus_message_set_no_reply(message.get(), true);
dbus_connection_send(session.get(), message.get(), nullptr);
dbus_connection_flush(session.get());
}
}
// A private connection also revokes the desktop inhibitor if UnInhibit
// could not be sent. Nothing is retained by unrelated D-Bus clients.
session.reset();
desktop_inhibited = false;
gnome = false;
cookie = 0;
}
#else
void Start() {
if (!SDL_DisableScreenSaver())
LOG_WARN("Cannot prevent display idle sleep: {}", SDL_GetError());
}
void Stop() { SDL_EnableScreenSaver(); }
#endif
};
ScreenSharingPower::ScreenSharingPower() : impl_(std::make_unique<Impl>()) {}
ScreenSharingPower::~ScreenSharingPower() { SetSharing(false); }
void ScreenSharingPower::SetSharing(bool sharing) {
if (sharing_ == sharing) return;
sharing_ = sharing;
if (sharing) {
impl_->Start();
LOG_INFO("Requested display and system idle-sleep inhibition for screen sharing");
} else {
impl_->Stop();
LOG_INFO("Released screen sharing power inhibitors");
}
}
} // namespace crossdesk
@@ -0,0 +1,33 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-24
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_SHARING_POWER_H_
#define _SCREEN_SHARING_POWER_H_
#include <memory>
namespace crossdesk {
// Owns temporary display/system idle-sleep inhibitors for the shared capture
// source. Call on the UI thread; repeated updates do not acquire extra requests.
class ScreenSharingPower {
public:
ScreenSharingPower();
~ScreenSharingPower();
ScreenSharingPower(const ScreenSharingPower&) = delete;
ScreenSharingPower& operator=(const ScreenSharingPower&) = delete;
void SetSharing(bool sharing);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
bool sharing_ = false;
};
} // namespace crossdesk
#endif
@@ -298,6 +298,21 @@ bool SessionDeviceManager::ShouldCaptureCursor() const {
});
}
bool SessionDeviceManager::HasConnectedController() const {
std::shared_lock lock(owner_.connection_status_mutex_);
return std::any_of(owner_.connection_status_.begin(),
owner_.connection_status_.end(), [](const auto& entry) {
return entry.second == ConnectionStatus::Connected;
});
}
void SessionDeviceManager::UpdateScreenSharingPower() {
// Wayland keeps capture warm after the last disconnect. Only live viewers
// justify keeping the host awake; an outgoing viewing session does not.
screen_sharing_power_.SetSharing(owner_.screen_capturer_is_started_ &&
HasConnectedController());
}
int SessionDeviceManager::StartScreenCapturer() {
#ifdef __APPLE__
if (!owner_.EnsureMacScreenRecordingPermission()) {
@@ -317,8 +332,10 @@ int SessionDeviceManager::StartScreenCapturer() {
cursor_capture_dirty_.store(false, std::memory_order_relaxed);
const bool show_cursor = ShouldCaptureCursor();
LOG_INFO("Start screen capturer, show cursor: {}", show_cursor);
screen_sharing_power_.SetSharing(HasConnectedController());
const int ret = screen_capturer_->Start(show_cursor);
if (ret != 0) {
screen_sharing_power_.SetSharing(false);
LOG_ERROR("Start screen capturer failed: {}", ret);
}
return ret;
@@ -329,6 +346,8 @@ int SessionDeviceManager::StopScreenCapturer() {
LOG_INFO("Stop screen capturer");
screen_capturer_->Stop();
}
owner_.screen_capturer_is_started_ = false;
screen_sharing_power_.SetSharing(false);
return 0;
}
@@ -647,6 +666,8 @@ void SessionDeviceManager::PushAudio(const std::string& remote_id,
}
void SessionDeviceManager::UpdateInteractions() {
// Release even when pending remote-input cleanup makes this tick return early.
UpdateScreenSharingPower();
#ifdef _WIN32
if (owner_.screen_capturer_is_started_ &&
cursor_capture_dirty_.load(std::memory_order_relaxed)) {
@@ -680,6 +701,8 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.screen_capturer_is_started_ = false;
}
UpdateScreenSharingPower();
if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) {
if (StartMouseController() == 0) {
owner_.mouse_controller_is_started_ = true;
@@ -823,6 +846,8 @@ std::vector<HostDisplay> SessionDeviceManager::host_display_list() const {
}
void SessionDeviceManager::DestroyDevices() {
owner_.screen_capturer_is_started_ = false;
screen_sharing_power_.SetSharing(false);
speaker_capture_.Shutdown();
StopMouseController(true);
if (screen_capturer_) {
@@ -23,6 +23,7 @@
#include "display_info.h"
#include "features/input/captured_keyboard_queue.h"
#include "features/input/remote_mouse_buttons.h"
#include "screen_sharing_power.h"
#include "speaker_capture_controller.h"
struct SDL_AudioStream;
@@ -105,6 +106,8 @@ private:
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
int fps);
bool ShouldCaptureCursor() const;
bool HasConnectedController() const;
void UpdateScreenSharingPower();
bool SyncDisplayInfo();
void SetDisplayInfoList(std::vector<DisplayInfo> displays);
void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps,
@@ -115,6 +118,7 @@ private:
std::unordered_map<std::string, SDL_AudioStream*> output_streams_;
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
ScreenCapturer *screen_capturer_ = nullptr;
ScreenSharingPower screen_sharing_power_;
std::atomic<bool> cursor_capture_dirty_{true};
SpeakerCaptureController speaker_capture_;
DeviceControllerFactory *device_controller_factory_ = nullptr;
@@ -11,6 +11,8 @@ namespace crossdesk {
int RunSlintRendererProbe() {
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
// Renderer probing should not inhibit automatic display sleep either.
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
if (!SDL_Init(SDL_INIT_VIDEO)) {
return 1;
}
+11 -7
View File
@@ -57,6 +57,16 @@ function setup_platform_settings()
"/usr/local/include/freetype2"
}, {system = true})
-- Screen sharing power inhibitors also use D-Bus in X11-only builds.
add_links("dbus-1")
add_existing_include_dirs({
"/usr/include/dbus-1.0",
"/usr/local/include/dbus-1.0"
}, {system = true})
for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do
add_includedirs(include_dir, {system = true})
end
if is_config("USE_DRM", true) then
add_links("drm")
add_defines("CROSSDESK_HAS_DRM=1")
@@ -69,11 +79,8 @@ function setup_platform_settings()
end
if is_config("USE_WAYLAND", true) then
add_links("dbus-1")
add_defines("CROSSDESK_HAS_WAYLAND_CAPTURER=1")
add_existing_include_dirs({
"/usr/include/dbus-1.0",
"/usr/local/include/dbus-1.0",
"/usr/include/pipewire-0.3",
"/usr/local/include/pipewire-0.3",
"/usr/include/pipewire",
@@ -85,9 +92,6 @@ function setup_platform_settings()
"/usr/local/include/spa",
"/opt/crossdesk-pipewire-sdk/include/spa-0.2"
}, {system = true})
for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do
add_includedirs(include_dir, {system = true})
end
else
add_defines("CROSSDESK_HAS_WAYLAND_CAPTURER=0")
end
@@ -98,7 +102,7 @@ function setup_platform_settings()
add_ldflags("-Wl,-ld_classic")
end
add_cxflags("-Wno-unused-variable")
add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore",
add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore", "IOKit",
"IOSurface",
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
"CoreAudio", "AudioToolbox")