Compare commits
3 Commits
8cac59d8bb
...
b708506e27
Author | SHA1 | Date |
---|---|---|
|
b708506e27 | |
|
c7b4c9d33a | |
|
4269ddb312 |
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
.vscode
|
.vscode
|
||||||
|
.aider*
|
|
@ -2,18 +2,387 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
|
||||||
|
dependencies = [
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.11.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"env_filter",
|
||||||
|
"jiff",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-static",
|
||||||
|
"log",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-static"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.77"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.174"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.27"
|
version = "0.4.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parsec-vdd"
|
name = "parsec-vdd"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"windows",
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"windows 0.61.3",
|
||||||
|
"wmi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -34,6 +403,73 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.103"
|
version = "2.0.103"
|
||||||
|
@ -45,12 +481,106 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.58.0",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.61.3"
|
version = "0.61.3"
|
||||||
|
@ -58,7 +588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-collections",
|
"windows-collections",
|
||||||
"windows-core",
|
"windows-core 0.61.2",
|
||||||
"windows-future",
|
"windows-future",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
"windows-numerics",
|
"windows-numerics",
|
||||||
|
@ -70,7 +600,20 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement 0.58.0",
|
||||||
|
"windows-interface 0.58.0",
|
||||||
|
"windows-result 0.2.0",
|
||||||
|
"windows-strings 0.1.0",
|
||||||
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -79,11 +622,11 @@ version = "0.61.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement 0.60.0",
|
||||||
"windows-interface",
|
"windows-interface 0.59.1",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
"windows-result",
|
"windows-result 0.3.4",
|
||||||
"windows-strings",
|
"windows-strings 0.4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -92,11 +635,22 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.61.2",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
"windows-threading",
|
"windows-threading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.60.0"
|
version = "0.60.0"
|
||||||
|
@ -108,6 +662,17 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.59.1"
|
version = "0.59.1"
|
||||||
|
@ -131,10 +696,19 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.61.2",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
@ -144,6 +718,16 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result 0.2.0",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -153,6 +737,31 @@ dependencies = [
|
||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "windows-threading"
|
name = "windows-threading"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -161,3 +770,66 @@ checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"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"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wmi"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff00ac1309d4c462be86f03a55e409509e8bf4323ec296aeb4b381dd9aabe6ec"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"windows 0.58.0",
|
||||||
|
"windows-core 0.58.0",
|
||||||
|
]
|
||||||
|
|
|
@ -5,6 +5,11 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
|
env_logger = "0.11"
|
||||||
|
thiserror = "1.0"
|
||||||
|
wmi = "0.13"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
[dependencies.windows]
|
[dependencies.windows]
|
||||||
version = "0.61.3"
|
version = "0.61.3"
|
||||||
|
|
|
@ -86,11 +86,9 @@ impl App {
|
||||||
let vdd_is_running = self.index != -1;
|
let vdd_is_running = self.index != -1;
|
||||||
|
|
||||||
if !has_physical_monitor && !vdd_is_running {
|
if !has_physical_monitor && !vdd_is_running {
|
||||||
// 没有物理显示器且虚拟显示器未运行,启动虚拟显示器
|
|
||||||
info!("No physical monitors detected, starting virtual display");
|
info!("No physical monitors detected, starting virtual display");
|
||||||
self.start_virtual_display();
|
self.start_virtual_display();
|
||||||
} else if has_physical_monitor && vdd_is_running {
|
} else if has_physical_monitor && vdd_is_running {
|
||||||
// 有物理显示器且虚拟显示器正在运行,停止虚拟显示器
|
|
||||||
info!("Physical monitor detected, stopping virtual display");
|
info!("Physical monitor detected, stopping virtual display");
|
||||||
self.stop_virtual_display();
|
self.stop_virtual_display();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod service;
|
||||||
|
|
||||||
|
pub use service::VddService;
|
|
@ -0,0 +1,124 @@
|
||||||
|
use crate::display::{DisplayManager, VirtualDisplayController};
|
||||||
|
use crate::device::{VddHandle, DeviceController, DeviceStatusChecker};
|
||||||
|
use crate::monitor::HardwareDetector;
|
||||||
|
use crate::config::{Config, VDD_ADAPTER_GUID, VDD_CLASS_GUID, VDD_HARDWARE_ID};
|
||||||
|
use crate::error::Result;
|
||||||
|
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
|
||||||
|
use std::time::Duration;
|
||||||
|
use log::{error, info, warn};
|
||||||
|
|
||||||
|
pub struct VddService {
|
||||||
|
handle: Arc<VddHandle>,
|
||||||
|
device_controller: DeviceController,
|
||||||
|
hardware_detector: Option<HardwareDetector>,
|
||||||
|
config: Config,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VddService {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
info!("Initializing VDD Service...");
|
||||||
|
|
||||||
|
// Check driver status first
|
||||||
|
DeviceStatusChecker::check_driver_status(&VDD_CLASS_GUID, VDD_HARDWARE_ID)?;
|
||||||
|
info!("VDD driver status: OK");
|
||||||
|
|
||||||
|
// Open device handle
|
||||||
|
let handle = Arc::new(VddHandle::open(&VDD_ADAPTER_GUID)?);
|
||||||
|
info!("VDD device handle opened successfully");
|
||||||
|
|
||||||
|
let config = Config::default();
|
||||||
|
let device_controller = DeviceController::new(config.clone());
|
||||||
|
|
||||||
|
// Initialize hardware detector
|
||||||
|
let hardware_detector = match HardwareDetector::new() {
|
||||||
|
Ok(detector) => {
|
||||||
|
info!("Hardware detector initialized successfully");
|
||||||
|
if let Err(e) = detector.log_hardware_summary() {
|
||||||
|
warn!("Failed to log hardware summary: {}", e);
|
||||||
|
}
|
||||||
|
Some(detector)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to initialize hardware detector: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
handle,
|
||||||
|
device_controller,
|
||||||
|
hardware_detector,
|
||||||
|
config,
|
||||||
|
running: Arc::new(AtomicBool::new(false)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) -> Result<()> {
|
||||||
|
info!("Starting VDD Service...");
|
||||||
|
self.running.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
// Create display manager for the monitor thread
|
||||||
|
let virtual_display_controller = VirtualDisplayController::new(self.device_controller.clone());
|
||||||
|
let mut display_manager = DisplayManager::new(virtual_display_controller);
|
||||||
|
|
||||||
|
// Start monitor watching thread
|
||||||
|
let handle_clone = Arc::clone(&self.handle);
|
||||||
|
let running_clone = Arc::clone(&self.running);
|
||||||
|
let monitor_config = self.config.clone();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
info!("Monitor detection thread started");
|
||||||
|
|
||||||
|
// Initial delay to let system stabilize
|
||||||
|
std::thread::sleep(Duration::from_millis(
|
||||||
|
monitor_config.display.virtual_display_settings.startup_delay_ms as u64
|
||||||
|
));
|
||||||
|
|
||||||
|
while running_clone.load(Ordering::SeqCst) {
|
||||||
|
if let Err(e) = display_manager.update_displays(&handle_clone) {
|
||||||
|
error!("Failed to update displays: {}", e);
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(monitor_config.monitor_check_interval_ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Monitor detection thread stopped");
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("VDD Service started successfully");
|
||||||
|
|
||||||
|
// Start update loop in main thread
|
||||||
|
self.update_loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) {
|
||||||
|
info!("Stopping VDD Service...");
|
||||||
|
self.running.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_hardware_info(&self) -> Option<crate::monitor::HardwareInfo> {
|
||||||
|
self.hardware_detector.as_ref()
|
||||||
|
.and_then(|detector| detector.get_hardware_info().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_loop(&self) -> Result<()> {
|
||||||
|
info!("VDD update loop started");
|
||||||
|
|
||||||
|
while self.running.load(Ordering::SeqCst) {
|
||||||
|
if let Err(e) = self.device_controller.update(&self.handle) {
|
||||||
|
error!("Failed to update VDD: {}", e);
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(self.config.vdd_update_interval_ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("VDD update loop stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VddService {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
info!("VDD Service dropped");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DisplayConfig {
|
||||||
|
pub auto_manage_virtual_display: bool,
|
||||||
|
pub prefer_wmi_detection: bool,
|
||||||
|
pub fallback_to_gdi: bool,
|
||||||
|
pub monitor_check_interval_ms: u64,
|
||||||
|
pub hardware_detection_timeout_ms: u32,
|
||||||
|
pub virtual_display_settings: VirtualDisplaySettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VirtualDisplaySettings {
|
||||||
|
pub default_width: u32,
|
||||||
|
pub default_height: u32,
|
||||||
|
pub default_refresh_rate: u32,
|
||||||
|
pub auto_remove_on_physical_connect: bool,
|
||||||
|
pub startup_delay_ms: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DisplayConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
auto_manage_virtual_display: true,
|
||||||
|
prefer_wmi_detection: true,
|
||||||
|
fallback_to_gdi: true,
|
||||||
|
monitor_check_interval_ms: 1000,
|
||||||
|
hardware_detection_timeout_ms: 5000,
|
||||||
|
virtual_display_settings: VirtualDisplaySettings::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VirtualDisplaySettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
default_width: 1920,
|
||||||
|
default_height: 1080,
|
||||||
|
default_refresh_rate: 60,
|
||||||
|
auto_remove_on_physical_connect: true,
|
||||||
|
startup_delay_ms: 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
use windows::core::GUID;
|
||||||
|
|
||||||
|
mod display;
|
||||||
|
pub use display::{DisplayConfig, VirtualDisplaySettings};
|
||||||
|
|
||||||
|
// Constants from the C header file
|
||||||
|
pub const VDD_DISPLAY_ID: &[u8] = b"PSCCDD0\0";
|
||||||
|
pub const VDD_DISPLAY_NAME: &[u8] = b"ParsecVDA\0";
|
||||||
|
|
||||||
|
// {00b41627-04c4-429e-a26e-0265cf50c8fa}
|
||||||
|
pub const VDD_ADAPTER_GUID: GUID = GUID::from_u128(0x00b41627_04c4_429e_a26e_0265cf50c8fa);
|
||||||
|
pub const VDD_ADAPTER_NAME: &[u8] = b"Parsec Virtual Display Adapter\0";
|
||||||
|
|
||||||
|
// {4d36e968-e325-11ce-bfc1-08002be10318}
|
||||||
|
pub const VDD_CLASS_GUID: GUID = GUID::from_u128(0x4d36e968_e325_11ce_bfc1_08002be10318);
|
||||||
|
pub const VDD_HARDWARE_ID: &[u8] = b"Root\\Parsec\\VDA\0";
|
||||||
|
|
||||||
|
pub const VDD_MAX_DISPLAYS: usize = 8;
|
||||||
|
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum VddCtlCode {
|
||||||
|
Add = 0x0022e004,
|
||||||
|
Remove = 0x0022a008,
|
||||||
|
Update = 0x0022a00c,
|
||||||
|
Version = 0x0022e010,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub monitor_check_interval_ms: u64,
|
||||||
|
pub vdd_update_interval_ms: u64,
|
||||||
|
pub io_timeout_ms: u32,
|
||||||
|
pub display: DisplayConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
monitor_check_interval_ms: 1000,
|
||||||
|
vdd_update_interval_ms: 100,
|
||||||
|
io_timeout_ms: 5000,
|
||||||
|
display: DisplayConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
use std::mem::{size_of, zeroed};
|
||||||
|
use windows::{
|
||||||
|
Win32::{
|
||||||
|
Foundation::{CloseHandle, ERROR_IO_PENDING, GetLastError, HANDLE, INVALID_HANDLE_VALUE},
|
||||||
|
System::{
|
||||||
|
IO::{DeviceIoControl, GetOverlappedResultEx, OVERLAPPED},
|
||||||
|
Threading::CreateEventA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use crate::device::handle::VddHandle;
|
||||||
|
use crate::config::{VddCtlCode, Config, VDD_MAX_DISPLAYS};
|
||||||
|
use crate::error::{Result, VddError};
|
||||||
|
|
||||||
|
pub struct DeviceController {
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceController {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_version(&self, handle: &VddHandle) -> Result<i32> {
|
||||||
|
self.io_control(handle, VddCtlCode::Version, None)
|
||||||
|
.map(|v| v as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&self, handle: &VddHandle) -> Result<()> {
|
||||||
|
self.io_control(handle, VddCtlCode::Update, None)
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_display(&self, handle: &VddHandle) -> Result<i32> {
|
||||||
|
let result = self.io_control(handle, VddCtlCode::Add, None)?;
|
||||||
|
self.update(handle)?; // Ping immediately after adding
|
||||||
|
Ok(result as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_display(&self, handle: &VddHandle, index: i32) -> Result<()> {
|
||||||
|
if index < 0 || index >= VDD_MAX_DISPLAYS as i32 {
|
||||||
|
return Err(VddError::InvalidDisplayIndex(index, VDD_MAX_DISPLAYS));
|
||||||
|
}
|
||||||
|
|
||||||
|
let index_data = (index as u16).to_be_bytes();
|
||||||
|
self.io_control(handle, VddCtlCode::Remove, Some(&index_data))?;
|
||||||
|
self.update(handle)?; // Ping immediately after removing
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn io_control(&self, handle: &VddHandle, code: VddCtlCode, data: Option<&[u8]>) -> Result<u32> {
|
||||||
|
let raw_handle = handle.raw_handle();
|
||||||
|
if raw_handle == INVALID_HANDLE_VALUE {
|
||||||
|
return Err(VddError::InvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let mut in_buffer = [0u8; 32];
|
||||||
|
if let Some(d) = data {
|
||||||
|
let len = d.len().min(in_buffer.len());
|
||||||
|
in_buffer[..len].copy_from_slice(&d[..len]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut overlapped: OVERLAPPED = zeroed();
|
||||||
|
let event = CreateEventA(None, true, false, None).map_err(|_| VddError::IoError)?;
|
||||||
|
overlapped.hEvent = event;
|
||||||
|
|
||||||
|
let mut out_buffer = 0u32;
|
||||||
|
let mut bytes_transferred = 0;
|
||||||
|
|
||||||
|
let result = DeviceIoControl(
|
||||||
|
raw_handle,
|
||||||
|
code as u32,
|
||||||
|
Some(in_buffer.as_ptr() as *const _),
|
||||||
|
in_buffer.len() as u32,
|
||||||
|
Some(&mut out_buffer as *mut _ as *mut _),
|
||||||
|
size_of::<u32>() as u32,
|
||||||
|
None,
|
||||||
|
Some(&mut overlapped),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle asynchronous operation
|
||||||
|
let final_result = if result.is_err() && GetLastError() == ERROR_IO_PENDING {
|
||||||
|
GetOverlappedResultEx(
|
||||||
|
raw_handle,
|
||||||
|
&overlapped,
|
||||||
|
&mut bytes_transferred,
|
||||||
|
self.config.io_timeout_ms,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.map_err(|_| VddError::Timeout)
|
||||||
|
} else {
|
||||||
|
result.map_err(|_| VddError::IoError)
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = CloseHandle(event);
|
||||||
|
|
||||||
|
match final_result {
|
||||||
|
Ok(_) => Ok(out_buffer),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for DeviceController {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
config: self.config.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
use std::mem::zeroed;
|
||||||
|
use windows::{
|
||||||
|
Win32::{
|
||||||
|
Devices::DeviceAndDriverInstallation::{
|
||||||
|
DIGCF_DEVICEINTERFACE, DIGCF_PRESENT, HDEVINFO, SETUP_DI_GET_CLASS_DEVS_FLAGS,
|
||||||
|
SP_DEVICE_INTERFACE_DATA, SP_DEVICE_INTERFACE_DETAIL_DATA_A,
|
||||||
|
SetupDiDestroyDeviceInfoList, SetupDiEnumDeviceInterfaces,
|
||||||
|
SetupDiGetClassDevsA, SetupDiGetDeviceInterfaceDetailA,
|
||||||
|
},
|
||||||
|
Foundation::{
|
||||||
|
CloseHandle, GENERIC_READ, GENERIC_WRITE, 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
core::{GUID, PCSTR},
|
||||||
|
};
|
||||||
|
use crate::error::{Result, VddError};
|
||||||
|
use std::mem::size_of;
|
||||||
|
|
||||||
|
pub struct VddHandle(HANDLE);
|
||||||
|
|
||||||
|
unsafe impl Send for VddHandle {}
|
||||||
|
unsafe impl Sync for VddHandle {}
|
||||||
|
|
||||||
|
impl VddHandle {
|
||||||
|
pub fn open(interface_guid: &GUID) -> Result<Self> {
|
||||||
|
open_device_handle(interface_guid)
|
||||||
|
.ok_or(VddError::DeviceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn raw_handle(&self) -> HANDLE {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VddHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
close_device_handle(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAII wrapper for HDEVINFO to ensure SetupDiDestroyDeviceInfoList is always called.
|
||||||
|
struct DevInfo(HDEVINFO);
|
||||||
|
|
||||||
|
impl DevInfo {
|
||||||
|
fn new(class_guid: Option<&GUID>, flags: SETUP_DI_GET_CLASS_DEVS_FLAGS) -> Option<Self> {
|
||||||
|
unsafe {
|
||||||
|
let class_guid_ptr = class_guid.map(|g| g as *const GUID);
|
||||||
|
let handle = SetupDiGetClassDevsA(class_guid_ptr, None, None, flags).ok()?;
|
||||||
|
Some(DevInfo(handle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DevInfo {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.0 != HDEVINFO(INVALID_HANDLE_VALUE.0 as isize) {
|
||||||
|
unsafe {
|
||||||
|
let _ = SetupDiDestroyDeviceInfoList(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_device_handle(interface_guid: &GUID) -> Option<VddHandle> {
|
||||||
|
let dev_info = DevInfo::new(Some(interface_guid), DIGCF_PRESENT | DIGCF_DEVICEINTERFACE)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
for i in 0.. {
|
||||||
|
let mut dev_interface_data: SP_DEVICE_INTERFACE_DATA = zeroed();
|
||||||
|
dev_interface_data.cbSize = size_of::<SP_DEVICE_INTERFACE_DATA>() as u32;
|
||||||
|
|
||||||
|
if SetupDiEnumDeviceInterfaces(
|
||||||
|
dev_info.0,
|
||||||
|
None,
|
||||||
|
interface_guid,
|
||||||
|
i,
|
||||||
|
&mut dev_interface_data,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(device_path) = get_device_path(&dev_info, &dev_interface_data) {
|
||||||
|
if let Some(handle) = create_device_handle(&device_path) {
|
||||||
|
return Some(VddHandle(handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_device_path(
|
||||||
|
dev_info: &DevInfo,
|
||||||
|
dev_interface_data: &SP_DEVICE_INTERFACE_DATA,
|
||||||
|
) -> Option<PCSTR> {
|
||||||
|
unsafe {
|
||||||
|
let mut detail_size = 0;
|
||||||
|
let _ = SetupDiGetDeviceInterfaceDetailA(
|
||||||
|
dev_info.0,
|
||||||
|
dev_interface_data,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
Some(&mut detail_size),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
if detail_size == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut detail_buffer = vec![0u8; detail_size as usize];
|
||||||
|
let detail = detail_buffer.as_mut_ptr() as *mut SP_DEVICE_INTERFACE_DETAIL_DATA_A;
|
||||||
|
(*detail).cbSize = size_of::<SP_DEVICE_INTERFACE_DETAIL_DATA_A>() as u32;
|
||||||
|
|
||||||
|
SetupDiGetDeviceInterfaceDetailA(
|
||||||
|
dev_info.0,
|
||||||
|
dev_interface_data,
|
||||||
|
Some(detail),
|
||||||
|
detail_size,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
Some(PCSTR((*detail).DevicePath.as_ptr() as *const u8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_device_handle(device_path: &PCSTR) -> Option<HANDLE> {
|
||||||
|
unsafe {
|
||||||
|
CreateFileA(
|
||||||
|
*device_path,
|
||||||
|
(GENERIC_READ | GENERIC_WRITE).0,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
None,
|
||||||
|
OPEN_EXISTING,
|
||||||
|
FILE_ATTRIBUTE_NORMAL
|
||||||
|
| FILE_FLAG_NO_BUFFERING
|
||||||
|
| FILE_FLAG_OVERLAPPED
|
||||||
|
| FILE_FLAG_WRITE_THROUGH,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_device_handle(handle: HANDLE) {
|
||||||
|
if handle != INVALID_HANDLE_VALUE {
|
||||||
|
unsafe {
|
||||||
|
let _ = CloseHandle(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
mod handle;
|
||||||
|
mod status;
|
||||||
|
mod control;
|
||||||
|
|
||||||
|
pub use handle::VddHandle;
|
||||||
|
pub use status::DeviceStatusChecker;
|
||||||
|
pub use control::DeviceController;
|
|
@ -0,0 +1,165 @@
|
||||||
|
use std::mem::{size_of, zeroed};
|
||||||
|
use windows::{
|
||||||
|
Win32::{
|
||||||
|
Devices::DeviceAndDriverInstallation::{
|
||||||
|
CM_DEVNODE_STATUS_FLAGS, CM_Get_DevNode_Status, CM_PROB, CM_PROB_DISABLED,
|
||||||
|
CM_PROB_DISABLED_SERVICE, CM_PROB_FAILED_POST_START, CM_PROB_HARDWARE_DISABLED,
|
||||||
|
CM_PROB_NEED_RESTART, CONFIGRET, DIGCF_PRESENT, DN_DRIVER_LOADED, DN_HAS_PROBLEM,
|
||||||
|
DN_STARTED, HDEVINFO, SP_DEVINFO_DATA, SPDRP_HARDWAREID, SetupDiEnumDeviceInfo,
|
||||||
|
SetupDiGetClassDevsA, SetupDiGetDeviceRegistryPropertyA,
|
||||||
|
},
|
||||||
|
Foundation::INVALID_HANDLE_VALUE,
|
||||||
|
System::Registry::{REG_MULTI_SZ, REG_SZ},
|
||||||
|
},
|
||||||
|
core::GUID,
|
||||||
|
};
|
||||||
|
use crate::error::{DeviceStatus, Result, VddError};
|
||||||
|
|
||||||
|
pub struct DeviceStatusChecker;
|
||||||
|
|
||||||
|
impl DeviceStatusChecker {
|
||||||
|
pub fn check_driver_status(class_guid: &GUID, device_id: &[u8]) -> Result<()> {
|
||||||
|
let status = query_device_status(class_guid, device_id);
|
||||||
|
match status {
|
||||||
|
DeviceStatus::Ok => Ok(()),
|
||||||
|
other => Err(VddError::DeviceStatus(other)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAII wrapper for HDEVINFO to ensure SetupDiDestroyDeviceInfoList is always called.
|
||||||
|
struct DevInfo(HDEVINFO);
|
||||||
|
|
||||||
|
impl DevInfo {
|
||||||
|
fn new(class_guid: Option<&GUID>, flags: u32) -> Option<Self> {
|
||||||
|
unsafe {
|
||||||
|
let class_guid_ptr = class_guid.map(|g| g as *const GUID);
|
||||||
|
let handle = SetupDiGetClassDevsA(class_guid_ptr, None, None, flags.into()).ok()?;
|
||||||
|
Some(DevInfo(handle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DevInfo {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.0 != HDEVINFO(INVALID_HANDLE_VALUE.0 as isize) {
|
||||||
|
unsafe {
|
||||||
|
let _ = windows::Win32::Devices::DeviceAndDriverInstallation::SetupDiDestroyDeviceInfoList(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_device_status(class_guid: &GUID, device_id: &[u8]) -> DeviceStatus {
|
||||||
|
let dev_info = match DevInfo::new(Some(class_guid), DIGCF_PRESENT) {
|
||||||
|
Some(info) => info,
|
||||||
|
None => return DeviceStatus::Inaccessible,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure device_id is null-terminated
|
||||||
|
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();
|
||||||
|
dev_info_data.cbSize = size_of::<SP_DEVINFO_DATA>() as u32;
|
||||||
|
|
||||||
|
if SetupDiEnumDeviceInfo(dev_info.0, device_index, &mut dev_info_data).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(hardware_ids) = get_device_hardware_ids(&dev_info, &dev_info_data) {
|
||||||
|
let target_id = &device_id[..device_id.len() - 1];
|
||||||
|
if hardware_ids.iter().any(|id| id.as_slice() == target_id) {
|
||||||
|
return get_device_status(&dev_info_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceStatus::NotInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_device_hardware_ids(
|
||||||
|
dev_info: &DevInfo,
|
||||||
|
dev_info_data: &SP_DEVINFO_DATA,
|
||||||
|
) -> Option<Vec<Vec<u8>>> {
|
||||||
|
unsafe {
|
||||||
|
let mut required_size = 0;
|
||||||
|
let _ = SetupDiGetDeviceRegistryPropertyA(
|
||||||
|
dev_info.0,
|
||||||
|
dev_info_data,
|
||||||
|
SPDRP_HARDWAREID,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(&mut required_size),
|
||||||
|
);
|
||||||
|
|
||||||
|
if required_size == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prop_buffer = vec![0u8; required_size as usize];
|
||||||
|
let mut reg_data_type = 0;
|
||||||
|
|
||||||
|
SetupDiGetDeviceRegistryPropertyA(
|
||||||
|
dev_info.0,
|
||||||
|
dev_info_data,
|
||||||
|
SPDRP_HARDWAREID,
|
||||||
|
Some(&mut reg_data_type),
|
||||||
|
Some(prop_buffer.as_mut_slice()),
|
||||||
|
Some(&mut required_size),
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
if reg_data_type == REG_SZ.0 || reg_data_type == REG_MULTI_SZ.0 {
|
||||||
|
Some(
|
||||||
|
prop_buffer
|
||||||
|
.split(|&b| b == 0)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_vec())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_device_status(dev_info_data: &SP_DEVINFO_DATA) -> DeviceStatus {
|
||||||
|
unsafe {
|
||||||
|
let mut dev_status = CM_DEVNODE_STATUS_FLAGS::default();
|
||||||
|
let mut dev_problem_num = CM_PROB::default();
|
||||||
|
|
||||||
|
if CM_Get_DevNode_Status(
|
||||||
|
&mut dev_status,
|
||||||
|
&mut dev_problem_num,
|
||||||
|
dev_info_data.DevInst,
|
||||||
|
0,
|
||||||
|
) != CONFIGRET(0)
|
||||||
|
{
|
||||||
|
return DeviceStatus::NotInstalled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if device is running normally
|
||||||
|
if (dev_status & (DN_DRIVER_LOADED | DN_STARTED)).0 == (DN_DRIVER_LOADED | DN_STARTED).0 {
|
||||||
|
return DeviceStatus::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for problems
|
||||||
|
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
|
||||||
|
}
|
||||||
|
n if n == CM_PROB_DISABLED_SERVICE => DeviceStatus::DisabledService,
|
||||||
|
n if n == CM_PROB_FAILED_POST_START => DeviceStatus::DriverError,
|
||||||
|
_ => DeviceStatus::UnknownProblem,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DeviceStatus::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
use crate::display::virtual_display::{VirtualDisplay, VirtualDisplayController};
|
||||||
|
use crate::monitor::MonitorDetector;
|
||||||
|
use crate::device::VddHandle;
|
||||||
|
use crate::error::Result;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
pub struct DisplayManager {
|
||||||
|
virtual_display_controller: VirtualDisplayController,
|
||||||
|
current_display: Option<VirtualDisplay>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayManager {
|
||||||
|
pub fn new(virtual_display_controller: VirtualDisplayController) -> Self {
|
||||||
|
Self {
|
||||||
|
virtual_display_controller,
|
||||||
|
current_display: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_displays(&mut self, handle: &VddHandle) -> Result<()> {
|
||||||
|
let has_physical = MonitorDetector::has_physical_monitors();
|
||||||
|
let has_virtual = self.current_display.is_some();
|
||||||
|
|
||||||
|
match (has_physical, has_virtual) {
|
||||||
|
(false, false) => {
|
||||||
|
info!("No physical monitors detected, starting virtual display");
|
||||||
|
self.start_virtual_display(handle)?;
|
||||||
|
}
|
||||||
|
(true, true) => {
|
||||||
|
info!("Physical monitor detected, stopping virtual display");
|
||||||
|
self.stop_virtual_display(handle)?;
|
||||||
|
}
|
||||||
|
_ => {} // No change needed
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_virtual_display(&mut self, handle: &VddHandle) -> Result<()> {
|
||||||
|
if self.current_display.is_none() {
|
||||||
|
let display = self.virtual_display_controller.create_display(handle)?;
|
||||||
|
info!("Virtual display added with index: {}", display.index());
|
||||||
|
self.current_display = Some(display);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_virtual_display(&mut self, handle: &VddHandle) -> Result<()> {
|
||||||
|
if let Some(display) = self.current_display.take() {
|
||||||
|
self.virtual_display_controller.remove_display(handle, display)?;
|
||||||
|
info!("Virtual display removed");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
mod virtual_display;
|
||||||
|
mod manager;
|
||||||
|
|
||||||
|
pub use virtual_display::{VirtualDisplay, VirtualDisplayController};
|
||||||
|
pub use manager::DisplayManager;
|
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::device::{VddHandle, DeviceController};
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VirtualDisplay {
|
||||||
|
index: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualDisplay {
|
||||||
|
pub fn new(index: i32) -> Self {
|
||||||
|
Self { index }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index(&self) -> i32 {
|
||||||
|
self.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VirtualDisplayController {
|
||||||
|
device_controller: DeviceController,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualDisplayController {
|
||||||
|
pub fn new(device_controller: DeviceController) -> Self {
|
||||||
|
Self { device_controller }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_display(&self, handle: &VddHandle) -> Result<VirtualDisplay> {
|
||||||
|
let index = self.device_controller.add_display(handle)?;
|
||||||
|
Ok(VirtualDisplay::new(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_display(&self, handle: &VddHandle, display: VirtualDisplay) -> Result<()> {
|
||||||
|
self.device_controller.remove_display(handle, display.index())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::config::VDD_MAX_DISPLAYS;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum VddError {
|
||||||
|
#[error("Device not found")]
|
||||||
|
DeviceNotFound,
|
||||||
|
#[error("Invalid handle")]
|
||||||
|
InvalidHandle,
|
||||||
|
#[error("I/O operation failed")]
|
||||||
|
IoError,
|
||||||
|
#[error("Operation timed out")]
|
||||||
|
Timeout,
|
||||||
|
#[error("Device status error: {0:?}")]
|
||||||
|
DeviceStatus(DeviceStatus),
|
||||||
|
#[error("Failed to add virtual display")]
|
||||||
|
AddDisplayFailed,
|
||||||
|
#[error("Failed to remove virtual display")]
|
||||||
|
RemoveDisplayFailed,
|
||||||
|
#[error("Invalid display index: {0} (max: {1})")]
|
||||||
|
InvalidDisplayIndex(i32, usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, VddError>;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum DeviceStatus {
|
||||||
|
Ok = 0,
|
||||||
|
Inaccessible,
|
||||||
|
Unknown,
|
||||||
|
UnknownProblem,
|
||||||
|
Disabled,
|
||||||
|
DriverError,
|
||||||
|
RestartRequired,
|
||||||
|
DisabledService,
|
||||||
|
NotInstalled,
|
||||||
|
}
|
469
src/lib.rs
469
src/lib.rs
|
@ -1,460 +1,11 @@
|
||||||
#[allow(non_snake_case)]
|
pub mod app;
|
||||||
use std::mem::{size_of, zeroed};
|
pub mod device;
|
||||||
use log::{trace,debug,info};
|
pub mod display;
|
||||||
use windows::{
|
pub mod monitor;
|
||||||
Win32::{
|
pub mod error;
|
||||||
Devices::DeviceAndDriverInstallation::{
|
pub mod config;
|
||||||
CM_DEVNODE_STATUS_FLAGS, CM_Get_DevNode_Status, CM_PROB, CM_PROB_DISABLED,
|
|
||||||
CM_PROB_DISABLED_SERVICE, CM_PROB_FAILED_POST_START, CM_PROB_HARDWARE_DISABLED,
|
|
||||||
CM_PROB_NEED_RESTART, CONFIGRET, DIGCF_DEVICEINTERFACE, DIGCF_PRESENT,
|
|
||||||
DN_DRIVER_LOADED, DN_HAS_PROBLEM, DN_STARTED, HDEVINFO, SETUP_DI_GET_CLASS_DEVS_FLAGS,
|
|
||||||
SP_DEVICE_INTERFACE_DATA, SP_DEVICE_INTERFACE_DETAIL_DATA_A, SP_DEVINFO_DATA,
|
|
||||||
SPDRP_HARDWAREID, SetupDiDestroyDeviceInfoList, SetupDiEnumDeviceInfo,
|
|
||||||
SetupDiEnumDeviceInterfaces, SetupDiGetClassDevsA, SetupDiGetDeviceInterfaceDetailA,
|
|
||||||
SetupDiGetDeviceRegistryPropertyA,
|
|
||||||
},
|
|
||||||
Foundation::{
|
|
||||||
CloseHandle, ERROR_IO_PENDING, GENERIC_READ, GENERIC_WRITE, GetLastError, HANDLE,
|
|
||||||
INVALID_HANDLE_VALUE,
|
|
||||||
},
|
|
||||||
Graphics::Gdi::{
|
|
||||||
DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_STATE_FLAGS, DISPLAY_DEVICEW, EnumDisplayDevicesW,
|
|
||||||
},
|
|
||||||
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},
|
|
||||||
Registry::{REG_MULTI_SZ, REG_SZ},
|
|
||||||
Threading::CreateEventA,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
core::{GUID, PCSTR, PWSTR},
|
|
||||||
};
|
|
||||||
|
|
||||||
// RAII wrapper for HDEVINFO to ensure SetupDiDestroyDeviceInfoList is always called.
|
// Re-export main types for public API
|
||||||
struct DevInfo(HDEVINFO);
|
pub use app::VddService;
|
||||||
|
pub use error::{VddError, Result, DeviceStatus};
|
||||||
impl DevInfo {
|
pub use device::VddHandle;
|
||||||
fn new(class_guid: Option<&GUID>, flags: SETUP_DI_GET_CLASS_DEVS_FLAGS) -> Option<Self> {
|
|
||||||
unsafe {
|
|
||||||
let class_guid_ptr = class_guid.map(|g| g as *const GUID);
|
|
||||||
let handle = SetupDiGetClassDevsA(class_guid_ptr, None, None, flags).ok()?;
|
|
||||||
Some(DevInfo(handle))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for DevInfo {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.0 != HDEVINFO(INVALID_HANDLE_VALUE.0 as isize) {
|
|
||||||
unsafe {
|
|
||||||
let _ = SetupDiDestroyDeviceInfoList(self.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RAII wrapper for HANDLE to ensure CloseHandle is always called.
|
|
||||||
// We expose this as the public handle type.
|
|
||||||
pub struct VddHandle(HANDLE);
|
|
||||||
|
|
||||||
unsafe impl Send for VddHandle {}
|
|
||||||
unsafe impl Sync for VddHandle {}
|
|
||||||
|
|
||||||
impl Drop for VddHandle {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
close_device_handle(self.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device Status Enum
|
|
||||||
// Derived traits allow for easy comparison, printing, and copying.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum DeviceStatus {
|
|
||||||
Ok = 0,
|
|
||||||
Inaccessible,
|
|
||||||
Unknown,
|
|
||||||
UnknownProblem,
|
|
||||||
Disabled,
|
|
||||||
DriverError,
|
|
||||||
RestartRequired,
|
|
||||||
DisabledService,
|
|
||||||
NotInstalled,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constants from the C header file.
|
|
||||||
// GUIDs are created from their string representations.
|
|
||||||
// C-style strings are represented as null-terminated byte slices.
|
|
||||||
pub const VDD_DISPLAY_ID: &[u8] = b"PSCCDD0\0";
|
|
||||||
pub const VDD_DISPLAY_NAME: &[u8] = b"ParsecVDA\0";
|
|
||||||
|
|
||||||
// {00b41627-04c4-429e-a26e-0265cf50c8fa}
|
|
||||||
pub const VDD_ADAPTER_GUID: GUID = GUID::from_u128(0x00b41627_04c4_429e_a26e_0265cf50c8fa);
|
|
||||||
pub const VDD_ADAPTER_NAME: &[u8] = b"Parsec Virtual Display Adapter\0";
|
|
||||||
|
|
||||||
// {4d36e968-e325-11ce-bfc1-08002be10318}
|
|
||||||
pub const VDD_CLASS_GUID: GUID = GUID::from_u128(0x4d36e968_e325_11ce_bfc1_08002be10318);
|
|
||||||
pub const VDD_HARDWARE_ID: &[u8] = b"Root\\Parsec\\VDA\0";
|
|
||||||
|
|
||||||
pub const VDD_MAX_DISPLAYS: usize = 8;
|
|
||||||
|
|
||||||
// IOCTL codes
|
|
||||||
#[repr(u32)]
|
|
||||||
pub enum VddCtlCode {
|
|
||||||
Add = 0x0022e004,
|
|
||||||
Remove = 0x0022a008,
|
|
||||||
Update = 0x0022a00c,
|
|
||||||
Version = 0x0022e010,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误处理类型
|
|
||||||
#[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) {
|
|
||||||
Some(info) => info,
|
|
||||||
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();
|
|
||||||
dev_info_data.cbSize = size_of::<SP_DEVINFO_DATA>() as u32;
|
|
||||||
|
|
||||||
if SetupDiEnumDeviceInfo(dev_info.0, device_index, &mut dev_info_data).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(hardware_ids) = get_device_hardware_ids(&dev_info, &dev_info_data) {
|
|
||||||
let target_id = &device_id[..device_id.len() - 1];
|
|
||||||
if hardware_ids.iter().any(|id| id.as_slice() == target_id) {
|
|
||||||
return get_device_status(&dev_info_data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DeviceStatus::NotInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取硬件ID获取逻辑
|
|
||||||
fn get_device_hardware_ids(
|
|
||||||
dev_info: &DevInfo,
|
|
||||||
dev_info_data: &SP_DEVINFO_DATA,
|
|
||||||
) -> Option<Vec<Vec<u8>>> {
|
|
||||||
unsafe {
|
|
||||||
let mut required_size = 0;
|
|
||||||
let _ = SetupDiGetDeviceRegistryPropertyA(
|
|
||||||
dev_info.0,
|
|
||||||
dev_info_data,
|
|
||||||
SPDRP_HARDWAREID,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(&mut required_size),
|
|
||||||
);
|
|
||||||
|
|
||||||
if required_size == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut prop_buffer = vec![0u8; required_size as usize];
|
|
||||||
let mut reg_data_type = 0;
|
|
||||||
|
|
||||||
SetupDiGetDeviceRegistryPropertyA(
|
|
||||||
dev_info.0,
|
|
||||||
dev_info_data,
|
|
||||||
SPDRP_HARDWAREID,
|
|
||||||
Some(&mut reg_data_type),
|
|
||||||
Some(prop_buffer.as_mut_slice()),
|
|
||||||
Some(&mut required_size),
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
if reg_data_type == REG_SZ.0 || reg_data_type == REG_MULTI_SZ.0 {
|
|
||||||
Some(
|
|
||||||
prop_buffer
|
|
||||||
.split(|&b| b == 0)
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.map(|s| s.to_vec())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取设备状态获取逻辑
|
|
||||||
fn get_device_status(dev_info_data: &SP_DEVINFO_DATA) -> DeviceStatus {
|
|
||||||
unsafe {
|
|
||||||
let mut dev_status = CM_DEVNODE_STATUS_FLAGS::default();
|
|
||||||
let mut dev_problem_num = CM_PROB::default();
|
|
||||||
|
|
||||||
if CM_Get_DevNode_Status(
|
|
||||||
&mut dev_status,
|
|
||||||
&mut dev_problem_num,
|
|
||||||
dev_info_data.DevInst,
|
|
||||||
0,
|
|
||||||
) != CONFIGRET(0)
|
|
||||||
{
|
|
||||||
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 {
|
|
||||||
n if n == CM_PROB_NEED_RESTART => DeviceStatus::RestartRequired,
|
|
||||||
n if n == CM_PROB_DISABLED || n == CM_PROB_HARDWARE_DISABLED => {
|
|
||||||
DeviceStatus::Disabled
|
|
||||||
}
|
|
||||||
n if n == CM_PROB_DISABLED_SERVICE => DeviceStatus::DisabledService,
|
|
||||||
n if n == CM_PROB_FAILED_POST_START => DeviceStatus::DriverError,
|
|
||||||
_ => DeviceStatus::UnknownProblem,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DeviceStatus::Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtain the device handle. Returns None if it fails.
|
|
||||||
pub fn open_device_handle(interface_guid: &GUID) -> Option<VddHandle> {
|
|
||||||
let dev_info = DevInfo::new(Some(interface_guid), DIGCF_PRESENT | DIGCF_DEVICEINTERFACE)?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
for i in 0.. {
|
|
||||||
let mut dev_interface_data: SP_DEVICE_INTERFACE_DATA = zeroed();
|
|
||||||
dev_interface_data.cbSize = size_of::<SP_DEVICE_INTERFACE_DATA>() as u32;
|
|
||||||
|
|
||||||
if SetupDiEnumDeviceInterfaces(
|
|
||||||
dev_info.0,
|
|
||||||
None,
|
|
||||||
interface_guid,
|
|
||||||
i,
|
|
||||||
&mut dev_interface_data,
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(device_path) = get_device_path(&dev_info, &dev_interface_data) {
|
|
||||||
if let Some(handle) = create_device_handle(&device_path) {
|
|
||||||
return Some(VddHandle(handle));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取设备路径获取逻辑
|
|
||||||
fn get_device_path(
|
|
||||||
dev_info: &DevInfo,
|
|
||||||
dev_interface_data: &SP_DEVICE_INTERFACE_DATA,
|
|
||||||
) -> Option<PCSTR> {
|
|
||||||
unsafe {
|
|
||||||
let mut detail_size = 0;
|
|
||||||
let _ = SetupDiGetDeviceInterfaceDetailA(
|
|
||||||
dev_info.0,
|
|
||||||
dev_interface_data,
|
|
||||||
None,
|
|
||||||
0,
|
|
||||||
Some(&mut detail_size),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
if detail_size == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut detail_buffer = vec![0u8; detail_size as usize];
|
|
||||||
let detail = detail_buffer.as_mut_ptr() as *mut SP_DEVICE_INTERFACE_DETAIL_DATA_A;
|
|
||||||
(*detail).cbSize = size_of::<SP_DEVICE_INTERFACE_DETAIL_DATA_A>() as u32;
|
|
||||||
|
|
||||||
SetupDiGetDeviceInterfaceDetailA(
|
|
||||||
dev_info.0,
|
|
||||||
dev_interface_data,
|
|
||||||
Some(detail),
|
|
||||||
detail_size,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
Some(PCSTR((*detail).DevicePath.as_ptr() as *const u8))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取文件句柄创建逻辑
|
|
||||||
fn create_device_handle(device_path: &PCSTR) -> Option<HANDLE> {
|
|
||||||
unsafe {
|
|
||||||
CreateFileA(
|
|
||||||
*device_path,
|
|
||||||
(GENERIC_READ | GENERIC_WRITE).0,
|
|
||||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
||||||
None,
|
|
||||||
OPEN_EXISTING,
|
|
||||||
FILE_ATTRIBUTE_NORMAL
|
|
||||||
| FILE_FLAG_NO_BUFFERING
|
|
||||||
| FILE_FLAG_OVERLAPPED
|
|
||||||
| FILE_FLAG_WRITE_THROUGH,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Release the device handle.
|
|
||||||
fn close_device_handle(handle: HANDLE) {
|
|
||||||
if handle != INVALID_HANDLE_VALUE {
|
|
||||||
unsafe {
|
|
||||||
let _ = CloseHandle(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let mut in_buffer = [0u8; 32];
|
|
||||||
if let Some(d) = data {
|
|
||||||
let len = d.len().min(in_buffer.len());
|
|
||||||
in_buffer[..len].copy_from_slice(&d[..len]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut overlapped: OVERLAPPED = zeroed();
|
|
||||||
let event = CreateEventA(None, true, false, None).map_err(|_| VddError::IoError)?;
|
|
||||||
overlapped.hEvent = event;
|
|
||||||
|
|
||||||
let mut out_buffer = 0u32;
|
|
||||||
let mut bytes_transferred = 0;
|
|
||||||
|
|
||||||
let result = DeviceIoControl(
|
|
||||||
vdd.0,
|
|
||||||
code as u32,
|
|
||||||
Some(in_buffer.as_ptr() as *const _),
|
|
||||||
in_buffer.len() as u32,
|
|
||||||
Some(&mut out_buffer as *mut _ as *mut _),
|
|
||||||
size_of::<u32>() as u32,
|
|
||||||
None,
|
|
||||||
Some(&mut overlapped),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 处理异步操作
|
|
||||||
let final_result = if result.is_err() && GetLastError() == ERROR_IO_PENDING {
|
|
||||||
GetOverlappedResultEx(
|
|
||||||
vdd.0,
|
|
||||||
&overlapped,
|
|
||||||
&mut bytes_transferred,
|
|
||||||
5000, // 5秒超时
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.map_err(|_| VddError::Timeout)
|
|
||||||
} else {
|
|
||||||
result.map_err(|_| VddError::IoError)
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = CloseHandle(event);
|
|
||||||
|
|
||||||
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)
|
|
||||||
.ok()
|
|
||||||
.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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()?;
|
|
||||||
vdd_update(vdd); // Ping immediately after adding
|
|
||||||
Some(result 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_physical_monitor_connected() -> bool {
|
|
||||||
// fetch windows monitor lists.
|
|
||||||
let mut physical_monitors = Vec::new();
|
|
||||||
let mut info = DISPLAY_DEVICEW {
|
|
||||||
cb: std::mem::size_of::<DISPLAY_DEVICEW>() as u32,
|
|
||||||
DeviceName: [0; 32],
|
|
||||||
DeviceString: [0; 128],
|
|
||||||
StateFlags: DISPLAY_DEVICE_STATE_FLAGS(0),
|
|
||||||
DeviceID: [0; 128],
|
|
||||||
DeviceKey: [0; 128],
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!("Found {} physical monitors", physical_monitors.len());
|
|
||||||
for (i, monitor) in physical_monitors.iter().enumerate() {
|
|
||||||
trace!("Physical Monitor {}: {}", i, monitor);
|
|
||||||
}
|
|
||||||
|
|
||||||
!physical_monitors.is_empty()
|
|
||||||
}
|
|
||||||
|
|
81
src/main.rs
81
src/main.rs
|
@ -1,47 +1,52 @@
|
||||||
use std::sync::{
|
use parsec_vdd::app::VddService;
|
||||||
Arc,
|
use log::{error, info};
|
||||||
atomic::{AtomicI32, Ordering},
|
use env_logger::Env;
|
||||||
};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
use parsec_vdd::{
|
|
||||||
DeviceStatus, VDD_ADAPTER_GUID, VDD_CLASS_GUID, VDD_HARDWARE_ID, is_physical_monitor_connected,
|
|
||||||
open_device_handle, query_device_status, vdd_add_display, vdd_remove_display, vdd_update,
|
|
||||||
};
|
|
||||||
|
|
||||||
static INDEX: AtomicI32 = AtomicI32::new(-1);
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let status: DeviceStatus = query_device_status(&VDD_CLASS_GUID, VDD_HARDWARE_ID);
|
// Initialize logging with timestamp
|
||||||
if status != DeviceStatus::Ok {
|
env_logger::Builder::from_env(Env::default().default_filter_or("info"))
|
||||||
panic!("Failed to query device status");
|
.format(|buf, record| {
|
||||||
|
writeln!(buf, "{} [{}] - {}",
|
||||||
|
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||||
|
record.level(),
|
||||||
|
record.args()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("=== Parsec VDD Service Starting ===");
|
||||||
|
|
||||||
|
let mut service = match VddService::new() {
|
||||||
|
Ok(service) => {
|
||||||
|
info!("VDD Service initialized successfully");
|
||||||
|
service
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to initialize VDD service: {}", e);
|
||||||
|
error!("Please ensure:");
|
||||||
|
error!(" 1. Parsec VDD driver is installed");
|
||||||
|
error!(" 2. Running with administrator privileges");
|
||||||
|
error!(" 3. Windows version is supported");
|
||||||
|
|
||||||
|
println!("Press Enter to exit...");
|
||||||
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
let handle = match open_device_handle(&VDD_ADAPTER_GUID) {
|
|
||||||
Some(handle) => Arc::new(handle),
|
|
||||||
None => panic!("Failed to open device handle"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle_clone = Arc::clone(&handle);
|
// Log hardware information if available
|
||||||
std::thread::spawn(move || {
|
if let Some(hardware) = service.get_hardware_info() {
|
||||||
loop {
|
info!("Detected {} video controllers and {} monitors",
|
||||||
vdd_update(&handle_clone);
|
hardware.video_controllers.len(),
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
hardware.monitors.len());
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
loop {
|
info!("Starting service main loop...");
|
||||||
let vdd_is_running = INDEX.load(Ordering::SeqCst) != -1;
|
if let Err(e) = service.start() {
|
||||||
let has_physicial_monitor = is_physical_monitor_connected();
|
error!("VDD service encountered an error: {}", e);
|
||||||
if !has_physicial_monitor && !vdd_is_running {
|
std::process::exit(1);
|
||||||
INDEX.store(
|
|
||||||
match vdd_add_display(&handle) {
|
|
||||||
Some(i) => i,
|
|
||||||
None => -1,
|
|
||||||
},
|
|
||||||
Ordering::SeqCst,
|
|
||||||
);
|
|
||||||
} else if has_physicial_monitor && vdd_is_running {
|
|
||||||
vdd_remove_display(&handle, INDEX.load(Ordering::SeqCst));
|
|
||||||
INDEX.store(-1, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!("VDD Service stopped normally");
|
||||||
}
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
use log::{debug, trace};
|
||||||
|
use windows::{
|
||||||
|
Win32::Graphics::Gdi::{
|
||||||
|
DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_STATE_FLAGS, DISPLAY_DEVICEW, EnumDisplayDevicesW,
|
||||||
|
},
|
||||||
|
core::PWSTR,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DisplayInfo {
|
||||||
|
pub device_name: String,
|
||||||
|
pub device_string: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_parsec_virtual: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MonitorDetector;
|
||||||
|
|
||||||
|
impl MonitorDetector {
|
||||||
|
pub fn has_physical_monitors() -> bool {
|
||||||
|
let monitors = Self::get_physical_monitors();
|
||||||
|
!monitors.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_physical_monitors() -> Vec<String> {
|
||||||
|
let mut physical_monitors = Vec::new();
|
||||||
|
let mut info = DISPLAY_DEVICEW {
|
||||||
|
cb: std::mem::size_of::<DISPLAY_DEVICEW>() as u32,
|
||||||
|
DeviceName: [0; 32],
|
||||||
|
DeviceString: [0; 128],
|
||||||
|
StateFlags: DISPLAY_DEVICE_STATE_FLAGS(0),
|
||||||
|
DeviceID: [0; 128],
|
||||||
|
DeviceKey: [0; 128],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
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);
|
||||||
|
// Exclude parsec virtual displays, only count physical displays
|
||||||
|
if !device_name.to_lowercase().contains("parsec") {
|
||||||
|
physical_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
physical_monitors
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_displays() -> Vec<DisplayInfo> {
|
||||||
|
let mut displays = Vec::new();
|
||||||
|
let mut info = DISPLAY_DEVICEW {
|
||||||
|
cb: std::mem::size_of::<DISPLAY_DEVICEW>() as u32,
|
||||||
|
DeviceName: [0; 32],
|
||||||
|
DeviceString: [0; 128],
|
||||||
|
StateFlags: DISPLAY_DEVICE_STATE_FLAGS(0),
|
||||||
|
DeviceID: [0; 128],
|
||||||
|
DeviceKey: [0; 128],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
while unsafe { EnumDisplayDevicesW(PWSTR::null(), i, &mut info, 0).as_bool() } {
|
||||||
|
let device_name = String::from_utf16_lossy(&info.DeviceName)
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string();
|
||||||
|
let device_string = String::from_utf16_lossy(&info.DeviceString)
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string();
|
||||||
|
let is_active = (info.StateFlags & DISPLAY_DEVICE_ACTIVE).0 != 0;
|
||||||
|
let is_parsec_virtual = device_name.to_lowercase().contains("parsec");
|
||||||
|
|
||||||
|
displays.push(DisplayInfo {
|
||||||
|
device_name,
|
||||||
|
device_string,
|
||||||
|
is_active,
|
||||||
|
is_parsec_virtual,
|
||||||
|
});
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
displays
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
use crate::monitor::wmi::{WmiDeviceManager, VideoController, DesktopMonitor};
|
||||||
|
use crate::error::Result;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HardwareInfo {
|
||||||
|
pub video_controllers: Vec<VideoControllerInfo>,
|
||||||
|
pub monitors: Vec<MonitorInfo>,
|
||||||
|
pub total_vram: u64,
|
||||||
|
pub has_dedicated_gpu: bool,
|
||||||
|
pub primary_adapter: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VideoControllerInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub device_id: String,
|
||||||
|
pub pnp_device_id: String,
|
||||||
|
pub driver_version: Option<String>,
|
||||||
|
pub adapter_ram: u64,
|
||||||
|
pub status: String,
|
||||||
|
pub is_primary: bool,
|
||||||
|
pub vendor: GpuVendor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MonitorInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub device_id: String,
|
||||||
|
pub manufacturer: Option<String>,
|
||||||
|
pub width: Option<u32>,
|
||||||
|
pub height: Option<u32>,
|
||||||
|
pub is_physical: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum GpuVendor {
|
||||||
|
Nvidia,
|
||||||
|
Amd,
|
||||||
|
Intel,
|
||||||
|
Microsoft, // Basic Display Adapter
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HardwareDetector {
|
||||||
|
wmi_manager: WmiDeviceManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HardwareDetector {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let wmi_manager = WmiDeviceManager::new()?;
|
||||||
|
Ok(Self { wmi_manager })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_hardware_info(&self) -> Result<HardwareInfo> {
|
||||||
|
let video_controllers = self.get_video_controller_info()?;
|
||||||
|
let monitors = self.get_monitor_info()?;
|
||||||
|
|
||||||
|
let total_vram = video_controllers.iter()
|
||||||
|
.map(|vc| vc.adapter_ram)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
let has_dedicated_gpu = video_controllers.iter()
|
||||||
|
.any(|vc| vc.vendor != GpuVendor::Microsoft && vc.adapter_ram > 0);
|
||||||
|
|
||||||
|
let primary_adapter = video_controllers.iter()
|
||||||
|
.find(|vc| vc.is_primary)
|
||||||
|
.map(|vc| vc.name.clone());
|
||||||
|
|
||||||
|
Ok(HardwareInfo {
|
||||||
|
video_controllers,
|
||||||
|
monitors,
|
||||||
|
total_vram,
|
||||||
|
has_dedicated_gpu,
|
||||||
|
primary_adapter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_controller_info(&self) -> Result<Vec<VideoControllerInfo>> {
|
||||||
|
let controllers = self.wmi_manager.get_video_controllers()?;
|
||||||
|
let mut controller_info = Vec::new();
|
||||||
|
|
||||||
|
for (index, controller) in controllers.into_iter().enumerate() {
|
||||||
|
if let (Some(name), Some(device_id)) = (&controller.name, &controller.device_id) {
|
||||||
|
let vendor = Self::detect_gpu_vendor(name);
|
||||||
|
let is_primary = index == 0; // First controller is typically primary
|
||||||
|
|
||||||
|
controller_info.push(VideoControllerInfo {
|
||||||
|
name: name.clone(),
|
||||||
|
device_id: device_id.clone(),
|
||||||
|
pnp_device_id: controller.pnp_device_id.unwrap_or_default(),
|
||||||
|
driver_version: controller.driver_version,
|
||||||
|
adapter_ram: controller.adapter_ram.unwrap_or(0),
|
||||||
|
status: controller.status.unwrap_or_else(|| "Unknown".to_string()),
|
||||||
|
is_primary,
|
||||||
|
vendor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Found {} video controllers", controller_info.len());
|
||||||
|
Ok(controller_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_monitor_info(&self) -> Result<Vec<MonitorInfo>> {
|
||||||
|
let monitors = self.wmi_manager.get_desktop_monitors()?;
|
||||||
|
let mut monitor_info = Vec::new();
|
||||||
|
|
||||||
|
for monitor in monitors {
|
||||||
|
if let (Some(name), Some(device_id)) = (&monitor.name, &monitor.device_id) {
|
||||||
|
let is_physical = Self::is_physical_monitor(name);
|
||||||
|
|
||||||
|
monitor_info.push(MonitorInfo {
|
||||||
|
name: name.clone(),
|
||||||
|
device_id: device_id.clone(),
|
||||||
|
manufacturer: monitor.manufacturer,
|
||||||
|
width: monitor.screen_width,
|
||||||
|
height: monitor.screen_height,
|
||||||
|
is_physical,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Found {} monitors", monitor_info.len());
|
||||||
|
Ok(monitor_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_gpu_vendor(name: &str) -> GpuVendor {
|
||||||
|
let name_lower = name.to_lowercase();
|
||||||
|
|
||||||
|
if name_lower.contains("nvidia") || name_lower.contains("geforce") || name_lower.contains("quadro") {
|
||||||
|
GpuVendor::Nvidia
|
||||||
|
} else if name_lower.contains("amd") || name_lower.contains("radeon") || name_lower.contains("ati") {
|
||||||
|
GpuVendor::Amd
|
||||||
|
} else if name_lower.contains("intel") {
|
||||||
|
GpuVendor::Intel
|
||||||
|
} else if name_lower.contains("microsoft") || name_lower.contains("basic display") {
|
||||||
|
GpuVendor::Microsoft
|
||||||
|
} else {
|
||||||
|
GpuVendor::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_physical_monitor(name: &str) -> bool {
|
||||||
|
let name_lower = name.to_lowercase();
|
||||||
|
!name_lower.contains("parsec") &&
|
||||||
|
!name_lower.contains("virtual") &&
|
||||||
|
!name_lower.contains("generic pnp") &&
|
||||||
|
!name_lower.contains("software")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_hardware_summary(&self) -> Result<()> {
|
||||||
|
let hardware = self.get_hardware_info()?;
|
||||||
|
|
||||||
|
info!("=== Hardware Summary ===");
|
||||||
|
info!("Video Controllers: {}", hardware.video_controllers.len());
|
||||||
|
info!("Total VRAM: {} MB", hardware.total_vram / (1024 * 1024));
|
||||||
|
info!("Has Dedicated GPU: {}", hardware.has_dedicated_gpu);
|
||||||
|
|
||||||
|
if let Some(primary) = &hardware.primary_adapter {
|
||||||
|
info!("Primary Adapter: {}", primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, controller) in hardware.video_controllers.iter().enumerate() {
|
||||||
|
info!(" GPU {}: {} ({:?}) - {} MB VRAM",
|
||||||
|
i, controller.name, controller.vendor,
|
||||||
|
controller.adapter_ram / (1024 * 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
let physical_monitors: Vec<_> = hardware.monitors.iter()
|
||||||
|
.filter(|m| m.is_physical)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Physical Monitors: {}", physical_monitors.len());
|
||||||
|
for (i, monitor) in physical_monitors.iter().enumerate() {
|
||||||
|
let resolution = match (monitor.width, monitor.height) {
|
||||||
|
(Some(w), Some(h)) => format!("{}x{}", w, h),
|
||||||
|
_ => "Unknown".to_string(),
|
||||||
|
};
|
||||||
|
info!(" Monitor {}: {} ({})", i, monitor.name, resolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
mod detector;
|
||||||
|
mod wmi;
|
||||||
|
mod hardware;
|
||||||
|
|
||||||
|
pub use detector::{MonitorDetector, DisplayInfo};
|
||||||
|
pub use wmi::{WmiDeviceManager, VideoController, DesktopMonitor};
|
||||||
|
pub use hardware::{HardwareDetector, HardwareInfo, VideoControllerInfo, MonitorInfo, GpuVendor};
|
|
@ -0,0 +1,116 @@
|
||||||
|
use crate::error::{Result, VddError};
|
||||||
|
use log::{debug, error, warn};
|
||||||
|
use std::time::Duration;
|
||||||
|
use wmi::{COMLibrary, WMIConnection, Variant};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VideoController {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub device_id: Option<String>,
|
||||||
|
pub pnp_device_id: Option<String>,
|
||||||
|
pub driver_version: Option<String>,
|
||||||
|
pub adapter_ram: Option<u64>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DesktopMonitor {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub device_id: Option<String>,
|
||||||
|
pub manufacturer: Option<String>,
|
||||||
|
pub screen_width: Option<u32>,
|
||||||
|
pub screen_height: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WmiDeviceManager {
|
||||||
|
wmi_con: WMIConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WmiDeviceManager {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let com_lib = COMLibrary::new().map_err(|e| {
|
||||||
|
error!("Failed to initialize COM library: {}", e);
|
||||||
|
VddError::IoError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let wmi_con = WMIConnection::new(com_lib).map_err(|e| {
|
||||||
|
error!("Failed to connect to WMI: {}", e);
|
||||||
|
VddError::IoError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self { wmi_con })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_video_controllers(&self) -> Result<Vec<VideoController>> {
|
||||||
|
let results: Vec<HashMap<String, Variant>> = self.wmi_con
|
||||||
|
.raw_query("SELECT Name, DeviceID, PNPDeviceID, DriverVersion, AdapterRAM, Status FROM Win32_VideoController")
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("WMI query for video controllers failed: {}", e);
|
||||||
|
VddError::IoError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut controllers = Vec::new();
|
||||||
|
for result in results {
|
||||||
|
controllers.push(VideoController {
|
||||||
|
name: Self::extract_string(&result, "Name"),
|
||||||
|
device_id: Self::extract_string(&result, "DeviceID"),
|
||||||
|
pnp_device_id: Self::extract_string(&result, "PNPDeviceID"),
|
||||||
|
driver_version: Self::extract_string(&result, "DriverVersion"),
|
||||||
|
adapter_ram: Self::extract_u64(&result, "AdapterRAM"),
|
||||||
|
status: Self::extract_string(&result, "Status"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Found {} video controllers via WMI", controllers.len());
|
||||||
|
Ok(controllers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_desktop_monitors(&self) -> Result<Vec<DesktopMonitor>> {
|
||||||
|
let results: Vec<HashMap<String, Variant>> = self.wmi_con
|
||||||
|
.raw_query("SELECT Name, DeviceID, MonitorManufacturer, ScreenWidth, ScreenHeight FROM Win32_DesktopMonitor")
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("WMI query for desktop monitors failed: {}", e);
|
||||||
|
VddError::IoError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut monitors = Vec::new();
|
||||||
|
for result in results {
|
||||||
|
monitors.push(DesktopMonitor {
|
||||||
|
name: Self::extract_string(&result, "Name"),
|
||||||
|
device_id: Self::extract_string(&result, "DeviceID"),
|
||||||
|
manufacturer: Self::extract_string(&result, "MonitorManufacturer"),
|
||||||
|
screen_width: Self::extract_u32(&result, "ScreenWidth"),
|
||||||
|
screen_height: Self::extract_u32(&result, "ScreenHeight"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Found {} desktop monitors via WMI", monitors.len());
|
||||||
|
Ok(monitors)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_string(map: &HashMap<String, Variant>, key: &str) -> Option<String> {
|
||||||
|
match map.get(key) {
|
||||||
|
Some(Variant::String(s)) => Some(s.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_u32(map: &HashMap<String, Variant>, key: &str) -> Option<u32> {
|
||||||
|
match map.get(key) {
|
||||||
|
Some(Variant::UI4(n)) => Some(*n),
|
||||||
|
Some(Variant::I4(n)) => Some(*n as u32),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_u64(map: &HashMap<String, Variant>, key: &str) -> Option<u64> {
|
||||||
|
match map.get(key) {
|
||||||
|
Some(Variant::UI8(n)) => Some(*n),
|
||||||
|
Some(Variant::I8(n)) => Some(*n as u64),
|
||||||
|
Some(Variant::UI4(n)) => Some(*n as u64),
|
||||||
|
Some(Variant::I4(n)) => Some(*n as u64),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue