/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */ #include "privacy_controller.h" #include #include #include "rd_log.h" namespace crossdesk { uint64_t PrivacyController::Now() { return std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count(); } PrivacyController::PrivacyController(Factory factory) { status_.state = PrivacyState::unsupported; std::strcpy(status_.reason, "Privacy screen is not available on this platform"); #ifdef _WIN32 if (!factory) factory = CreateWindowsPrivacyBackend; #elif defined(__APPLE__) if (!factory) factory = CreateMacPrivacyBackend; #endif thread_ = std::thread([this, factory] { Run(factory); }); } PrivacyController::~PrivacyController() { Shutdown(); } void PrivacyController::WakeLocked() { if (backend_) backend_->Wake(); wake_.notify_all(); } void PrivacyController::Shutdown() { { std::lock_guard lock(mutex_); quit_ = true; WakeLocked(); } if (thread_.joinable()) thread_.join(); #ifdef __APPLE__ FlushMacPrivacyTasks(); #endif } PrivacyStatus PrivacyController::Snapshot() const { std::lock_guard lock(mutex_); return status_; } void PrivacyController::SetText(const PrivacyScreenText& text) { std::lock_guard lock(mutex_); text_ = text; } void PrivacyController::SetState(PrivacyState state, const std::string& reason) { status_.state = state; status_.remote_paused = false; // Wire compatibility; privacy never pauses a session. ++status_.revision; std::strncpy(status_.reason, reason.c_str(), sizeof(status_.reason) - 1); status_.reason[sizeof(status_.reason) - 1] = '\0'; LOG_INFO("Privacy: state={}, overlay={}, input_blocked={}, reason={}", static_cast(state), status_.overlay_active, status_.input_blocked, reason); } void PrivacyController::Enable(bool block_input) { std::lock_guard lock(mutex_); if (quit_ || engaged_.load() || disable_pending_) return; if (!status_.supported || (block_input && !status_.input_block_supported)) { SetState(PrivacyState::unsupported, "Privacy screen is unavailable"); return; } engaged_.store(true); block_requested_ = block_input; automatic_enable_ = false; enable_pending_ = true; failure_.clear(); started_ = Now(); SetState(PrivacyState::starting, "Enabling privacy screen"); WakeLocked(); } void PrivacyController::EnableOnConnection() { std::lock_guard lock(mutex_); if (quit_ || (engaged_.load() && !disable_pending_)) return; // A reconnect may arrive before asynchronous AppKit recovery has completed. engaged_.store(true); block_requested_ = true; automatic_enable_ = true; enable_pending_ = true; failure_.clear(); started_ = Now(); SetState(PrivacyState::starting, "Enabling privacy on connection"); WakeLocked(); } void PrivacyController::Disable() { std::lock_guard lock(mutex_); if (quit_) return; enable_pending_ = false; failure_.clear(); disable_pending_ = true; SetState(PrivacyState::stopping, "Restoring local desktop and input"); WakeLocked(); } void PrivacyController::Disconnected() { Disable(); } void PrivacyController::FailLocked(const std::string& reason) { if (!engaged_.load() || disable_pending_) return; failure_ = reason; enable_pending_ = false; disable_pending_ = true; SetState(PrivacyState::failed, reason); WakeLocked(); } void PrivacyController::Fail(const std::string& reason) { std::lock_guard lock(mutex_); FailLocked(reason); } void PrivacyController::CaptureChanged(bool running) { std::lock_guard lock(mutex_); // Windows announces a stopped capture while preparing the first session. // Keep automatic privacy pending until capture starts, within its deadline. // Once capture was running or backend enable began, a stop remains a fault. const bool waiting_for_initial_capture = automatic_enable_ && enable_pending_ && !capture_running_; capture_running_ = running; if (!running && !waiting_for_initial_capture) FailLocked("Capture changed; privacy screen is being turned off"); WakeLocked(); } void PrivacyController::Run(Factory factory) { std::unique_lock lock(mutex_); try { if (factory) backend_ = factory(); while (!quit_) { if (last_capability_check_ == 0 || (!engaged_.load() && Now() - last_capability_check_ >= 2000)) { last_capability_check_ = Now(); const auto caps = backend_ ? backend_->Query() : PrivacyCapabilities{}; const bool changed = status_.supported != caps.overlay || status_.input_block_supported != caps.block_input; status_.supported = caps.overlay; status_.input_block_supported = caps.block_input; if (!engaged_.load() && (changed || status_.revision == 0)) SetState(caps.overlay ? PrivacyState::off : PrivacyState::unsupported, caps.reason); } if (disable_pending_) { if (backend_) backend_->Recover(); if (backend_ && backend_->RecoveryPending()) { lock.unlock(); backend_->WaitForEvents(25); lock.lock(); continue; } disable_pending_ = false; if (backend_ && !backend_->IsRecovered()) { enable_pending_ = false; disable_pending_ = true; // Retry native cleanup without affecting the session. SetState(PrivacyState::failed, "Could not release every privacy resource"); } else { status_.overlay_active = status_.input_blocked = false; if (!enable_pending_) { engaged_.store(false); SetState(failure_.empty() ? PrivacyState::off : PrivacyState::failed, failure_.empty() ? "Privacy screen is off" : failure_); } } } if (enable_pending_) { const auto caps = backend_ ? backend_->Query() : PrivacyCapabilities{}; status_.supported = caps.overlay; status_.input_block_supported = caps.block_input; if (!caps.overlay || (block_requested_ && !caps.block_input)) { FailLocked("Privacy screen unavailable: " + caps.reason); } else if (!capture_running_) { if (!automatic_enable_ || Now() - started_ > 10000) FailLocked("No desktop capture available for privacy screen"); } else { enable_pending_ = false; std::string error; if (!backend_->Enable(block_requested_, text_, error)) { FailLocked(error); } else { status_.overlay_active = true; status_.input_blocked = block_requested_; started_ = Now(); } } } const auto health = backend_ ? backend_->Poll() : PrivacyHealth{}; if (engaged_.load() && !disable_pending_) { if (health.emergency_exit) { failure_.clear(); enable_pending_ = false; disable_pending_ = true; SetState(PrivacyState::stopping, "Local shortcut: restoring desktop and input"); } else if (!health.failure.empty()) { FailLocked(health.failure); } else if (!enable_pending_ && status_.state == PrivacyState::starting) { if (health.ready) SetState(PrivacyState::on, "Privacy screen is on"); else if (Now() - started_ > 3000) FailLocked("Privacy screen did not become ready"); } } if (backend_) { const unsigned timeout = engaged_.load() || disable_pending_ ? 25 : 2000; lock.unlock(); backend_->WaitForEvents(timeout); lock.lock(); } else { wake_.wait(lock, [this] { return quit_ || enable_pending_ || disable_pending_; }); } } } catch (const std::exception& e) { if (!lock.owns_lock()) lock.lock(); SetState(PrivacyState::failed, std::string("Privacy component failed: ") + e.what()); } if (backend_) { backend_->Recover(); backend_.reset(); } } } // namespace crossdesk