[feat] add macOS privacy screen and keep remote sessions running

This commit is contained in:
kunkundi
2026-09-13 00:38:40 +08:00
parent 23bf2ff8b8
commit a6e325f190
38 changed files with 1161 additions and 1046 deletions
@@ -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_ &&