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