Files
crossdesk/apps/desktop/src/privacy/privacy_controller.cpp
T

209 lines
7.8 KiB
C++

/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
#include "privacy_controller.h"
#include <chrono>
#include <cstring>
#include "rd_log.h"
namespace crossdesk {
uint64_t PrivacyController::Now() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
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<int>(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