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,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_
|
||||
Reference in New Issue
Block a user