Compare commits

..

No commits in common. "886e6b3770a475262b0858fe7ecf115f79de2f2d" and "91e9cb05c2d0234657a1363cd56a3ff27429099c" have entirely different histories.

7 changed files with 327 additions and 159 deletions

1
.gitignore vendored
View File

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

224
Cargo.lock generated
View File

@ -2,18 +2,140 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "clap"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "parsec-vdd"
version = "0.1.0"
dependencies = [
"clap",
"log",
"windows",
"windows-service",
]
[[package]]
@ -34,6 +156,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.103"
@ -51,6 +179,18 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "widestring"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
name = "windows"
version = "0.61.3"
@ -144,6 +284,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-service"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac"
dependencies = [
"bitflags",
"widestring",
"windows-sys",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
@ -153,6 +304,31 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@ -161,3 +337,51 @@ checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@ -4,7 +4,9 @@ version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.40", features = ["derive"] }
log = "0.4.27"
windows-service = "0.8.0"
[dependencies.windows]
version = "0.61.3"

View File

@ -1,14 +0,0 @@
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,16 +1,21 @@
use log::{debug, error, info, trace};
use parsec_vdd::{
open_device_handle, query_device_status, vdd_add_display, vdd_remove_display, vdd_update, vdd_version, DeviceStatus, VddHandle, VDD_ADAPTER_GUID, VDD_CLASS_GUID, VDD_HARDWARE_ID
};
use log::{error, info, trace};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, Ordering},
Arc,
};
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 {
@ -44,9 +49,15 @@ impl App {
app
}
fn is_physical_monitor_connected() -> bool {
// 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 {
// fetch windows monitor lists.
let mut physical_monitors = Vec::new();
let mut monitors = Vec::new();
let mut info = DISPLAY_DEVICEW {
cb: std::mem::size_of::<DISPLAY_DEVICEW>() as u32,
DeviceName: [0; 32],
@ -55,101 +66,66 @@ 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();
debug!("Found monitor: {}", device_name);
// 排除 parsec 虚拟显示器,只统计物理显示器
if !device_name.to_lowercase().contains("parsec") {
physical_monitors.push(device_name);
if device_name.to_lowercase().contains("parsec") {
i += 1;
continue;
}
monitors.push(device_name);
}
i += 1;
}
trace!("Found {} physical monitors", physical_monitors.len());
for (i, monitor) in physical_monitors.iter().enumerate() {
trace!("Physical Monitor {}: {}", i, monitor);
trace!("Found {} monitors", monitors.len());
for (i, monitor) in monitors.iter().enumerate() {
trace!("Monitor {}: {}", i, monitor);
}
!physical_monitors.is_empty()
!monitors.is_empty()
}
pub fn watch_monitors(&mut self) {
loop {
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) {
if !self.if_monitor_connected() && !self.running.load(Ordering::SeqCst) {
self.start();
break;
} else {
self.stop();
}
std::thread::sleep(std::time::Duration::from_millis(1000));
}
}
fn start_virtual_display(&mut self) {
pub fn start(&mut self) {
if self.index == -1 {
self.index = vdd_add_display(&self.handle).unwrap_or(-1);
if self.index == -1 {
error!("Failed to add virtual display");
error!("Failed to add 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) {
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;
}
vdd_update(&self.handle);
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

@ -14,17 +14,17 @@ use windows::{
SetupDiGetDeviceRegistryPropertyA,
},
Foundation::{
CloseHandle, ERROR_IO_PENDING, GENERIC_READ, GENERIC_WRITE, GetLastError, HANDLE,
INVALID_HANDLE_VALUE,
CloseHandle, ERROR_IO_PENDING, GENERIC_READ, GENERIC_WRITE,
GetLastError, HANDLE,
},
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, GetOverlappedResultEx, OVERLAPPED},
IO::{DeviceIoControl, OVERLAPPED, GetOverlappedResultEx},
Registry::{REG_MULTI_SZ, REG_SZ},
Threading::CreateEventA,
Threading::{CreateEventA},
},
},
core::{GUID, PCSTR},
@ -42,10 +42,9 @@ impl DevInfo {
}
}
}
impl Drop for DevInfo {
fn drop(&mut self) {
if self.0 != HDEVINFO(INVALID_HANDLE_VALUE.0 as isize) {
if !self.0.is_invalid() {
unsafe {
let _ = SetupDiDestroyDeviceInfoList(self.0);
}
@ -104,16 +103,16 @@ pub enum VddCtlCode {
Version = 0x0022e010,
}
// 错误处理类型
#[derive(Debug)]
pub enum VddError {
DeviceNotFound,
InvalidHandle,
IoError,
Timeout,
// 简化的错误处理宏
macro_rules! win_try {
($expr:expr) => {
match $expr {
Ok(val) => val,
Err(_) => return None,
}
};
}
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) {
@ -121,11 +120,6 @@ 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();
@ -209,14 +203,9 @@ fn get_device_status(dev_info_data: &SP_DEVINFO_DATA) -> DeviceStatus {
return DeviceStatus::NotInstalled;
}
// 检查设备是否正常运行
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 {
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 {
n if n == CM_PROB_NEED_RESTART => DeviceStatus::RestartRequired,
n if n == CM_PROB_DISABLED || n == CM_PROB_HARDWARE_DISABLED => {
DeviceStatus::Disabled
@ -224,9 +213,8 @@ 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,
}
} else {
DeviceStatus::Unknown
},
_ => DeviceStatus::Unknown,
}
}
}
@ -321,7 +309,7 @@ fn create_device_handle(device_path: &PCSTR) -> Option<HANDLE> {
/// Release the device handle.
fn close_device_handle(handle: HANDLE) {
if handle != INVALID_HANDLE_VALUE {
if !handle.is_invalid() {
unsafe {
let _ = CloseHandle(handle);
}
@ -330,9 +318,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]>) -> VddResult<u32> {
if vdd.0 == INVALID_HANDLE_VALUE {
return Err(VddError::InvalidHandle);
fn vdd_io_control(vdd: &VddHandle, code: VddCtlCode, data: Option<&[u8]>) -> Option<u32> {
if vdd.0.is_invalid() {
return None;
}
unsafe {
@ -343,7 +331,7 @@ fn vdd_io_control(vdd: &VddHandle, code: VddCtlCode, data: Option<&[u8]>) -> Vdd
}
let mut overlapped: OVERLAPPED = zeroed();
let event = CreateEventA(None, true, false, None).map_err(|_| VddError::IoError)?;
let event = win_try!(CreateEventA(None, true, false, None));
overlapped.hEvent = event;
let mut out_buffer = 0u32;
@ -360,60 +348,43 @@ fn vdd_io_control(vdd: &VddHandle, code: VddCtlCode, data: Option<&[u8]>) -> Vdd
Some(&mut overlapped),
);
// 处理异步操作
let final_result = if result.is_err() && GetLastError() == ERROR_IO_PENDING {
GetOverlappedResultEx(
// 简化的异步处理
if result.is_err() && GetLastError() == ERROR_IO_PENDING {
win_try!(GetOverlappedResultEx(
vdd.0,
&overlapped,
&mut bytes_transferred,
5000, // 5秒超时
false,
)
.map_err(|_| VddError::Timeout)
} else {
result.map_err(|_| VddError::IoError)
};
5000,
false
));
}
let _ = CloseHandle(event);
match final_result {
Ok(_) => Ok(out_buffer),
Err(e) => Err(e),
}
Some(out_buffer)
}
}
/// Query VDD minor version.
pub fn vdd_version(vdd: &VddHandle) -> Option<i32> {
vdd_io_control(vdd, VddCtlCode::Version, None)
.ok()
.map(|v| v as i32)
vdd_io_control(vdd, VddCtlCode::Version, None).map(|v| v as i32)
}
/// Update/ping to VDD to keep displays alive.
pub fn vdd_update(vdd: &VddHandle) -> bool {
vdd_io_control(vdd, VddCtlCode::Update, None).is_ok()
pub fn vdd_update(vdd: &VddHandle) {
vdd_io_control(vdd, VddCtlCode::Update, None);
}
/// 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).ok()?;
let result = vdd_io_control(vdd, VddCtlCode::Add, None);
vdd_update(vdd); // Ping immediately after adding
Some(result as i32)
result.map(|v| v as i32)
}
/// Remove/unplug a virtual display.
pub fn vdd_remove_display(vdd: &VddHandle, index: i32) -> bool {
// 验证索引范围
if index < 0 || index >= VDD_MAX_DISPLAYS as i32 {
return false;
}
pub fn vdd_remove_display(vdd: &VddHandle, index: i32) {
// The driver expects the index as a 16-bit big-endian integer.
let index_data = (index as u16).to_be_bytes();
let result = vdd_io_control(vdd, VddCtlCode::Remove, Some(&index_data)).is_ok();
if result {
vdd_update(vdd); // Ping immediately after removing
}
result
vdd_io_control(vdd, VddCtlCode::Remove, Some(&index_data));
vdd_update(vdd); // Ping immediately after removing
}

View File

@ -1,8 +1,18 @@
#[macro_use]
extern crate windows_service;
mod app;
use app::App;
fn main() {
let mut app = App::new();
app.watch_monitors();
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();
}