mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 11:04:25 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
89
src/platform/capabilities.rs
Normal file
89
src/platform/capabilities.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
//! Runtime platform mode and feature capability reporting.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PlatformMode {
|
||||
Linux,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl PlatformMode {
|
||||
pub const fn current() -> Self {
|
||||
if cfg!(windows) {
|
||||
Self::Windows
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Linux => "Linux",
|
||||
Self::Windows => "Windows",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FeatureCapability {
|
||||
pub available: bool,
|
||||
pub backends: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub selected_backend: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
impl FeatureCapability {
|
||||
pub fn available(backends: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
||||
let backends = backends.into_iter().map(Into::into).collect();
|
||||
Self {
|
||||
available: true,
|
||||
backends,
|
||||
selected_backend: None,
|
||||
reason: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unsupported(reason: impl Into<String>) -> Self {
|
||||
Self {
|
||||
available: false,
|
||||
backends: Vec::new(),
|
||||
selected_backend: None,
|
||||
reason: Some(reason.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_selected_backend(mut self, backend: Option<String>) -> Self {
|
||||
self.selected_backend = backend;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlatformCapabilities {
|
||||
pub mode: PlatformMode,
|
||||
pub mode_label: &'static str,
|
||||
pub video_capture: FeatureCapability,
|
||||
pub encoder: FeatureCapability,
|
||||
pub hid: FeatureCapability,
|
||||
pub atx: FeatureCapability,
|
||||
pub msd: FeatureCapability,
|
||||
pub otg: FeatureCapability,
|
||||
pub audio: FeatureCapability,
|
||||
pub rustdesk: FeatureCapability,
|
||||
pub diagnostics: FeatureCapability,
|
||||
pub extensions: FeatureCapability,
|
||||
pub service_installation: FeatureCapability,
|
||||
}
|
||||
|
||||
impl PlatformCapabilities {
|
||||
pub fn current() -> Self {
|
||||
match PlatformMode::current() {
|
||||
PlatformMode::Linux => crate::platform::linux::capabilities(),
|
||||
PlatformMode::Windows => crate::platform::windows::capabilities(),
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/platform/defaults.rs
Normal file
62
src/platform/defaults.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::config::{AppConfig, AtxDriverType, HidBackend};
|
||||
|
||||
pub fn apply(config: &mut AppConfig) {
|
||||
if cfg!(windows) {
|
||||
apply_windows(config);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_windows(config: &mut AppConfig) {
|
||||
config.msd.enabled = false;
|
||||
config.hid.otg_udc = None;
|
||||
if config.hid.backend == HidBackend::Otg {
|
||||
config.hid.backend = HidBackend::None;
|
||||
}
|
||||
if config.hid.ch9329_port == "/dev/ttyUSB0" {
|
||||
config.hid.ch9329_port = "COM3".to_string();
|
||||
}
|
||||
if !config.initialized {
|
||||
config.audio.enabled = false;
|
||||
config.audio.device.clear();
|
||||
}
|
||||
|
||||
if matches!(
|
||||
config.atx.power.driver,
|
||||
AtxDriverType::Gpio | AtxDriverType::UsbRelay
|
||||
) {
|
||||
config.atx.power.driver = AtxDriverType::None;
|
||||
}
|
||||
if matches!(
|
||||
config.atx.reset.driver,
|
||||
AtxDriverType::Gpio | AtxDriverType::UsbRelay
|
||||
) {
|
||||
config.atx.reset.driver = AtxDriverType::None;
|
||||
}
|
||||
if !config.initialized
|
||||
&& config.atx.power.driver == AtxDriverType::None
|
||||
&& config.atx.power.device.is_empty()
|
||||
{
|
||||
config.atx.power.driver = AtxDriverType::Serial;
|
||||
config.atx.power.device = "COM4".to_string();
|
||||
config.atx.power.pin = 1;
|
||||
config.atx.power.baud_rate = 9600;
|
||||
}
|
||||
if !config.initialized
|
||||
&& config.atx.reset.driver == AtxDriverType::None
|
||||
&& config.atx.reset.device.is_empty()
|
||||
{
|
||||
config.atx.reset.driver = AtxDriverType::Serial;
|
||||
config.atx.reset.device = "COM4".to_string();
|
||||
config.atx.reset.pin = 2;
|
||||
config.atx.reset.baud_rate = 9600;
|
||||
}
|
||||
|
||||
config
|
||||
.video
|
||||
.device
|
||||
.get_or_insert_with(|| "auto".to_string());
|
||||
config
|
||||
.video
|
||||
.format
|
||||
.get_or_insert_with(|| "MJPEG".to_string());
|
||||
}
|
||||
23
src/platform/linux.rs
Normal file
23
src/platform/linux.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! Linux platform capabilities.
|
||||
|
||||
use super::{FeatureCapability, PlatformCapabilities, PlatformMode};
|
||||
|
||||
pub fn capabilities() -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
mode: PlatformMode::Linux,
|
||||
mode_label: PlatformMode::Linux.label(),
|
||||
video_capture: FeatureCapability::available(["v4l2"]),
|
||||
encoder: FeatureCapability::available([
|
||||
"software", "vaapi", "nvenc", "qsv", "amf", "rkmpp", "v4l2m2m",
|
||||
]),
|
||||
hid: FeatureCapability::available(["otg", "ch9329", "none"]),
|
||||
atx: FeatureCapability::available(["gpio", "usb_relay", "serial", "wol", "none"]),
|
||||
msd: FeatureCapability::available(["configfs"]),
|
||||
otg: FeatureCapability::available(["configfs"]),
|
||||
audio: FeatureCapability::available(["alsa"]),
|
||||
rustdesk: FeatureCapability::available(["builtin"]),
|
||||
diagnostics: FeatureCapability::available(["linux"]),
|
||||
extensions: FeatureCapability::available(["linux"]),
|
||||
service_installation: FeatureCapability::available(["systemd"]),
|
||||
}
|
||||
}
|
||||
10
src/platform/mod.rs
Normal file
10
src/platform/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Platform selection and capability reporting.
|
||||
|
||||
pub mod capabilities;
|
||||
pub mod defaults;
|
||||
pub mod linux;
|
||||
#[cfg(unix)]
|
||||
pub mod usb_reset;
|
||||
pub mod windows;
|
||||
|
||||
pub use capabilities::{FeatureCapability, PlatformCapabilities, PlatformMode};
|
||||
174
src/platform/usb_reset.rs
Normal file
174
src/platform/usb_reset.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
//! USB device enumeration and reset via sysfs `authorized`.
|
||||
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn usb_device_dir_for_v4l_sysfs(device_link: &Path) -> io::Result<PathBuf> {
|
||||
let mut p = device_link.canonicalize()?;
|
||||
loop {
|
||||
if p.join("busnum").is_file() && p.join("devnum").is_file() {
|
||||
return Ok(p);
|
||||
}
|
||||
p = p
|
||||
.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no USB parent in sysfs"))?
|
||||
.to_path_buf();
|
||||
if p.as_os_str().is_empty() || p == Path::new("/") {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"reached sysfs root without USB device",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UsbDeviceInfo {
|
||||
pub bus_num: u32,
|
||||
pub dev_num: u32,
|
||||
pub id_vendor: String,
|
||||
pub id_product: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub product: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manufacturer: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub speed: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub authorized: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub driver: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub video_device: Option<String>,
|
||||
}
|
||||
|
||||
fn read_sysfs_str(dir: &Path, attr: &str) -> Option<String> {
|
||||
std::fs::read_to_string(dir.join(attr))
|
||||
.ok()
|
||||
.map(|s| s.trim_end().to_string())
|
||||
}
|
||||
|
||||
fn read_sysfs_u32(dir: &Path, attr: &str) -> Option<u32> {
|
||||
read_sysfs_str(dir, attr).and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
fn build_usb_to_video_map() -> std::collections::HashMap<String, String> {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
let v4l_class = Path::new("/sys/class/video4linux");
|
||||
let entries = match std::fs::read_dir(v4l_class) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return map,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = match name.to_str() {
|
||||
Some(s) if s.starts_with("video") => s,
|
||||
_ => continue,
|
||||
};
|
||||
let device_link = v4l_class.join(name_str).join("device");
|
||||
if let Ok(usb_dir) = usb_device_dir_for_v4l_sysfs(&device_link) {
|
||||
if let Some(key) = usb_dir.file_name().and_then(|k| k.to_str()) {
|
||||
map.insert(key.to_string(), format!("/dev/{}", name_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
pub fn list_usb_devices() -> Vec<UsbDeviceInfo> {
|
||||
let usb_bus = Path::new("/sys/bus/usb/devices");
|
||||
let entries = match std::fs::read_dir(usb_bus) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
|
||||
let video_map = build_usb_to_video_map();
|
||||
|
||||
let mut devices: Vec<UsbDeviceInfo> = entries
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let dir = entry.path();
|
||||
let bus_num = read_sysfs_u32(&dir, "busnum")?;
|
||||
let dev_num = read_sysfs_u32(&dir, "devnum")?;
|
||||
|
||||
let id_vendor = read_sysfs_str(&dir, "idVendor").unwrap_or_default();
|
||||
let id_product = read_sysfs_str(&dir, "idProduct").unwrap_or_default();
|
||||
|
||||
let product = read_sysfs_str(&dir, "product");
|
||||
let manufacturer = read_sysfs_str(&dir, "manufacturer");
|
||||
let speed = read_sysfs_str(&dir, "speed");
|
||||
|
||||
let authorized = if dir.join("authorized").exists() {
|
||||
read_sysfs_str(&dir, "authorized")
|
||||
.and_then(|s| s.trim().parse::<u8>().ok())
|
||||
.map(|v| v != 0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let driver = std::fs::read_link(dir.join("driver"))
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|f| f.to_string_lossy().to_string()));
|
||||
|
||||
let dir_name = dir.file_name()?.to_str()?.to_string();
|
||||
let video_device = video_map.get(&dir_name).cloned();
|
||||
|
||||
Some(UsbDeviceInfo {
|
||||
bus_num,
|
||||
dev_num,
|
||||
id_vendor,
|
||||
id_product,
|
||||
product,
|
||||
manufacturer,
|
||||
speed,
|
||||
authorized,
|
||||
driver,
|
||||
video_device,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
devices.sort_by(|a, b| (a.bus_num, a.dev_num).cmp(&(b.bus_num, b.dev_num)));
|
||||
devices
|
||||
}
|
||||
|
||||
pub fn reset_usb_device(bus_num: u32, dev_num: u32) -> io::Result<()> {
|
||||
let usb_bus = Path::new("/sys/bus/usb/devices");
|
||||
let entries = std::fs::read_dir(usb_bus)?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let dir = entry.path();
|
||||
if read_sysfs_u32(&dir, "busnum") != Some(bus_num)
|
||||
|| read_sysfs_u32(&dir, "devnum") != Some(dev_num)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let authorized = dir.join("authorized");
|
||||
if !authorized.exists() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("device {bus_num}-{dev_num} has no authorized attribute"),
|
||||
));
|
||||
}
|
||||
std::fs::write(&authorized, b"0")?;
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
std::fs::write(&authorized, b"1")?;
|
||||
|
||||
let wait_until = Instant::now() + Duration::from_secs(2);
|
||||
while !dir.join("busnum").exists() {
|
||||
if Instant::now() >= wait_until {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("USB device {bus_num}-{dev_num} not found in sysfs"),
|
||||
))
|
||||
}
|
||||
33
src/platform/windows.rs
Normal file
33
src/platform/windows.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Windows platform capabilities.
|
||||
|
||||
use super::{FeatureCapability, PlatformCapabilities, PlatformMode};
|
||||
|
||||
pub fn capabilities() -> PlatformCapabilities {
|
||||
let linux_only = "unsupported on Windows";
|
||||
PlatformCapabilities {
|
||||
mode: PlatformMode::Windows,
|
||||
mode_label: PlatformMode::Windows.label(),
|
||||
video_capture: FeatureCapability::available(["directshow_uvc", "mjpeg"])
|
||||
.with_selected_backend(Some("directshow_uvc".to_string())),
|
||||
encoder: FeatureCapability::available([
|
||||
"ffmpeg_h264",
|
||||
"ffmpeg_h265",
|
||||
"ffmpeg_vp8",
|
||||
"ffmpeg_vp9",
|
||||
"software",
|
||||
"mjpeg",
|
||||
]),
|
||||
hid: FeatureCapability::available(["ch9329", "none"])
|
||||
.with_selected_backend(Some("ch9329".to_string())),
|
||||
atx: FeatureCapability::available(["serial", "wol", "none"]),
|
||||
msd: FeatureCapability::unsupported(linux_only),
|
||||
otg: FeatureCapability::unsupported(linux_only),
|
||||
audio: FeatureCapability::available(["wasapi", "opus"])
|
||||
.with_selected_backend(Some("wasapi".to_string())),
|
||||
rustdesk: FeatureCapability::available(["builtin", "tcp_direct", "relay"])
|
||||
.with_selected_backend(Some("builtin".to_string())),
|
||||
diagnostics: FeatureCapability::available(["windows"]),
|
||||
extensions: FeatureCapability::available(["windows_safe"]),
|
||||
service_installation: FeatureCapability::available(["windows_service"]),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user