mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 01:46:47 +08:00
[feat] add headless compatibility capture and usbmmidd virtual display plugging
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_DISPLAY_PRESENCE_H_
|
||||
#define CROSSDESK_DISPLAY_PRESENCE_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Without any attached monitor, Windows keeps the desktop on a phantom display
|
||||
// target with an OS-chosen resolution. DXGI has no output to duplicate there,
|
||||
// but GDI still reads the composed desktop and input injection keeps working.
|
||||
enum class DisplayPresence { present, headless, unknown };
|
||||
|
||||
struct DisplayPresenceProbe {
|
||||
bool query_succeeded = false; // QueryDisplayConfig returned a topology
|
||||
size_t active_paths = 0; // active display paths, phantom included
|
||||
size_t available_targets = 0; // active paths whose monitor is present
|
||||
size_t desktop_devices = 0; // adapters attached to the desktop
|
||||
size_t attached_monitors = 0; // monitor devices reported active
|
||||
};
|
||||
|
||||
// Pure policy, kept separate from Win32 so it can be tested with fixtures.
|
||||
inline DisplayPresence ClassifyDisplayPresence(
|
||||
const DisplayPresenceProbe& probe) {
|
||||
if (probe.query_succeeded) {
|
||||
if (probe.available_targets > 0) return DisplayPresence::present;
|
||||
if (probe.active_paths > 0 || probe.desktop_devices > 0)
|
||||
return DisplayPresence::headless;
|
||||
return DisplayPresence::unknown;
|
||||
}
|
||||
if (probe.attached_monitors > 0) return DisplayPresence::present;
|
||||
if (probe.desktop_devices > 0) return DisplayPresence::headless;
|
||||
return DisplayPresence::unknown;
|
||||
}
|
||||
|
||||
inline DisplayPresenceProbe ProbeDisplayPresence() {
|
||||
DisplayPresenceProbe probe;
|
||||
// The buffer size can change between the two calls during a topology
|
||||
// change; a few retries cover that window.
|
||||
for (int attempt = 0; attempt < 4 && !probe.query_succeeded; ++attempt) {
|
||||
UINT32 path_count = 0;
|
||||
UINT32 mode_count = 0;
|
||||
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_count,
|
||||
&mode_count) != ERROR_SUCCESS) break;
|
||||
std::vector<DISPLAYCONFIG_PATH_INFO> paths((std::max)(path_count, 1u));
|
||||
std::vector<DISPLAYCONFIG_MODE_INFO> modes((std::max)(mode_count, 1u));
|
||||
const LONG status = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_count,
|
||||
paths.data(), &mode_count,
|
||||
modes.data(), nullptr);
|
||||
if (status == ERROR_INSUFFICIENT_BUFFER) continue;
|
||||
if (status != ERROR_SUCCESS) break;
|
||||
probe.query_succeeded = true;
|
||||
for (UINT32 index = 0; index < path_count; ++index) {
|
||||
if (!(paths[index].flags & DISPLAYCONFIG_PATH_ACTIVE)) continue;
|
||||
++probe.active_paths;
|
||||
// A removed monitor can leave its path active while the target is gone.
|
||||
if (paths[index].targetInfo.targetAvailable) ++probe.available_targets;
|
||||
}
|
||||
}
|
||||
for (DWORD index = 0;; ++index) {
|
||||
DISPLAY_DEVICEW adapter{sizeof(adapter)};
|
||||
if (!EnumDisplayDevicesW(nullptr, index, &adapter, 0)) break;
|
||||
if (!(adapter.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP)) continue;
|
||||
++probe.desktop_devices;
|
||||
for (DWORD child = 0;; ++child) {
|
||||
DISPLAY_DEVICEW monitor{sizeof(monitor)};
|
||||
if (!EnumDisplayDevicesW(adapter.DeviceName, child, &monitor, 0)) break;
|
||||
if (monitor.StateFlags & DISPLAY_DEVICE_ACTIVE) ++probe.attached_monitors;
|
||||
}
|
||||
}
|
||||
return probe;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_DISPLAY_PRESENCE_H_
|
||||
@@ -122,15 +122,25 @@ int ScreenCapturerGdi::ResetToInitialMonitor() {
|
||||
void ScreenCapturerGdi::CaptureLoop() {
|
||||
int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
|
||||
HDC screen_dc = GetDC(nullptr);
|
||||
bool dc_error_logged = false;
|
||||
while (running_) {
|
||||
if (paused_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
if (!screen_dc) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
// The screen DC can be unavailable during a desktop switch; retry so
|
||||
// capture resumes once the desktop is reachable again.
|
||||
if (!dc_error_logged) {
|
||||
LOG_WARN("GDI: GetDC(nullptr) failed, error={}; retrying",
|
||||
GetLastError());
|
||||
dc_error_logged = true;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
|
||||
screen_dc = GetDC(nullptr);
|
||||
continue;
|
||||
}
|
||||
dc_error_logged = false;
|
||||
|
||||
int idx = monitor_index_.load();
|
||||
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
|
||||
@@ -157,11 +167,22 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
// GDI is the only backend on a headless desktop, where odd DIB sizes and
|
||||
// exhausted GDI handles are plausible; never dereference a failed alloc.
|
||||
void* bits = nullptr;
|
||||
HDC mem_dc = CreateCompatibleDC(screen_dc);
|
||||
HBITMAP dib =
|
||||
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
HGDIOBJ old = SelectObject(mem_dc, dib);
|
||||
HBITMAP dib = mem_dc ? CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS,
|
||||
&bits, nullptr, 0)
|
||||
: nullptr;
|
||||
HGDIOBJ old = (dib && bits) ? SelectObject(mem_dc, dib) : nullptr;
|
||||
if (!old) {
|
||||
LOG_WARN("GDI: frame buffer allocation failed ({}x{}), error={}", width,
|
||||
height, GetLastError());
|
||||
if (dib) DeleteObject(dib);
|
||||
if (mem_dc) DeleteDC(mem_dc);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
|
||||
continue;
|
||||
}
|
||||
|
||||
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
|
||||
SRCCOPY | CAPTUREBLT);
|
||||
@@ -202,7 +223,7 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
|
||||
}
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
if (screen_dc) ReleaseDC(nullptr, screen_dc);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <display_stream_id.h>
|
||||
#include "captured_nv12_frame.h"
|
||||
#include "captured_cursor_state.h"
|
||||
#include "display_presence.h"
|
||||
#include "dxgi_cursor_state.h"
|
||||
#include "interactive_desktop.h"
|
||||
#include "interactive_state.h"
|
||||
@@ -30,6 +31,7 @@
|
||||
#include "secure_desktop_status_poller.h"
|
||||
#include "service_host.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "../virtual_display/usbmmidd_virtual_display.h"
|
||||
#include "wgc_plugin_api.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -180,6 +182,63 @@ const char* CaptureBackendName(const ScreenCapturer* capturer) {
|
||||
return "unavailable";
|
||||
}
|
||||
|
||||
// Debug aid: CROSSDESK_FORCE_HEADLESS=1 makes a host that has monitors behave
|
||||
// as if it had none until a usbmmidd monitor is plugged, so the virtual
|
||||
// display path can be exercised without unplugging cables. Once the virtual
|
||||
// monitor is attached the real probe takes over again.
|
||||
bool ForcedHeadlessForTesting() {
|
||||
static const bool forced = [] {
|
||||
wchar_t value[8] = {0};
|
||||
const DWORD length =
|
||||
GetEnvironmentVariableW(L"CROSSDESK_FORCE_HEADLESS", value, 8);
|
||||
return length > 0 && length < 8 && value[0] == L'1';
|
||||
}();
|
||||
return forced && CountUsbmmiddMonitors() == 0;
|
||||
}
|
||||
|
||||
// Headless compatibility mode: no monitor is attached, so the desktop lives on
|
||||
// a phantom target at an OS-chosen resolution. DXGI cannot duplicate it and
|
||||
// WGC has no real monitor item; GDI reads the composed desktop. This is a
|
||||
// best-effort baseline that needs no driver. Only an explicit Auto selection
|
||||
// takes this shortcut; a pinned method keeps its own failure semantics.
|
||||
bool DetectHeadlessDesktop(ScreenCaptureMethod method) {
|
||||
if (method != ScreenCaptureMethod::Auto) return false;
|
||||
if (ForcedHeadlessForTesting()) {
|
||||
static std::atomic<bool> logged{false};
|
||||
if (!logged.exchange(true)) {
|
||||
LOG_WARN("Windows capturer: CROSSDESK_FORCE_HEADLESS=1 set; treating the "
|
||||
"desktop as headless until a usbmmidd monitor is plugged");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const auto probe = ProbeDisplayPresence();
|
||||
const auto presence = ClassifyDisplayPresence(probe);
|
||||
// Sessions probe repeatedly; report transitions rather than every probe.
|
||||
static std::atomic<int> last_logged{-1};
|
||||
if (last_logged.exchange(static_cast<int>(presence)) !=
|
||||
static_cast<int>(presence)) {
|
||||
if (presence == DisplayPresence::headless) {
|
||||
LOG_WARN(
|
||||
"Windows capturer: no attached display (active_paths={}, "
|
||||
"available_targets={}, desktop_devices={}, monitors={}); entering "
|
||||
"headless compatibility mode. Resolution is fixed by Windows and "
|
||||
"capture uses GDI. Attach a monitor or an HDMI dummy plug to "
|
||||
"restore GPU capture.",
|
||||
probe.active_paths, probe.available_targets, probe.desktop_devices,
|
||||
probe.attached_monitors);
|
||||
} else if (presence == DisplayPresence::unknown) {
|
||||
LOG_WARN("Windows capturer: display presence unknown (query_ok={}, "
|
||||
"desktop_devices={}); using the normal backend order",
|
||||
probe.query_succeeded, probe.desktop_devices);
|
||||
} else {
|
||||
LOG_INFO("Windows capturer: attached display detected "
|
||||
"(available_targets={}, monitors={})",
|
||||
probe.available_targets, probe.attached_monitors);
|
||||
}
|
||||
}
|
||||
return presence == DisplayPresence::headless;
|
||||
}
|
||||
|
||||
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
|
||||
bool show_cursor,
|
||||
const std::string& stage,
|
||||
@@ -502,25 +561,22 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
};
|
||||
|
||||
impl_.reset();
|
||||
const bool headless = DetectHeadlessDesktop(capture_method_);
|
||||
headless_compat_.store(headless, std::memory_order_relaxed);
|
||||
int ret = -1;
|
||||
auto try_init = [&](std::unique_ptr<ScreenCapturer> candidate) {
|
||||
for (const auto& create : PreferredBackends(headless)) {
|
||||
std::unique_ptr<ScreenCapturer> candidate = create();
|
||||
ret = candidate ? candidate->Init(fps_, cb_) : -1;
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Windows capturer: {} init failed (ret={})",
|
||||
CaptureBackendName(candidate.get()), ret);
|
||||
return false;
|
||||
if (ret == 0) {
|
||||
impl_ = std::move(candidate);
|
||||
break;
|
||||
}
|
||||
impl_ = std::move(candidate);
|
||||
return true;
|
||||
};
|
||||
// Short-circuiting also keeps the unused WGC plugin unloaded.
|
||||
if ((IsBackendEnabled(ScreenCaptureMethod::Dxgi) &&
|
||||
try_init(std::make_unique<ScreenCapturerDxgi>())) ||
|
||||
(IsBackendEnabled(ScreenCaptureMethod::Wgc) &&
|
||||
try_init(WgcPluginCapturer::Create())) ||
|
||||
(IsBackendEnabled(ScreenCaptureMethod::Gdi) &&
|
||||
try_init(std::make_unique<ScreenCapturerGdi>()))) {
|
||||
LOG_INFO("Windows capturer: using {}", CaptureBackendName(impl_.get()));
|
||||
LOG_WARN("Windows capturer: {} init failed (ret={})",
|
||||
CaptureBackendName(candidate.get()), ret);
|
||||
}
|
||||
if (impl_) {
|
||||
LOG_INFO("Windows capturer: using {}{}", CaptureBackendName(impl_.get()),
|
||||
headless ? " (headless compatibility mode)" : "");
|
||||
BuildCanonicalFromImpl();
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
return 0;
|
||||
@@ -533,6 +589,9 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
|
||||
int ScreenCapturerWin::Destroy() {
|
||||
Stop();
|
||||
// Stop() releases the virtual display on a worker; finish it here so the
|
||||
// provisioner is idle before its owner goes away.
|
||||
JoinVirtualDisplayThread();
|
||||
paused_.store(false, std::memory_order_relaxed);
|
||||
if (impl_) {
|
||||
impl_->Destroy();
|
||||
@@ -651,6 +710,51 @@ bool ScreenCapturerWin::TryStartBackend(
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<ScreenCapturerWin::BackendFactory>
|
||||
ScreenCapturerWin::PreferredBackends(bool headless) const {
|
||||
// A headless desktop goes straight to GDI: DXGI has no output to duplicate
|
||||
// and WGC has no monitor item.
|
||||
std::vector<BackendFactory> factories;
|
||||
if (!headless && IsBackendEnabled(ScreenCaptureMethod::Dxgi)) {
|
||||
factories.push_back([] { return std::make_unique<ScreenCapturerDxgi>(); });
|
||||
}
|
||||
if (!headless && IsBackendEnabled(ScreenCaptureMethod::Wgc)) {
|
||||
factories.push_back([] { return WgcPluginCapturer::Create(); });
|
||||
}
|
||||
if (IsBackendEnabled(ScreenCaptureMethod::Gdi)) {
|
||||
factories.push_back([] { return std::make_unique<ScreenCapturerGdi>(); });
|
||||
}
|
||||
return factories;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::StartPreferredBackend(bool headless,
|
||||
int monitor_index) {
|
||||
for (const auto& create : PreferredBackends(headless)) {
|
||||
if (TryStartBackend(create(), monitor_index)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::ResetPostSecureDesktopState() {
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::MarkStarted() {
|
||||
running_.store(true, std::memory_order_relaxed);
|
||||
NotifyPrivacyCapture(true);
|
||||
ResetPostSecureDesktopState();
|
||||
if (!secure_capture_thread_.joinable()) {
|
||||
secure_capture_thread_ =
|
||||
std::thread([this]() { SecureDesktopCaptureLoop(); });
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
if (!impl_) return -1;
|
||||
if (running_.load(std::memory_order_relaxed)) {
|
||||
@@ -670,8 +774,40 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
// open; logical stream IDs must remain stable.
|
||||
const int requested_monitor =
|
||||
monitor_index_.load(std::memory_order_relaxed);
|
||||
// A monitor may have been attached or removed since the last session. Pick
|
||||
// the backend for the desktop as it is now, not as it was at Init.
|
||||
const bool headless = DetectHeadlessDesktop(capture_method_);
|
||||
// The virtual display is provisioned off this thread (Start() runs on the
|
||||
// UI thread and a driver install can take minutes). Begin in compatibility
|
||||
// mode; MaybeAdoptVirtualDisplay() switches to a GPU backend once ready.
|
||||
JoinVirtualDisplayThread();
|
||||
virtual_display_ready_.store(false, std::memory_order_relaxed);
|
||||
const bool was_headless =
|
||||
headless_compat_.exchange(headless, std::memory_order_relaxed);
|
||||
const bool backend_is_gdi = dynamic_cast<ScreenCapturerGdi*>(impl_.get()) != nullptr;
|
||||
int ret = 0;
|
||||
if (headless != was_headless || (headless && !backend_is_gdi)) {
|
||||
LOG_INFO("Windows capturer: desktop presence changed (headless={}), "
|
||||
"reselecting the capture backend", headless);
|
||||
impl_->Destroy();
|
||||
// Keep an instance around on failure so the next Start can retry.
|
||||
std::unique_ptr<ScreenCapturer> previous;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
previous = std::move(impl_);
|
||||
}
|
||||
if (!StartPreferredBackend(headless, requested_monitor)) {
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
impl_ = std::move(previous);
|
||||
LOG_ERROR("Windows capturer: no capture backend could start");
|
||||
return -1;
|
||||
}
|
||||
ret = MarkStarted();
|
||||
if (ret == 0 && headless) BeginVirtualDisplayProvisioning();
|
||||
return ret;
|
||||
}
|
||||
impl_->Destroy();
|
||||
int ret = impl_->Init(fps_, cb_);
|
||||
ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
RebuildAliasesFromImpl();
|
||||
// Canonical wire/input slots may differ from this backend's refreshed
|
||||
@@ -706,42 +842,100 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
}
|
||||
}
|
||||
|
||||
running_.store(true, std::memory_order_relaxed);
|
||||
NotifyPrivacyCapture(true);
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
if (!secure_capture_thread_.joinable()) {
|
||||
secure_capture_thread_ =
|
||||
std::thread([this]() { SecureDesktopCaptureLoop(); });
|
||||
}
|
||||
return 0;
|
||||
ret = MarkStarted();
|
||||
if (ret == 0 && headless) BeginVirtualDisplayProvisioning();
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Stop() {
|
||||
NotifyPrivacyCapture(false);
|
||||
running_.store(false, std::memory_order_relaxed);
|
||||
last_capture_progress_tick_.store(0, std::memory_order_relaxed);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
int ret = 0;
|
||||
// Drain management operations before stopping/replacing their backend.
|
||||
virtual_display_.RequestCancel();
|
||||
StopSecureCaptureThread();
|
||||
JoinVirtualDisplayThread();
|
||||
ResetPostSecureDesktopState();
|
||||
if (impl_) {
|
||||
ret = impl_->Stop();
|
||||
}
|
||||
SharedCapturedCursorState().Reset();
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
// After the backend released its duplication of the virtual output. The
|
||||
// unplug itself can take seconds; do not hold the UI thread for it.
|
||||
BeginVirtualDisplayRelease();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::JoinVirtualDisplayThread() {
|
||||
if (virtual_display_thread_.joinable()) virtual_display_thread_.join();
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::BeginVirtualDisplayProvisioning() {
|
||||
JoinVirtualDisplayThread();
|
||||
virtual_display_thread_ = std::thread([this] {
|
||||
// A usbmmidd virtual monitor turns the phantom desktop into a real
|
||||
// display target, so the GPU backends and a chosen resolution apply.
|
||||
std::string error;
|
||||
if (virtual_display_.Acquire(kDefaultVirtualDisplayMode, &error)) {
|
||||
virtual_display_ready_.store(true, std::memory_order_release);
|
||||
} else if (error != "cancelled") {
|
||||
LOG_WARN("Windows capturer: no usbmmidd virtual display ({}); staying "
|
||||
"in headless compatibility mode",
|
||||
error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::BeginVirtualDisplayRelease() {
|
||||
JoinVirtualDisplayThread();
|
||||
virtual_display_ready_.store(false, std::memory_order_relaxed);
|
||||
if (!virtual_display_.NeedsRelease()) return;
|
||||
virtual_display_thread_ = std::thread([this] { virtual_display_.Release(); });
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::MaybeAdoptVirtualDisplay() {
|
||||
if (!virtual_display_ready_.exchange(false, std::memory_order_acq_rel))
|
||||
return;
|
||||
if (!running_.load(std::memory_order_relaxed)) return;
|
||||
if (DetectHeadlessDesktop(capture_method_)) {
|
||||
LOG_WARN("Windows capturer: virtual display plugged but the desktop "
|
||||
"still reports no display; staying in compatibility mode");
|
||||
return;
|
||||
}
|
||||
const int current_monitor = monitor_index_.load(std::memory_order_relaxed);
|
||||
LOG_INFO("Windows capturer: virtual display ready, switching from "
|
||||
"compatibility mode to a GPU backend");
|
||||
NotifyPrivacyCapture(false);
|
||||
// Public entry points dereference impl_ under impl_mutex_; take the old
|
||||
// backend out under the lock, then tear it down without holding it.
|
||||
std::unique_ptr<ScreenCapturer> previous;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
previous = std::move(impl_);
|
||||
}
|
||||
if (previous) previous->Destroy();
|
||||
if (StartPreferredBackend(false, current_monitor)) {
|
||||
headless_compat_.store(false, std::memory_order_relaxed);
|
||||
// The owner refreshes its display list and input mapping every tick from
|
||||
// GetDisplayInfoList(), so the new geometry is picked up without a hint.
|
||||
} else {
|
||||
LOG_ERROR("Windows capturer: no GPU backend started on the virtual "
|
||||
"display; restoring compatibility capture");
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
impl_ = std::move(previous);
|
||||
}
|
||||
// RestoreMonitor takes impl_mutex_ itself (via SwitchTo).
|
||||
if (impl_ && impl_->Init(fps_, cb_) == 0) {
|
||||
RestoreMonitor(current_monitor);
|
||||
impl_->Start(show_cursor_.load(std::memory_order_relaxed));
|
||||
}
|
||||
}
|
||||
NotifyPrivacyCapture(true);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Pause(int monitor_index) {
|
||||
paused_.store(true, std::memory_order_relaxed);
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
@@ -949,21 +1143,15 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((IsBackendEnabled(ScreenCaptureMethod::Dxgi) &&
|
||||
TryStartBackend(std::make_unique<ScreenCapturerDxgi>(), current_monitor)) ||
|
||||
(IsBackendEnabled(ScreenCaptureMethod::Wgc) &&
|
||||
TryStartBackend(WgcPluginCapturer::Create(), current_monitor)) ||
|
||||
(IsBackendEnabled(ScreenCaptureMethod::Gdi) &&
|
||||
TryStartBackend(std::make_unique<ScreenCapturerGdi>(), current_monitor))) {
|
||||
if (StartPreferredBackend(headless_compat_.load(std::memory_order_relaxed),
|
||||
current_monitor)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (impl_) {
|
||||
LOG_WARN(
|
||||
"Windows capturer: all backend restart attempts after secure desktop "
|
||||
"failed (last_ret={})",
|
||||
ret);
|
||||
}
|
||||
LOG_WARN(
|
||||
"Windows capturer: all backend restart attempts after secure desktop "
|
||||
"failed (last_ret={})",
|
||||
ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1405,6 +1593,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
|
||||
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
MaybeAdoptVirtualDisplay();
|
||||
CheckCaptureProgress(now);
|
||||
if (post_secure_restart_pending) {
|
||||
if (now >= post_secure_restart_deadline_tick) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -22,6 +23,8 @@
|
||||
#include "screen_capturer.h"
|
||||
#include "screen_capture_method.h"
|
||||
#include "privacy_controller.h"
|
||||
// Relative: platform include directories stay private to the capture target.
|
||||
#include "../virtual_display/virtual_display_provisioner.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -50,6 +53,8 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
int GetCurrentMonitorIndex() const override;
|
||||
// True while this session captures a usbmmidd monitor it plugged itself.
|
||||
bool IsVirtualDisplayActive() const { return virtual_display_.active(); }
|
||||
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
|
||||
// Set before Init(), while capture is stopped.
|
||||
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
|
||||
@@ -58,6 +63,7 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
static bool CurrentFrameIsFromSecureDesktop();
|
||||
|
||||
private:
|
||||
using BackendFactory = std::function<std::unique_ptr<ScreenCapturer>()>;
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
// Serializes backend replacement (secure-desktop recovery runs on the
|
||||
// management thread) against the public entry points that dereference it.
|
||||
@@ -87,6 +93,16 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
bool applied_show_cursor_ = true;
|
||||
std::atomic<bool> headless_compat_{false};
|
||||
// Session-scoped: plugged on a headless desktop after Start() returns and
|
||||
// released after Stop(), so an attached physical monitor is never shadowed
|
||||
// between sessions. Both run on virtual_display_thread_ because the helper
|
||||
// may need up to two minutes to install the driver; Start() itself begins
|
||||
// in GDI compatibility mode and the management thread adopts the virtual
|
||||
// display (switching to a GPU backend) once virtual_display_ready_ is set.
|
||||
VirtualDisplayProvisioner virtual_display_;
|
||||
std::thread virtual_display_thread_;
|
||||
std::atomic<bool> virtual_display_ready_{false};
|
||||
std::atomic<int> monitor_index_{0};
|
||||
std::atomic<bool> secure_desktop_capture_active_{false};
|
||||
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
|
||||
@@ -112,8 +128,16 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
void BuildCanonicalFromImpl();
|
||||
void RebuildAliasesFromImpl(bool preserve_backend_slots = false);
|
||||
void RestoreMonitor(int monitor_index);
|
||||
// Backends in preference order for the current desktop; each factory is
|
||||
// invoked lazily so an unused WGC plugin is never loaded.
|
||||
std::vector<BackendFactory> PreferredBackends(bool headless) const;
|
||||
bool TryStartBackend(std::unique_ptr<ScreenCapturer> candidate,
|
||||
int monitor_index);
|
||||
bool StartPreferredBackend(bool headless, int monitor_index);
|
||||
void ResetPostSecureDesktopState();
|
||||
// Common tail of every successful Start(): publish running state and
|
||||
// launch the management thread.
|
||||
int MarkStarted();
|
||||
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame = nullptr,
|
||||
@@ -121,6 +145,13 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
void StopSecureCaptureThread();
|
||||
bool RestartCaptureBackendAfterSecureDesktop();
|
||||
void CheckCaptureProgress(ULONGLONG now);
|
||||
// Virtual display provisioning runs off the caller's thread.
|
||||
void JoinVirtualDisplayThread();
|
||||
void BeginVirtualDisplayProvisioning();
|
||||
void BeginVirtualDisplayRelease();
|
||||
// Management thread: once the worker plugged the virtual display, replace
|
||||
// the GDI compatibility backend with a GPU backend on that display.
|
||||
void MaybeAdoptVirtualDisplay();
|
||||
void ApplyCursorCaptureSetting();
|
||||
void SecureDesktopCaptureLoop();
|
||||
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -25,6 +26,8 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "usbmmidd_policy.h"
|
||||
#include "usbmmidd_virtual_display.h"
|
||||
#include "windows_cursor_state.h"
|
||||
#include "cursor_draw.h"
|
||||
#include "windows_input_marker.h"
|
||||
@@ -453,8 +456,193 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
|
||||
return json.dump();
|
||||
}
|
||||
|
||||
// Driver install and monitor plug/unplug take seconds. They run on a worker
|
||||
// so the status pipe (polled by the service every second) stays responsive;
|
||||
// the GUI polls the status command until |busy| clears.
|
||||
struct VirtualDisplayJobState {
|
||||
std::mutex mutex;
|
||||
std::thread worker;
|
||||
bool busy = false;
|
||||
// Monitors this helper plugged and has not yet unplugged. Unplug releases
|
||||
// only these, so a monitor the user plugged for other software survives.
|
||||
int plugged_by_helper = 0;
|
||||
// An unplug that arrived while a plug was running; the plug worker honours
|
||||
// it on completion so a cancelled session never leaves a monitor behind.
|
||||
bool unplug_queued = false;
|
||||
std::string last_action;
|
||||
std::string last_error;
|
||||
|
||||
~VirtualDisplayJobState() {
|
||||
if (worker.joinable()) worker.join();
|
||||
}
|
||||
};
|
||||
|
||||
// The helper pipe grants read/write to every authenticated user so that the
|
||||
// GUI can poll status without elevation. Plug/unplug install a driver and
|
||||
// change the interactive desktop's topology as SYSTEM, so those commands are
|
||||
// accepted only from a CrossDesk executable in this helper's own directory.
|
||||
bool IsTrustedVirtualDisplayClient(HANDLE pipe) {
|
||||
ULONG client_pid = 0;
|
||||
if (!GetNamedPipeClientProcessId(pipe, &client_pid)) return false;
|
||||
HANDLE process =
|
||||
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, client_pid);
|
||||
if (process == nullptr) return false;
|
||||
wchar_t image[MAX_PATH] = {0};
|
||||
DWORD length = MAX_PATH;
|
||||
const bool queried =
|
||||
QueryFullProcessImageNameW(process, 0, image, &length) && length > 0;
|
||||
CloseHandle(process);
|
||||
if (!queried) return false;
|
||||
|
||||
wchar_t own[MAX_PATH] = {0};
|
||||
const DWORD own_length = GetModuleFileNameW(nullptr, own, MAX_PATH);
|
||||
if (own_length == 0 || own_length >= MAX_PATH) return false;
|
||||
std::error_code ignored;
|
||||
const std::filesystem::path client(image);
|
||||
if (_wcsicmp(client.filename().c_str(), L"CrossDesk.exe") != 0) return false;
|
||||
const std::filesystem::path client_directory =
|
||||
std::filesystem::weakly_canonical(client.parent_path(), ignored);
|
||||
const std::filesystem::path own_directory = std::filesystem::weakly_canonical(
|
||||
std::filesystem::path(own).parent_path(), ignored);
|
||||
return _wcsicmp(client_directory.c_str(), own_directory.c_str()) == 0;
|
||||
}
|
||||
|
||||
std::string BuildVirtualDisplayStatusResponse(VirtualDisplayJobState* job) {
|
||||
const crossdesk::UsbmmiddStatus status = crossdesk::QueryUsbmmiddStatus();
|
||||
Json json;
|
||||
json["ok"] = true;
|
||||
json["package_available"] = status.package_available;
|
||||
json["package_directory"] = status.package_directory;
|
||||
json["driver_installed"] = status.driver_installed;
|
||||
json["plugged_monitors"] = status.plugged_monitors;
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
json["busy"] = job->busy;
|
||||
json["last_action"] = job->last_action;
|
||||
json["last_error"] = job->last_error;
|
||||
return json.dump();
|
||||
}
|
||||
|
||||
std::string StartVirtualDisplayJob(
|
||||
VirtualDisplayJobState* job, const char* action,
|
||||
std::function<bool(std::string*)> work) {
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
if (job->busy) return BuildErrorJson("virtual_display_busy");
|
||||
// A finished worker no longer touches the mutex; joining under it is safe.
|
||||
if (job->worker.joinable()) job->worker.join();
|
||||
job->busy = true;
|
||||
job->last_action = action;
|
||||
job->last_error.clear();
|
||||
LOG_INFO("Session helper virtual display job started: {}", action);
|
||||
job->worker = std::thread([job, action, work = std::move(work)]() {
|
||||
std::string error;
|
||||
const bool ok = work(&error);
|
||||
if (ok) {
|
||||
LOG_INFO("Session helper virtual display job finished: {}", action);
|
||||
} else {
|
||||
LOG_WARN("Session helper virtual display job failed: {}, error={}",
|
||||
action, error);
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
job->busy = false;
|
||||
job->last_error = ok ? std::string() : (error.empty() ? "unknown" : error);
|
||||
});
|
||||
Json json;
|
||||
json["ok"] = true;
|
||||
json["pending"] = true;
|
||||
return json.dump();
|
||||
}
|
||||
|
||||
std::string HandleVirtualDisplayCommand(const std::string& command,
|
||||
HANDLE pipe,
|
||||
VirtualDisplayJobState* job,
|
||||
bool* handled) {
|
||||
*handled = true;
|
||||
if (command == crossdesk::kCrossDeskVirtualDisplayStatusCommand) {
|
||||
return BuildVirtualDisplayStatusResponse(job);
|
||||
}
|
||||
const bool mutating =
|
||||
command == crossdesk::kCrossDeskVirtualDisplayUnplugCommand ||
|
||||
command.rfind(crossdesk::kCrossDeskVirtualDisplayPlugCommandPrefix, 0) ==
|
||||
0;
|
||||
if (mutating && !IsTrustedVirtualDisplayClient(pipe)) {
|
||||
LOG_WARN("Session helper rejected virtual display command '{}' from an "
|
||||
"untrusted client",
|
||||
command);
|
||||
return BuildErrorJson("virtual_display_client_not_allowed",
|
||||
ERROR_ACCESS_DENIED);
|
||||
}
|
||||
if (command == crossdesk::kCrossDeskVirtualDisplayUnplugCommand) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
if (job->busy && job->last_action == "plug") {
|
||||
job->unplug_queued = true;
|
||||
Json json;
|
||||
json["ok"] = true;
|
||||
json["pending"] = true;
|
||||
json["queued"] = true;
|
||||
return json.dump();
|
||||
}
|
||||
}
|
||||
return StartVirtualDisplayJob(job, "unplug", [job](std::string* error) {
|
||||
int count = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
count = job->plugged_by_helper;
|
||||
}
|
||||
if (!crossdesk::ReleaseUsbmmiddDisplays(count, error)) return false;
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
job->plugged_by_helper = 0;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
crossdesk::VirtualDisplayMode mode;
|
||||
if (crossdesk::ParseVirtualDisplayPlugCommand(command, &mode)) {
|
||||
return StartVirtualDisplayJob(job, "plug", [job, mode](std::string* error) {
|
||||
{
|
||||
// This job only starts when no other job runs, so any unplug queued
|
||||
// behind an earlier plug has already been honoured.
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
job->unplug_queued = false;
|
||||
}
|
||||
int plugged = 0;
|
||||
const bool ok = crossdesk::ProvisionUsbmmiddDisplay(mode, &plugged, error);
|
||||
int to_release = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
job->plugged_by_helper += plugged;
|
||||
if (job->unplug_queued) {
|
||||
job->unplug_queued = false;
|
||||
to_release = job->plugged_by_helper;
|
||||
}
|
||||
}
|
||||
if (to_release > 0) {
|
||||
LOG_INFO("Session helper virtual display: releasing {} monitor(s) "
|
||||
"for an unplug queued during the plug",
|
||||
to_release);
|
||||
std::string release_error;
|
||||
if (crossdesk::ReleaseUsbmmiddDisplays(to_release, &release_error)) {
|
||||
std::lock_guard<std::mutex> lock(job->mutex);
|
||||
job->plugged_by_helper = 0;
|
||||
} else {
|
||||
LOG_WARN("Session helper virtual display: queued unplug failed, "
|
||||
"error={}",
|
||||
release_error);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
});
|
||||
}
|
||||
if (command.rfind(crossdesk::kCrossDeskVirtualDisplayPlugCommandPrefix, 0) ==
|
||||
0) {
|
||||
return BuildErrorJson("invalid_virtual_display_mode");
|
||||
}
|
||||
*handled = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
|
||||
HelperState* helper_state) {
|
||||
HelperState* helper_state,
|
||||
VirtualDisplayJobState* virtual_display_job) {
|
||||
IpcSecurityAttributes security_attributes;
|
||||
SECURITY_ATTRIBUTES* pipe_attributes = nullptr;
|
||||
if (security_attributes.Initialize()) {
|
||||
@@ -523,11 +711,14 @@ void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
|
||||
bytes_read > 0) {
|
||||
std::string command(buffer, buffer + bytes_read);
|
||||
std::string response;
|
||||
bool handled = false;
|
||||
if (command == crossdesk::kCrossDeskSessionHelperStatusCommand) {
|
||||
response = BuildHelperStatusResponse(helper_state);
|
||||
} else if (command == "ping") {
|
||||
response = "{\"ok\":true,\"reply\":\"pong\"}";
|
||||
} else {
|
||||
} else if (response = HandleVirtualDisplayCommand(
|
||||
command, pipe, virtual_display_job, &handled);
|
||||
!handled) {
|
||||
response = BuildErrorJson("unknown_command");
|
||||
}
|
||||
|
||||
@@ -1865,8 +2056,9 @@ int main(int argc, char* argv[]) {
|
||||
GetLastError());
|
||||
}
|
||||
|
||||
VirtualDisplayJobState virtual_display_job;
|
||||
std::thread ipc_thread(HelperIpcServerLoop, stop_event, current_session_id,
|
||||
&helper_state);
|
||||
&helper_state, &virtual_display_job);
|
||||
|
||||
std::string last_desktop_name;
|
||||
bool last_lock_app = false;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_USBMMIDD_POLICY_H_
|
||||
#define CROSSDESK_USBMMIDD_POLICY_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cwctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Amyuni "USB Mobile Monitor" indirect display driver (usbmmidd_v2). The
|
||||
// package ships deviceinstaller64.exe, usbmmIdd.inf/.cat and usbmmIdd.dll. It
|
||||
// is bundled from apps/desktop/resources/windows/usbmmidd_v2 by the installer
|
||||
// and the portable archive; a user may also drop it next to the executables.
|
||||
inline constexpr wchar_t kUsbmmiddHardwareId[] = L"usbmmidd";
|
||||
// Set when CrossDesk itself installed the driver. The NSIS uninstaller reads
|
||||
// it (64-bit registry view) and removes the driver only in that case.
|
||||
inline constexpr wchar_t kUsbmmiddInstallMarkerKey[] = L"SOFTWARE\\CrossDesk";
|
||||
inline constexpr wchar_t kUsbmmiddInstallMarkerValue[] =
|
||||
L"UsbmmiddDriverInstalled";
|
||||
inline constexpr wchar_t kUsbmmiddAdapterDescription[] =
|
||||
L"USB Mobile Monitor Virtual Display";
|
||||
#if defined(_WIN64)
|
||||
inline constexpr wchar_t kUsbmmiddInstallerName[] = L"deviceinstaller64.exe";
|
||||
#else
|
||||
inline constexpr wchar_t kUsbmmiddInstallerName[] = L"deviceinstaller.exe";
|
||||
#endif
|
||||
inline constexpr wchar_t kUsbmmiddInfName[] = L"usbmmidd.inf";
|
||||
inline constexpr const wchar_t* kUsbmmiddPackageDirectories[] = {
|
||||
L"usbmmidd_v2", L"usbmmidd", L"drivers\\usbmmidd_v2"};
|
||||
// The driver accepts up to four monitors; remote capture needs one.
|
||||
inline constexpr int kUsbmmiddMaxMonitors = 4;
|
||||
|
||||
// Session helper IPC sub-protocol (see session_helper_shared.h for the pipe).
|
||||
// Plug/unplug run asynchronously in the helper; poll the status command.
|
||||
inline constexpr char kCrossDeskVirtualDisplayStatusCommand[] =
|
||||
"virtual-display-status";
|
||||
inline constexpr char kCrossDeskVirtualDisplayPlugCommandPrefix[] =
|
||||
"virtual-display-plug:";
|
||||
inline constexpr char kCrossDeskVirtualDisplayUnplugCommand[] =
|
||||
"virtual-display-unplug";
|
||||
|
||||
struct VirtualDisplayMode {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool operator==(const VirtualDisplayMode& other) const {
|
||||
return width == other.width && height == other.height;
|
||||
}
|
||||
bool operator!=(const VirtualDisplayMode& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
inline constexpr VirtualDisplayMode kDefaultVirtualDisplayMode{1920, 1080};
|
||||
|
||||
inline bool ContainsCaseInsensitive(const std::wstring& haystack,
|
||||
const std::wstring& needle) {
|
||||
if (needle.empty()) return true;
|
||||
if (haystack.size() < needle.size()) return false;
|
||||
const auto it = std::search(
|
||||
haystack.begin(), haystack.end(), needle.begin(), needle.end(),
|
||||
[](wchar_t a, wchar_t b) { return std::towlower(a) == std::towlower(b); });
|
||||
return it != haystack.end();
|
||||
}
|
||||
|
||||
// EnumDisplayDevices reports the adapter description plus a PnP id such as
|
||||
// "PCI\VEN_..." or "ROOT\DISPLAY\0000". The IDD adapter carries the hardware
|
||||
// id in its device id; the description is a second signal for older packages.
|
||||
inline bool IsUsbmmiddAdapter(const std::wstring& device_string,
|
||||
const std::wstring& device_id) {
|
||||
return ContainsCaseInsensitive(device_id, kUsbmmiddHardwareId) ||
|
||||
ContainsCaseInsensitive(device_string, kUsbmmiddAdapterDescription);
|
||||
}
|
||||
|
||||
// Exact match first; otherwise the largest mode that fits inside the request,
|
||||
// so a 4K request on a 1080p-only package still yields the best available
|
||||
// picture; otherwise the smallest mode the driver offers.
|
||||
inline bool ChooseVirtualDisplayMode(
|
||||
const std::vector<VirtualDisplayMode>& supported,
|
||||
VirtualDisplayMode requested, VirtualDisplayMode* chosen) {
|
||||
if (chosen == nullptr || supported.empty()) return false;
|
||||
const VirtualDisplayMode* best = nullptr;
|
||||
const VirtualDisplayMode* smallest = nullptr;
|
||||
auto area = [](const VirtualDisplayMode& mode) {
|
||||
return static_cast<long long>(mode.width) * mode.height;
|
||||
};
|
||||
for (const auto& mode : supported) {
|
||||
if (mode.width <= 0 || mode.height <= 0) continue;
|
||||
if (mode == requested) {
|
||||
*chosen = mode;
|
||||
return true;
|
||||
}
|
||||
if (mode.width <= requested.width && mode.height <= requested.height &&
|
||||
(best == nullptr || area(mode) > area(*best))) {
|
||||
best = &mode;
|
||||
}
|
||||
if (smallest == nullptr || area(mode) < area(*smallest)) smallest = &mode;
|
||||
}
|
||||
if (best != nullptr) {
|
||||
*chosen = *best;
|
||||
return true;
|
||||
}
|
||||
if (smallest != nullptr) {
|
||||
*chosen = *smallest;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// "virtual-display-plug:1920x1080"
|
||||
inline std::string BuildVirtualDisplayPlugCommand(VirtualDisplayMode mode) {
|
||||
return std::string(kCrossDeskVirtualDisplayPlugCommandPrefix) +
|
||||
std::to_string(mode.width) + "x" + std::to_string(mode.height);
|
||||
}
|
||||
|
||||
inline bool ParseVirtualDisplayPlugCommand(const std::string& command,
|
||||
VirtualDisplayMode* mode) {
|
||||
if (mode == nullptr) return false;
|
||||
const std::string prefix(kCrossDeskVirtualDisplayPlugCommandPrefix);
|
||||
if (command.rfind(prefix, 0) != 0) return false;
|
||||
const std::string payload = command.substr(prefix.size());
|
||||
const size_t separator = payload.find('x');
|
||||
if (separator == std::string::npos || separator == 0 ||
|
||||
separator + 1 >= payload.size()) {
|
||||
return false;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const long width = std::strtol(payload.c_str(), &end, 10);
|
||||
if (end != payload.c_str() + separator) return false;
|
||||
const long height = std::strtol(payload.c_str() + separator + 1, &end, 10);
|
||||
if (end == nullptr || *end != '\0') return false;
|
||||
if (width <= 0 || height <= 0 || width > 16384 || height > 16384) return false;
|
||||
mode->width = static_cast<int>(width);
|
||||
mode->height = static_cast<int>(height);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_USBMMIDD_POLICY_H_
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "usbmmidd_virtual_display.h"
|
||||
|
||||
// clang-format off
|
||||
#include <Windows.h>
|
||||
#include <initguid.h> // defines GUID_DEVCLASS_DISPLAY in this unit
|
||||
#include <SetupAPI.h>
|
||||
#include <devguid.h>
|
||||
// clang-format on
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr DWORD kInstallerPollIntervalMs = 250;
|
||||
constexpr DWORD kDriverInstallTimeoutMs = 90 * 1000;
|
||||
constexpr DWORD kInstallerCommandTimeoutMs = 20 * 1000;
|
||||
|
||||
std::string Narrow(const std::wstring& value) {
|
||||
if (value.empty()) return {};
|
||||
const int size = WideCharToMultiByte(CP_UTF8, 0, value.data(),
|
||||
static_cast<int>(value.size()), nullptr,
|
||||
0, nullptr, nullptr);
|
||||
std::string result(static_cast<size_t>((std::max)(size, 0)), '\0');
|
||||
if (size > 0) {
|
||||
WideCharToMultiByte(CP_UTF8, 0, value.data(),
|
||||
static_cast<int>(value.size()), result.data(), size,
|
||||
nullptr, nullptr);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void SetError(std::string* error, const std::string& message) {
|
||||
if (error != nullptr) *error = message;
|
||||
}
|
||||
|
||||
std::filesystem::path CurrentExecutableDirectory() {
|
||||
wchar_t module_path[MAX_PATH] = {0};
|
||||
const DWORD length = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
|
||||
if (length == 0 || length >= MAX_PATH) return {};
|
||||
return std::filesystem::path(module_path).parent_path();
|
||||
}
|
||||
|
||||
struct UsbmmiddAdapter {
|
||||
std::wstring device_name; // \\.\DISPLAYn
|
||||
bool attached_to_desktop = false;
|
||||
};
|
||||
|
||||
std::vector<UsbmmiddAdapter> EnumerateUsbmmiddAdapters() {
|
||||
std::vector<UsbmmiddAdapter> adapters;
|
||||
for (DWORD index = 0;; ++index) {
|
||||
DISPLAY_DEVICEW device{sizeof(device)};
|
||||
if (!EnumDisplayDevicesW(nullptr, index, &device, 0)) break;
|
||||
if (!IsUsbmmiddAdapter(device.DeviceString, device.DeviceID)) continue;
|
||||
UsbmmiddAdapter adapter;
|
||||
adapter.device_name = device.DeviceName;
|
||||
adapter.attached_to_desktop =
|
||||
(device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) != 0;
|
||||
adapters.push_back(std::move(adapter));
|
||||
}
|
||||
return adapters;
|
||||
}
|
||||
|
||||
// Runs "<installer> <arguments>" hidden with the package folder as the
|
||||
// working directory (the installer resolves usbmmIdd.dll relative to it).
|
||||
bool RunUsbmmiddInstaller(const UsbmmiddPackage& package,
|
||||
const std::wstring& arguments, DWORD timeout_ms,
|
||||
std::string* error) {
|
||||
if (!package.Available()) {
|
||||
SetError(error, "usbmmidd_package_missing");
|
||||
return false;
|
||||
}
|
||||
std::wstring command_line =
|
||||
L"\"" + package.installer.wstring() + L"\" " + arguments;
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process{};
|
||||
const std::wstring working_directory = package.directory.wstring();
|
||||
if (!CreateProcessW(package.installer.c_str(), command_line.data(), nullptr,
|
||||
nullptr, FALSE, CREATE_NO_WINDOW, nullptr,
|
||||
working_directory.c_str(), &startup, &process)) {
|
||||
const DWORD code = GetLastError();
|
||||
// 740 (ERROR_ELEVATION_REQUIRED) is the non-elevated GUI fallback path.
|
||||
SetError(error, "usbmmidd_installer_launch_failed:" + std::to_string(code));
|
||||
return false;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
const DWORD wait = WaitForSingleObject(process.hProcess, timeout_ms);
|
||||
DWORD exit_code = 0;
|
||||
if (wait != WAIT_OBJECT_0) {
|
||||
TerminateProcess(process.hProcess, 1);
|
||||
CloseHandle(process.hProcess);
|
||||
SetError(error, wait == WAIT_TIMEOUT ? "usbmmidd_installer_timeout"
|
||||
: "usbmmidd_installer_wait_failed");
|
||||
return false;
|
||||
}
|
||||
GetExitCodeProcess(process.hProcess, &exit_code);
|
||||
CloseHandle(process.hProcess);
|
||||
if (exit_code != 0) {
|
||||
SetError(error, "usbmmidd_installer_exit:" + std::to_string(exit_code));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Waits until |predicate| holds or the deadline passes.
|
||||
template <typename Predicate>
|
||||
bool WaitUntil(DWORD timeout_ms, Predicate predicate) {
|
||||
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
|
||||
while (true) {
|
||||
if (predicate()) return true;
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (now >= deadline) return false;
|
||||
Sleep(static_cast<DWORD>(
|
||||
(std::min)(static_cast<ULONGLONG>(kInstallerPollIntervalMs),
|
||||
deadline - now)));
|
||||
}
|
||||
}
|
||||
|
||||
// The uninstaller removes the driver only when CrossDesk installed it, so a
|
||||
// driver the user set up for other software is left alone. The helper runs as
|
||||
// SYSTEM and the in-process path is elevated, so HKLM is writable here; a
|
||||
// failure only costs a stale device node after uninstall.
|
||||
void MarkUsbmmiddDriverInstalledByCrossDesk() {
|
||||
HKEY key = nullptr;
|
||||
const LONG open = RegCreateKeyExW(HKEY_LOCAL_MACHINE, kUsbmmiddInstallMarkerKey,
|
||||
0, nullptr, 0, KEY_SET_VALUE, nullptr, &key,
|
||||
nullptr);
|
||||
if (open != ERROR_SUCCESS) {
|
||||
LOG_WARN("usbmmidd: failed to record the driver install marker, error={}",
|
||||
open);
|
||||
return;
|
||||
}
|
||||
const DWORD value = 1;
|
||||
const LONG set = RegSetValueExW(key, kUsbmmiddInstallMarkerValue, 0, REG_DWORD,
|
||||
reinterpret_cast<const BYTE*>(&value),
|
||||
sizeof(value));
|
||||
RegCloseKey(key);
|
||||
if (set != ERROR_SUCCESS) {
|
||||
LOG_WARN("usbmmidd: failed to record the driver install marker, error={}",
|
||||
set);
|
||||
}
|
||||
}
|
||||
|
||||
bool DeviceHasUsbmmiddHardwareId(HDEVINFO device_info,
|
||||
SP_DEVINFO_DATA* device_data) {
|
||||
DWORD required = 0;
|
||||
SetupDiGetDeviceRegistryPropertyW(device_info, device_data, SPDRP_HARDWAREID,
|
||||
nullptr, nullptr, 0, &required);
|
||||
if (required == 0) return false;
|
||||
std::vector<wchar_t> buffer(required / sizeof(wchar_t) + 2, L'\0');
|
||||
if (!SetupDiGetDeviceRegistryPropertyW(
|
||||
device_info, device_data, SPDRP_HARDWAREID, nullptr,
|
||||
reinterpret_cast<BYTE*>(buffer.data()),
|
||||
static_cast<DWORD>(buffer.size() * sizeof(wchar_t)), nullptr)) {
|
||||
return false;
|
||||
}
|
||||
// REG_MULTI_SZ: hardware ids separated by NUL, terminated by a double NUL.
|
||||
for (const wchar_t* id = buffer.data(); *id != L'\0'; id += wcslen(id) + 1) {
|
||||
if (ContainsCaseInsensitive(id, kUsbmmiddHardwareId)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<VirtualDisplayMode> EnumerateModes(const std::wstring& device) {
|
||||
std::vector<VirtualDisplayMode> modes;
|
||||
for (DWORD index = 0;; ++index) {
|
||||
DEVMODEW mode{};
|
||||
mode.dmSize = sizeof(mode);
|
||||
if (!EnumDisplaySettingsExW(device.c_str(), index, &mode, 0)) break;
|
||||
if (mode.dmBitsPerPel != 32) continue;
|
||||
const VirtualDisplayMode candidate{static_cast<int>(mode.dmPelsWidth),
|
||||
static_cast<int>(mode.dmPelsHeight)};
|
||||
if (std::find(modes.begin(), modes.end(), candidate) == modes.end()) {
|
||||
modes.push_back(candidate);
|
||||
}
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UsbmmiddPackage LocateUsbmmiddPackage() {
|
||||
UsbmmiddPackage package;
|
||||
const std::filesystem::path base = CurrentExecutableDirectory();
|
||||
if (base.empty()) return package;
|
||||
std::error_code ignored;
|
||||
for (const wchar_t* relative : kUsbmmiddPackageDirectories) {
|
||||
const std::filesystem::path directory = base / relative;
|
||||
const std::filesystem::path installer = directory / kUsbmmiddInstallerName;
|
||||
if (!std::filesystem::is_regular_file(installer, ignored)) continue;
|
||||
package.directory = directory;
|
||||
package.installer = installer;
|
||||
const std::filesystem::path inf = directory / kUsbmmiddInfName;
|
||||
if (std::filesystem::is_regular_file(inf, ignored)) package.inf = inf;
|
||||
break;
|
||||
}
|
||||
return package;
|
||||
}
|
||||
|
||||
int CountUsbmmiddMonitors() {
|
||||
int count = 0;
|
||||
for (const auto& adapter : EnumerateUsbmmiddAdapters()) {
|
||||
if (adapter.attached_to_desktop) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool IsUsbmmiddDriverInstalled() {
|
||||
const HDEVINFO device_info = SetupDiGetClassDevsW(
|
||||
&GUID_DEVCLASS_DISPLAY, nullptr, nullptr, DIGCF_PRESENT);
|
||||
if (device_info == INVALID_HANDLE_VALUE) {
|
||||
// Fall back to the adapter list; it misses an installed-but-idle driver.
|
||||
return !EnumerateUsbmmiddAdapters().empty();
|
||||
}
|
||||
bool installed = false;
|
||||
SP_DEVINFO_DATA device_data{};
|
||||
device_data.cbSize = sizeof(device_data);
|
||||
for (DWORD index = 0;
|
||||
!installed && SetupDiEnumDeviceInfo(device_info, index, &device_data);
|
||||
++index) {
|
||||
installed = DeviceHasUsbmmiddHardwareId(device_info, &device_data);
|
||||
}
|
||||
SetupDiDestroyDeviceInfoList(device_info);
|
||||
return installed || !EnumerateUsbmmiddAdapters().empty();
|
||||
}
|
||||
|
||||
UsbmmiddStatus QueryUsbmmiddStatus() {
|
||||
UsbmmiddStatus status;
|
||||
const UsbmmiddPackage package = LocateUsbmmiddPackage();
|
||||
status.package_available = package.Available();
|
||||
status.package_directory = Narrow(package.directory.wstring());
|
||||
status.driver_installed = IsUsbmmiddDriverInstalled();
|
||||
status.plugged_monitors = CountUsbmmiddMonitors();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool InstallUsbmmiddDriver(const UsbmmiddPackage& package, std::string* error) {
|
||||
if (IsUsbmmiddDriverInstalled()) return true;
|
||||
if (package.inf.empty()) {
|
||||
SetError(error, "usbmmidd_inf_missing");
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("usbmmidd: installing driver from '{}'",
|
||||
Narrow(package.directory.wstring()));
|
||||
const std::wstring arguments = L"install \"" + package.inf.wstring() +
|
||||
L"\" " + kUsbmmiddHardwareId;
|
||||
if (!RunUsbmmiddInstaller(package, arguments, kDriverInstallTimeoutMs,
|
||||
error)) {
|
||||
return false;
|
||||
}
|
||||
if (!WaitUntil(5000, IsUsbmmiddDriverInstalled)) {
|
||||
SetError(error, "usbmmidd_driver_not_present_after_install");
|
||||
return false;
|
||||
}
|
||||
MarkUsbmmiddDriverInstalledByCrossDesk();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlugUsbmmiddMonitor(const UsbmmiddPackage& package, DWORD timeout_ms,
|
||||
std::string* error) {
|
||||
const int before = CountUsbmmiddMonitors();
|
||||
if (before >= kUsbmmiddMaxMonitors) {
|
||||
SetError(error, "usbmmidd_monitor_limit");
|
||||
return false;
|
||||
}
|
||||
if (!RunUsbmmiddInstaller(package, L"enableidd 1",
|
||||
kInstallerCommandTimeoutMs, error)) {
|
||||
return false;
|
||||
}
|
||||
if (!WaitUntil(timeout_ms,
|
||||
[before] { return CountUsbmmiddMonitors() > before; })) {
|
||||
SetError(error, "usbmmidd_monitor_not_attached");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UnplugUsbmmiddMonitor(const UsbmmiddPackage& package, DWORD timeout_ms,
|
||||
std::string* error) {
|
||||
const int before = CountUsbmmiddMonitors();
|
||||
if (before == 0) return true;
|
||||
if (!RunUsbmmiddInstaller(package, L"enableidd 0",
|
||||
kInstallerCommandTimeoutMs, error)) {
|
||||
return false;
|
||||
}
|
||||
if (!WaitUntil(timeout_ms,
|
||||
[before] { return CountUsbmmiddMonitors() < before; })) {
|
||||
SetError(error, "usbmmidd_monitor_still_attached");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApplyUsbmmiddDisplayMode(VirtualDisplayMode requested,
|
||||
std::string* error) {
|
||||
std::wstring device;
|
||||
for (const auto& adapter : EnumerateUsbmmiddAdapters()) {
|
||||
if (adapter.attached_to_desktop) {
|
||||
device = adapter.device_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (device.empty()) {
|
||||
SetError(error, "usbmmidd_monitor_not_attached");
|
||||
return false;
|
||||
}
|
||||
VirtualDisplayMode chosen;
|
||||
if (!ChooseVirtualDisplayMode(EnumerateModes(device), requested, &chosen)) {
|
||||
SetError(error, "usbmmidd_no_display_modes");
|
||||
return false;
|
||||
}
|
||||
DEVMODEW current{};
|
||||
current.dmSize = sizeof(current);
|
||||
if (EnumDisplaySettingsExW(device.c_str(), ENUM_CURRENT_SETTINGS, ¤t,
|
||||
0) &&
|
||||
static_cast<int>(current.dmPelsWidth) == chosen.width &&
|
||||
static_cast<int>(current.dmPelsHeight) == chosen.height) {
|
||||
return true;
|
||||
}
|
||||
DEVMODEW mode{};
|
||||
mode.dmSize = sizeof(mode);
|
||||
mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;
|
||||
mode.dmPelsWidth = static_cast<DWORD>(chosen.width);
|
||||
mode.dmPelsHeight = static_cast<DWORD>(chosen.height);
|
||||
mode.dmBitsPerPel = 32;
|
||||
const LONG result = ChangeDisplaySettingsExW(device.c_str(), &mode, nullptr,
|
||||
CDS_UPDATEREGISTRY, nullptr);
|
||||
if (result != DISP_CHANGE_SUCCESSFUL) {
|
||||
SetError(error, "usbmmidd_mode_change_failed:" + std::to_string(result));
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("usbmmidd: display '{}' set to {}x{} (requested {}x{})",
|
||||
Narrow(device), chosen.width, chosen.height, requested.width,
|
||||
requested.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProvisionUsbmmiddDisplay(VirtualDisplayMode mode, int* plugged,
|
||||
std::string* error) {
|
||||
if (plugged != nullptr) *plugged = 0;
|
||||
const UsbmmiddPackage package = LocateUsbmmiddPackage();
|
||||
if (!package.Available()) {
|
||||
SetError(error, "usbmmidd_package_missing");
|
||||
return false;
|
||||
}
|
||||
if (!InstallUsbmmiddDriver(package, error)) return false;
|
||||
if (CountUsbmmiddMonitors() == 0) {
|
||||
if (!PlugUsbmmiddMonitor(package, 10 * 1000, error)) return false;
|
||||
if (plugged != nullptr) *plugged = 1;
|
||||
}
|
||||
std::string mode_error;
|
||||
if (!ApplyUsbmmiddDisplayMode(mode, &mode_error)) {
|
||||
LOG_WARN("usbmmidd: keeping the driver default mode, error={}", mode_error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReleaseUsbmmiddDisplays(int count, std::string* error) {
|
||||
const int to_unplug = (std::min)(count, CountUsbmmiddMonitors());
|
||||
if (to_unplug <= 0) return true;
|
||||
const UsbmmiddPackage package = LocateUsbmmiddPackage();
|
||||
if (!package.Available()) {
|
||||
SetError(error, "usbmmidd_package_missing");
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < to_unplug; ++index) {
|
||||
if (!UnplugUsbmmiddMonitor(package, 10 * 1000, error)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_USBMMIDD_VIRTUAL_DISPLAY_H_
|
||||
#define CROSSDESK_USBMMIDD_VIRTUAL_DISPLAY_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "usbmmidd_policy.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Plug/unplug talks to the driver through Amyuni's installer and needs
|
||||
// administrator rights inside the interactive session. The session helper
|
||||
// (SYSTEM, console session) is the intended caller; the GUI only calls these
|
||||
// directly when it is elevated and no helper is reachable.
|
||||
|
||||
struct UsbmmiddPackage {
|
||||
std::filesystem::path directory;
|
||||
std::filesystem::path installer;
|
||||
std::filesystem::path inf; // empty when only the installer is present
|
||||
bool Available() const { return !installer.empty(); }
|
||||
};
|
||||
|
||||
// Looks next to the running executable (usbmmidd_v2\, usbmmidd\,
|
||||
// drivers\usbmmidd_v2\). Both the GUI and the helper live in the same folder.
|
||||
UsbmmiddPackage LocateUsbmmiddPackage();
|
||||
|
||||
struct UsbmmiddStatus {
|
||||
bool package_available = false;
|
||||
bool driver_installed = false;
|
||||
int plugged_monitors = 0;
|
||||
std::string package_directory;
|
||||
};
|
||||
UsbmmiddStatus QueryUsbmmiddStatus();
|
||||
|
||||
// Adapters attached to the desktop, i.e. monitors currently plugged in.
|
||||
int CountUsbmmiddMonitors();
|
||||
// Root-enumerated device node present, plugged or not.
|
||||
bool IsUsbmmiddDriverInstalled();
|
||||
|
||||
// Each step is idempotent so a retry after a partial failure converges.
|
||||
bool InstallUsbmmiddDriver(const UsbmmiddPackage& package, std::string* error);
|
||||
bool PlugUsbmmiddMonitor(const UsbmmiddPackage& package, DWORD timeout_ms,
|
||||
std::string* error);
|
||||
bool UnplugUsbmmiddMonitor(const UsbmmiddPackage& package, DWORD timeout_ms,
|
||||
std::string* error);
|
||||
// Applies the closest supported mode to the first plugged usbmmidd monitor.
|
||||
bool ApplyUsbmmiddDisplayMode(VirtualDisplayMode requested, std::string* error);
|
||||
|
||||
// Installs the driver on first use, plugs one monitor when none is attached
|
||||
// and applies |mode|. A mode failure is logged but keeps the display.
|
||||
// |plugged| receives how many monitors this call plugged (0 when an existing
|
||||
// usbmmidd monitor was reused) so the caller can release exactly those.
|
||||
bool ProvisionUsbmmiddDisplay(VirtualDisplayMode mode, int* plugged,
|
||||
std::string* error);
|
||||
// Unplugs up to |count| usbmmidd monitors, never more than are attached. A
|
||||
// monitor plugged by the user or another program is left alone.
|
||||
bool ReleaseUsbmmiddDisplays(int count, std::string* error);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_USBMMIDD_VIRTUAL_DISPLAY_H_
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "virtual_display_provisioner.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "display_presence.h"
|
||||
#include "named_pipe_deadline.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "usbmmidd_virtual_display.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
using Json = nlohmann::json;
|
||||
|
||||
constexpr DWORD kHelperStatusTimeoutMs = 500;
|
||||
constexpr DWORD kHelperCommandTimeoutMs = 2000;
|
||||
constexpr DWORD kHelperPollIntervalMs = 250;
|
||||
constexpr DWORD kPlugTimeoutMs = 20 * 1000;
|
||||
constexpr DWORD kInstallAndPlugTimeoutMs = 120 * 1000;
|
||||
constexpr DWORD kUnplugTimeoutMs = 10 * 1000;
|
||||
constexpr DWORD kPresenceSettleTimeoutMs = 5 * 1000;
|
||||
|
||||
struct HelperVirtualDisplayStatus {
|
||||
bool ok = false;
|
||||
bool package_available = false;
|
||||
bool driver_installed = false;
|
||||
int plugged_monitors = 0;
|
||||
bool busy = false;
|
||||
std::string last_error;
|
||||
std::string error; // transport or helper-reported error
|
||||
};
|
||||
|
||||
std::wstring OwnSessionHelperPipeName() {
|
||||
DWORD session_id = 0xFFFFFFFF;
|
||||
if (!ProcessIdToSessionId(GetCurrentProcessId(), &session_id)) return {};
|
||||
return GetCrossDeskSessionHelperPipeName(session_id);
|
||||
}
|
||||
|
||||
bool QueryHelper(const std::wstring& pipe, const std::string& command,
|
||||
DWORD timeout_ms, Json* json, std::string* error) {
|
||||
std::vector<uint8_t> response;
|
||||
DWORD code = 0;
|
||||
if (!QueryNamedPipeWithDeadline(pipe, command, timeout_ms, &response, error,
|
||||
&code)) {
|
||||
*error += ":" + std::to_string(code);
|
||||
return false;
|
||||
}
|
||||
*json = Json::parse(response.begin(), response.end(), nullptr, false);
|
||||
if (json->is_discarded() || !json->is_object()) {
|
||||
*error = "invalid_helper_json";
|
||||
return false;
|
||||
}
|
||||
if (!json->value("ok", false)) {
|
||||
*error = json->value("error", std::string("helper_error"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
HelperVirtualDisplayStatus QueryHelperStatus(const std::wstring& pipe) {
|
||||
HelperVirtualDisplayStatus status;
|
||||
Json json;
|
||||
if (!QueryHelper(pipe, kCrossDeskVirtualDisplayStatusCommand,
|
||||
kHelperStatusTimeoutMs, &json, &status.error)) {
|
||||
return status;
|
||||
}
|
||||
status.ok = true;
|
||||
status.package_available = json.value("package_available", false);
|
||||
status.driver_installed = json.value("driver_installed", false);
|
||||
status.plugged_monitors = json.value("plugged_monitors", 0);
|
||||
status.busy = json.value("busy", false);
|
||||
status.last_error = json.value("last_error", std::string());
|
||||
return status;
|
||||
}
|
||||
|
||||
// Waits for the helper job to finish; returns false with the job's error.
|
||||
// |cancelled| stops the wait early (the helper job itself keeps running).
|
||||
bool WaitForHelperJob(const std::wstring& pipe, DWORD timeout_ms,
|
||||
const std::function<bool()>& cancelled,
|
||||
HelperVirtualDisplayStatus* final_status,
|
||||
std::string* error) {
|
||||
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
|
||||
while (true) {
|
||||
*final_status = QueryHelperStatus(pipe);
|
||||
if (!final_status->ok) {
|
||||
*error = final_status->error;
|
||||
return false;
|
||||
}
|
||||
if (!final_status->busy) {
|
||||
if (!final_status->last_error.empty()) {
|
||||
*error = final_status->last_error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (cancelled()) {
|
||||
*error = "cancelled";
|
||||
return false;
|
||||
}
|
||||
if (GetTickCount64() >= deadline) {
|
||||
*error = "helper_job_timeout";
|
||||
return false;
|
||||
}
|
||||
Sleep(kHelperPollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
bool WaitForDisplayPresence(DWORD timeout_ms,
|
||||
const std::function<bool()>& cancelled) {
|
||||
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
|
||||
while (true) {
|
||||
if (ClassifyDisplayPresence(ProbeDisplayPresence()) ==
|
||||
DisplayPresence::present) {
|
||||
return true;
|
||||
}
|
||||
if (cancelled() || GetTickCount64() >= deadline) return false;
|
||||
Sleep(kHelperPollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool VirtualDisplayProvisioner::AcquireViaHelper(VirtualDisplayMode mode,
|
||||
std::string* error,
|
||||
bool* helper_reachable) {
|
||||
*helper_reachable = false;
|
||||
const std::wstring pipe = OwnSessionHelperPipeName();
|
||||
if (pipe.empty()) {
|
||||
*error = "session_id_unavailable";
|
||||
return false;
|
||||
}
|
||||
HelperVirtualDisplayStatus status = QueryHelperStatus(pipe);
|
||||
if (!status.ok) {
|
||||
*error = status.error;
|
||||
return false;
|
||||
}
|
||||
*helper_reachable = true;
|
||||
if (!status.package_available) {
|
||||
*error = "usbmmidd_package_missing";
|
||||
return false;
|
||||
}
|
||||
const auto cancelled = [this] { return Cancelled(); };
|
||||
if (status.busy) {
|
||||
// A previous unplug may still be running; let it finish first.
|
||||
if (!WaitForHelperJob(pipe, kUnplugTimeoutMs, cancelled, &status, error))
|
||||
return false;
|
||||
}
|
||||
Json json;
|
||||
if (!QueryHelper(pipe, BuildVirtualDisplayPlugCommand(mode),
|
||||
kHelperCommandTimeoutMs, &json, error)) {
|
||||
return false;
|
||||
}
|
||||
const DWORD timeout =
|
||||
status.driver_installed ? kPlugTimeoutMs : kInstallAndPlugTimeoutMs;
|
||||
if (!WaitForHelperJob(pipe, timeout, cancelled, &status, error)) {
|
||||
// The helper finishes the plug on its own; Release() must undo it.
|
||||
if (*error == "cancelled") pending_helper_release_ = true;
|
||||
return false;
|
||||
}
|
||||
if (status.plugged_monitors <= 0) {
|
||||
*error = "usbmmidd_monitor_not_attached";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VirtualDisplayProvisioner::AcquireLocally(VirtualDisplayMode mode,
|
||||
std::string* error) {
|
||||
if (!LocateUsbmmiddPackage().Available()) {
|
||||
*error = "usbmmidd_package_missing";
|
||||
return false;
|
||||
}
|
||||
return ProvisionUsbmmiddDisplay(mode, &plugged_locally_, error);
|
||||
}
|
||||
|
||||
bool VirtualDisplayProvisioner::Acquire(VirtualDisplayMode mode,
|
||||
std::string* error) {
|
||||
std::string local_error;
|
||||
if (error == nullptr) error = &local_error;
|
||||
if (active_.load(std::memory_order_relaxed)) return true;
|
||||
cancel_.store(false, std::memory_order_relaxed);
|
||||
|
||||
bool helper_reachable = false;
|
||||
const ULONGLONG started = GetTickCount64();
|
||||
bool ok = AcquireViaHelper(mode, error, &helper_reachable);
|
||||
if (ok) {
|
||||
via_helper_ = true;
|
||||
} else if (!helper_reachable && *error != "cancelled") {
|
||||
LOG_INFO("Virtual display: session helper unreachable ({}), trying "
|
||||
"in-process provisioning",
|
||||
*error);
|
||||
ok = AcquireLocally(mode, error);
|
||||
via_helper_ = false;
|
||||
}
|
||||
if (!ok) {
|
||||
LOG_WARN("Virtual display: provisioning failed after {} ms, error={}",
|
||||
GetTickCount64() - started, *error);
|
||||
return false;
|
||||
}
|
||||
// The desktop moves onto the new target asynchronously; DXGI needs it.
|
||||
if (!WaitForDisplayPresence(kPresenceSettleTimeoutMs,
|
||||
[this] { return Cancelled(); })) {
|
||||
LOG_WARN("Virtual display: monitor plugged but no display target became "
|
||||
"available within {} ms",
|
||||
kPresenceSettleTimeoutMs);
|
||||
}
|
||||
active_.store(true, std::memory_order_relaxed);
|
||||
LOG_INFO("Virtual display: usbmmidd monitor ready via {} in {} ms "
|
||||
"(requested {}x{})",
|
||||
via_helper_ ? "session helper" : "this process",
|
||||
GetTickCount64() - started, mode.width, mode.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
void VirtualDisplayProvisioner::Release() {
|
||||
const bool was_active = active_.exchange(false, std::memory_order_relaxed);
|
||||
// via_helper_ is only set by a completed helper plug; a cancelled one is
|
||||
// tracked by pending_helper_release_ instead.
|
||||
const bool release_helper =
|
||||
(via_helper_ && was_active) || pending_helper_release_;
|
||||
const bool release_local = !release_helper && plugged_locally_ > 0;
|
||||
if (!release_helper && !release_local) return;
|
||||
std::string error;
|
||||
bool ok = false;
|
||||
if (release_helper) {
|
||||
const std::wstring pipe = OwnSessionHelperPipeName();
|
||||
Json json;
|
||||
HelperVirtualDisplayStatus status;
|
||||
ok = !pipe.empty() &&
|
||||
QueryHelper(pipe, kCrossDeskVirtualDisplayUnplugCommand,
|
||||
kHelperCommandTimeoutMs, &json, &error);
|
||||
// Wait for the unplug so a Start() right after this Stop() probes the
|
||||
// desktop without the outgoing monitor. A plug still running in the helper
|
||||
// queues the unplug behind itself; the next Acquire() waits for that job.
|
||||
if (ok && !json.value("queued", false)) {
|
||||
ok = WaitForHelperJob(pipe, kUnplugTimeoutMs, [] { return false; },
|
||||
&status, &error);
|
||||
}
|
||||
if (ok) pending_helper_release_ = false;
|
||||
} else {
|
||||
ok = ReleaseUsbmmiddDisplays(plugged_locally_, &error);
|
||||
if (ok) plugged_locally_ = 0;
|
||||
}
|
||||
if (ok) {
|
||||
LOG_INFO("Virtual display: usbmmidd monitor released");
|
||||
} else {
|
||||
LOG_WARN("Virtual display: release failed, error={}", error);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_VIRTUAL_DISPLAY_PROVISIONER_H_
|
||||
#define CROSSDESK_VIRTUAL_DISPLAY_PROVISIONER_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include "usbmmidd_policy.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Session-scoped usbmmidd virtual display for a host without any monitor.
|
||||
// Acquire() asks the session helper (SYSTEM in the console session) to
|
||||
// install/plug the driver; without a reachable helper an elevated GUI does it
|
||||
// itself. Release() unplugs only what this object plugged.
|
||||
class VirtualDisplayProvisioner {
|
||||
public:
|
||||
VirtualDisplayProvisioner() = default;
|
||||
~VirtualDisplayProvisioner() { Release(); }
|
||||
VirtualDisplayProvisioner(const VirtualDisplayProvisioner&) = delete;
|
||||
VirtualDisplayProvisioner& operator=(const VirtualDisplayProvisioner&) =
|
||||
delete;
|
||||
|
||||
bool Acquire(VirtualDisplayMode mode, std::string* error);
|
||||
void Release();
|
||||
bool active() const { return active_.load(std::memory_order_relaxed); }
|
||||
// True when Release() has something to undo. Read on the owner's thread
|
||||
// after the provisioning worker has been joined.
|
||||
bool NeedsRelease() const {
|
||||
return active() || pending_helper_release_ || plugged_locally_ > 0;
|
||||
}
|
||||
// Makes a blocking Acquire() return early with "cancelled" at its next poll
|
||||
// (helper install/plug can take up to two minutes). Cleared by Acquire().
|
||||
void RequestCancel() { cancel_.store(true, std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
// |helper_reachable| tells the caller whether a local fallback makes sense.
|
||||
bool AcquireViaHelper(VirtualDisplayMode mode, std::string* error,
|
||||
bool* helper_reachable);
|
||||
bool AcquireLocally(VirtualDisplayMode mode, std::string* error);
|
||||
bool Cancelled() const { return cancel_.load(std::memory_order_relaxed); }
|
||||
|
||||
// Acquire/Release run on the owner's provisioning worker; active() and
|
||||
// RequestCancel() are used from other threads.
|
||||
std::atomic<bool> active_{false};
|
||||
std::atomic<bool> cancel_{false};
|
||||
bool via_helper_ = false;
|
||||
// A cancelled helper plug still completes in the helper; Release() undoes it.
|
||||
bool pending_helper_release_ = false;
|
||||
// Monitors plugged by this process (helper-plugged ones are tracked there).
|
||||
int plugged_locally_ = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_VIRTUAL_DISPLAY_PROVISIONER_H_
|
||||
@@ -230,9 +230,13 @@ function setup_targets()
|
||||
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp",
|
||||
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp",
|
||||
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_factory.cpp")
|
||||
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp",
|
||||
"apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.cpp")
|
||||
add_includedirs("apps/desktop/src/platform/windows",
|
||||
"apps/desktop/src/platform/windows/screen_capturer",
|
||||
"apps/desktop/src/platform/windows/service")
|
||||
"apps/desktop/src/platform/windows/service",
|
||||
"apps/desktop/src/platform/windows/virtual_display")
|
||||
add_syslinks("Setupapi", {public = true})
|
||||
elseif is_os("macosx") then
|
||||
add_deps("privacy")
|
||||
add_mxxflags("-fobjc-arc")
|
||||
@@ -470,13 +474,15 @@ function setup_targets()
|
||||
set_kind("binary")
|
||||
add_packages("libyuv")
|
||||
add_deps("rd_log", "path_manager")
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32", "Setupapi")
|
||||
add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp")
|
||||
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp")
|
||||
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
|
||||
{defines = windows_helper_resource_defines})
|
||||
add_includedirs("apps/desktop/src/common",
|
||||
"apps/desktop/src/platform/windows/service",
|
||||
"apps/desktop/src/platform/windows/screen_capturer")
|
||||
"apps/desktop/src/platform/windows/screen_capturer",
|
||||
"apps/desktop/src/platform/windows/virtual_display")
|
||||
add_includedirs("apps/desktop/src/platform/windows/input",
|
||||
"apps/desktop/src/platform/common/input")
|
||||
add_includedirs("libs/wire/include")
|
||||
|
||||
Reference in New Issue
Block a user