[feat] add Windows privacy screen and prefer DXGI capture

This commit is contained in:
dijunkun
2026-09-11 16:11:50 +08:00
parent 4f200095f4
commit ceb0a1654a
57 changed files with 4208 additions and 217 deletions
@@ -25,6 +25,16 @@ constexpr auto kPresenceRefreshInterval = std::chrono::seconds(30);
constexpr auto kPresenceRetryInterval = std::chrono::seconds(1);
} // namespace
bool GuiRuntime::HasActiveSession() {
if (stream_window_inited_) return true;
std::shared_lock lock(connection_status_mutex_);
return std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto& entry) {
return entry.second == ConnectionStatus::Connecting ||
entry.second == ConnectionStatus::Connected;
});
}
void GuiRuntime::HandleConnectionStatusChange() {
if (!signal_connected_ || !peer_) {
return;
@@ -121,6 +131,9 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
void GuiRuntime::HandleServerControllerDisconnected(
const std::string& remote_id, const char* reason) {
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
#ifdef _WIN32
devices_.ReleaseRemoteMouseButtons();
#endif
{
std::lock_guard lock(remote_pointer_input_mutex_);
last_remote_pointer_input_time_.erase(remote_id);
@@ -151,6 +164,10 @@ void GuiRuntime::HandleServerControllerDisconnected(
return;
}
LOG_INFO("Last remote controller disconnected; recovering privacy, reason={}",
reason ? reason : "unknown");
privacy_.Disconnected();
need_to_create_server_window_.store(false, std::memory_order_release);
need_to_destroy_server_window_.store(true, std::memory_order_release);
is_server_mode_ = false;
@@ -8,6 +8,7 @@
#define _GUI_RUNTIME_H_
#include <atomic>
#include <deque>
#include <memory>
#include <string>
@@ -16,6 +17,7 @@
#include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "privacy_controller.h"
#include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h"
@@ -55,9 +57,13 @@ class GuiRuntime : protected gui_detail::GuiState {
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandlePresenceProbeTimeout();
bool HasActiveSession();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration();
void HandlePrivacy();
bool IsAuthorizedController(const std::string& remote_id);
void QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command);
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
@@ -87,6 +93,12 @@ class GuiRuntime : protected gui_detail::GuiState {
bool EnsureMacAccessibilityPermission();
#endif
PrivacyController privacy_;
std::mutex privacy_commands_mutex_;
std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_;
uint32_t last_privacy_revision_ = 0;
uint64_t last_privacy_status_tick_ = 0;
bool privacy_was_paused_ = false;
ClipboardController clipboard_;
SessionDeviceManager devices_;
FileTransferManager transfers_;
@@ -88,6 +88,8 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
std::string source_id = std::string(src_id, src_id_size);
if (!runtime->privacy_.RemoteAllowed() &&
(source_id == runtime->clipboard_label_ || source_id == runtime->file_label_)) return;
if (source_id == runtime->file_label_) {
std::string remote_user_id = std::string(user_id, user_id_size);
@@ -147,6 +149,48 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
std::string remote_id(user_id, user_id_size);
if (remote_action.type == ControlType::privacy_status) {
if (auto props = runtime->FindRemoteSession(remote_id)) {
if (source_id != props->control_data_label_) return;
std::lock_guard lock(props->privacy_status_mutex_);
if (!props->privacy_status_received_ ||
static_cast<int32_t>(remote_action.ps.revision - props->privacy_status_.revision) >= 0) {
props->privacy_status_ = remote_action.ps;
props->privacy_status_received_ = true;
props->privacy_status_tick_ = SDL_GetTicks();
if (static_cast<int32_t>(remote_action.ps.revision - props->privacy_request_revision_) > 0 &&
remote_action.ps.state != PrivacyState::starting &&
remote_action.ps.state != PrivacyState::stopping)
props->privacy_command_pending_ = false;
}
}
return;
}
if (remote_action.type == ControlType::privacy_command) {
if (source_id == runtime->control_data_label_)
runtime->QueuePrivacyCommand(remote_id, remote_action.pc);
return;
}
const bool operates_host = remote_action.type == ControlType::mouse ||
remote_action.type == ControlType::keyboard || remote_action.type == ControlType::keyboard_state ||
remote_action.type == ControlType::service_command || remote_action.type == ControlType::display_id ||
remote_action.type == ControlType::audio_capture;
if (operates_host) {
if (!runtime->IsAuthorizedController(remote_id)) return;
#ifdef _WIN32
// KeyboardController checks the desktop immediately before each native
// key injection, including state reconciliation and cleanup. Avoid a
// second synchronous session query for every received keyboard packet.
const bool keyboard_input = remote_action.type == ControlType::keyboard ||
remote_action.type == ControlType::keyboard_state;
if (!keyboard_input && runtime->privacy_.Engaged() &&
!IsWindowsPrivacyDesktopAvailable()) {
runtime->privacy_.Fail("Secure desktop or lock screen; privacy cannot cover system security UI. Remote input paused.");
return;
}
#endif
if (!runtime->privacy_.RemoteAllowed()) return;
}
if (remote_action.type == ControlType::service_status) {
if (auto props = runtime->FindRemoteSession(remote_id)) {
runtime->ApplyRemoteServiceStatus(*props, remote_action.ss);
@@ -155,6 +199,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
if (remote_action.type == ControlType::service_command) {
if (runtime->privacy_.Engaged()) {
runtime->privacy_.Fail("Security shortcuts require leaving privacy mode; remote operation paused. Turn privacy off first.");
return;
}
#if _WIN32
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
runtime->pending_windows_service_sas_.store(true,
@@ -258,7 +306,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
std::chrono::steady_clock::now();
}
#if _WIN32
if (runtime->local_service_status_received_ &&
if (!runtime->privacy_.Engaged() && runtime->local_service_status_received_ &&
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
remote_action.type != ControlType::keyboard &&
remote_action.type != ControlType::keyboard_state) {
@@ -405,6 +405,20 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->show_connection_status_window_ = true;
{
std::unique_lock lock(runtime->connection_status_mutex_);
#ifdef _WIN32
if (status == ConnectionStatus::Connected &&
runtime->config_center_->IsEnablePrivacyScreen() &&
std::none_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto& entry) {
return entry.second == ConnectionStatus::Connected;
})) {
// Pause before Connected becomes observable by capture/input threads.
// Only the first controller takes over local input. A later join must
// not release an existing controller's keys or re-enable privacy that
// the current session explicitly turned off.
runtime->privacy_.EnableOnConnection();
}
#endif
runtime->connection_status_[remote_id] = status;
}
@@ -0,0 +1,92 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include <shared_mutex>
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
// Connections appear here only after the existing password/signalling/RTC
// admission path. Outgoing/viewer sessions live in remote_sessions_ instead.
std::shared_lock lock(connection_status_mutex_);
const auto found = connection_status_.find(remote_id);
return found != connection_status_.end() &&
found->second == ConnectionStatus::Connected;
}
void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
const PrivacyCommand& command) {
if (!IsAuthorizedController(remote_id)) return;
std::lock_guard lock(privacy_commands_mutex_);
if (privacy_commands_.size() < 32)
privacy_commands_.emplace_back(remote_id, command);
}
void GuiRuntime::HandlePrivacy() {
std::deque<std::pair<std::string, PrivacyCommand>> commands;
{
std::lock_guard lock(privacy_commands_mutex_);
commands.swap(privacy_commands_);
}
for (const auto& [remote_id, command] : commands) {
if (!IsAuthorizedController(remote_id)) continue;
if (command.flag == PrivacyCommandFlag::enable) {
privacy_.Enable(command.block_local_input);
} else if (command.flag == PrivacyCommandFlag::disable) {
privacy_.Disable();
}
last_privacy_status_tick_ = 0;
}
const auto status = privacy_.Snapshot();
if (status.remote_paused && !privacy_was_paused_) {
keyboard_.ReleaseAllRemotePressedKeys("privacy_paused");
}
#ifdef _WIN32
if (status.remote_paused && IsWindowsPrivacyDesktopAvailable())
devices_.ReleaseRemoteMouseButtons();
#endif
if (privacy_was_paused_ && !status.remote_paused &&
status.state == PrivacyState::off && start_screen_capturer_) {
// A capture failure may have stopped the underlying capturer. Reuse the
// normal start/retry path when the user explicitly leaves privacy mode.
screen_capturer_is_started_ = devices_.StartScreenCapturer() == 0;
}
privacy_was_paused_ = status.remote_paused;
const uint64_t now = SDL_GetTicks();
if (!peer_ || (last_privacy_status_tick_ != 0 &&
status.revision == last_privacy_revision_ &&
now - last_privacy_status_tick_ < 1000))
return;
std::vector<std::string> controllers;
{
std::shared_lock lock(connection_status_mutex_);
for (const auto& [id, state] : connection_status_)
if (state == ConnectionStatus::Connected) controllers.push_back(id);
}
RemoteAction action{};
action.type = ControlType::privacy_status;
action.ps = status;
const auto message = action.to_json();
bool sent = true;
for (const auto& id : controllers) {
const int result = SendReliableDataFrameToPeer(
peer_, message.data(), message.size(), control_data_label_.c_str(),
id.data(), id.size());
if (result != 0) {
sent = false;
LOG_WARN("Privacy status send failed, peer={}, ret={}", id, result);
}
}
if (sent) {
last_privacy_revision_ = status.revision;
last_privacy_status_tick_ = now;
}
}
} // namespace crossdesk
@@ -157,6 +157,14 @@ struct RemoteSession {
std::string audio_capture_button_label_ = "Audio Capture";
std::string remote_host_name_;
bool remote_service_status_received_ = false;
std::mutex privacy_status_mutex_;
PrivacyStatus privacy_status_{};
bool privacy_status_received_ = false;
bool privacy_command_pending_ = false;
bool privacy_block_local_input_ = true;
uint64_t privacy_command_tick_ = 0;
uint32_t privacy_request_revision_ = 0;
uint64_t privacy_status_tick_ = 0;
bool remote_service_available_ = false;
std::string remote_interactive_stage_;
std::vector<DisplayInfo> display_info_list_;
@@ -184,4 +192,4 @@ using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail
#endif
#endif