mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 18:03:08 +08:00
[feat] add macOS privacy screen and keep remote sessions running
This commit is contained in:
@@ -115,8 +115,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
||||
bool force_broadcast = false;
|
||||
if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) {
|
||||
if (privacy_.Engaged()) {
|
||||
privacy_.Fail("Queued security shortcut cancelled during privacy mode; remote operation paused");
|
||||
return;
|
||||
privacy_.Fail("Security shortcut requested; privacy screen will turn off");
|
||||
}
|
||||
const std::string response =
|
||||
QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <wtsapi32.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "../windows_thread_dpi.h"
|
||||
@@ -19,8 +18,6 @@
|
||||
#include "privacy_band_guard.h"
|
||||
#include "privacy_cursor_guard.h"
|
||||
#include "privacy_input_guard.h"
|
||||
#include "privacy_probe_pattern.h"
|
||||
#include "privacy_probe_renderer.h"
|
||||
#include "privacy_window_visibility.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -118,7 +115,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
if (!PrivacyBandGuard::Available(high_band_error))
|
||||
return {false, false, high_band_error};
|
||||
return {true, true,
|
||||
"High-band privacy; live capture exclusion will be verified"};
|
||||
"High-band privacy screen available"};
|
||||
}
|
||||
|
||||
bool Enable(bool block_input, const PrivacyScreenText& text,
|
||||
@@ -169,19 +166,9 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
std::remove(window_events_.begin(), window_events_.end(), nullptr),
|
||||
window_events_.end());
|
||||
if (window_events_.empty()) event_owner_ = nullptr;
|
||||
// Remove verification panels before exposing the local desktop.
|
||||
for (auto& window : windows_) {
|
||||
Destroy(window.probe);
|
||||
}
|
||||
std::string band_error;
|
||||
band_guard_.Stop(band_error);
|
||||
for (auto& window : windows_) {
|
||||
if (band_guard_.stopped()) window.cover = nullptr;
|
||||
}
|
||||
windows_.erase(
|
||||
std::remove_if(windows_.begin(), windows_.end(),
|
||||
[](const auto& w) { return !w.cover && !w.probe; }),
|
||||
windows_.end());
|
||||
if (band_guard_.stopped()) windows_.clear();
|
||||
input_guard_.Stop();
|
||||
block_input_ = false;
|
||||
if (hotkey_) {
|
||||
@@ -231,12 +218,12 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
health_error_ = Error("MsgWaitForMultipleObjectsEx privacy thread");
|
||||
}
|
||||
|
||||
PrivacyHealth Poll(bool force_check = false) override {
|
||||
PrivacyHealth Poll() override {
|
||||
MSG message{};
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
if (message.message == WM_QUIT)
|
||||
health_error_ =
|
||||
"Privacy message thread is exiting; remote operation paused";
|
||||
"Privacy message thread is exiting";
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
@@ -251,7 +238,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
if (windows_.empty()) 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 (!force_check && !layout_changed_ &&
|
||||
if (!layout_changed_ &&
|
||||
GetTickCount64() - last_check_ < 100) {
|
||||
if (coverage_dirty_ && last_health_.failure.empty()) {
|
||||
coverage_dirty_ = false;
|
||||
@@ -273,8 +260,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
std::string cursor_error;
|
||||
cursor_guard_.Stop(cursor_error);
|
||||
return {
|
||||
"Secure desktop, lock screen or unavailable input desktop; remote "
|
||||
"operation paused. Privacy windows cannot cover system security UI.",
|
||||
"Secure desktop, lock screen or unavailable input desktop; "
|
||||
"privacy screen will turn off",
|
||||
false};
|
||||
}
|
||||
std::string cursor_error;
|
||||
@@ -283,8 +270,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
if (FAILED(SHQueryUserNotificationState(&shell_state)) ||
|
||||
shell_state == QUNS_RUNNING_D3D_FULL_SCREEN) {
|
||||
return {
|
||||
"Exclusive fullscreen or unknown display presentation; remote "
|
||||
"operation paused",
|
||||
"Exclusive fullscreen or unknown display presentation",
|
||||
false};
|
||||
}
|
||||
MonitorEnumeration monitors;
|
||||
@@ -304,16 +290,14 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
});
|
||||
if (changed || layout_changed_) {
|
||||
layout_changed_ = false;
|
||||
// Retain old covers until new monitor regions are covered. Never
|
||||
// automatically resume remote work after a topology/DPI transition.
|
||||
// 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; coverage rebuilt, remote operation "
|
||||
"paused. Turn privacy off and reconnect to refresh display "
|
||||
"mapping.";
|
||||
"Display layout/DPI changed; privacy screen will turn off";
|
||||
}
|
||||
if (!health_error_.empty()) return {health_error_, false};
|
||||
BOOL composed = FALSE;
|
||||
@@ -349,8 +333,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
sizeof(cloaked))) ||
|
||||
cloaked) {
|
||||
return {
|
||||
"Privacy window visibility, geometry or capture affinity was lost; "
|
||||
"remote operation paused",
|
||||
"Privacy window visibility, geometry or capture affinity was lost",
|
||||
false};
|
||||
}
|
||||
}
|
||||
@@ -369,7 +352,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
if (above_band == 0 || above_band == 1) continue;
|
||||
if (std::any_of(windows_.begin(), windows_.end(),
|
||||
[above](const auto& own) {
|
||||
return above == own.cover || above == own.probe;
|
||||
return above == own.cover;
|
||||
}))
|
||||
continue;
|
||||
RECT bounds{}, intersection{};
|
||||
@@ -390,8 +373,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
now - last_obstruction_log_ >= 1000) {
|
||||
LOG_WARN(
|
||||
"Potential privacy cover overlap: class={}, process={}, "
|
||||
"band={}, bounds=({}, {})-({}, {}), exstyle=0x{:x}; "
|
||||
"remote operation paused",
|
||||
"band={}, bounds=({}, {})-({}, {}), exstyle=0x{:x}",
|
||||
window_class, process_id, above_band, bounds.left, bounds.top,
|
||||
bounds.right, bounds.bottom,
|
||||
GetWindowLongPtrW(above, GWL_EXSTYLE));
|
||||
@@ -399,10 +381,9 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
last_obstruction_log_ = now;
|
||||
}
|
||||
// HWND order cannot prove compositor order, but this window may
|
||||
// display content. Retain protection and require explicit recovery.
|
||||
// display content. Report failure so optional privacy is released.
|
||||
return {
|
||||
"A visible window may overlap privacy coverage; "
|
||||
"remote operation paused. Turn privacy off to recover.",
|
||||
"A visible window may overlap privacy coverage",
|
||||
false};
|
||||
}
|
||||
}
|
||||
@@ -410,100 +391,11 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
return {health_error_, false};
|
||||
}
|
||||
|
||||
public:
|
||||
bool PaintChallenge(uint32_t value, const std::string& title,
|
||||
std::string& error) override {
|
||||
using Pattern = PrivacyProbePattern;
|
||||
for (auto& w : windows_) {
|
||||
if (!w.probe && !CreateProbe(w, error)) return false;
|
||||
PrivacyProbeImage image;
|
||||
if (!RenderPrivacyProbe(title, PrivacyWindowDpi(w.probe),
|
||||
w.rect.right - w.rect.left - 2 * Pattern::kOffset,
|
||||
w.rect.bottom - w.rect.top - 2 * Pattern::kOffset,
|
||||
value, image, error)) {
|
||||
LOG_ERROR("Privacy verification panel: {}", error);
|
||||
return false;
|
||||
}
|
||||
HDC screen = GetDC(nullptr);
|
||||
HDC memory = CreateCompatibleDC(screen);
|
||||
BITMAPINFO info{};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = image.width;
|
||||
info.bmiHeader.biHeight = -image.height;
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
void* bits = nullptr;
|
||||
HBITMAP bitmap =
|
||||
CreateDIBSection(screen, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
bool ok = screen && memory && bitmap && bits;
|
||||
if (ok) {
|
||||
std::memcpy(bits, image.pixels.data(),
|
||||
image.pixels.size() * sizeof(uint32_t));
|
||||
const auto old = SelectObject(memory, bitmap);
|
||||
POINT position{w.rect.left + Pattern::kOffset,
|
||||
w.rect.top + Pattern::kOffset},
|
||||
source{};
|
||||
SIZE size{image.width, image.height};
|
||||
ok = old && old != HGDI_ERROR;
|
||||
if (ok) {
|
||||
ok = UpdateLayeredWindow(w.probe, screen, &position, &size, memory,
|
||||
&source, 0, nullptr, ULW_OPAQUE);
|
||||
SelectObject(memory, old);
|
||||
}
|
||||
}
|
||||
if (!ok) error = Error("Present opaque privacy verification panel");
|
||||
if (bitmap) DeleteObject(bitmap);
|
||||
if (memory) DeleteDC(memory);
|
||||
if (screen) ReleaseDC(nullptr, screen);
|
||||
if (!ok) return false;
|
||||
// Present only after the complete text and square bitmap is ready.
|
||||
if (!IsWindowVisible(w.probe) &&
|
||||
(!SetWindowPos(
|
||||
w.probe, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW) ||
|
||||
!IsWindowVisible(w.probe))) {
|
||||
error = Error("Show privacy verification panel beneath cover");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClearChallenges(std::string& error) override {
|
||||
for (auto& w : windows_) {
|
||||
Destroy(w.probe);
|
||||
if (w.probe) {
|
||||
error = "Could not remove the temporary capture verification window";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HasMonitor(int left, int top, int width, int height) const override {
|
||||
// Metadata only; also serialized against Poll by PrivacyController.
|
||||
return std::any_of(windows_.begin(), windows_.end(), [&](const auto& w) {
|
||||
return w.rect.left == left && w.rect.top == top &&
|
||||
w.rect.right - left == width && w.rect.bottom - top == height;
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
struct Window {
|
||||
RECT rect{};
|
||||
HWND cover = nullptr;
|
||||
HWND probe = nullptr;
|
||||
};
|
||||
|
||||
static void Destroy(HWND& hwnd) {
|
||||
if (hwnd && IsWindow(hwnd) && !DestroyWindow(hwnd)) {
|
||||
Error("DestroyWindow");
|
||||
return;
|
||||
}
|
||||
hwnd = nullptr;
|
||||
}
|
||||
|
||||
bool Initialize(std::string& error) {
|
||||
if (sink_) return true;
|
||||
// This dedicated thread owns no existing application windows. Never alter
|
||||
@@ -537,22 +429,6 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CreateProbe(Window& w, std::string& error) {
|
||||
constexpr DWORD styles = WS_EX_TOPMOST | WS_EX_TOOLWINDOW |
|
||||
WS_EX_NOACTIVATE | WS_EX_LAYERED |
|
||||
WS_EX_TRANSPARENT;
|
||||
w.probe =
|
||||
CreateWindowExW(styles, kClassName, L"CrossDesk capture verification",
|
||||
WS_POPUP, w.rect.left + PrivacyProbePattern::kOffset,
|
||||
w.rect.top + PrivacyProbePattern::kOffset, 1, 1,
|
||||
nullptr, nullptr, GetModuleHandleW(nullptr), this);
|
||||
if (!w.probe) {
|
||||
error = Error("CreateWindowExW capture challenge");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReconcileMonitors(std::string& error) {
|
||||
MonitorEnumeration monitors;
|
||||
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
|
||||
@@ -576,26 +452,18 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
error = "High-band broker has not covered every physical monitor";
|
||||
return false;
|
||||
}
|
||||
windows_.push_back({rect, reinterpret_cast<HWND>(cover->hwnd), nullptr});
|
||||
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);
|
||||
}
|
||||
for (auto it = windows_.begin(); it != windows_.end();) {
|
||||
if (std::none_of(monitors.rects.begin(), monitors.rects.end(),
|
||||
[&](const RECT& r) { return SameRect(it->rect, r); })) {
|
||||
Destroy(it->probe);
|
||||
it->cover = nullptr; // The broker owns cover destruction.
|
||||
if (!it->probe)
|
||||
it = windows_.erase(it);
|
||||
else {
|
||||
error = "Old privacy window could not be released";
|
||||
return false;
|
||||
}
|
||||
} else
|
||||
++it;
|
||||
}
|
||||
// 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());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -619,14 +487,6 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
}
|
||||
if (msg == WM_NCHITTEST) return HTTRANSPARENT;
|
||||
if (msg == WM_CLOSE) return 0;
|
||||
if (msg == WM_PAINT) {
|
||||
// UpdateLayeredWindow owns the complete probe image; WM_PAINT only
|
||||
// validates the update region and never draws a second copy.
|
||||
PAINTSTRUCT paint{};
|
||||
BeginPaint(hwnd, &paint);
|
||||
EndPaint(hwnd, &paint);
|
||||
return 0;
|
||||
}
|
||||
if ((msg == WM_DISPLAYCHANGE || msg == WM_DPICHANGED) && self) {
|
||||
self->layout_changed_ = true;
|
||||
return 0;
|
||||
@@ -641,7 +501,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
hwnd == self->sink_ || GetAncestor(hwnd, GA_ROOT) != hwnd)
|
||||
return;
|
||||
for (const auto& w : self->windows_)
|
||||
if (hwnd == w.cover || hwnd == w.probe) return;
|
||||
if (hwnd == w.cover) return;
|
||||
// OUTOFCONTEXT delivers on this window thread. Only mark work here;
|
||||
// monitor/session queries and Z-order changes remain in the control loop.
|
||||
const auto get_band = reinterpret_cast<GetPrivacyWindowBand>(
|
||||
|
||||
@@ -135,7 +135,7 @@ bool PrivacyCursorGuard::Healthy(std::string& error) {
|
||||
if (restore_pending_ || (process_ && !helper_failure_reported_))
|
||||
RestoreAfterFailure(error);
|
||||
helper_failure_reported_ = true;
|
||||
error = "System cursor helper exited; remote operation paused";
|
||||
error = "System cursor helper exited; privacy screen will turn off";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
namespace crossdesk {
|
||||
|
||||
// The owner controls lifecycle; the dedicated message thread owns LL hooks.
|
||||
// Rendering, capture verification and session queries never run on this thread.
|
||||
// Rendering and session queries never run on this thread.
|
||||
class PrivacyInputGuard {
|
||||
public:
|
||||
~PrivacyInputGuard();
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "privacy_probe_renderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "privacy_probe_pattern.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
struct Canvas {
|
||||
HDC dc = CreateCompatibleDC(nullptr);
|
||||
HFONT font = nullptr;
|
||||
HGDIOBJ old_font = nullptr;
|
||||
HBITMAP bitmap = nullptr;
|
||||
HGDIOBJ old_bitmap = nullptr;
|
||||
~Canvas() {
|
||||
if (old_bitmap) SelectObject(dc, old_bitmap);
|
||||
if (old_font) SelectObject(dc, old_font);
|
||||
if (bitmap) DeleteObject(bitmap);
|
||||
if (font) DeleteObject(font);
|
||||
if (dc) DeleteDC(dc);
|
||||
}
|
||||
};
|
||||
bool Failed(const char* operation, std::string& error) {
|
||||
error = std::string(operation) + " (Windows error " +
|
||||
std::to_string(GetLastError()) + ")";
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool RenderPrivacyProbe(const std::string& title, UINT dpi, int maximum_width,
|
||||
int maximum_height, uint32_t value,
|
||||
PrivacyProbeImage& image, std::string& error) {
|
||||
using Pattern = PrivacyProbePattern;
|
||||
const int length =
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, title.data(),
|
||||
static_cast<int>(title.size()), nullptr, 0);
|
||||
if (length <= 0) return Failed("Decode privacy verification text", error);
|
||||
std::wstring text(length, L'\0');
|
||||
if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, title.data(),
|
||||
static_cast<int>(title.size()), text.data(), length))
|
||||
return Failed("Decode privacy verification text", error);
|
||||
const int available_text_width =
|
||||
maximum_width - Pattern::kTextLeft - Pattern::kPadding;
|
||||
if (available_text_width < 80 ||
|
||||
maximum_height < Pattern::kSide + 2 * Pattern::kPadding) {
|
||||
error = "Display is too small for the privacy verification panel";
|
||||
return false;
|
||||
}
|
||||
Canvas canvas;
|
||||
if (!canvas.dc)
|
||||
return Failed("Create privacy verification drawing context", error);
|
||||
canvas.font = CreateFontW(
|
||||
-MulDiv(28, static_cast<int>(dpi), 96), 0, 0, 0, FW_SEMIBOLD, FALSE,
|
||||
FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
||||
ANTIALIASED_QUALITY, DEFAULT_PITCH, L"Microsoft YaHei UI");
|
||||
if (!canvas.font) return Failed("Create privacy verification font", error);
|
||||
canvas.old_font = SelectObject(canvas.dc, canvas.font);
|
||||
if (!canvas.old_font || canvas.old_font == HGDI_ERROR) {
|
||||
canvas.old_font = nullptr;
|
||||
return Failed("Select privacy verification font", error);
|
||||
}
|
||||
SIZE extent{};
|
||||
if (!GetTextExtentPoint32W(canvas.dc, text.data(), length, &extent))
|
||||
return Failed("Measure privacy verification text", error);
|
||||
const int text_width =
|
||||
(std::min)(available_text_width,
|
||||
(std::min)(static_cast<int>(extent.cx) + 4,
|
||||
MulDiv(520, static_cast<int>(dpi), 96)));
|
||||
RECT text_bounds{0, 0, text_width, 0};
|
||||
constexpr UINT text_flags = DT_LEFT | DT_WORDBREAK | DT_NOPREFIX;
|
||||
if (!DrawTextW(canvas.dc, text.data(), length, &text_bounds,
|
||||
text_flags | DT_CALCRECT))
|
||||
return Failed("Lay out privacy verification text", error);
|
||||
const int width = Pattern::kTextLeft + text_width + Pattern::kPadding;
|
||||
const int height =
|
||||
(std::max)(Pattern::kSide, static_cast<int>(text_bounds.bottom)) +
|
||||
2 * Pattern::kPadding;
|
||||
if (height > maximum_height || text_bounds.right > text_width) {
|
||||
error =
|
||||
"Display is too small for localized privacy verification text at this "
|
||||
"DPI";
|
||||
return false;
|
||||
}
|
||||
BITMAPINFO info{};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = width;
|
||||
info.bmiHeader.biHeight = -height;
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
void* bits = nullptr;
|
||||
canvas.bitmap =
|
||||
CreateDIBSection(canvas.dc, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
if (!canvas.bitmap || !bits)
|
||||
return Failed("Create privacy verification image", error);
|
||||
canvas.old_bitmap = SelectObject(canvas.dc, canvas.bitmap);
|
||||
if (!canvas.old_bitmap || canvas.old_bitmap == HGDI_ERROR) {
|
||||
canvas.old_bitmap = nullptr;
|
||||
return Failed("Select privacy verification image", error);
|
||||
}
|
||||
auto pixels = static_cast<uint32_t*>(bits);
|
||||
std::fill(pixels, pixels + static_cast<size_t>(width) * height, 0xFFF4F6F9u);
|
||||
for (int y = 0; y < height; ++y)
|
||||
for (int x = 0; x < width; ++x)
|
||||
if (x == 0 || y == 0 || x == width - 1 || y == height - 1)
|
||||
pixels[static_cast<size_t>(y) * width + x] = 0xFFD0D6E0u;
|
||||
SetBkMode(canvas.dc, TRANSPARENT);
|
||||
SetTextColor(canvas.dc, RGB(34, 46, 64));
|
||||
RECT text_rect{Pattern::kTextLeft, (height - text_bounds.bottom) / 2,
|
||||
width - Pattern::kPadding, (height + text_bounds.bottom) / 2};
|
||||
// Explicit clipping makes font fallback/overhang unable to reach the code.
|
||||
IntersectClipRect(canvas.dc, text_rect.left, text_rect.top, text_rect.right,
|
||||
text_rect.bottom);
|
||||
if (!DrawTextW(canvas.dc, text.data(), length, &text_rect, text_flags))
|
||||
return Failed("Draw privacy verification text", error);
|
||||
if (!GdiFlush()) return Failed("Flush privacy verification text", error);
|
||||
for (size_t index = 0; index < static_cast<size_t>(width) * height; ++index)
|
||||
pixels[index] |= 0xFF000000u;
|
||||
for (int row = 0; row < Pattern::kRows; ++row)
|
||||
for (int column = 0; column < Pattern::kColumns; ++column) {
|
||||
const uint32_t color =
|
||||
Pattern::White(value, column, row) ? 0xFFFFFFFFu : 0xFF000000u;
|
||||
for (int y = 0; y < Pattern::kCell; ++y)
|
||||
for (int x = 0; x < Pattern::kCell; ++x)
|
||||
pixels[static_cast<size_t>(Pattern::kPadding + row * Pattern::kCell +
|
||||
y) *
|
||||
width +
|
||||
Pattern::kPadding + column * Pattern::kCell + x] = color;
|
||||
}
|
||||
image.width = width;
|
||||
image.height = height;
|
||||
image.pixels.assign(pixels, pixels + static_cast<size_t>(width) * height);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_PROBE_RENDERER_H_
|
||||
#define _PRIVACY_PROBE_RENDERER_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct PrivacyProbeImage {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<uint32_t> pixels;
|
||||
};
|
||||
|
||||
// Produces an opaque BGRA panel. The localized text and the machine-readable
|
||||
// square occupy disjoint rectangles. Caller runs on the owning window thread.
|
||||
bool RenderPrivacyProbe(const std::string& title, UINT dpi, int maximum_width,
|
||||
int maximum_height, uint32_t value,
|
||||
PrivacyProbeImage& image, std::string& error);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_PROBE_RENDERER_H_
|
||||
@@ -330,8 +330,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
timeout_ms, &frame_info, desktop_resource.GetAddressOf());
|
||||
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
|
||||
// DXGI explicitly reports an unchanged desktop. Re-emit only a frame
|
||||
// successfully acquired for this monitor, so a static screen still has
|
||||
// a delivery heartbeat after temporary privacy probes are removed.
|
||||
// successfully acquired for this monitor to keep delivering static frames.
|
||||
if (cached_frame_valid && cached_generation == duplication_generation_ &&
|
||||
cached_monitor == monitor_index_.load() && callback_ && nv12_frame_) {
|
||||
const auto stream_id = MakeDisplayStreamId(cached_monitor);
|
||||
@@ -342,7 +341,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
continue;
|
||||
}
|
||||
// Never replay a cached image after an acquisition/conversion failure or
|
||||
// a backend rebuild. Normal error/reset handling must pause privacy.
|
||||
// a backend rebuild. Normal error/reset handling releases optional privacy.
|
||||
cached_frame_valid = false;
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
||||
|
||||
@@ -339,7 +339,7 @@ ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
|
||||
|
||||
void ScreenCapturerWin::NotifyPrivacyCapture(bool running) {
|
||||
if (privacy_)
|
||||
privacy_->CaptureChanged(CaptureBackendName(impl_.get()), running);
|
||||
privacy_->CaptureChanged(running);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
@@ -358,7 +358,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
const char* reported_stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame) {
|
||||
if (size == ScreenCapturer::kBackendReset) {
|
||||
if (privacy_) privacy_->CaptureInterrupted();
|
||||
if (privacy_) privacy_->Fail("Capture restarted; privacy screen will turn off");
|
||||
return;
|
||||
}
|
||||
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
|
||||
@@ -367,7 +367,6 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
|
||||
const char* raw_stream_id = reported_stream_id ? reported_stream_id : "";
|
||||
std::string mapped_stream_id;
|
||||
DisplayInfo privacy_display{"", 0, 0, 0, 0};
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
auto it = stream_id_alias_.find(raw_stream_id);
|
||||
@@ -381,21 +380,9 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
mapped_stream_id = ResolveDisplayStreamId(
|
||||
mapped_stream_id.c_str(), canonical_displays_.size(),
|
||||
monitor_index_.load(std::memory_order_relaxed));
|
||||
for (size_t index = 0; index < canonical_displays_.size(); ++index) {
|
||||
if (mapped_stream_id == MakeDisplayStreamId(index)) {
|
||||
privacy_display = canonical_displays_[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (privacy_) {
|
||||
if (privacy_->Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
|
||||
privacy_->Fail("Secure or unavailable desktop; privacy cannot cover system security UI. Remote operation paused.");
|
||||
return;
|
||||
}
|
||||
if (!privacy_->ObserveFrame(data, size > 0 ? static_cast<size_t>(size) : 0,
|
||||
w, h, privacy_display.left, privacy_display.top,
|
||||
privacy_display.width, privacy_display.height)) return;
|
||||
if (privacy_ && privacy_->Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
|
||||
privacy_->Fail("Secure or unavailable desktop; privacy screen will turn off");
|
||||
}
|
||||
if (mapped_stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_.exchange(true,
|
||||
@@ -477,13 +464,12 @@ void ScreenCapturerWin::EmitCapturedFrame(
|
||||
unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame,
|
||||
bool from_secure_desktop) {
|
||||
// A helper IPC already in flight when enable began must not bypass the
|
||||
// verified ordinary-desktop callback when it completes later.
|
||||
// Secure-desktop capture takes priority; remove privacy without dropping
|
||||
// the helper frame or delaying the remote session.
|
||||
if (from_secure_desktop && privacy_ && privacy_->Engaged()) {
|
||||
privacy_->Fail("Secure desktop helper frame arrived during privacy; remote operation paused");
|
||||
return;
|
||||
privacy_->Fail("Secure desktop helper frame arrived; privacy screen will turn off");
|
||||
}
|
||||
if (!cb_orig_ || (privacy_ && !privacy_->RemoteAllowed())) {
|
||||
if (!cb_orig_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -650,12 +636,11 @@ int ScreenCapturerWin::Resume(int monitor_index) {
|
||||
|
||||
int ScreenCapturerWin::SwitchTo(int monitor_index) {
|
||||
if (!impl_) return -1;
|
||||
if (privacy_) privacy_->DisplayChanging();
|
||||
const int ret = impl_->SwitchTo(monitor_index);
|
||||
if (ret == 0) {
|
||||
monitor_index_.store(monitor_index, std::memory_order_relaxed);
|
||||
} else if (privacy_) {
|
||||
privacy_->Fail("Display switch failed; remote operation paused");
|
||||
privacy_->Fail("Display switch failed; privacy screen will turn off");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -1128,13 +1113,11 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
std::vector<uint8_t> secure_frame;
|
||||
|
||||
while (running_.load(std::memory_order_relaxed)) {
|
||||
// A privacy session must NEVER continue using the helper's secure-desktop
|
||||
// GDI frames. Its HWNDs belong to the ordinary interactive desktop only.
|
||||
// Privacy covers belong to the ordinary desktop. Drop privacy on a
|
||||
// secure desktop, while continuing the normal helper capture path.
|
||||
if (privacy_ && privacy_->Engaged()) {
|
||||
if (!IsWindowsPrivacyDesktopAvailable())
|
||||
privacy_->Fail("Secure desktop or lock screen; remote operation paused. Privacy cannot cover Windows security UI.");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(25));
|
||||
continue;
|
||||
privacy_->Fail("Secure desktop or lock screen; privacy screen will turn off. Privacy cannot cover Windows security UI.");
|
||||
}
|
||||
if (paused_.load(std::memory_order_relaxed)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
@@ -45,7 +45,7 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
void SetPrivacyController(PrivacyController* privacy) { privacy_ = privacy; }
|
||||
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
|
||||
// Set before Init(), while capture is stopped.
|
||||
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user