mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 17:33:21 +08:00
[feat] add Windows privacy screen and prefer DXGI capture
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user