mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-16 00:33:22 +08:00
[fix] keep Windows privacy screen across display changes and validate covers
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user