mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-05 21:52:46 +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();
|
void HandlePrivacy();
|
||||||
bool IsAuthorizedController(const std::string& remote_id);
|
bool IsAuthorizedController(const std::string& remote_id);
|
||||||
void SetControllerPrivacySupport(const std::string& remote_id, bool supported);
|
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 QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command);
|
||||||
|
|
||||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||||
|
|||||||
@@ -363,14 +363,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
{
|
{
|
||||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
std::unique_lock lock(runtime->connection_status_mutex_);
|
||||||
if (status == ConnectionStatus::Connected) {
|
if (status == ConnectionStatus::Connected) {
|
||||||
// Wait for explicit desktop capability before enabling privacy or
|
// Local privacy requires explicit desktop capability. Host info may
|
||||||
// sending privacy messages. Legacy and web controllers never opt in.
|
// arrive before Connected; consume that saved capability here.
|
||||||
runtime->privacy_sessions_.Connected(
|
const bool automatic_enable = runtime->privacy_sessions_.Connected(
|
||||||
remote_id, runtime->config_center_->IsEnablePrivacyScreen());
|
remote_id, runtime->config_center_->IsEnablePrivacyScreen());
|
||||||
if (!runtime->privacy_sessions_.CanEnable() &&
|
runtime->ApplyPrivacyAdmission(automatic_enable);
|
||||||
runtime->privacy_.Engaged()) {
|
|
||||||
runtime->privacy_.Disable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
runtime->connection_status_[remote_id] = status;
|
runtime->connection_status_[remote_id] = status;
|
||||||
runtime->devices_.OnControllerConnectionsChanged();
|
runtime->devices_.OnControllerConnectionsChanged();
|
||||||
|
|||||||
@@ -25,14 +25,27 @@ void GuiRuntime::SetControllerPrivacySupport(const std::string& remote_id,
|
|||||||
bool supported) {
|
bool supported) {
|
||||||
std::unique_lock lock(connection_status_mutex_);
|
std::unique_lock lock(connection_status_mutex_);
|
||||||
const auto found = connection_status_.find(remote_id);
|
const auto found = connection_status_.find(remote_id);
|
||||||
if (found == connection_status_.end() ||
|
if (found == connection_status_.end()) return;
|
||||||
found->second != ConnectionStatus::Connected) 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 =
|
const bool automatic_enable =
|
||||||
privacy_sessions_.SetSupported(remote_id, supported);
|
privacy_sessions_.SetSupported(remote_id, supported);
|
||||||
|
ApplyPrivacyAdmission(automatic_enable &&
|
||||||
|
config_center_->IsEnablePrivacyScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiRuntime::ApplyPrivacyAdmission(bool automatic_enable) {
|
||||||
if (!privacy_sessions_.CanEnable()) {
|
if (!privacy_sessions_.CanEnable()) {
|
||||||
if (privacy_.Engaged()) privacy_.Disable();
|
if (privacy_.Engaged()) privacy_.Disable();
|
||||||
} else if (automatic_enable && config_center_->IsEnablePrivacyScreen()) {
|
} else if (automatic_enable) {
|
||||||
privacy_.EnableOnConnection();
|
privacy_.EnableOnConnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,8 +58,10 @@ void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
|
|||||||
found->second != ConnectionStatus::Connected ||
|
found->second != ConnectionStatus::Connected ||
|
||||||
!privacy_sessions_.Supports(remote_id)) return;
|
!privacy_sessions_.Supports(remote_id)) return;
|
||||||
std::lock_guard lock(privacy_commands_mutex_);
|
std::lock_guard lock(privacy_commands_mutex_);
|
||||||
if (privacy_commands_.size() < 32)
|
if (privacy_commands_.size() >= 32) return;
|
||||||
privacy_commands_.emplace_back(remote_id, command);
|
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() {
|
void GuiRuntime::HandlePrivacy() {
|
||||||
@@ -71,17 +86,15 @@ void GuiRuntime::HandlePrivacy() {
|
|||||||
}
|
}
|
||||||
last_privacy_status_tick_ = 0;
|
last_privacy_status_tick_ = 0;
|
||||||
}
|
}
|
||||||
auto status = privacy_.Snapshot();
|
PrivacyStatus status{};
|
||||||
const uint64_t now = SDL_GetTicks();
|
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::vector<std::string> controllers;
|
||||||
{
|
{
|
||||||
std::shared_lock lock(connection_status_mutex_);
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
|
status = privacy_.Snapshot();
|
||||||
for (const auto& [id, state] : connection_status_)
|
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);
|
controllers.push_back(id);
|
||||||
if (!privacy_sessions_.CanEnable()) {
|
if (!privacy_sessions_.CanEnable()) {
|
||||||
status.state = PrivacyState::unsupported;
|
status.state = PrivacyState::unsupported;
|
||||||
@@ -91,6 +104,10 @@ void GuiRuntime::HandlePrivacy() {
|
|||||||
"A connected controller does not support privacy screen");
|
"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{};
|
RemoteAction action{};
|
||||||
action.type = ControlType::privacy_status;
|
action.type = ControlType::privacy_status;
|
||||||
action.ps = status;
|
action.ps = status;
|
||||||
|
|||||||
@@ -13,19 +13,37 @@
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
// Guarded by GuiRuntime's connection_status_mutex_. Privacy covers the shared
|
// Guarded by GuiRuntime's connection_status_mutex_. Privacy covers the local
|
||||||
// desktop, so every connected controller must explicitly support it.
|
// displays, so every connected controller must explicitly support its controls.
|
||||||
class PrivacySessionPolicy {
|
class PrivacySessionPolicy {
|
||||||
public:
|
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);
|
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;
|
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) {
|
void Disconnected(const std::string& remote_id) {
|
||||||
controllers_.erase(remote_id);
|
controllers_.erase(remote_id);
|
||||||
|
early_support_.erase(remote_id);
|
||||||
if (automatic_controller_ == remote_id) automatic_controller_.clear();
|
if (automatic_controller_ == remote_id) automatic_controller_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +52,7 @@ class PrivacySessionPolicy {
|
|||||||
bool SetSupported(const std::string& remote_id, bool supported) {
|
bool SetSupported(const std::string& remote_id, bool supported) {
|
||||||
const auto entry = controllers_.find(remote_id);
|
const auto entry = controllers_.find(remote_id);
|
||||||
if (entry == controllers_.end()) return false;
|
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;
|
if (automatic_controller_ != remote_id) return false;
|
||||||
automatic_controller_.clear();
|
automatic_controller_.clear();
|
||||||
return CanEnable();
|
return CanEnable();
|
||||||
@@ -53,9 +71,10 @@ class PrivacySessionPolicy {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_map<std::string, bool> controllers_;
|
std::unordered_map<std::string, bool> controllers_;
|
||||||
|
std::unordered_map<std::string, bool> early_support_;
|
||||||
std::string automatic_controller_;
|
std::string automatic_controller_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
#include "../../windows_thread_dpi.h"
|
#include "../../windows_thread_dpi.h"
|
||||||
#include "../privacy_band_ipc.h"
|
#include "../privacy_band_ipc.h"
|
||||||
|
#include "../privacy_capture_exclusion.h"
|
||||||
|
#include "../privacy_topology.h"
|
||||||
#include "privacy_cover_renderer.h"
|
#include "privacy_cover_renderer.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -24,11 +26,6 @@ constexpr DWORD kCoverStyles = WS_EX_TOPMOST | WS_EX_NOACTIVATE |
|
|||||||
WS_EX_TRANSPARENT;
|
WS_EX_TRANSPARENT;
|
||||||
HMODULE module = nullptr;
|
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 {
|
class Windows {
|
||||||
public:
|
public:
|
||||||
explicit Windows(PrivacyBandShared* shared) : shared_(shared) {}
|
explicit Windows(PrivacyBandShared* shared) : shared_(shared) {}
|
||||||
@@ -59,6 +56,7 @@ class Windows {
|
|||||||
return atom_ ? Reconcile() : Fail(L"Register high-band window class");
|
return atom_ ? Reconcile() : Fail(L"Register high-band window class");
|
||||||
}
|
}
|
||||||
bool Reconcile() {
|
bool Reconcile() {
|
||||||
|
InterlockedExchange(&shared_->topology_pending, 1);
|
||||||
std::vector<RECT> monitors;
|
std::vector<RECT> monitors;
|
||||||
if (!EnumDisplayMonitors(
|
if (!EnumDisplayMonitors(
|
||||||
nullptr, nullptr,
|
nullptr, nullptr,
|
||||||
@@ -68,16 +66,37 @@ class Windows {
|
|||||||
result.push_back(*rect);
|
result.push_back(*rect);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
},
|
},
|
||||||
reinterpret_cast<LPARAM>(&monitors)) ||
|
reinterpret_cast<LPARAM>(&monitors)))
|
||||||
monitors.empty())
|
|
||||||
return Fail(L"Enumerate high-band displays");
|
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) {
|
for (const auto& rect : monitors) {
|
||||||
const auto found =
|
const auto found =
|
||||||
std::find_if(windows_.begin(), windows_.end(),
|
std::find_if(windows_.begin(), windows_.end(),
|
||||||
[&](const auto& w) { return SameRect(w.rect, rect); });
|
[&](const auto& w) { return SameRect(w.rect, rect); });
|
||||||
if (found != windows_.end()) continue;
|
if (found != windows_.end()) {
|
||||||
changed = true;
|
// 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,
|
HWND hwnd = create_(kCoverStyles, atom_, L"CrossDesk Privacy", WS_POPUP,
|
||||||
rect.left, rect.top, rect.right - rect.left,
|
rect.left, rect.top, rect.right - rect.left,
|
||||||
rect.bottom - rect.top, nullptr, nullptr, module,
|
rect.bottom - rect.top, nullptr, nullptr, module,
|
||||||
@@ -86,7 +105,7 @@ class Windows {
|
|||||||
windows_.push_back({reinterpret_cast<uint64_t>(hwnd), rect});
|
windows_.push_back({reinterpret_cast<uint64_t>(hwnd), rect});
|
||||||
if (!SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA))
|
if (!SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA))
|
||||||
return Fail(L"Make high-band cover opaque");
|
return Fail(L"Make high-band cover opaque");
|
||||||
if (!SetWindowDisplayAffinity(hwnd, 0x11))
|
if (!ExcludePrivacyWindowFromCapture(hwnd))
|
||||||
return Fail(L"Exclude high-band cover from capture");
|
return Fail(L"Exclude high-band cover from capture");
|
||||||
const BOOL yes = TRUE;
|
const BOOL yes = TRUE;
|
||||||
const HRESULT peek = DwmSetWindowAttribute(hwnd, DWMWA_EXCLUDED_FROM_PEEK,
|
const HRESULT peek = DwmSetWindowAttribute(hwnd, DWMWA_EXCLUDED_FROM_PEEK,
|
||||||
@@ -114,13 +133,16 @@ class Windows {
|
|||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!Validate()) return false;
|
if (!Validate()) {
|
||||||
if (changed) {
|
// Another topology event can race enumeration/presentation. Retry while
|
||||||
InterlockedIncrement(&shared_->sequence);
|
// retaining every surviving cover; actual validation errors stay fatal.
|
||||||
shared_->count = static_cast<uint32_t>(windows_.size());
|
return InterlockedCompareExchange(&shared_->error, 0, 0) == ERROR_SUCCESS;
|
||||||
std::copy(windows_.begin(), windows_.end(), shared_->windows);
|
|
||||||
InterlockedIncrement(&shared_->sequence);
|
|
||||||
}
|
}
|
||||||
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
bool Validate() {
|
bool Validate() {
|
||||||
@@ -130,13 +152,17 @@ class Windows {
|
|||||||
DWORD band = 0, affinity = 0, flags = 0, cloaked = 0;
|
DWORD band = 0, affinity = 0, flags = 0, cloaked = 0;
|
||||||
BYTE alpha = 0;
|
BYTE alpha = 0;
|
||||||
RECT rect{};
|
RECT rect{};
|
||||||
|
if (!GetWindowRect(hwnd, &rect) || !SameRect(rect, w.rect)) {
|
||||||
|
changed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if ((GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & kCoverStyles) !=
|
if ((GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & kCoverStyles) !=
|
||||||
kCoverStyles ||
|
kCoverStyles ||
|
||||||
!get_band_(hwnd, &band) || band != kPrivacyWindowBand ||
|
!get_band_(hwnd, &band) || band != kPrivacyWindowBand ||
|
||||||
!GetWindowDisplayAffinity(hwnd, &affinity) || affinity != 0x11 ||
|
!GetWindowDisplayAffinity(hwnd, &affinity) ||
|
||||||
|
affinity != kPrivacyWindowDisplayAffinity ||
|
||||||
!GetLayeredWindowAttributes(hwnd, nullptr, &alpha, &flags) ||
|
!GetLayeredWindowAttributes(hwnd, nullptr, &alpha, &flags) ||
|
||||||
alpha != 255 || flags != LWA_ALPHA || !IsWindowVisible(hwnd) ||
|
alpha != 255 || flags != LWA_ALPHA || !IsWindowVisible(hwnd) ||
|
||||||
!GetWindowRect(hwnd, &rect) || !SameRect(rect, w.rect) ||
|
|
||||||
FAILED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked,
|
FAILED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked,
|
||||||
sizeof(cloaked))) ||
|
sizeof(cloaked))) ||
|
||||||
cloaked)
|
cloaked)
|
||||||
|
|||||||
@@ -16,15 +16,13 @@
|
|||||||
#include "../windows_thread_dpi.h"
|
#include "../windows_thread_dpi.h"
|
||||||
#include "privacy_backend.h"
|
#include "privacy_backend.h"
|
||||||
#include "privacy_band_guard.h"
|
#include "privacy_band_guard.h"
|
||||||
|
#include "privacy_capture_exclusion.h"
|
||||||
#include "privacy_cursor_guard.h"
|
#include "privacy_cursor_guard.h"
|
||||||
#include "privacy_input_guard.h"
|
#include "privacy_input_guard.h"
|
||||||
|
#include "privacy_topology.h"
|
||||||
#include "privacy_window_visibility.h"
|
#include "privacy_window_visibility.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
|
||||||
#ifndef WDA_EXCLUDEFROMCAPTURE
|
|
||||||
#define WDA_EXCLUDEFROMCAPTURE 0x00000011
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -46,11 +44,6 @@ bool DesktopName(HDESK desktop, wchar_t (&name)[256]) {
|
|||||||
sizeof(name), &needed);
|
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 {
|
struct MonitorEnumeration {
|
||||||
std::vector<RECT> rects;
|
std::vector<RECT> rects;
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
@@ -141,6 +134,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
return false;
|
return false;
|
||||||
if (!band_guard_.Start(text, error)) return false;
|
if (!band_guard_.Start(text, error)) return false;
|
||||||
if (!ReconcileMonitors(error)) return false;
|
if (!ReconcileMonitors(error)) return false;
|
||||||
|
layout_changed_ = false;
|
||||||
event_owner_ = this;
|
event_owner_ = this;
|
||||||
for (DWORD event : {EVENT_OBJECT_SHOW, EVENT_OBJECT_REORDER,
|
for (DWORD event : {EVENT_OBJECT_SHOW, EVENT_OBJECT_REORDER,
|
||||||
EVENT_OBJECT_LOCATIONCHANGE}) {
|
EVENT_OBJECT_LOCATIONCHANGE}) {
|
||||||
@@ -193,6 +187,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
last_obstruction_ = nullptr;
|
last_obstruction_ = nullptr;
|
||||||
last_obstruction_log_ = 0;
|
last_obstruction_log_ = 0;
|
||||||
layout_changed_ = false;
|
layout_changed_ = false;
|
||||||
|
topology_pending_ = false;
|
||||||
|
topology_pending_since_ = 0;
|
||||||
coverage_dirty_ = false;
|
coverage_dirty_ = false;
|
||||||
emergency_ = false;
|
emergency_ = false;
|
||||||
health_error_.clear();
|
health_error_.clear();
|
||||||
@@ -235,12 +231,14 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
std::string error;
|
std::string error;
|
||||||
if (!input_guard_.Healthy(error)) return {error, false};
|
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
|
// Input messages wake the thread immediately; expensive health queries
|
||||||
// must retain their own cadence instead of running once per input event.
|
// must retain their own cadence instead of running once per input event.
|
||||||
if (!layout_changed_ &&
|
// Window events only dirty the coverage check, which is cheap enough to
|
||||||
GetTickCount64() - last_check_ < 100) {
|
// run between the throttled full health passes.
|
||||||
if (coverage_dirty_ && last_health_.failure.empty()) {
|
if (!layout_changed_ && GetTickCount64() - last_check_ < 100) {
|
||||||
|
if (coverage_dirty_ && last_health_.failure.empty() &&
|
||||||
|
!topology_pending_) {
|
||||||
coverage_dirty_ = false;
|
coverage_dirty_ = false;
|
||||||
last_health_ = CheckCoverage();
|
last_health_ = CheckCoverage();
|
||||||
}
|
}
|
||||||
@@ -253,6 +251,21 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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() {
|
PrivacyHealth CheckHealth() {
|
||||||
if (!health_error_.empty()) return {health_error_, false};
|
if (!health_error_.empty()) return {health_error_, false};
|
||||||
if (!band_guard_.Healthy(health_error_)) 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",
|
"Exclusive fullscreen or unknown display presentation",
|
||||||
false};
|
false};
|
||||||
}
|
}
|
||||||
MonitorEnumeration monitors;
|
if (!ReconcileMonitors(health_error_)) return {health_error_, false};
|
||||||
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
|
if (topology_pending_) return TopologyPending();
|
||||||
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};
|
|
||||||
BOOL composed = FALSE;
|
BOOL composed = FALSE;
|
||||||
if (FAILED(DwmIsCompositionEnabled(&composed)) || !composed) {
|
if (FAILED(DwmIsCompositionEnabled(&composed)) || !composed) {
|
||||||
return {
|
return {
|
||||||
"Desktop composition stopped; privacy coverage cannot be guaranteed",
|
"Desktop composition stopped; privacy coverage cannot be guaranteed",
|
||||||
false};
|
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() {
|
PrivacyHealth CheckCoverage() {
|
||||||
@@ -328,7 +324,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
&layered_flags) ||
|
&layered_flags) ||
|
||||||
alpha != 255 || layered_flags != LWA_ALPHA ||
|
alpha != 255 || layered_flags != LWA_ALPHA ||
|
||||||
!GetWindowDisplayAffinity(w.cover, &affinity) ||
|
!GetWindowDisplayAffinity(w.cover, &affinity) ||
|
||||||
affinity != WDA_EXCLUDEFROMCAPTURE ||
|
affinity != kPrivacyWindowDisplayAffinity ||
|
||||||
FAILED(DwmGetWindowAttribute(w.cover, DWMWA_CLOAKED, &cloaked,
|
FAILED(DwmGetWindowAttribute(w.cover, DWMWA_CLOAKED, &cloaked,
|
||||||
sizeof(cloaked))) ||
|
sizeof(cloaked))) ||
|
||||||
cloaked) {
|
cloaked) {
|
||||||
@@ -430,40 +426,42 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ReconcileMonitors(std::string& error) {
|
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;
|
MonitorEnumeration monitors;
|
||||||
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
|
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
|
||||||
reinterpret_cast<LPARAM>(&monitors)) ||
|
reinterpret_cast<LPARAM>(&monitors)) || !monitors.ok) {
|
||||||
!monitors.ok || monitors.rects.empty()) {
|
|
||||||
error = Error("EnumDisplayMonitors");
|
error = Error("EnumDisplayMonitors");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::vector<PrivacyBandWindow> covers;
|
std::vector<PrivacyBandWindow> covers;
|
||||||
if (!band_guard_.Windows(covers, error)) return false;
|
if (!band_guard_.Windows(covers, error)) {
|
||||||
for (const RECT& rect : monitors.rects) {
|
if (GetLastError() == ERROR_RETRY) return wait_for_broker();
|
||||||
auto existing =
|
return false;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
// Cover windows are owned and released by the broker.
|
if (!MatchPrivacyTopology(monitors.rects, covers)) return wait_for_broker();
|
||||||
windows_.erase(std::remove_if(windows_.begin(), windows_.end(),
|
|
||||||
[&](const auto& window) {
|
std::vector<Window> updated;
|
||||||
return std::none_of(monitors.rects.begin(), monitors.rects.end(),
|
for (const auto& cover : covers) {
|
||||||
[&](const RECT& rect) { return SameRect(window.rect, rect); });
|
HWND hwnd = reinterpret_cast<HWND>(cover.hwnd);
|
||||||
}), windows_.end());
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +524,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
bool hotkey_ = false;
|
bool hotkey_ = false;
|
||||||
bool emergency_ = false;
|
bool emergency_ = false;
|
||||||
bool layout_changed_ = false;
|
bool layout_changed_ = false;
|
||||||
|
bool topology_pending_ = false;
|
||||||
|
uint64_t topology_pending_since_ = 0;
|
||||||
bool coverage_dirty_ = false;
|
bool coverage_dirty_ = false;
|
||||||
uint64_t last_check_ = 0;
|
uint64_t last_check_ = 0;
|
||||||
PrivacyHealth last_health_;
|
PrivacyHealth last_health_;
|
||||||
|
|||||||
@@ -119,10 +119,12 @@ bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
|
|||||||
// Keep both images immutable throughout creation/injection and the session.
|
// Keep both images immutable throughout creation/injection and the session.
|
||||||
image_lock_ = CreateFileW(broker_path_.c_str(), GENERIC_READ, FILE_SHARE_READ,
|
image_lock_ = CreateFileW(broker_path_.c_str(), GENERIC_READ, FILE_SHARE_READ,
|
||||||
nullptr, OPEN_EXISTING, 0, nullptr);
|
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,
|
dll_lock_ = CreateFileW(dll.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||||
OPEN_EXISTING, 0, nullptr);
|
OPEN_EXISTING, 0, nullptr);
|
||||||
if (image_lock_ == INVALID_HANDLE_VALUE || dll_lock_ == INVALID_HANDLE_VALUE)
|
if (dll_lock_ == INVALID_HANDLE_VALUE)
|
||||||
return Failed("Lock privacy broker images", error);
|
return Failed("Lock privacy window module", error);
|
||||||
|
|
||||||
SECURITY_ATTRIBUTES inherit{sizeof(inherit), nullptr, TRUE};
|
SECURITY_ATTRIBUTES inherit{sizeof(inherit), nullptr, TRUE};
|
||||||
ScopedHandle parent;
|
ScopedHandle parent;
|
||||||
@@ -131,14 +133,14 @@ bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
|
|||||||
SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, TRUE,
|
SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, TRUE,
|
||||||
0))
|
0))
|
||||||
return Failed("Duplicate privacy parent process handle", error);
|
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);
|
stop_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
|
||||||
|
if (!stop_) return Failed("Create high-band broker stop event", error);
|
||||||
ready_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
|
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,
|
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, &inherit, PAGE_READWRITE,
|
||||||
0, sizeof(PrivacyBandShared), nullptr);
|
0, sizeof(PrivacyBandShared), nullptr);
|
||||||
if (!stop_ || !ready_ || !mapping_) {
|
if (!mapping_) return Failed("Create high-band broker shared memory", error);
|
||||||
const auto code = GetLastError();
|
|
||||||
return Failed("Create high-band broker IPC", error, code);
|
|
||||||
}
|
|
||||||
shared_ = static_cast<PrivacyBandShared*>(MapViewOfFile(
|
shared_ = static_cast<PrivacyBandShared*>(MapViewOfFile(
|
||||||
mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyBandShared)));
|
mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyBandShared)));
|
||||||
if (!shared_) {
|
if (!shared_) {
|
||||||
@@ -271,25 +273,30 @@ bool PrivacyBandGuard::Healthy(std::string& error) {
|
|||||||
bool PrivacyBandGuard::Windows(std::vector<PrivacyBandWindow>& windows,
|
bool PrivacyBandGuard::Windows(std::vector<PrivacyBandWindow>& windows,
|
||||||
std::string& error) {
|
std::string& error) {
|
||||||
if (!Healthy(error)) return false;
|
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) {
|
for (int attempt = 0; attempt < 4; ++attempt) {
|
||||||
const LONG before = InterlockedCompareExchange(&shared_->sequence, 0, 0);
|
const LONG before = InterlockedCompareExchange(&shared_->sequence, 0, 0);
|
||||||
if (before & 1) continue;
|
if (before & 1) continue;
|
||||||
const auto count = shared_->count;
|
const auto count = shared_->count;
|
||||||
if (count == 0 || count > kPrivacyMaxMonitors) break;
|
if (count > kPrivacyMaxMonitors) break;
|
||||||
windows.assign(shared_->windows, shared_->windows + count);
|
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) {
|
for (const auto& window : windows) {
|
||||||
DWORD pid = 0;
|
DWORD pid = 0;
|
||||||
GetWindowThreadProcessId(reinterpret_cast<HWND>(window.hwnd), &pid);
|
GetWindowThreadProcessId(reinterpret_cast<HWND>(window.hwnd), &pid);
|
||||||
if (pid != process_id())
|
if (pid != process_id())
|
||||||
return Failed("Invalid high-band window owner", error,
|
return retry();
|
||||||
ERROR_INVALID_HANDLE);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Failed("High-band monitor snapshot is changing or invalid", error,
|
return retry();
|
||||||
ERROR_RETRY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PrivacyBandGuard::Stop(std::string& error) {
|
bool PrivacyBandGuard::Stop(std::string& error) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
constexpr uint32_t kPrivacyBandMagic = 0x43534231;
|
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 DWORD kPrivacyWindowBand = 18; // ZBID_ABOVELOCK_UX, current desktop.
|
||||||
constexpr size_t kPrivacyMaxMonitors = 64;
|
constexpr size_t kPrivacyMaxMonitors = 64;
|
||||||
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
|
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
|
||||||
@@ -36,6 +36,7 @@ struct PrivacyBandShared {
|
|||||||
volatile LONG sequence = 0;
|
volatile LONG sequence = 0;
|
||||||
volatile LONG error = ERROR_IO_PENDING;
|
volatile LONG error = ERROR_IO_PENDING;
|
||||||
volatile LONG active = 0;
|
volatile LONG active = 0;
|
||||||
|
volatile LONG topology_pending = 0;
|
||||||
alignas(8) volatile LONG64 heartbeat = 0;
|
alignas(8) volatile LONG64 heartbeat = 0;
|
||||||
wchar_t operation[128]{};
|
wchar_t operation[128]{};
|
||||||
wchar_t unlock_hint[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) {
|
if (size == ScreenCapturer::kBackendReset) {
|
||||||
SharedCapturedCursorState().Reset();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (data && size > 0 && w > 0 && h > 0) {
|
if (data && size > 0 && w > 0 && h > 0) {
|
||||||
@@ -578,6 +578,8 @@ void ScreenCapturerWin::EmitCapturedFrame(
|
|||||||
} restore_source{
|
} restore_source{
|
||||||
std::exchange(current_frame_from_secure_desktop, from_secure_desktop)};
|
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_frame) {
|
||||||
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
|
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
|
||||||
LOG_INFO("Windows capturer native frame output enabled (type={})",
|
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);
|
const int ret = impl_->SwitchTo(backend_index);
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
monitor_index_.store(monitor_index, std::memory_order_relaxed);
|
monitor_index_.store(monitor_index, std::memory_order_relaxed);
|
||||||
} else if (privacy_) {
|
|
||||||
privacy_->Fail("Display switch failed; privacy screen will turn off");
|
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ void PrivacyController::EnableOnConnection() {
|
|||||||
}
|
}
|
||||||
void PrivacyController::Disable() {
|
void PrivacyController::Disable() {
|
||||||
std::lock_guard lock(mutex_);
|
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;
|
enable_pending_ = false;
|
||||||
failure_.clear();
|
failure_.clear();
|
||||||
disable_pending_ = true;
|
disable_pending_ = true;
|
||||||
@@ -118,6 +119,9 @@ void PrivacyController::CaptureChanged(bool running) {
|
|||||||
WakeLocked();
|
WakeLocked();
|
||||||
}
|
}
|
||||||
void PrivacyController::Run(Factory factory) {
|
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_);
|
std::unique_lock lock(mutex_);
|
||||||
try {
|
try {
|
||||||
if (factory) backend_ = factory();
|
if (factory) backend_ = factory();
|
||||||
@@ -134,7 +138,11 @@ void PrivacyController::Run(Factory factory) {
|
|||||||
SetState(caps.overlay ? PrivacyState::off : PrivacyState::unsupported, caps.reason);
|
SetState(caps.overlay ? PrivacyState::off : PrivacyState::unsupported, caps.reason);
|
||||||
}
|
}
|
||||||
if (disable_pending_) {
|
if (disable_pending_) {
|
||||||
if (backend_) backend_->Recover();
|
if (backend_) {
|
||||||
|
lock.unlock();
|
||||||
|
backend_->Recover();
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
if (backend_ && backend_->RecoveryPending()) {
|
if (backend_ && backend_->RecoveryPending()) {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
backend_->WaitForEvents(25);
|
backend_->WaitForEvents(25);
|
||||||
@@ -145,7 +153,10 @@ void PrivacyController::Run(Factory factory) {
|
|||||||
if (backend_ && !backend_->IsRecovered()) {
|
if (backend_ && !backend_->IsRecovered()) {
|
||||||
enable_pending_ = false;
|
enable_pending_ = false;
|
||||||
disable_pending_ = true; // Retry native cleanup without affecting the session.
|
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 {
|
} else {
|
||||||
status_.overlay_active = status_.input_blocked = false;
|
status_.overlay_active = status_.input_blocked = false;
|
||||||
if (!enable_pending_) {
|
if (!enable_pending_) {
|
||||||
@@ -166,12 +177,18 @@ void PrivacyController::Run(Factory factory) {
|
|||||||
FailLocked("No desktop capture available for privacy screen");
|
FailLocked("No desktop capture available for privacy screen");
|
||||||
} else {
|
} else {
|
||||||
enable_pending_ = false;
|
enable_pending_ = false;
|
||||||
|
const bool block_input = block_requested_;
|
||||||
|
const PrivacyScreenText text = text_;
|
||||||
std::string error;
|
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);
|
FailLocked(error);
|
||||||
} else {
|
} else {
|
||||||
status_.overlay_active = true;
|
status_.overlay_active = true;
|
||||||
status_.input_blocked = block_requested_;
|
status_.input_blocked = block_input;
|
||||||
started_ = Now();
|
started_ = Now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,8 +220,19 @@ void PrivacyController::Run(Factory factory) {
|
|||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
if (!lock.owns_lock()) lock.lock();
|
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());
|
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
|
} // namespace crossdesk
|
||||||
|
|||||||
Reference in New Issue
Block a user