mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-19 10:23:10 +08:00
[feat] add macOS privacy screen and keep remote sessions running
This commit is contained in:
@@ -1029,7 +1029,6 @@ void GuiApplication::InitializeSettings() {
|
||||
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_]});
|
||||
}
|
||||
|
||||
@@ -1237,6 +1236,8 @@ void GuiApplication::ResetSettingsUi() {
|
||||
ui_->main->set_capture_method_visible(true);
|
||||
ui_->main->set_capture_method_index(
|
||||
static_cast<int>(config_center_->GetScreenCaptureMethod()));
|
||||
#endif
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
ui_->main->set_privacy_setting_available(true);
|
||||
#else
|
||||
ui_->main->set_privacy_setting_available(false);
|
||||
@@ -2569,14 +2570,10 @@ void GuiApplication::SyncStreamWindow() {
|
||||
const int language = localization_language_index_;
|
||||
std::string text = !connected || !props->privacy_status_received_
|
||||
? localization::privacy_screen[language]
|
||||
: controls.timeout || controls.stale ? localization::privacy_timeout[language]
|
||||
: controls.warning ? localization::privacy_paused[language]
|
||||
: controls.unsupported ? localization::privacy_unsupported[language]
|
||||
: controls.enabled ? localization::privacy_on[language]
|
||||
: !status.supported ? localization::privacy_unsupported[language]
|
||||
: controls.off ? localization::privacy_off[language]
|
||||
: localization::privacy_screen[language];
|
||||
if (connected && controls.warning && status.reason[0] != '\0')
|
||||
text += std::string("\n") + status.reason;
|
||||
(*ui_->stream)->set_privacy_status_text(UiText(text));
|
||||
}
|
||||
int remote_cursor_shape =
|
||||
@@ -2822,13 +2819,17 @@ void GuiApplication::SyncServerWindow() {
|
||||
});
|
||||
}
|
||||
|
||||
// Keep the local connection panel out of captured privacy sessions and
|
||||
// transitions; it is unnecessary while the physical screens are covered.
|
||||
// macOS covers the local panel with capture-excluded privacy windows. Keep
|
||||
// the panel alive so remote controllers can still see and operate it.
|
||||
bool show_controller_window = has_connected_controller;
|
||||
#if !defined(__APPLE__)
|
||||
// Preserve the existing panel behavior on other platforms.
|
||||
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;
|
||||
show_controller_window = show_controller_window && !privacy_covering;
|
||||
#endif
|
||||
if (show_controller_window && !ui_->server) {
|
||||
ui_->server.emplace(ui::ServerWindow::create());
|
||||
RegisterFontAwesome((*ui_->server)->window());
|
||||
@@ -2998,7 +2999,6 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
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_));
|
||||
@@ -3026,6 +3026,8 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
}
|
||||
main->set_capture_method_index(
|
||||
static_cast<int>(config_center_->GetScreenCaptureMethod()));
|
||||
#endif
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
if (config_center_->SetPrivacyScreen(main->get_privacy_on_connect_enabled()) !=
|
||||
0) {
|
||||
main->set_privacy_on_connect_enabled(
|
||||
@@ -3365,7 +3367,7 @@ void GuiApplication::Cleanup() {
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
privacy_.Fail("Application exiting; remote operation paused");
|
||||
privacy_.Disable();
|
||||
clipboard_.Shutdown();
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
keyboard_.ReleaseAllRemotePressedKeys("application_exit");
|
||||
|
||||
@@ -22,12 +22,10 @@ struct TranslationRow {
|
||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||
X(privacy_screen, u8"隐私屏", "Privacy screen", u8"Приватный экран") \
|
||||
X(privacy_enable, u8"开启隐私屏", "Enable privacy screen", u8"Включить приватный экран") \
|
||||
X(privacy_disable, u8"关闭隐私屏 / 恢复操作", "Turn privacy off / recover", u8"Выключить / восстановить") \
|
||||
X(privacy_disable, u8"关闭隐私屏", "Disable privacy screen", u8"Выключить приватный экран") \
|
||||
X(privacy_on, u8"隐私屏:已开启", "Privacy screen: on", u8"Приватный экран: включён") \
|
||||
X(privacy_off, u8"隐私屏:已关闭", "Privacy screen: off", u8"Приватный экран: выключен") \
|
||||
X(privacy_unsupported, u8"隐私屏:被控端暂不支持", "Privacy screen: unavailable on this host", 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. Click the privacy screen button to turn it off and recover.", u8"Приватность не готова; нажмите кнопку приватного экрана для выключения и восстановления управления.") \
|
||||
X(privacy_unsupported, u8"隐私屏:不可用", "Privacy screen: unavailable", 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", \
|
||||
@@ -250,8 +248,6 @@ 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"Чтобы отключить приватный экран, нажмите") \
|
||||
|
||||
@@ -32,7 +32,7 @@ SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
|
||||
SpeakerCapturerFactory().Create());
|
||||
},
|
||||
[this](unsigned char *data, size_t size, const char *) {
|
||||
if (!owner_.peer_ || !owner_.privacy_.RemoteAllowed()) return;
|
||||
if (!owner_.peer_) return;
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
@@ -79,9 +79,9 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
screen_capturer_ =
|
||||
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
|
||||
}
|
||||
screen_capturer_->SetPrivacyController(&owner_.privacy_);
|
||||
#ifdef _WIN32
|
||||
if (auto windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_)) {
|
||||
windows->SetPrivacyController(&owner_.privacy_);
|
||||
windows->SetCaptureMethod(owner_.config_center_->GetScreenCaptureMethod());
|
||||
}
|
||||
#endif
|
||||
@@ -102,7 +102,6 @@ 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;
|
||||
@@ -165,9 +164,6 @@ 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;
|
||||
@@ -530,7 +526,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -212,13 +212,11 @@ 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_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
|
||||
owner_.privacy_.Fail("Secure desktop or locked session; remote key injection paused");
|
||||
return false;
|
||||
owner_.privacy_.Fail("Secure desktop or locked session; privacy screen will turn off");
|
||||
}
|
||||
if (!owner_.privacy_.Engaged() && owner_.local_service_status_received_ &&
|
||||
if (owner_.local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
|
||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||
key_code, is_down, scan_code, extended, 1000);
|
||||
@@ -252,7 +250,6 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
}
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,6 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
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,8 +88,6 @@ 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);
|
||||
|
||||
@@ -185,11 +183,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
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;
|
||||
runtime->privacy_.Fail("Secure desktop or lock screen; privacy screen will turn off");
|
||||
}
|
||||
#endif
|
||||
if (!runtime->privacy_.RemoteAllowed()) return;
|
||||
}
|
||||
if (remote_action.type == ControlType::service_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
@@ -200,8 +196,7 @@ 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;
|
||||
runtime->privacy_.Fail("Security shortcut requested; privacy screen will turn off");
|
||||
}
|
||||
#if _WIN32
|
||||
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
|
||||
@@ -306,7 +301,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
std::chrono::steady_clock::now();
|
||||
}
|
||||
#if _WIN32
|
||||
if (!runtime->privacy_.Engaged() && runtime->local_service_status_received_ &&
|
||||
if (runtime->local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
|
||||
remote_action.type != ControlType::keyboard &&
|
||||
remote_action.type != ControlType::keyboard_state) {
|
||||
|
||||
@@ -405,14 +405,14 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
runtime->show_connection_status_window_ = true;
|
||||
{
|
||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
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.
|
||||
// Request optional privacy without delaying connection or capture.
|
||||
// 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.
|
||||
|
||||
@@ -18,38 +18,33 @@ struct PrivacyControlState {
|
||||
bool enabled = false;
|
||||
bool off = false;
|
||||
bool unsupported = false;
|
||||
bool warning = false;
|
||||
bool stale = false;
|
||||
bool timeout = false;
|
||||
bool can_toggle = false;
|
||||
};
|
||||
|
||||
// Use the same host-confirmed state for presentation and command admission.
|
||||
// An engaged or failed screen can still be disabled when its status goes stale.
|
||||
// Missing confirmation or a failure makes only this toolbar control unavailable.
|
||||
inline PrivacyControlState GetPrivacyControlState(
|
||||
const PrivacyStatus& status, bool received, bool command_pending,
|
||||
uint64_t status_age_ms, uint64_t command_age_ms) {
|
||||
PrivacyControlState result;
|
||||
result.timeout = command_pending && command_age_ms > 15000;
|
||||
result.stale = received && status_age_ms > 5000;
|
||||
const bool request_pending = command_pending && !result.timeout;
|
||||
const bool fresh = received && !result.stale;
|
||||
const bool timeout = command_pending && command_age_ms > 15000;
|
||||
result.unsupported = timeout ||
|
||||
(received && (status_age_ms > 5000 || !status.supported ||
|
||||
status.state == PrivacyState::failed));
|
||||
if (result.unsupported) return result;
|
||||
|
||||
result.active = status.overlay_active || status.remote_paused ||
|
||||
status.state == PrivacyState::on ||
|
||||
status.state == PrivacyState::starting ||
|
||||
status.state == PrivacyState::stopping ||
|
||||
status.state == PrivacyState::failed;
|
||||
// A command in flight only gates repeated clicks; it adds no display state.
|
||||
result.enabled = fresh && !result.timeout &&
|
||||
result.enabled = received &&
|
||||
status.state == PrivacyState::on && status.overlay_active &&
|
||||
!status.remote_paused;
|
||||
result.off = fresh && !result.timeout &&
|
||||
result.off = received &&
|
||||
status.state == PrivacyState::off && !result.active;
|
||||
result.unsupported = received && !status.supported;
|
||||
result.warning = result.timeout || result.stale ||
|
||||
(received && status.state == PrivacyState::failed);
|
||||
result.can_toggle = !request_pending &&
|
||||
(result.active || (fresh && status.supported));
|
||||
result.can_toggle = !command_pending && (result.active || received);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,20 +44,6 @@ void GuiRuntime::HandlePrivacy() {
|
||||
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_ &&
|
||||
|
||||
Reference in New Issue
Block a user