mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-19 02:13:03 +08:00
[feat] add Windows privacy screen and prefer DXGI capture
This commit is contained in:
@@ -1027,6 +1027,9 @@ void GuiApplication::InitializeSettings() {
|
||||
language_button_value_ = localization_language_index_;
|
||||
localization_language_ =
|
||||
static_cast<ConfigCenter::LANGUAGE>(localization_language_index_);
|
||||
privacy_.SetText({
|
||||
localization::privacy_verification[localization_language_index_],
|
||||
localization::privacy_screen_unlock_hint[localization_language_index_]});
|
||||
}
|
||||
|
||||
bool GuiApplication::InitializeSDL() {
|
||||
@@ -1227,6 +1230,11 @@ void GuiApplication::ResetSettingsUi() {
|
||||
ui_->main->set_self_hosted_enabled(enable_self_hosted_);
|
||||
ui_->main->set_autostart_enabled(enable_autostart_);
|
||||
ui_->main->set_daemon_enabled(enable_daemon_);
|
||||
#ifdef _WIN32
|
||||
ui_->main->set_privacy_setting_visible(true);
|
||||
ui_->main->set_privacy_on_connect_enabled(
|
||||
config_center_->IsEnablePrivacyScreen());
|
||||
#endif
|
||||
ui_->main->set_file_save_path(file_transfer_save_path_buf_);
|
||||
ui_->main->set_server_host(signal_server_ip_self_);
|
||||
ui_->main->set_server_port(signal_server_port_self_);
|
||||
@@ -1587,6 +1595,37 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
|
||||
props->control_data_label_.c_str());
|
||||
});
|
||||
stream->on_toggle_privacy([this] {
|
||||
auto props = SelectedSession();
|
||||
if (!props || !props->peer_ || !props->control_mouse_ ||
|
||||
props->connection_status_.load() != ConnectionStatus::Connected) return;
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::privacy_command;
|
||||
{
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
const auto& status = props->privacy_status_;
|
||||
action.pc.flag = status.overlay_active || status.remote_paused || status.state == PrivacyState::on ||
|
||||
status.state == PrivacyState::failed ? PrivacyCommandFlag::disable : PrivacyCommandFlag::enable;
|
||||
action.pc.block_local_input = props->privacy_block_local_input_;
|
||||
props->privacy_command_pending_ = true;
|
||||
props->privacy_command_tick_ = SDL_GetTicks();
|
||||
props->privacy_request_revision_ = status.revision;
|
||||
}
|
||||
const auto message = action.to_json();
|
||||
if (SendReliableDataFrame(props->peer_, message.data(), message.size(),
|
||||
props->control_data_label_.c_str()) != 0) {
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
props->privacy_command_pending_ = false;
|
||||
props->privacy_status_received_ = false;
|
||||
LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_);
|
||||
}
|
||||
});
|
||||
stream->on_toggle_privacy_input([this] {
|
||||
if (auto props = SelectedSession()) {
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
props->privacy_block_local_input_ = !props->privacy_block_local_input_;
|
||||
}
|
||||
});
|
||||
stream->on_toggle_mouse_control([this] {
|
||||
auto props = SelectedSession();
|
||||
if (!props || !props->connection_established_) {
|
||||
@@ -1794,6 +1833,7 @@ void GuiApplication::Tick() {
|
||||
HandleConnectionStatusChange();
|
||||
HandlePendingPresenceProbe();
|
||||
HandlePresenceProbeTimeout();
|
||||
HandlePrivacy();
|
||||
HandleWindowsServiceIntegration();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
SyncXWaylandWindowActivation();
|
||||
@@ -2104,7 +2144,7 @@ void GuiApplication::SyncMainWindow() {
|
||||
ui_->main->set_latest_version(UiText(latest_version_));
|
||||
ui_->main->set_release_name(UiText(release_name_));
|
||||
ui_->main->set_release_date(UiText(release_date_));
|
||||
ui_->main->set_settings_session_active(stream_window_inited_);
|
||||
ui_->main->set_settings_session_active(HasActiveSession());
|
||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
||||
!defined(__arm__) && USE_CUDA) || \
|
||||
defined(__APPLE__))
|
||||
@@ -2507,6 +2547,29 @@ void GuiApplication::SyncStreamWindow() {
|
||||
? localization::receiving_screen[localization_language_index_]
|
||||
: std::string{}));
|
||||
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
|
||||
{
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
const auto& status = props->privacy_status_;
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
const bool timeout = props->privacy_command_pending_ && now - props->privacy_command_tick_ > 15000;
|
||||
const bool stale = props->privacy_status_received_ && now - props->privacy_status_tick_ > 5000;
|
||||
const bool pending = props->privacy_command_pending_ && !timeout;
|
||||
const bool active = status.overlay_active || status.remote_paused || status.state == PrivacyState::on || status.state == PrivacyState::failed;
|
||||
(*ui_->stream)->set_privacy_active(active);
|
||||
(*ui_->stream)->set_privacy_can_toggle(props->control_mouse_ &&
|
||||
props->connection_status_.load() == ConnectionStatus::Connected && !pending &&
|
||||
(active || (props->privacy_status_received_ && status.supported && !stale)));
|
||||
(*ui_->stream)->set_privacy_can_change_input(props->control_mouse_ && !active && !pending && status.input_block_supported);
|
||||
(*ui_->stream)->set_privacy_block_input(active ? status.input_blocked : props->privacy_block_local_input_);
|
||||
(*ui_->stream)->set_privacy_remote_paused(status.remote_paused || (active && (stale || timeout)));
|
||||
const int language = localization_language_index_;
|
||||
std::string text = !props->privacy_status_received_ ? localization::privacy_unknown[language] :
|
||||
(timeout || stale) ? localization::privacy_timeout[language] :
|
||||
pending ? localization::privacy_pending[language] :
|
||||
status.remote_paused ? localization::privacy_paused[language] : status.reason;
|
||||
if (status.remote_paused && !pending) text += std::string("\n") + status.reason;
|
||||
(*ui_->stream)->set_privacy_status_text(UiText(text));
|
||||
}
|
||||
int remote_cursor_shape =
|
||||
static_cast<int>(RemoteCursorShape::default_cursor);
|
||||
bool remote_cursor_active = false;
|
||||
@@ -2750,7 +2813,14 @@ void GuiApplication::SyncServerWindow() {
|
||||
});
|
||||
}
|
||||
|
||||
if (has_connected_controller && !ui_->server) {
|
||||
// Keep the local connection panel out of captured privacy sessions and
|
||||
// transitions; it is unnecessary while the physical screens are covered.
|
||||
const auto privacy_status = privacy_.Snapshot();
|
||||
const bool privacy_covering = privacy_status.overlay_active ||
|
||||
privacy_status.state == PrivacyState::starting ||
|
||||
privacy_status.state == PrivacyState::stopping;
|
||||
const bool show_controller_window = has_connected_controller && !privacy_covering;
|
||||
if (show_controller_window && !ui_->server) {
|
||||
ui_->server.emplace(ui::ServerWindow::create());
|
||||
RegisterFontAwesome((*ui_->server)->window());
|
||||
(*ui_->server)->set_controllers(ui_->controller_model);
|
||||
@@ -2774,7 +2844,7 @@ void GuiApplication::SyncServerWindow() {
|
||||
server_window_created_ = true;
|
||||
server_window_inited_ = true;
|
||||
}
|
||||
if (!has_connected_controller && ui_->server) {
|
||||
if (!show_controller_window && ui_->server) {
|
||||
(*ui_->server)->hide();
|
||||
ui_->server.reset();
|
||||
server_window_created_ = false;
|
||||
@@ -2918,6 +2988,9 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
|
||||
localization_language_index_ = language_button_value_;
|
||||
config_center_->SetLanguage(localization_language_);
|
||||
privacy_.SetText({
|
||||
localization::privacy_verification[localization_language_index_],
|
||||
localization::privacy_screen_unlock_hint[localization_language_index_]});
|
||||
config_center_->SetVideoQuality(
|
||||
static_cast<ConfigCenter::VIDEO_QUALITY>(video_quality_button_value_));
|
||||
config_center_->SetVideoFrameRate(static_cast<ConfigCenter::VIDEO_FRAME_RATE>(
|
||||
@@ -2934,6 +3007,13 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
config_center_->SetSelfHosted(enable_self_hosted_);
|
||||
config_center_->SetAutostart(enable_autostart_);
|
||||
config_center_->SetDaemon(enable_daemon_);
|
||||
#ifdef _WIN32
|
||||
if (config_center_->SetPrivacyScreen(main->get_privacy_on_connect_enabled()) !=
|
||||
0) {
|
||||
main->set_privacy_on_connect_enabled(
|
||||
config_center_->IsEnablePrivacyScreen());
|
||||
}
|
||||
#endif
|
||||
|
||||
const std::string path(main->get_file_save_path());
|
||||
std::memset(file_transfer_save_path_buf_, 0,
|
||||
@@ -2965,7 +3045,7 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
file_transfer_save_path_last_ = path;
|
||||
ui_->localized_language = -1;
|
||||
|
||||
if (!stream_window_inited_) {
|
||||
if (!HasActiveSession()) {
|
||||
CloseAllRemoteSessions();
|
||||
CreateConnectionPeer();
|
||||
}
|
||||
@@ -3267,9 +3347,12 @@ void GuiApplication::Cleanup() {
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
privacy_.Fail("Application exiting; remote operation paused");
|
||||
clipboard_.Shutdown();
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
keyboard_.ReleaseAllRemotePressedKeys("application_exit");
|
||||
devices_.DestroyDevices();
|
||||
privacy_.Shutdown();
|
||||
devices_.DestroyFactories();
|
||||
CloseAllRemoteSessions();
|
||||
WaitForSessionCleanup();
|
||||
|
||||
@@ -20,6 +20,13 @@ struct TranslationRow {
|
||||
|
||||
// Single source of truth for all UI strings.
|
||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||
X(privacy_enable, u8"开启隐私屏", "Enable privacy screen", u8"Включить приватный экран") \
|
||||
X(privacy_disable, u8"关闭隐私屏 / 恢复操作", "Turn privacy off / recover", u8"Выключить / восстановить") \
|
||||
X(privacy_block_input, u8"开启时屏蔽本地输入", "Block local input on enable", u8"Блокировать локальный ввод") \
|
||||
X(privacy_unknown, u8"隐私屏:未收到被控端确认(旧版本可能不支持)", "Privacy: no host confirmation (older hosts may not support it)", u8"Приватность: нет подтверждения от узла") \
|
||||
X(privacy_pending, u8"隐私屏:等待被控端完成验证", "Privacy: waiting for host verification", u8"Приватность: ожидание проверки") \
|
||||
X(privacy_timeout, u8"隐私屏状态未知:被控端确认超时,可关闭后重试", "Privacy state unknown: host confirmation timed out; turn off to recover", u8"Статус неизвестен: время ожидания истекло") \
|
||||
X(privacy_paused, u8"隐私保护未就绪,远程操作已暂停。可在快捷菜单关闭隐私屏恢复。", "Privacy protection is not ready; remote operation paused. Turn privacy off in the shortcut menu to recover.", u8"Приватность не готова; удалённое управление приостановлено.") \
|
||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||
@@ -202,6 +209,8 @@ struct TranslationRow {
|
||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
|
||||
X(privacy_on_connect, u8"被连接时开启隐私屏:", "Privacy screen on connect:", \
|
||||
u8"Приватность при подключении:") \
|
||||
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
|
||||
u8"Вступит в силу после перезапуска") \
|
||||
X(select_file, u8"选择文件发送", "Select File to Send", \
|
||||
@@ -237,6 +246,11 @@ struct TranslationRow {
|
||||
u8"Для работы приложения требуются следующие разрешения:") \
|
||||
X(show_main_window, u8"显示主界面", "Show Main Window", \
|
||||
u8"Показать главное окно") \
|
||||
X(privacy_verification, u8"隐私屏验证", "Privacy screen verification", \
|
||||
u8"Проверка приватности") \
|
||||
X(privacy_screen_unlock_hint, u8"按下快捷键,解除隐私屏", \
|
||||
"To turn off the privacy screen, press", \
|
||||
u8"Чтобы отключить приватный экран, нажмите") \
|
||||
X(exit_program, u8"退出", "Exit", u8"Выход")
|
||||
|
||||
inline constexpr TranslationRow kTranslationRows[] = {
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "speaker_capturer_factory.h"
|
||||
#ifdef _WIN32
|
||||
#include "platform/windows/screen_capturer/screen_capturer_win.h"
|
||||
#include "platform/windows/input/mouse/mouse_controller.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
@@ -28,7 +32,7 @@ SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
|
||||
SpeakerCapturerFactory().Create());
|
||||
},
|
||||
[this](unsigned char *data, size_t size, const char *) {
|
||||
if (!owner_.peer_) return;
|
||||
if (!owner_.peer_ || !owner_.privacy_.RemoteAllowed()) return;
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
@@ -74,6 +78,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
if (!screen_capturer_) {
|
||||
screen_capturer_ =
|
||||
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
|
||||
#ifdef _WIN32
|
||||
if (auto windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_))
|
||||
windows->SetPrivacyController(&owner_.privacy_);
|
||||
#endif
|
||||
}
|
||||
|
||||
last_frame_time_ = {};
|
||||
@@ -92,6 +100,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
||||
const char *display_name,
|
||||
const MiniRtcNativeVideoFrame *native_frame) {
|
||||
if (!owner_.privacy_.RemoteAllowed()) return;
|
||||
const auto now_time = std::chrono::steady_clock::now();
|
||||
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
||||
return;
|
||||
@@ -154,6 +163,9 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
}
|
||||
|
||||
MiniRtcVideoFrame frame{};
|
||||
// A new controller can connect after this callback's first gate check
|
||||
// while it is collecting the Connected peer IDs above.
|
||||
if (!owner_.privacy_.RemoteAllowed()) return;
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.width = width;
|
||||
@@ -216,6 +228,13 @@ int SessionDeviceManager::StopScreenCapturer() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
void SessionDeviceManager::ReleaseRemoteMouseButtons() {
|
||||
if (auto mouse = dynamic_cast<PlatformMouseController*>(mouse_controller_))
|
||||
mouse->ReleasePressedButtons();
|
||||
}
|
||||
#endif
|
||||
|
||||
void SessionDeviceManager::StartSpeakerCapturer() {
|
||||
speaker_capture_.SetEnabled(true);
|
||||
}
|
||||
@@ -488,6 +507,7 @@ void SessionDeviceManager::DrainCapturedKeyboardInput() {
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
inputs.swap(captured_keyboard_inputs_);
|
||||
}
|
||||
if (owner_.privacy_.Snapshot().input_blocked) return;
|
||||
for (const CapturedKeyboardInput &input : inputs) {
|
||||
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
|
||||
input.scan_code, input.extended);
|
||||
@@ -508,6 +528,7 @@ bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
||||
|
||||
void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
|
||||
int selected_display) {
|
||||
if (!owner_.privacy_.RemoteAllowed()) return;
|
||||
if (mouse_controller_) {
|
||||
mouse_controller_->SendMouseCommand(action, selected_display);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ public:
|
||||
int InitializeScreenCapturer();
|
||||
int StartScreenCapturer();
|
||||
int StopScreenCapturer();
|
||||
#ifdef _WIN32
|
||||
void ReleaseRemoteMouseButtons();
|
||||
#endif
|
||||
// Requests are asynchronous; Stop drains/revokes audio delivery before
|
||||
// returning so the connection peer can be destroyed safely.
|
||||
void StartSpeakerCapturer();
|
||||
@@ -102,4 +105,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -212,8 +212,13 @@ int KeyboardController::SendKeyCommand(int key_code, bool is_down,
|
||||
|
||||
bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
if (is_down && !owner_.privacy_.RemoteAllowed()) return false;
|
||||
#if _WIN32
|
||||
if (owner_.local_service_status_received_ &&
|
||||
if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
|
||||
owner_.privacy_.Fail("Secure desktop or locked session; remote key injection paused");
|
||||
return false;
|
||||
}
|
||||
if (!owner_.privacy_.Engaged() && owner_.local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
|
||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||
key_code, is_down, scan_code, extended, 1000);
|
||||
@@ -246,6 +251,8 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
// Recheck after the desktop query; key-up cleanup remains allowed while paused.
|
||||
if (is_down && !owner_.privacy_.RemoteAllowed()) return false;
|
||||
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
@@ -342,11 +349,50 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
||||
}
|
||||
for (const PressedKey& key : keys_to_release) {
|
||||
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
||||
if (!InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
||||
#ifdef _WIN32
|
||||
if (owner_.privacy_.Engaged()) {
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
pending_privacy_releases_[key.key_code] = key;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseAllRemotePressedKeys(const char* reason) {
|
||||
std::vector<std::string> remotes;
|
||||
{
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
for (const auto& [id, state] : remote_states_) remotes.push_back(id);
|
||||
}
|
||||
for (const auto& id : remotes) ReleaseRemotePressedKeys(id, reason);
|
||||
CheckRemoteTimeouts();
|
||||
}
|
||||
|
||||
void KeyboardController::CheckRemoteTimeouts() {
|
||||
#ifdef _WIN32
|
||||
bool pending = false;
|
||||
{
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
pending = !pending_privacy_releases_.empty();
|
||||
}
|
||||
// A key-up rejected on the secure desktop is retained until the ordinary
|
||||
// desktop returns. Never route cleanup through the security UI helper.
|
||||
if (pending && IsWindowsPrivacyDesktopAvailable()) {
|
||||
std::unordered_map<int, PressedKey> releases;
|
||||
{
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
releases.swap(pending_privacy_releases_);
|
||||
}
|
||||
for (const auto& [code, key] : releases) {
|
||||
if (!owner_.devices_.SendKeyboardCommand(code, false, key.scan_code, key.extended)) {
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
pending_privacy_releases_[code] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
std::vector<std::string> timed_out_remotes;
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
const RemoteAction &remote_action);
|
||||
void ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
const char *reason);
|
||||
void ReleaseAllRemotePressedKeys(const char* reason);
|
||||
void CheckRemoteTimeouts();
|
||||
|
||||
private:
|
||||
@@ -58,6 +59,9 @@ private:
|
||||
uint32_t last_heartbeat_tick_ = 0;
|
||||
std::unordered_map<std::string, RemoteState> remote_states_;
|
||||
std::mutex remote_states_mutex_;
|
||||
#ifdef _WIN32
|
||||
std::unordered_map<int, PressedKey> pending_privacy_releases_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,6 +50,7 @@ export global UiStrings {
|
||||
in-out property <string> self-hosted: "Self-hosted server";
|
||||
in-out property <string> autostart: "Start with system";
|
||||
in-out property <string> daemon: "Run in background";
|
||||
in-out property <string> privacy-on-connect: "Privacy screen on connect";
|
||||
in-out property <string> file-save-path: "File save path";
|
||||
in-out property <string> default-desktop: "Desktop";
|
||||
in-out property <string> server-host: "Server host";
|
||||
@@ -246,6 +247,8 @@ export component MainWindow inherits Window {
|
||||
in-out property <bool> self-hosted-enabled: false;
|
||||
in-out property <bool> autostart-enabled: false;
|
||||
in-out property <bool> daemon-enabled: false;
|
||||
in-out property <bool> privacy-setting-visible: false;
|
||||
in-out property <bool> privacy-on-connect-enabled: true;
|
||||
in-out property <string> file-save-path: "";
|
||||
in-out property <string> server-host: "";
|
||||
in-out property <string> server-port: "";
|
||||
@@ -1445,6 +1448,11 @@ export component MainWindow inherits Window {
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
|
||||
}
|
||||
if root.privacy-setting-visible: Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.privacy-on-connect-enabled; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
|
||||
@@ -25,6 +25,9 @@ export struct NetworkStatsRow {
|
||||
export global StreamStrings {
|
||||
in-out property <string> select-display: "Select Display";
|
||||
in-out property <string> send-shortcut: "Send Shortcut";
|
||||
in-out property <string> privacy-enable: "Enable privacy screen";
|
||||
in-out property <string> privacy-disable: "Turn privacy off / recover";
|
||||
in-out property <string> privacy-block-input: "Block local input on enable";
|
||||
in-out property <string> control-mouse: "Control Mouse";
|
||||
in-out property <string> release-mouse: "Release Mouse";
|
||||
in-out property <string> audio: "Audio Capture";
|
||||
@@ -134,6 +137,12 @@ export component StreamWindow inherits Window {
|
||||
in property <[string]> displays;
|
||||
in-out property <int> selected-display: 0;
|
||||
in property <bool> mouse-control-enabled: true;
|
||||
in property <bool> privacy-can-toggle: false;
|
||||
in property <bool> privacy-active: false;
|
||||
in property <bool> privacy-block-input: true;
|
||||
in property <bool> privacy-can-change-input: false;
|
||||
in property <bool> privacy-remote-paused: false;
|
||||
in property <string> privacy-status-text: "Privacy screen: waiting for host";
|
||||
// Numeric values mirror crossdesk::RemoteCursorShape and Slint's MouseCursor.
|
||||
in property <bool> remote-cursor-active: false;
|
||||
in property <int> remote-cursor-shape: 0;
|
||||
@@ -154,6 +163,8 @@ export component StreamWindow inherits Window {
|
||||
callback switch-display(int);
|
||||
callback send-shortcut(string);
|
||||
callback toggle-mouse-control;
|
||||
callback toggle-privacy;
|
||||
callback toggle-privacy-input;
|
||||
callback toggle-audio;
|
||||
callback select-file;
|
||||
callback close-file-transfer;
|
||||
@@ -232,9 +243,9 @@ export component StreamWindow inherits Window {
|
||||
let shortcut-menu-y = control.y + 40px;
|
||||
let over-shortcut-menu = root.shortcut-menu-open
|
||||
&& pointer-x >= shortcut-menu-x
|
||||
&& pointer-x <= shortcut-menu-x + 150px
|
||||
&& pointer-x <= shortcut-menu-x + 360px
|
||||
&& pointer-y >= shortcut-menu-y
|
||||
&& pointer-y <= shortcut-menu-y + 68px;
|
||||
&& pointer-y <= shortcut-menu-y + 200px;
|
||||
return over-control || over-display-menu || over-shortcut-menu;
|
||||
}
|
||||
|
||||
@@ -943,7 +954,7 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
|
||||
if root.shortcut-menu-open: Rectangle {
|
||||
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 150px; height: 68px;
|
||||
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 360px; height: 200px;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
|
||||
VerticalLayout {
|
||||
padding: 4px; spacing: 1px;
|
||||
@@ -959,9 +970,34 @@ export component StreamWindow inherits Window {
|
||||
clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
height: 29px; background: privacy-touch.has-hover ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: root.privacy-active ? StreamStrings.privacy-disable : StreamStrings.privacy-enable; color: root.privacy-can-toggle ? #30343b : #8a8f96; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
privacy-touch := TouchArea {
|
||||
enabled: root.privacy-can-toggle;
|
||||
pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } }
|
||||
clicked => { root.toggle-privacy(); }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
height: 29px; background: privacy-input-touch.has-hover ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: (root.privacy-block-input ? "[✓] " : "[ ] ") + StreamStrings.privacy-block-input; color: root.privacy-can-change-input ? #30343b : #8a8f96; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
privacy-input-touch := TouchArea {
|
||||
enabled: root.privacy-can-change-input;
|
||||
pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } }
|
||||
clicked => { root.toggle-privacy-input(); }
|
||||
}
|
||||
}
|
||||
Text { height: 68px; text: root.privacy-status-text; wrap: word-wrap; color: #555b66; font-size: ImGuiFontStyle.body; }
|
||||
}
|
||||
}
|
||||
|
||||
if root.privacy-remote-paused: Rectangle {
|
||||
x: 0px; y: 65px; width: parent.width; height: parent.height - 65px;
|
||||
background: #16181d; z: 6;
|
||||
Text { x: 30px; width: parent.width - 60px; text: root.privacy-status-text; color: #eeeeee; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
|
||||
// Transfer progress overlay at bottom-left.
|
||||
if root.file-transfer-visible: Rectangle {
|
||||
x: 18px; y: parent.height - min(210px, parent.height * 0.35); width: min(430px, parent.width * 0.6); height: min(190px, parent.height * 0.32);
|
||||
|
||||
@@ -71,6 +71,7 @@ inline int ApplyMainWindowStrings(
|
||||
Text(localization::self_hosted_server_config[language]));
|
||||
strings.set_autostart(Text(localization::enable_autostart[language]));
|
||||
strings.set_daemon(Text(localization::enable_daemon[language]));
|
||||
strings.set_privacy_on_connect(Text(localization::privacy_on_connect[language]));
|
||||
strings.set_file_save_path(
|
||||
Text(localization::file_transfer_save_path[language]));
|
||||
strings.set_default_desktop(Text(localization::default_desktop[language]));
|
||||
@@ -138,6 +139,9 @@ inline int ApplyStreamWindowStrings(
|
||||
auto& strings = window->template global<ui::StreamStrings>();
|
||||
strings.set_select_display(Text(localization::select_display[language]));
|
||||
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
|
||||
strings.set_privacy_enable(Text(localization::privacy_enable[language]));
|
||||
strings.set_privacy_disable(Text(localization::privacy_disable[language]));
|
||||
strings.set_privacy_block_input(Text(localization::privacy_block_input[language]));
|
||||
strings.set_control_mouse(Text(localization::control_mouse[language]));
|
||||
strings.set_release_mouse(Text(localization::release_mouse[language]));
|
||||
strings.set_audio(Text(localization::audio_capture[language]));
|
||||
@@ -167,4 +171,4 @@ inline int ApplyStreamWindowStrings(
|
||||
} // namespace crossdesk::ui_localization
|
||||
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user