[fix] keep Windows privacy screen across display changes and validate covers

This commit is contained in:
dijunkun
2026-09-21 01:10:30 +08:00
parent 276385b816
commit fb556a763d
12 changed files with 296 additions and 134 deletions
@@ -64,6 +64,9 @@ class GuiRuntime : protected gui_detail::GuiState {
void HandlePrivacy();
bool IsAuthorizedController(const std::string& remote_id);
void SetControllerPrivacySupport(const std::string& remote_id, bool supported);
// Caller holds connection_status_mutex_ exclusively. Turns privacy off when
// any connected controller lacks support, or on for the first automatic one.
void ApplyPrivacyAdmission(bool automatic_enable);
void QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command);
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
@@ -363,14 +363,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{
std::unique_lock lock(runtime->connection_status_mutex_);
if (status == ConnectionStatus::Connected) {
// Wait for explicit desktop capability before enabling privacy or
// sending privacy messages. Legacy and web controllers never opt in.
runtime->privacy_sessions_.Connected(
// Local privacy requires explicit desktop capability. Host info may
// arrive before Connected; consume that saved capability here.
const bool automatic_enable = runtime->privacy_sessions_.Connected(
remote_id, runtime->config_center_->IsEnablePrivacyScreen());
if (!runtime->privacy_sessions_.CanEnable() &&
runtime->privacy_.Engaged()) {
runtime->privacy_.Disable();
}
runtime->ApplyPrivacyAdmission(automatic_enable);
}
runtime->connection_status_[remote_id] = status;
runtime->devices_.OnControllerConnectionsChanged();
@@ -25,14 +25,27 @@ void GuiRuntime::SetControllerPrivacySupport(const std::string& remote_id,
bool supported) {
std::unique_lock lock(connection_status_mutex_);
const auto found = connection_status_.find(remote_id);
if (found == connection_status_.end() ||
found->second != ConnectionStatus::Connected) return;
if (found == connection_status_.end()) return;
if (found->second != ConnectionStatus::Connected) {
if (found->second == ConnectionStatus::Connecting ||
found->second == ConnectionStatus::Gathering) {
privacy_sessions_.RememberSupport(remote_id, supported);
LOG_INFO("Privacy: retained early controller capability, peer={}, supported={}",
remote_id, supported);
}
return;
}
const bool automatic_enable =
privacy_sessions_.SetSupported(remote_id, supported);
ApplyPrivacyAdmission(automatic_enable &&
config_center_->IsEnablePrivacyScreen());
}
void GuiRuntime::ApplyPrivacyAdmission(bool automatic_enable) {
if (!privacy_sessions_.CanEnable()) {
if (privacy_.Engaged()) privacy_.Disable();
} else if (automatic_enable && config_center_->IsEnablePrivacyScreen()) {
} else if (automatic_enable) {
privacy_.EnableOnConnection();
}
}
@@ -45,8 +58,10 @@ void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
found->second != ConnectionStatus::Connected ||
!privacy_sessions_.Supports(remote_id)) return;
std::lock_guard lock(privacy_commands_mutex_);
if (privacy_commands_.size() < 32)
privacy_commands_.emplace_back(remote_id, command);
if (privacy_commands_.size() >= 32) return;
LOG_INFO("Privacy: queued controller command, peer={}, flag={}", remote_id,
static_cast<int>(command.flag));
privacy_commands_.emplace_back(remote_id, command);
}
void GuiRuntime::HandlePrivacy() {
@@ -71,17 +86,15 @@ void GuiRuntime::HandlePrivacy() {
}
last_privacy_status_tick_ = 0;
}
auto status = privacy_.Snapshot();
PrivacyStatus status{};
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_);
status = privacy_.Snapshot();
for (const auto& [id, state] : connection_status_)
if (state == ConnectionStatus::Connected && privacy_sessions_.Supports(id))
if (state == ConnectionStatus::Connected &&
privacy_sessions_.Supports(id))
controllers.push_back(id);
if (!privacy_sessions_.CanEnable()) {
status.state = PrivacyState::unsupported;
@@ -91,6 +104,10 @@ void GuiRuntime::HandlePrivacy() {
"A connected controller does not support privacy screen");
}
}
if (!peer_ || (last_privacy_status_tick_ != 0 &&
status.revision == last_privacy_revision_ &&
now - last_privacy_status_tick_ < 1000))
return;
RemoteAction action{};
action.type = ControlType::privacy_status;
action.ps = status;
@@ -13,19 +13,37 @@
namespace crossdesk {
// Guarded by GuiRuntime's connection_status_mutex_. Privacy covers the shared
// desktop, so every connected controller must explicitly support it.
// Guarded by GuiRuntime's connection_status_mutex_. Privacy covers the local
// displays, so every connected controller must explicitly support its controls.
class PrivacySessionPolicy {
public:
void Connected(const std::string& remote_id, bool automatic_privacy) {
// Web controllers render through the browser and expose no privacy UI.
static bool IsWebController(const std::string& remote_id) {
return remote_id.find("web") != std::string::npos;
}
bool Connected(const std::string& remote_id, bool automatic_privacy) {
const auto [entry, inserted] = controllers_.try_emplace(remote_id, false);
if (inserted && controllers_.size() == 1 && automatic_privacy) {
if (inserted && controllers_.size() == 1 && automatic_privacy &&
!IsWebController(remote_id)) {
automatic_controller_ = remote_id;
}
const auto early = early_support_.find(remote_id);
if (early == early_support_.end()) return false;
const bool supported = early->second;
early_support_.erase(early);
return SetSupported(remote_id, supported);
}
// Only transport-authenticated metadata from a connecting session is cached
// by the caller. It grants no control until Connected admits that session.
void RememberSupport(const std::string& remote_id, bool supported) {
early_support_[remote_id] = supported;
}
void Disconnected(const std::string& remote_id) {
controllers_.erase(remote_id);
early_support_.erase(remote_id);
if (automatic_controller_ == remote_id) automatic_controller_.clear();
}
@@ -34,7 +52,7 @@ class PrivacySessionPolicy {
bool SetSupported(const std::string& remote_id, bool supported) {
const auto entry = controllers_.find(remote_id);
if (entry == controllers_.end()) return false;
entry->second = supported && remote_id.find("web") == std::string::npos;
entry->second = supported && !IsWebController(remote_id);
if (automatic_controller_ != remote_id) return false;
automatic_controller_.clear();
return CanEnable();
@@ -53,9 +71,10 @@ class PrivacySessionPolicy {
private:
std::unordered_map<std::string, bool> controllers_;
std::unordered_map<std::string, bool> early_support_;
std::string automatic_controller_;
};
} // namespace crossdesk
#endif
#endif
@@ -14,6 +14,8 @@
#include "../../windows_thread_dpi.h"
#include "../privacy_band_ipc.h"
#include "../privacy_capture_exclusion.h"
#include "../privacy_topology.h"
#include "privacy_cover_renderer.h"
namespace {
@@ -24,11 +26,6 @@ constexpr DWORD kCoverStyles = WS_EX_TOPMOST | WS_EX_NOACTIVATE |
WS_EX_TRANSPARENT;
HMODULE module = nullptr;
bool SameRect(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right &&
a.bottom == b.bottom;
}
class Windows {
public:
explicit Windows(PrivacyBandShared* shared) : shared_(shared) {}
@@ -59,6 +56,7 @@ class Windows {
return atom_ ? Reconcile() : Fail(L"Register high-band window class");
}
bool Reconcile() {
InterlockedExchange(&shared_->topology_pending, 1);
std::vector<RECT> monitors;
if (!EnumDisplayMonitors(
nullptr, nullptr,
@@ -68,16 +66,37 @@ class Windows {
result.push_back(*rect);
return TRUE;
},
reinterpret_cast<LPARAM>(&monitors)) ||
monitors.empty())
reinterpret_cast<LPARAM>(&monitors)))
return Fail(L"Enumerate high-band displays");
bool changed = monitors.size() != windows_.size();
const bool changed = monitors.size() != windows_.size() ||
std::any_of(monitors.begin(), monitors.end(), [&](const RECT& rect) {
return std::none_of(windows_.begin(), windows_.end(),
[&](const auto& window) { return SameRect(rect, window.rect); });
});
// Mark the shared snapshot busy BEFORE replacing/destroying any HWND.
// Readers must not validate a stale handle from a previous topology.
struct PublishGuard {
PrivacyBandShared* shared;
~PublishGuard() { InterlockedIncrement(&shared->sequence); }
} publishing{shared_};
InterlockedIncrement(&shared_->sequence);
for (const auto& rect : monitors) {
const auto found =
std::find_if(windows_.begin(), windows_.end(),
[&](const auto& w) { return SameRect(w.rect, rect); });
if (found != windows_.end()) continue;
changed = true;
if (found != windows_.end()) {
// Windows can reposition a top-level window during DPI/hotplug before
// delivering WM_DISPLAYCHANGE. Restore the intended monitor bounds.
HWND hwnd = reinterpret_cast<HWND>(found->hwnd);
RECT actual{};
if ((!GetWindowRect(hwnd, &actual) || !SameRect(actual, rect) ||
!IsWindowVisible(hwnd)) &&
!SetWindowPos(hwnd, nullptr, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW))
return Fail(L"Reposition high-band cover");
continue;
}
HWND hwnd = create_(kCoverStyles, atom_, L"CrossDesk Privacy", WS_POPUP,
rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, nullptr, nullptr, module,
@@ -86,7 +105,7 @@ class Windows {
windows_.push_back({reinterpret_cast<uint64_t>(hwnd), rect});
if (!SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA))
return Fail(L"Make high-band cover opaque");
if (!SetWindowDisplayAffinity(hwnd, 0x11))
if (!ExcludePrivacyWindowFromCapture(hwnd))
return Fail(L"Exclude high-band cover from capture");
const BOOL yes = TRUE;
const HRESULT peek = DwmSetWindowAttribute(hwnd, DWMWA_EXCLUDED_FROM_PEEK,
@@ -114,13 +133,16 @@ class Windows {
++it;
}
}
if (!Validate()) return false;
if (changed) {
InterlockedIncrement(&shared_->sequence);
shared_->count = static_cast<uint32_t>(windows_.size());
std::copy(windows_.begin(), windows_.end(), shared_->windows);
InterlockedIncrement(&shared_->sequence);
if (!Validate()) {
// Another topology event can race enumeration/presentation. Retry while
// retaining every surviving cover; actual validation errors stay fatal.
return InterlockedCompareExchange(&shared_->error, 0, 0) == ERROR_SUCCESS;
}
// A previous reconciliation may have changed windows_ but failed geometry
// validation. Publish on every success so retries cannot leave stale HWNDs.
shared_->count = static_cast<uint32_t>(windows_.size());
std::copy(windows_.begin(), windows_.end(), shared_->windows);
InterlockedExchange(&shared_->topology_pending, 0);
return true;
}
bool Validate() {
@@ -130,13 +152,17 @@ class Windows {
DWORD band = 0, affinity = 0, flags = 0, cloaked = 0;
BYTE alpha = 0;
RECT rect{};
if (!GetWindowRect(hwnd, &rect) || !SameRect(rect, w.rect)) {
changed = true;
return false;
}
if ((GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & kCoverStyles) !=
kCoverStyles ||
!get_band_(hwnd, &band) || band != kPrivacyWindowBand ||
!GetWindowDisplayAffinity(hwnd, &affinity) || affinity != 0x11 ||
!GetWindowDisplayAffinity(hwnd, &affinity) ||
affinity != kPrivacyWindowDisplayAffinity ||
!GetLayeredWindowAttributes(hwnd, nullptr, &alpha, &flags) ||
alpha != 255 || flags != LWA_ALPHA || !IsWindowVisible(hwnd) ||
!GetWindowRect(hwnd, &rect) || !SameRect(rect, w.rect) ||
FAILED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked,
sizeof(cloaked))) ||
cloaked)
@@ -16,15 +16,13 @@
#include "../windows_thread_dpi.h"
#include "privacy_backend.h"
#include "privacy_band_guard.h"
#include "privacy_capture_exclusion.h"
#include "privacy_cursor_guard.h"
#include "privacy_input_guard.h"
#include "privacy_topology.h"
#include "privacy_window_visibility.h"
#include "rd_log.h"
#ifndef WDA_EXCLUDEFROMCAPTURE
#define WDA_EXCLUDEFROMCAPTURE 0x00000011
#endif
namespace crossdesk {
namespace {
@@ -46,11 +44,6 @@ bool DesktopName(HDESK desktop, wchar_t (&name)[256]) {
sizeof(name), &needed);
}
bool SameRect(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right &&
a.bottom == b.bottom;
}
struct MonitorEnumeration {
std::vector<RECT> rects;
bool ok = true;
@@ -141,6 +134,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
return false;
if (!band_guard_.Start(text, error)) return false;
if (!ReconcileMonitors(error)) return false;
layout_changed_ = false;
event_owner_ = this;
for (DWORD event : {EVENT_OBJECT_SHOW, EVENT_OBJECT_REORDER,
EVENT_OBJECT_LOCATIONCHANGE}) {
@@ -193,6 +187,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
last_obstruction_ = nullptr;
last_obstruction_log_ = 0;
layout_changed_ = false;
topology_pending_ = false;
topology_pending_since_ = 0;
coverage_dirty_ = false;
emergency_ = false;
health_error_.clear();
@@ -235,12 +231,14 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
std::string error;
if (!input_guard_.Healthy(error)) return {error, false};
}
if (windows_.empty()) return {health_error_, false};
if (band_guard_.stopped()) return {health_error_, false};
// Input messages wake the thread immediately; expensive health queries
// must retain their own cadence instead of running once per input event.
if (!layout_changed_ &&
GetTickCount64() - last_check_ < 100) {
if (coverage_dirty_ && last_health_.failure.empty()) {
// Window events only dirty the coverage check, which is cheap enough to
// run between the throttled full health passes.
if (!layout_changed_ && GetTickCount64() - last_check_ < 100) {
if (coverage_dirty_ && last_health_.failure.empty() &&
!topology_pending_) {
coverage_dirty_ = false;
last_health_ = CheckCoverage();
}
@@ -253,6 +251,21 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
}
private:
// The broker reconciles hotplug asynchronously; while it does, coverage
// cannot be verified. Preserve intent for a bounded time only: a broker that
// never settles must not keep privacy "on" without any verification.
static constexpr uint64_t kTopologyPendingLimitMs = 3000;
PrivacyHealth TopologyPending() {
const uint64_t now = GetTickCount64();
if (topology_pending_since_ == 0) topology_pending_since_ = now;
if (now - topology_pending_since_ >= kTopologyPendingLimitMs) {
return {"Privacy cover topology did not settle after a display change",
false};
}
return {{}, false, false};
}
PrivacyHealth CheckHealth() {
if (!health_error_.empty()) return {health_error_, false};
if (!band_guard_.Healthy(health_error_)) return {health_error_, false};
@@ -273,40 +286,23 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
"Exclusive fullscreen or unknown display presentation",
false};
}
MonitorEnumeration monitors;
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
reinterpret_cast<LPARAM>(&monitors)) ||
!monitors.ok) {
return {Error("EnumDisplayMonitors"), false};
}
const bool changed =
monitors.rects.size() != windows_.size() ||
std::any_of(monitors.rects.begin(), monitors.rects.end(),
[this](const RECT& rect) {
return std::none_of(windows_.begin(), windows_.end(),
[&](const auto& w) {
return SameRect(w.rect, rect);
});
});
if (changed || layout_changed_) {
layout_changed_ = false;
// Retain old covers until the monitor check completes, then release
// privacy so capture can refresh its display mapping independently.
std::string error;
if (!ReconcileMonitors(error))
health_error_ = error;
else
health_error_ =
"Display layout/DPI changed; privacy screen will turn off";
}
if (!health_error_.empty()) return {health_error_, false};
if (!ReconcileMonitors(health_error_)) return {health_error_, false};
if (topology_pending_) return TopologyPending();
BOOL composed = FALSE;
if (FAILED(DwmIsCompositionEnabled(&composed)) || !composed) {
return {
"Desktop composition stopped; privacy coverage cannot be guaranteed",
false};
}
return CheckCoverage();
auto coverage = CheckCoverage();
if (!coverage.failure.empty()) {
// The broker may replace an HWND after the snapshot above. Refresh once
// before treating a stale geometry/handle as loss of privacy coverage.
if (!ReconcileMonitors(health_error_)) return {health_error_, false};
if (topology_pending_) return TopologyPending();
coverage = CheckCoverage();
}
return coverage;
}
PrivacyHealth CheckCoverage() {
@@ -328,7 +324,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
&layered_flags) ||
alpha != 255 || layered_flags != LWA_ALPHA ||
!GetWindowDisplayAffinity(w.cover, &affinity) ||
affinity != WDA_EXCLUDEFROMCAPTURE ||
affinity != kPrivacyWindowDisplayAffinity ||
FAILED(DwmGetWindowAttribute(w.cover, DWMWA_CLOAKED, &cloaked,
sizeof(cloaked))) ||
cloaked) {
@@ -430,40 +426,42 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
}
bool ReconcileMonitors(std::string& error) {
const auto wait_for_broker = [&] {
topology_pending_ = true;
// Preserve intent while a healthy broker reconciles hotplug. Broker
// failure/heartbeat checks still detect real faults independently.
error.clear();
return true;
};
MonitorEnumeration monitors;
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
reinterpret_cast<LPARAM>(&monitors)) ||
!monitors.ok || monitors.rects.empty()) {
reinterpret_cast<LPARAM>(&monitors)) || !monitors.ok) {
error = Error("EnumDisplayMonitors");
return false;
}
std::vector<PrivacyBandWindow> covers;
if (!band_guard_.Windows(covers, error)) return false;
for (const RECT& rect : monitors.rects) {
auto existing =
std::find_if(windows_.begin(), windows_.end(),
[&](const auto& w) { return SameRect(w.rect, rect); });
if (existing != windows_.end()) continue;
const auto cover = std::find_if(covers.begin(), covers.end(),
[&](const auto& candidate) {
return SameRect(candidate.rect, rect);
});
if (cover == covers.end()) {
error = "High-band broker has not covered every physical monitor";
return false;
}
windows_.push_back({rect, reinterpret_cast<HWND>(cover->hwnd)});
LOG_INFO(
"Privacy high-band cover ready: bounds=({}, {})-({}, {}), "
"affinity=0x11, alpha=255",
rect.left, rect.top, rect.right, rect.bottom);
if (!band_guard_.Windows(covers, error)) {
if (GetLastError() == ERROR_RETRY) return wait_for_broker();
return false;
}
// Cover windows are owned and released by the broker.
windows_.erase(std::remove_if(windows_.begin(), windows_.end(),
[&](const auto& window) {
return std::none_of(monitors.rects.begin(), monitors.rects.end(),
[&](const RECT& rect) { return SameRect(window.rect, rect); });
}), windows_.end());
if (!MatchPrivacyTopology(monitors.rects, covers)) return wait_for_broker();
std::vector<Window> updated;
for (const auto& cover : covers) {
HWND hwnd = reinterpret_cast<HWND>(cover.hwnd);
if (std::none_of(windows_.begin(), windows_.end(), [&](const auto& old) {
return old.cover == hwnd && SameRect(old.rect, cover.rect);
})) {
LOG_INFO("Privacy: reconciled local cover without changing session state: "
"bounds=({}, {})-({}, {}), affinity=0x11",
cover.rect.left, cover.rect.top, cover.rect.right, cover.rect.bottom);
}
updated.push_back({cover.rect, hwnd});
}
windows_ = std::move(updated);
layout_changed_ = false;
topology_pending_ = false;
topology_pending_since_ = 0;
return true;
}
@@ -526,6 +524,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
bool hotkey_ = false;
bool emergency_ = false;
bool layout_changed_ = false;
bool topology_pending_ = false;
uint64_t topology_pending_since_ = 0;
bool coverage_dirty_ = false;
uint64_t last_check_ = 0;
PrivacyHealth last_health_;
@@ -119,10 +119,12 @@ bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
// Keep both images immutable throughout creation/injection and the session.
image_lock_ = CreateFileW(broker_path_.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
if (image_lock_ == INVALID_HANDLE_VALUE)
return Failed("Lock privacy broker image", error);
dll_lock_ = CreateFileW(dll.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, 0, nullptr);
if (image_lock_ == INVALID_HANDLE_VALUE || dll_lock_ == INVALID_HANDLE_VALUE)
return Failed("Lock privacy broker images", error);
if (dll_lock_ == INVALID_HANDLE_VALUE)
return Failed("Lock privacy window module", error);
SECURITY_ATTRIBUTES inherit{sizeof(inherit), nullptr, TRUE};
ScopedHandle parent;
@@ -131,14 +133,14 @@ bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, TRUE,
0))
return Failed("Duplicate privacy parent process handle", error);
// Check each object as it is created so the reported error is its own.
stop_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
if (!stop_) return Failed("Create high-band broker stop event", error);
ready_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
if (!ready_) return Failed("Create high-band broker ready event", error);
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, &inherit, PAGE_READWRITE,
0, sizeof(PrivacyBandShared), nullptr);
if (!stop_ || !ready_ || !mapping_) {
const auto code = GetLastError();
return Failed("Create high-band broker IPC", error, code);
}
if (!mapping_) return Failed("Create high-band broker shared memory", error);
shared_ = static_cast<PrivacyBandShared*>(MapViewOfFile(
mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyBandShared)));
if (!shared_) {
@@ -271,25 +273,30 @@ bool PrivacyBandGuard::Healthy(std::string& error) {
bool PrivacyBandGuard::Windows(std::vector<PrivacyBandWindow>& windows,
std::string& error) {
if (!Healthy(error)) return false;
const auto retry = [&] {
error = "High-band monitor snapshot is being updated";
SetLastError(ERROR_RETRY);
return false;
};
if (InterlockedCompareExchange(&shared_->topology_pending, 0, 0)) return retry();
for (int attempt = 0; attempt < 4; ++attempt) {
const LONG before = InterlockedCompareExchange(&shared_->sequence, 0, 0);
if (before & 1) continue;
const auto count = shared_->count;
if (count == 0 || count > kPrivacyMaxMonitors) break;
if (count > kPrivacyMaxMonitors) break;
windows.assign(shared_->windows, shared_->windows + count);
if (before == InterlockedCompareExchange(&shared_->sequence, 0, 0)) {
if (before == InterlockedCompareExchange(&shared_->sequence, 0, 0) &&
!InterlockedCompareExchange(&shared_->topology_pending, 0, 0)) {
for (const auto& window : windows) {
DWORD pid = 0;
GetWindowThreadProcessId(reinterpret_cast<HWND>(window.hwnd), &pid);
if (pid != process_id())
return Failed("Invalid high-band window owner", error,
ERROR_INVALID_HANDLE);
return retry();
}
return true;
}
}
return Failed("High-band monitor snapshot is changing or invalid", error,
ERROR_RETRY);
return retry();
}
bool PrivacyBandGuard::Stop(std::string& error) {
@@ -14,7 +14,7 @@
namespace crossdesk {
constexpr uint32_t kPrivacyBandMagic = 0x43534231;
constexpr uint32_t kPrivacyBandVersion = 2;
constexpr uint32_t kPrivacyBandVersion = 3;
constexpr DWORD kPrivacyWindowBand = 18; // ZBID_ABOVELOCK_UX, current desktop.
constexpr size_t kPrivacyMaxMonitors = 64;
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
@@ -36,6 +36,7 @@ struct PrivacyBandShared {
volatile LONG sequence = 0;
volatile LONG error = ERROR_IO_PENDING;
volatile LONG active = 0;
volatile LONG topology_pending = 0;
alignas(8) volatile LONG64 heartbeat = 0;
wchar_t operation[128]{};
wchar_t unlock_hint[128]{};
@@ -0,0 +1,30 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-21
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_CAPTURE_EXCLUSION_H_
#define _PRIVACY_CAPTURE_EXCLUSION_H_
#include <Windows.h>
namespace crossdesk {
// WDA_EXCLUDEFROMCAPTURE: show the opaque cover locally while capture receives
// the desktop behind it. WDA_MONITOR would instead produce an empty/black area.
inline constexpr DWORD kPrivacyWindowDisplayAffinity = 0x00000011;
inline bool ExcludePrivacyWindowFromCapture(HWND window) {
if (!SetWindowDisplayAffinity(window, kPrivacyWindowDisplayAffinity))
return false;
DWORD affinity = 0;
if (!GetWindowDisplayAffinity(window, &affinity)) return false;
if (affinity == kPrivacyWindowDisplayAffinity) return true;
SetLastError(ERROR_INVALID_STATE);
return false;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-21
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_TOPOLOGY_H_
#define _PRIVACY_TOPOLOGY_H_
#include <algorithm>
#include <vector>
#include "privacy_band_ipc.h"
namespace crossdesk {
inline bool SameRect(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right &&
a.bottom == b.bottom;
}
inline bool MatchPrivacyTopology(const std::vector<RECT>& monitors,
const std::vector<PrivacyBandWindow>& covers) {
return monitors.size() == covers.size() &&
std::all_of(monitors.begin(), monitors.end(), [&](const RECT& monitor) {
return std::any_of(covers.begin(), covers.end(), [&](const auto& cover) {
return SameRect(cover.rect, monitor);
});
});
}
} // namespace crossdesk
#endif
@@ -430,7 +430,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
}
if (size == ScreenCapturer::kBackendReset) {
SharedCapturedCursorState().Reset();
if (privacy_) privacy_->Fail("Capture restarted; privacy screen will turn off");
// Backend recovery is not a user request to disable local privacy.
return;
}
if (data && size > 0 && w > 0 && h > 0) {
@@ -578,6 +578,8 @@ void ScreenCapturerWin::EmitCapturedFrame(
} restore_source{
std::exchange(current_frame_from_secure_desktop, from_secure_desktop)};
// Privacy is a local, capture-excluded window. Always deliver the actual
// desktop pixels, including while that window is starting or stopping.
if (native_frame) {
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
LOG_INFO("Windows capturer native frame output enabled (type={})",
@@ -773,8 +775,6 @@ int ScreenCapturerWin::SwitchTo(int monitor_index) {
const int ret = impl_->SwitchTo(backend_index);
if (ret == 0) {
monitor_index_.store(monitor_index, std::memory_order_relaxed);
} else if (privacy_) {
privacy_->Fail("Display switch failed; privacy screen will turn off");
}
return ret;
}
@@ -85,7 +85,8 @@ void PrivacyController::EnableOnConnection() {
}
void PrivacyController::Disable() {
std::lock_guard lock(mutex_);
if (quit_) return;
// Nothing to restore when privacy is already off and no cleanup is retrying.
if (quit_ || (!engaged_.load() && !disable_pending_)) return;
enable_pending_ = false;
failure_.clear();
disable_pending_ = true;
@@ -118,6 +119,9 @@ void PrivacyController::CaptureChanged(bool running) {
WakeLocked();
}
void PrivacyController::Run(Factory factory) {
// Only this thread touches backend_; Wake() is safe to call concurrently.
// Native enable/teardown can block for seconds, so those calls run with the
// mutex released to keep Snapshot()/Enable()/Disable() responsive.
std::unique_lock lock(mutex_);
try {
if (factory) backend_ = factory();
@@ -134,7 +138,11 @@ void PrivacyController::Run(Factory factory) {
SetState(caps.overlay ? PrivacyState::off : PrivacyState::unsupported, caps.reason);
}
if (disable_pending_) {
if (backend_) backend_->Recover();
if (backend_) {
lock.unlock();
backend_->Recover();
lock.lock();
}
if (backend_ && backend_->RecoveryPending()) {
lock.unlock();
backend_->WaitForEvents(25);
@@ -145,7 +153,10 @@ void PrivacyController::Run(Factory factory) {
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");
// Report the stuck cleanup once; every retry would otherwise bump
// the revision and rebroadcast the status to all controllers.
if (status_.state != PrivacyState::failed)
SetState(PrivacyState::failed, "Could not release every privacy resource");
} else {
status_.overlay_active = status_.input_blocked = false;
if (!enable_pending_) {
@@ -166,12 +177,18 @@ void PrivacyController::Run(Factory factory) {
FailLocked("No desktop capture available for privacy screen");
} else {
enable_pending_ = false;
const bool block_input = block_requested_;
const PrivacyScreenText text = text_;
std::string error;
if (!backend_->Enable(block_requested_, text_, error)) {
lock.unlock();
const bool enabled = backend_->Enable(block_input, text, error);
lock.lock();
if (!enabled) {
// No-op if a disable arrived meanwhile; that path recovers anyway.
FailLocked(error);
} else {
status_.overlay_active = true;
status_.input_blocked = block_requested_;
status_.input_blocked = block_input;
started_ = Now();
}
}
@@ -203,8 +220,19 @@ void PrivacyController::Run(Factory factory) {
}
} catch (const std::exception& e) {
if (!lock.owns_lock()) lock.lock();
// The worker is gone: nothing can resolve a later Enable(), so report the
// fault and stop accepting requests instead of parking them in "starting".
status_.supported = status_.input_block_supported = false;
status_.overlay_active = status_.input_blocked = false;
engaged_.store(false);
quit_ = true;
SetState(PrivacyState::failed, std::string("Privacy component failed: ") + e.what());
}
if (backend_) { backend_->Recover(); backend_.reset(); }
if (backend_) {
lock.unlock();
backend_->Recover();
lock.lock();
backend_.reset();
}
}
} // namespace crossdesk