main
maye 2025-06-18 15:14:45 +08:00
parent 91e9cb05c2
commit 78ad72951d
7 changed files with 225 additions and 100 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
.vscode

62
Cargo.lock generated
View File

@ -104,6 +104,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "daemon"
version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e2112c4a5cd067d08601da91abd9de4c47cd157e8a80edf41d9382d07e9d51c"
dependencies = [
"kernel32-sys",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "heck"
version = "0.5.0"
@ -116,6 +127,22 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "log"
version = "0.4.27"
@ -133,6 +160,7 @@ name = "parsec-vdd"
version = "0.1.0"
dependencies = [
"clap",
"daemon",
"log",
"windows",
"windows-service",
@ -191,6 +219,40 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.61.3"

View File

@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
clap = { version = "4.5.40", features = ["derive"] }
daemon = "0.0.8"
log = "0.4.27"
windows-service = "0.8.0"

14
readme.md Normal file
View File

@ -0,0 +1,14 @@
inspire [parsec-vdd](https://github.com/nomi-san/parsec-vdd), but I don't need any GUI, multiple monitor, and etc.
I just want a tool which can create a fallback virtual display device when my physical display device is not available.
Like what parsec do.
So here is my first rust exercise project.
Do expect there are some bugs and I will fix them when I have time.
### how to compile?
well, I wrote this shit on linux.
so compile with `cargo build --target x86_64-pc-windows-gnu --release`
### how to use?

View File

@ -1,21 +1,17 @@
use log::{error, info, trace};
use log::{debug, error, info, trace};
use parsec_vdd::{
DeviceStatus, VDD_ADAPTER_GUID, VDD_CLASS_GUID, VDD_HARDWARE_ID, VddHandle, open_device_handle,
query_device_status, vdd_add_display, vdd_remove_display, vdd_update, vdd_version,
};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
Arc,
atomic::{AtomicBool, Ordering},
};
use windows::{
Win32::Graphics::Gdi::{
DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_STATE_FLAGS, DISPLAY_DEVICEW, EnumDisplayDevicesW,
},
core::PWSTR,
Win32::{
Graphics::Gdi::{
EnumDisplayDevicesW, DISPLAY_DEVICEW, DISPLAY_DEVICE_ACTIVE,
DISPLAY_DEVICE_STATE_FLAGS,
},
},
};
use parsec_vdd::{
VDD_ADAPTER_GUID, VDD_CLASS_GUID, VDD_HARDWARE_ID, VddHandle,
open_device_handle, query_device_status, vdd_add_display,
vdd_remove_display, vdd_update, vdd_version, DeviceStatus,
};
pub struct App {
@ -49,15 +45,9 @@ impl App {
app
}
// reorder monitors, and make parsec-vdd monitor have the highest id.
pub fn reorder(&self) -> u8 {
// TODO: implement reorder logic
0
}
fn if_monitor_connected() -> bool {
fn is_physical_monitor_connected() -> bool {
// fetch windows monitor lists.
let mut monitors = Vec::new();
let mut physical_monitors = Vec::new();
let mut info = DISPLAY_DEVICEW {
cb: std::mem::size_of::<DISPLAY_DEVICEW>() as u32,
DeviceName: [0; 32],
@ -66,66 +56,101 @@ impl App {
DeviceID: [0; 128],
DeviceKey: [0; 128],
};
let mut i = 0;
while unsafe {
EnumDisplayDevicesW(
PWSTR::null(),
i,
&mut info,
0
).as_bool()
} {
while unsafe { EnumDisplayDevicesW(PWSTR::null(), i, &mut info, 0).as_bool() } {
if (info.StateFlags & DISPLAY_DEVICE_ACTIVE).0 != 0 {
let device_name = String::from_utf16_lossy(&info.DeviceName)
.trim_end_matches('\0')
.to_string();
if device_name.to_lowercase().contains("parsec") {
i += 1;
continue;
debug!("Found monitor: {}", device_name);
// 排除 parsec 虚拟显示器,只统计物理显示器
if !device_name.to_lowercase().contains("parsec") {
physical_monitors.push(device_name);
}
monitors.push(device_name);
}
i += 1;
}
trace!("Found {} monitors", monitors.len());
for (i, monitor) in monitors.iter().enumerate() {
trace!("Monitor {}: {}", i, monitor);
trace!("Found {} physical monitors", physical_monitors.len());
for (i, monitor) in physical_monitors.iter().enumerate() {
trace!("Physical Monitor {}: {}", i, monitor);
}
!monitors.is_empty()
!physical_monitors.is_empty()
}
pub fn watch_monitors(&mut self) {
loop {
if !self.if_monitor_connected() && !self.running.load(Ordering::SeqCst) {
self.start();
break;
} else {
self.stop();
let has_physical_monitor = App::is_physical_monitor_connected();
let vdd_is_running = self.index != -1;
if !has_physical_monitor && !vdd_is_running {
// 没有物理显示器且虚拟显示器未运行,启动虚拟显示器
info!("No physical monitors detected, starting virtual display");
self.start_virtual_display();
} else if has_physical_monitor && vdd_is_running {
// 有物理显示器且虚拟显示器正在运行,停止虚拟显示器
info!("Physical monitor detected, stopping virtual display");
self.stop_virtual_display();
}
if !self.running.load(Ordering::SeqCst) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(1000));
}
}
pub fn start(&mut self) {
fn start_virtual_display(&mut self) {
if self.index == -1 {
self.index = vdd_add_display(&self.handle).unwrap_or(-1);
if self.index == -1 {
error!("Failed to add display");
error!("Failed to add virtual display");
return;
}
info!("Virtual display added with index: {}", self.index);
}
}
fn stop_virtual_display(&mut self) {
if self.index != -1 {
vdd_remove_display(&self.handle, self.index);
self.index = -1;
info!("Virtual display removed");
}
}
pub fn start(&mut self) {
self.running.store(true, Ordering::SeqCst);
while self.running.load(Ordering::SeqCst) {
vdd_update(&self.handle);
if self.index != -1 {
vdd_update(&self.handle);
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
pub fn keep_alive(&mut self) {
loop {
if !self.running.load(Ordering::SeqCst) {
std::thread::sleep(std::time::Duration::from_millis(1000));
continue;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
vdd_remove_display(&self.handle, self.index);
info!("Removed display");
}
pub fn stop(&self) {
self.running.store(false, Ordering::SeqCst);
}
}
impl Drop for App {
fn drop(&mut self) {
self.stop();
self.stop_virtual_display();
}
}

View File

@ -1,5 +1,6 @@
#[allow(non_snake_case)]
use std::mem::{size_of, zeroed};
use std::ptr;
use windows::{
Win32::{
@ -14,17 +15,17 @@ use windows::{
SetupDiGetDeviceRegistryPropertyA,
},
Foundation::{
CloseHandle, ERROR_IO_PENDING, GENERIC_READ, GENERIC_WRITE,
GetLastError, HANDLE,
CloseHandle, ERROR_IO_PENDING, GENERIC_READ, GENERIC_WRITE, GetLastError, HANDLE,
INVALID_HANDLE_VALUE,
},
Storage::FileSystem::{
CreateFileA, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_NO_BUFFERING, FILE_FLAG_OVERLAPPED,
FILE_FLAG_WRITE_THROUGH, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
},
System::{
IO::{DeviceIoControl, OVERLAPPED, GetOverlappedResultEx},
IO::{DeviceIoControl, GetOverlappedResultEx, OVERLAPPED},
Registry::{REG_MULTI_SZ, REG_SZ},
Threading::{CreateEventA},
Threading::CreateEventA,
},
},
core::{GUID, PCSTR},
@ -42,9 +43,10 @@ impl DevInfo {
}
}
}
impl Drop for DevInfo {
fn drop(&mut self) {
if !self.0.is_invalid() {
if self.0 != HDEVINFO(INVALID_HANDLE_VALUE.0 as isize) {
unsafe {
let _ = SetupDiDestroyDeviceInfoList(self.0);
}
@ -103,16 +105,16 @@ pub enum VddCtlCode {
Version = 0x0022e010,
}
// 简化的错误处理宏
macro_rules! win_try {
($expr:expr) => {
match $expr {
Ok(val) => val,
Err(_) => return None,
}
};
// 错误处理类型
#[derive(Debug)]
pub enum VddError {
DeviceNotFound,
InvalidHandle,
IoError,
Timeout,
}
type VddResult<T> = Result<T, VddError>;
/// Query the driver status.
pub fn query_device_status(class_guid: &GUID, device_id: &[u8]) -> DeviceStatus {
let dev_info = match DevInfo::new(Some(class_guid), DIGCF_PRESENT) {
@ -120,6 +122,11 @@ pub fn query_device_status(class_guid: &GUID, device_id: &[u8]) -> DeviceStatus
None => return DeviceStatus::Inaccessible,
};
// 确保 device_id 以 null 结尾
if device_id.is_empty() || device_id[device_id.len() - 1] != 0 {
return DeviceStatus::Unknown;
}
unsafe {
for device_index in 0.. {
let mut dev_info_data: SP_DEVINFO_DATA = zeroed();
@ -203,9 +210,14 @@ fn get_device_status(dev_info_data: &SP_DEVINFO_DATA) -> DeviceStatus {
return DeviceStatus::NotInstalled;
}
match dev_status {
s if (s & (DN_DRIVER_LOADED | DN_STARTED)).0 != 0 => DeviceStatus::Ok,
s if (s & DN_HAS_PROBLEM).0 != 0 => match dev_problem_num {
// 检查设备是否正常运行
if (dev_status & (DN_DRIVER_LOADED | DN_STARTED)).0 == (DN_DRIVER_LOADED | DN_STARTED).0 {
return DeviceStatus::Ok;
}
// 检查是否有问题
if (dev_status & DN_HAS_PROBLEM).0 != 0 {
match dev_problem_num {
n if n == CM_PROB_NEED_RESTART => DeviceStatus::RestartRequired,
n if n == CM_PROB_DISABLED || n == CM_PROB_HARDWARE_DISABLED => {
DeviceStatus::Disabled
@ -213,8 +225,9 @@ fn get_device_status(dev_info_data: &SP_DEVINFO_DATA) -> DeviceStatus {
n if n == CM_PROB_DISABLED_SERVICE => DeviceStatus::DisabledService,
n if n == CM_PROB_FAILED_POST_START => DeviceStatus::DriverError,
_ => DeviceStatus::UnknownProblem,
},
_ => DeviceStatus::Unknown,
}
} else {
DeviceStatus::Unknown
}
}
}
@ -309,7 +322,7 @@ fn create_device_handle(device_path: &PCSTR) -> Option<HANDLE> {
/// Release the device handle.
fn close_device_handle(handle: HANDLE) {
if !handle.is_invalid() {
if handle != INVALID_HANDLE_VALUE {
unsafe {
let _ = CloseHandle(handle);
}
@ -318,9 +331,9 @@ fn close_device_handle(handle: HANDLE) {
/// Generic DeviceIoControl for all IoControl codes.
/// Returns the output buffer value from the driver, or None on failure.
fn vdd_io_control(vdd: &VddHandle, code: VddCtlCode, data: Option<&[u8]>) -> Option<u32> {
if vdd.0.is_invalid() {
return None;
fn vdd_io_control(vdd: &VddHandle, code: VddCtlCode, data: Option<&[u8]>) -> VddResult<u32> {
if vdd.0 == INVALID_HANDLE_VALUE {
return Err(VddError::InvalidHandle);
}
unsafe {
@ -331,7 +344,7 @@ fn vdd_io_control(vdd: &VddHandle, code: VddCtlCode, data: Option<&[u8]>) -> Opt
}
let mut overlapped: OVERLAPPED = zeroed();
let event = win_try!(CreateEventA(None, true, false, None));
let event = CreateEventA(None, true, false, None).map_err(|_| VddError::IoError)?;
overlapped.hEvent = event;
let mut out_buffer = 0u32;
@ -348,43 +361,60 @@ fn vdd_io_control(vdd: &VddHandle, code: VddCtlCode, data: Option<&[u8]>) -> Opt
Some(&mut overlapped),
);
// 简化的异步处理
if result.is_err() && GetLastError() == ERROR_IO_PENDING {
win_try!(GetOverlappedResultEx(
// 处理异步操作
let final_result = if result.is_err() && GetLastError() == ERROR_IO_PENDING {
GetOverlappedResultEx(
vdd.0,
&overlapped,
&mut bytes_transferred,
5000,
false
));
}
5000, // 5秒超时
false,
)
.map_err(|_| VddError::Timeout)
} else {
result.map_err(|_| VddError::IoError)
};
let _ = CloseHandle(event);
Some(out_buffer)
match final_result {
Ok(_) => Ok(out_buffer),
Err(e) => Err(e),
}
}
}
/// Query VDD minor version.
pub fn vdd_version(vdd: &VddHandle) -> Option<i32> {
vdd_io_control(vdd, VddCtlCode::Version, None).map(|v| v as i32)
vdd_io_control(vdd, VddCtlCode::Version, None)
.ok()
.map(|v| v as i32)
}
/// Update/ping to VDD to keep displays alive.
pub fn vdd_update(vdd: &VddHandle) {
vdd_io_control(vdd, VddCtlCode::Update, None);
pub fn vdd_update(vdd: &VddHandle) -> bool {
vdd_io_control(vdd, VddCtlCode::Update, None).is_ok()
}
/// Add/plug a virtual display. Returns the index of the added display.
pub fn vdd_add_display(vdd: &VddHandle) -> Option<i32> {
let result = vdd_io_control(vdd, VddCtlCode::Add, None);
let result = vdd_io_control(vdd, VddCtlCode::Add, None).ok()?;
vdd_update(vdd); // Ping immediately after adding
result.map(|v| v as i32)
Some(result as i32)
}
/// Remove/unplug a virtual display.
pub fn vdd_remove_display(vdd: &VddHandle, index: i32) {
pub fn vdd_remove_display(vdd: &VddHandle, index: i32) -> bool {
// 验证索引范围
if index < 0 || index >= VDD_MAX_DISPLAYS as i32 {
return false;
}
// The driver expects the index as a 16-bit big-endian integer.
let index_data = (index as u16).to_be_bytes();
vdd_io_control(vdd, VddCtlCode::Remove, Some(&index_data));
vdd_update(vdd); // Ping immediately after removing
let result = vdd_io_control(vdd, VddCtlCode::Remove, Some(&index_data)).is_ok();
if result {
vdd_update(vdd); // Ping immediately after removing
}
result
}

View File

@ -1,18 +1,10 @@
#[macro_use]
extern crate windows_service;
mod app;
use app::App;
use std::ffi::OsString;
use windows_service::{service_dispatcher, define_windows_service};
define_windows_service!(ffi_service_main, service_main);
fn service_main(_arguments: Vec<OsString>) {
let _app = App::new();
}
fn main() {
service_dispatcher::start("parsec-vdd", ffi_service_main).unwrap();
let mut app = App::new();
app.watch_monitors();
}