init
parent
91e9cb05c2
commit
78ad72951d
|
@ -1 +1,2 @@
|
|||
/target
|
||||
.vscode
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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?
|
125
src/app.rs
125
src/app.rs
|
@ -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();
|
||||
}
|
||||
}
|
108
src/lib.rs
108
src/lib.rs
|
@ -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
|
||||
}
|
||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue