From 887f29096fd2c382c5fd6b78edc9501798392d30 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Wed, 29 Jul 2026 13:56:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20OTG=20=20HID=20?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E5=94=A4=E9=86=92=E5=92=8C=20OTG=20=20MSD=20?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=90=8D=E7=A7=B0=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在补丁内核上启用 HID 写入唤醒及 USB 远程唤醒描述符 - 支持全局配置 Flash 和 CD-ROM INQUIRY 字符串 - 兼容普通内核的通用 INQUIRY 属性 - 调整 OTG 功能布局并更新大容量 DVD 镜像提示 --- src/config/schema/web.rs | 48 ++++++++++ src/otg/configfs.rs | 10 +++ src/otg/hid.rs | 5 ++ src/otg/manager.rs | 30 +++++-- src/otg/msd.rs | 145 +++++++++++++++++++++++++++---- src/otg/service.rs | 24 ++++- src/web/handlers/config/apply.rs | 16 ++-- src/web/handlers/config/types.rs | 14 +++ web/src/i18n/en-US.ts | 4 +- web/src/i18n/zh-CN.ts | 4 +- web/src/types/generated.ts | 4 + web/src/views/SettingsView.vue | 110 +++++++++++++++-------- 12 files changed, 347 insertions(+), 67 deletions(-) diff --git a/src/config/schema/web.rs b/src/config/schema/web.rs index c3795c5c..83db9248 100644 --- a/src/config/schema/web.rs +++ b/src/config/schema/web.rs @@ -49,18 +49,51 @@ impl Default for VideoConfig { pub struct MsdConfig { pub enabled: bool, pub msd_dir: String, + pub flash_inquiry_string: String, + pub cdrom_inquiry_string: String, } +pub const DEFAULT_FLASH_INQUIRY_STRING: &str = "One-KVM Virtual Flash"; +pub const DEFAULT_CDROM_INQUIRY_STRING: &str = "One-KVM Virtual CD-ROM"; +pub const MAX_INQUIRY_STRING_BYTES: usize = 28; + impl Default for MsdConfig { fn default() -> Self { Self { enabled: true, msd_dir: String::new(), + flash_inquiry_string: DEFAULT_FLASH_INQUIRY_STRING.to_string(), + cdrom_inquiry_string: DEFAULT_CDROM_INQUIRY_STRING.to_string(), } } } impl MsdConfig { + pub fn validate(&self) -> crate::error::Result<()> { + Self::validate_inquiry_string("Flash", &self.flash_inquiry_string)?; + Self::validate_inquiry_string("CD-ROM", &self.cdrom_inquiry_string) + } + + pub fn validate_inquiry_string(kind: &str, value: &str) -> crate::error::Result<()> { + let value = value.trim(); + if value.is_empty() { + return Err(crate::error::AppError::BadRequest(format!( + "MSD {kind} inquiry string cannot be empty" + ))); + } + if value.len() > MAX_INQUIRY_STRING_BYTES { + return Err(crate::error::AppError::BadRequest(format!( + "MSD {kind} inquiry string must be at most {MAX_INQUIRY_STRING_BYTES} bytes" + ))); + } + if !value.bytes().all(|byte| (0x20..=0x7e).contains(&byte)) { + return Err(crate::error::AppError::BadRequest(format!( + "MSD {kind} inquiry string must contain printable ASCII characters only" + ))); + } + Ok(()) + } + pub fn msd_dir_path(&self) -> std::path::PathBuf { std::path::PathBuf::from(&self.msd_dir) } @@ -123,3 +156,18 @@ impl Default for WebConfig { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn msd_inquiry_strings_default_and_validate() { + assert!(MsdConfig::default().validate().is_ok()); + assert!(MsdConfig::validate_inquiry_string("Flash", " Custom Drive ").is_ok()); + assert!(MsdConfig::validate_inquiry_string("Flash", "").is_err()); + assert!(MsdConfig::validate_inquiry_string("Flash", &"x".repeat(29)).is_err()); + assert!(MsdConfig::validate_inquiry_string("CD-ROM", "虚拟光驱").is_err()); + assert!(MsdConfig::validate_inquiry_string("CD-ROM", "bad\tname").is_err()); + } +} diff --git a/src/otg/configfs.rs b/src/otg/configfs.rs index 1e95b925..5b959a52 100644 --- a/src/otg/configfs.rs +++ b/src/otg/configfs.rs @@ -106,6 +106,16 @@ pub fn write_file(path: &Path, content: &str) -> Result<()> { Ok(()) } +/// Write an optional configfs/sysfs attribute when the running kernel exposes it. +/// This keeps newer kernel enhancements compatible with older kernels. +pub fn write_file_if_exists(path: &Path, content: &str) -> Result { + if !path.exists() { + return Ok(false); + } + write_file(path, content)?; + Ok(true) +} + pub fn write_bytes(path: &Path, data: &[u8]) -> Result<()> { let mut file = File::create(path) .map_err(|e| AppError::Internal(format!("Failed to create {}: {}", path.display(), e)))?; diff --git a/src/otg/hid.rs b/src/otg/hid.rs index fe1fbbb0..6eff23ea 100644 --- a/src/otg/hid.rs +++ b/src/otg/hid.rs @@ -3,6 +3,7 @@ use tracing::debug; use super::configfs::{ create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file, + write_file_if_exists, }; use super::function::GadgetFunction; use super::report_desc::{ @@ -143,6 +144,10 @@ impl GadgetFunction for HidFunction { self.func_type.report_desc(self.keyboard_leds), )?; + // Supported by the PiKVM HID kernel patch. Older kernels simply do + // not expose this ConfigFS attribute. + let _ = write_file_if_exists(&func_path.join("wakeup_on_write"), "1")?; + debug!( "Created HID function: {} at {}", self.name(), diff --git a/src/otg/manager.rs b/src/otg/manager.rs index 94fc65e2..147c6c7a 100644 --- a/src/otg/manager.rs +++ b/src/otg/manager.rs @@ -4,12 +4,12 @@ use tracing::{debug, error, info, warn}; use super::configfs::{ configfs_path, create_dir, create_symlink, find_udc, is_configfs_available, remove_dir, - remove_file, write_file, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, DEFAULT_USB_PRODUCT_ID, - DEFAULT_USB_VENDOR_ID, USB_BCD_USB, + remove_file, write_file, write_file_if_exists, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, + DEFAULT_USB_PRODUCT_ID, DEFAULT_USB_VENDOR_ID, USB_BCD_USB, }; use super::function::GadgetFunction; use super::hid::HidFunction; -use super::msd::MsdFunction; +use super::msd::{MsdFunction, MsdInquiryStrings}; use super::network::NetworkFunction; use crate::config::OtgNetworkConfig; use crate::error::{AppError, Result}; @@ -134,8 +134,12 @@ impl OtgGadgetManager { Ok(device_path) } - pub fn add_msd(&mut self, lun_capacity: u8) -> Result { - let func = MsdFunction::new(self.msd_instance, lun_capacity)?; + pub fn add_msd( + &mut self, + lun_capacity: u8, + inquiry_strings: MsdInquiryStrings, + ) -> Result { + let func = MsdFunction::new(self.msd_instance, lun_capacity, inquiry_strings)?; let func_clone = func.clone(); self.add_function(Box::new(func))?; self.msd_instance += 1; @@ -196,6 +200,22 @@ impl OtgGadgetManager { func.link(&self.config_path, &self.gadget_path)?; } + // A host only enables USB remote wakeup when the configuration + // descriptor advertises it. Enable the descriptor bit only when the + // running kernel supports the HID wakeup_on_write attribute. + let hid_wakeup_supported = self.functions.iter().any(|func| { + func.name().starts_with("hid.") + && self + .gadget_path + .join("functions") + .join(func.name()) + .join("wakeup_on_write") + .exists() + }); + if hid_wakeup_supported { + let _ = write_file_if_exists(&self.config_path.join("bmAttributes"), "0xA0")?; + } + debug!("OTG USB Gadget setup complete"); Ok(()) } diff --git a/src/otg/msd.rs b/src/otg/msd.rs index 2dbc7f5d..15fb2c86 100644 --- a/src/otg/msd.rs +++ b/src/otg/msd.rs @@ -5,6 +5,7 @@ use tracing::{debug, info, warn}; use super::configfs::{create_dir, create_symlink, remove_dir, remove_file, write_file}; use super::function::GadgetFunction; +use crate::config::{MsdConfig, DEFAULT_CDROM_INQUIRY_STRING, DEFAULT_FLASH_INQUIRY_STRING}; use crate::error::{AppError, MsdErrorCode, Result}; const MEDIA_TYPE_REBIND_DELAY_MS: u64 = 300; @@ -56,14 +57,39 @@ impl MsdLunConfig { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MsdInquiryStrings { + pub flash: String, + pub cdrom: String, +} + +impl Default for MsdInquiryStrings { + fn default() -> Self { + Self { + flash: DEFAULT_FLASH_INQUIRY_STRING.to_string(), + cdrom: DEFAULT_CDROM_INQUIRY_STRING.to_string(), + } + } +} + +impl From<&MsdConfig> for MsdInquiryStrings { + fn from(config: &MsdConfig) -> Self { + Self { + flash: config.flash_inquiry_string.clone(), + cdrom: config.cdrom_inquiry_string.clone(), + } + } +} + #[derive(Debug, Clone)] pub struct MsdFunction { name: String, lun_capacity: u8, + inquiry_strings: MsdInquiryStrings, } impl MsdFunction { - pub fn new(instance: u8, lun_capacity: u8) -> Result { + pub fn new(instance: u8, lun_capacity: u8, inquiry_strings: MsdInquiryStrings) -> Result { if lun_capacity != 1 && lun_capacity != 8 { return Err(AppError::BadRequest(format!( "MSD LUN capacity must be 1 or 8, got {lun_capacity}" @@ -73,6 +99,7 @@ impl MsdFunction { Ok(Self { name: format!("mass_storage.usb{}", instance), lun_capacity, + inquiry_strings, }) } @@ -212,6 +239,23 @@ impl MsdFunction { current_cdrom != if config.cdrom { "1" } else { "0" } } + fn inquiry_string_path(lun_path: &Path, cdrom: bool) -> Option { + let cdrom_path = lun_path.join("inquiry_string_cdrom"); + if cdrom && cdrom_path.exists() { + return Some(cdrom_path); + } + let generic_path = lun_path.join("inquiry_string"); + generic_path.exists().then_some(generic_path) + } + + fn inquiry_string(&self, cdrom: bool) -> &str { + if cdrom { + &self.inquiry_strings.cdrom + } else { + &self.inquiry_strings.flash + } + } + fn configure_lun_attributes( &self, lun_path: &Path, @@ -256,6 +300,7 @@ impl MsdFunction { lun, current_cdrom, new_cdrom ); write_file(&lun_path.join("cdrom"), new_cdrom)?; + self.write_inquiry_string(lun_path, config.cdrom)?; } if current_ro != new_ro { debug!("Updating LUN {} ro: {} -> {}", lun, current_ro, new_ro); @@ -320,6 +365,26 @@ impl MsdFunction { Ok(()) } + fn write_inquiry_string(&self, lun_path: &Path, cdrom: bool) -> Result<()> { + if let Some(path) = Self::inquiry_string_path(lun_path, cdrom) { + write_file(&path, self.inquiry_string(cdrom))?; + } + Ok(()) + } + + fn write_inquiry_strings(&self, lun_path: &Path) -> Result<()> { + let generic_path = lun_path.join("inquiry_string"); + if generic_path.exists() { + write_file(&generic_path, &self.inquiry_strings.flash)?; + } + + let cdrom_path = lun_path.join("inquiry_string_cdrom"); + if cdrom_path.exists() { + write_file(&cdrom_path, &self.inquiry_strings.cdrom)?; + } + Ok(()) + } + pub async fn disconnect_lun_async(&self, gadget_path: &Path, lun: u8) -> Result<()> { let gadget_path = gadget_path.to_path_buf(); let this = self.clone(); @@ -455,6 +520,7 @@ impl GadgetFunction for MsdFunction { for lun in 0..self.lun_capacity { self.clear_lun_unbound(gadget_path, lun)?; + self.write_inquiry_strings(&self.lun_path(gadget_path, lun))?; } debug!("Created MSD function: {}", self.name()); @@ -526,6 +592,10 @@ mod tests { use super::*; use tempfile::TempDir; + fn test_msd(capacity: u8) -> MsdFunction { + MsdFunction::new(0, capacity, MsdInquiryStrings::default()).unwrap() + } + #[test] fn test_lun_config_cdrom() { let config = MsdLunConfig::cdrom(PathBuf::from("/tmp/test.iso")); @@ -542,21 +612,62 @@ mod tests { assert!(config.removable); } + #[test] + fn inquiry_string_uses_cdrom_attribute_with_stock_fallback() { + let msd = MsdFunction::new( + 0, + 1, + MsdInquiryStrings { + flash: "Custom Flash".into(), + cdrom: "Custom Optical".into(), + }, + ) + .unwrap(); + let patched = TempDir::new().unwrap(); + std::fs::write(patched.path().join("inquiry_string"), b"generic\n").unwrap(); + std::fs::write(patched.path().join("inquiry_string_cdrom"), b"cdrom\n").unwrap(); + + msd.write_inquiry_strings(patched.path()).unwrap(); + + assert_eq!( + std::fs::read_to_string(patched.path().join("inquiry_string_cdrom")) + .unwrap() + .trim(), + "Custom Optical" + ); + assert_eq!( + std::fs::read_to_string(patched.path().join("inquiry_string")) + .unwrap() + .trim(), + "Custom Flash" + ); + + let stock = TempDir::new().unwrap(); + std::fs::write(stock.path().join("inquiry_string"), b"generic\n").unwrap(); + msd.write_inquiry_string(stock.path(), true).unwrap(); + assert_eq!( + std::fs::read_to_string(stock.path().join("inquiry_string")) + .unwrap() + .trim(), + "Custom Optical" + ); + } + #[test] fn test_msd_function_name() { - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); assert_eq!(msd.name(), "mass_storage.usb0"); assert_eq!(msd.lun_capacity, 1); - let multi = MsdFunction::new(0, 8).unwrap(); + let multi = test_msd(8); assert_eq!(multi.lun_capacity, 8); } #[test] fn test_msd_function_rejects_invalid_capacity() { - assert!(MsdFunction::new(0, 0).is_err()); - assert!(MsdFunction::new(0, 2).is_err()); - assert!(MsdFunction::new(0, 9).is_err()); + assert!(MsdFunction::new(0, 0, MsdInquiryStrings::default()).is_err()); + assert!(MsdFunction::new(0, 2, MsdInquiryStrings::default()).is_err()); + assert!(MsdFunction::new(0, 9, MsdInquiryStrings::default()).is_err()); } #[test] @@ -575,7 +686,7 @@ mod tests { std::fs::create_dir_all(&lun_path).unwrap(); std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap(); std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap(); - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); msd.disconnect_lun(temp_dir.path(), 0).unwrap(); @@ -595,7 +706,7 @@ mod tests { let lun_path = temp_dir.path().join("functions/mass_storage.usb0/lun.0"); std::fs::create_dir_all(&lun_path).unwrap(); std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap(); - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); msd.disconnect_lun(temp_dir.path(), 0).unwrap(); @@ -610,7 +721,7 @@ mod tests { let lun_path = temp_dir.path().join("functions/mass_storage.usb0/lun.0"); std::fs::create_dir_all(lun_path.join("forced_eject")).unwrap(); std::fs::write(lun_path.join("file"), b"backing.img\n").unwrap(); - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); msd.disconnect_lun(temp_dir.path(), 0).unwrap(); @@ -629,7 +740,7 @@ mod tests { std::fs::write(lun_path.join("file"), format!("backing-{lun}.img\n")).unwrap(); std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap(); } - let msd = MsdFunction::new(0, 8).unwrap(); + let msd = test_msd(8); msd.disconnect_lun(temp_dir.path(), 1).unwrap(); @@ -656,7 +767,7 @@ mod tests { for capacity in [1, 8] { let temp_dir = TempDir::new().unwrap(); std::fs::create_dir_all(temp_dir.path().join("functions")).unwrap(); - let msd = MsdFunction::new(0, capacity).unwrap(); + let msd = test_msd(capacity); msd.create(temp_dir.path()).unwrap(); @@ -678,7 +789,7 @@ mod tests { std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap(); let image_path = temp_dir.path().join("test.img"); std::fs::write(&image_path, b"image").unwrap(); - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::disk(image_path, false)) .unwrap(); @@ -711,7 +822,7 @@ mod tests { std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap(); let image_path = temp_dir.path().join("test.iso"); std::fs::write(&image_path, b"iso").unwrap(); - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path.clone())) .unwrap(); @@ -751,7 +862,7 @@ mod tests { std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap(); let image_path = temp_dir.path().join("test.iso"); std::fs::write(&image_path, b"iso").unwrap(); - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); assert!(msd .configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path),) @@ -771,7 +882,7 @@ mod tests { for lun in 1..8 { std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap(); } - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); msd.cleanup(temp_dir.path()).unwrap(); @@ -788,7 +899,7 @@ mod tests { std::fs::write(lun_path.join("file"), format!("backing-{lun}.img\n")).unwrap(); std::fs::write(lun_path.join("forced_eject"), b"0\n").unwrap(); } - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); // Ordinary files do not disappear with configfs groups, so cleanup is // expected to report directory-removal failures in this test fixture. @@ -809,7 +920,7 @@ mod tests { for lun in 0..2 { std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap(); } - let msd = MsdFunction::new(0, 1).unwrap(); + let msd = test_msd(1); let error = msd.cleanup(temp_dir.path()).unwrap_err(); diff --git a/src/otg/service.rs b/src/otg/service.rs index 64bc480c..29d081c5 100644 --- a/src/otg/service.rs +++ b/src/otg/service.rs @@ -6,7 +6,7 @@ use typeshare::typeshare; use super::bridge::NetworkBridgeRuntime; use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager}; -use super::msd::{MsdFunction, MsdLunConfig}; +use super::msd::{MsdFunction, MsdInquiryStrings, MsdLunConfig}; use crate::config::{ HidBackend, HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions, OtgNetworkConfig, UacConfig, @@ -62,6 +62,7 @@ pub(crate) struct OtgDesiredState { pub keyboard_leds: bool, pub msd_enabled: bool, pub msd_lun_capacity: u8, + pub msd_inquiry_strings: MsdInquiryStrings, pub network: OtgNetworkConfig, pub uac: UacConfig, } @@ -75,6 +76,7 @@ impl Default for OtgDesiredState { keyboard_leds: false, msd_enabled: false, msd_lun_capacity: 1, + msd_inquiry_strings: MsdInquiryStrings::default(), network: OtgNetworkConfig::default(), uac: UacConfig::default(), } @@ -90,6 +92,7 @@ impl OtgDesiredState { ) -> Result { network.validate()?; uac.validate()?; + msd.validate()?; let hid_functions = if hid.backend == HidBackend::Otg { let functions = hid.constrained_otg_functions(); Some(functions) @@ -115,6 +118,7 @@ impl OtgDesiredState { keyboard_leds: hid.effective_otg_keyboard_leds(), msd_enabled: msd.enabled, msd_lun_capacity: 1, + msd_inquiry_strings: MsdInquiryStrings::from(msd), network: network.clone(), uac: if uac.enabled { uac.clone() @@ -142,6 +146,7 @@ struct OtgServiceState { pub hid_enabled: bool, pub msd_enabled: bool, pub msd_lun_capacity: u8, + pub msd_inquiry_strings: MsdInquiryStrings, pub network: OtgNetworkConfig, pub uac: UacConfig, pub configured_udc: Option, @@ -160,6 +165,7 @@ impl Default for OtgServiceState { hid_enabled: false, msd_enabled: false, msd_lun_capacity: 1, + msd_inquiry_strings: MsdInquiryStrings::default(), network: OtgNetworkConfig::default(), uac: UacConfig::default(), configured_udc: None, @@ -355,6 +361,7 @@ impl OtgService { && state.hid_enabled == desired.hid_enabled() && state.msd_enabled == desired.msd_enabled && state.msd_lun_capacity == desired.msd_lun_capacity + && state.msd_inquiry_strings == desired.msd_inquiry_strings && state.network == desired.network && state.uac == desired.uac && state.configured_udc == desired.udc @@ -395,6 +402,7 @@ impl OtgService { state.hid_enabled = false; state.msd_enabled = false; state.msd_lun_capacity = 1; + state.msd_inquiry_strings = MsdInquiryStrings::default(); state.network = OtgNetworkConfig::default(); state.uac = UacConfig::default(); state.configured_udc = None; @@ -505,7 +513,10 @@ impl OtgService { } let msd_func = if desired.msd_enabled { - match manager.add_msd(desired.msd_lun_capacity) { + match manager.add_msd( + desired.msd_lun_capacity, + desired.msd_inquiry_strings.clone(), + ) { Ok(func) => { debug!("MSD function added to gadget"); Some(func) @@ -592,6 +603,7 @@ impl OtgService { state.hid_enabled = desired.hid_enabled(); state.msd_enabled = desired.msd_enabled; state.msd_lun_capacity = desired.msd_lun_capacity; + state.msd_inquiry_strings = desired.msd_inquiry_strings.clone(); state.network = desired.network.clone(); state.uac = desired.uac.clone(); state.configured_udc = Some(udc); @@ -715,6 +727,14 @@ mod tests { assert_ne!(single, multi); } + #[test] + fn inquiry_strings_participate_in_desired_state_equality() { + let original = OtgDesiredState::default(); + let mut changed = original.clone(); + changed.msd_inquiry_strings.flash = "Custom Flash".to_string(); + assert_ne!(original, changed); + } + #[test] fn onecloud_full_composite_is_not_rejected_before_configfs() { let hid = HidConfig { diff --git a/src/web/handlers/config/apply.rs b/src/web/handlers/config/apply.rs index 6bc70f5b..be073e4c 100644 --- a/src/web/handlers/config/apply.rs +++ b/src/web/handlers/config/apply.rs @@ -277,6 +277,9 @@ pub async fn apply_msd_config( let old_msd_enabled = old_config.enabled; let new_msd_enabled = effective_new_msd_enabled; let msd_dir_changed = old_config.msd_dir != new_config.msd_dir; + let inquiry_strings_changed = old_config.flash_inquiry_string + != new_config.flash_inquiry_string + || old_config.cdrom_inquiry_string != new_config.cdrom_inquiry_string; tracing::info!( "MSD enabled: old={}, new={}", @@ -286,6 +289,9 @@ pub async fn apply_msd_config( if msd_dir_changed { tracing::info!("MSD directory changed: {}", new_config.msd_dir); } + if inquiry_strings_changed { + tracing::info!("MSD inquiry strings changed"); + } let msd_dir = new_config.msd_dir_path(); if let Err(e) = std::fs::create_dir_all(msd_dir.join("images")) { @@ -295,12 +301,12 @@ pub async fn apply_msd_config( tracing::warn!("Failed to create MSD ventoy directory: {}", e); } - let needs_reload = options.force || old_msd_enabled != new_msd_enabled || msd_dir_changed; + let needs_reload = options.force + || old_msd_enabled != new_msd_enabled + || msd_dir_changed + || inquiry_strings_changed; if !needs_reload { - tracing::info!( - "MSD enabled state unchanged ({}) and directory unchanged, no reload needed", - new_msd_enabled - ); + tracing::info!("MSD configuration unchanged, no reload needed"); return Ok(()); } diff --git a/src/web/handlers/config/types.rs b/src/web/handlers/config/types.rs index 5565397e..3ec9299f 100644 --- a/src/web/handlers/config/types.rs +++ b/src/web/handlers/config/types.rs @@ -476,6 +476,8 @@ impl OtgNetworkConfigUpdate { pub struct MsdConfigUpdate { pub enabled: Option, pub msd_dir: Option, + pub flash_inquiry_string: Option, + pub cdrom_inquiry_string: Option, } #[cfg(unix)] @@ -492,6 +494,12 @@ impl MsdConfigUpdate { )); } } + if let Some(ref value) = self.flash_inquiry_string { + MsdConfig::validate_inquiry_string("Flash", value)?; + } + if let Some(ref value) = self.cdrom_inquiry_string { + MsdConfig::validate_inquiry_string("CD-ROM", value)?; + } Ok(()) } @@ -502,6 +510,12 @@ impl MsdConfigUpdate { if let Some(ref dir) = self.msd_dir { config.msd_dir = dir.trim().to_string(); } + if let Some(ref value) = self.flash_inquiry_string { + config.flash_inquiry_string = value.trim().to_string(); + } + if let Some(ref value) = self.cdrom_inquiry_string { + config.cdrom_inquiry_string = value.trim().to_string(); + } } } diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index de2f11e2..1249c4aa 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -409,7 +409,7 @@ export default { download: 'Download', downloadComplete: 'Download complete', largeFileWarning: '>2.2GB', - largeFileTooltip: 'File is larger than 2.2GB, please use Flash mode to mount', + largeFileTooltip: 'If DVD image support is not enabled in the kernel, only the first 2.2 GB is accessible after mounting.', operationInProgress: 'Operation in progress, please wait', selectDriveSize: 'Select virtual drive size', driveSpaceUnknown: 'Unable to read available space for the MSD directory filesystem', @@ -644,6 +644,8 @@ export default { username: 'Username', password: 'Password', msdDir: 'MSD directory', + msdFlashInquiryString: 'Flash device name', + msdCdromInquiryString: 'CD-ROM device name', atxPowerButton: 'Power Button', atxResetButton: 'Reset Button', atxPowerManagement: 'Power Management', diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index 571f55e3..b4da1bf5 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -408,7 +408,7 @@ export default { download: '下载', downloadComplete: '下载完成', largeFileWarning: '>2.2GB', - largeFileTooltip: '文件大于 2.2GB,请使用 Flash 模式挂载', + largeFileTooltip: '若内核未启用 DVD 镜像支持,挂载后仅前 2.2 GB 可访问。', operationInProgress: '操作进行中,请稍候', selectDriveSize: '选择虚拟驱动器大小', driveSpaceUnknown: '无法读取 MSD 目录所在储存空间的剩余可用空间', @@ -643,6 +643,8 @@ export default { username: '用户名', password: '密码', msdDir: 'MSD 目录', + msdFlashInquiryString: 'Flash 设备名称', + msdCdromInquiryString: 'CD-ROM 设备名称', atxPowerButton: '电源按钮', atxResetButton: '重启按钮', atxPowerManagement: '电源管理', diff --git a/web/src/types/generated.ts b/web/src/types/generated.ts index bb11ac9d..abe7545d 100644 --- a/web/src/types/generated.ts +++ b/web/src/types/generated.ts @@ -86,6 +86,8 @@ export interface OtgNetworkConfig { export interface MsdConfig { enabled: boolean; msd_dir: string; + flash_inquiry_string: string; + cdrom_inquiry_string: string; } export enum AtxDriverType { @@ -546,6 +548,8 @@ export interface HidConfigUpdate { export interface MsdConfigUpdate { enabled?: boolean; msd_dir?: string; + flash_inquiry_string?: string; + cdrom_inquiry_string?: string; } export interface NetworkInterfaceInfo { diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index dfbb47c1..e77830a1 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -675,6 +675,8 @@ const config = ref({ hid_ch9329_hybrid_mouse: false, msd_enabled: false, msd_dir: '', + msd_flash_inquiry_string: 'One-KVM Virtual Flash', + msd_cdrom_inquiry_string: 'One-KVM Virtual CD-ROM', otg_network_enabled: false, otg_network_driver: 'ncm' as 'ncm' | 'ecm' | 'rndis', otg_network_interface: '', @@ -1013,6 +1015,18 @@ const isHidFunctionSelectionValid = computed(() => { return !!(f.keyboard || f.mouse_relative || f.mouse_absolute || f.consumer) }) +function isValidInquiryString(value: string): boolean { + return /^[\x20-\x7e]{1,28}$/.test(value.trim()) +} + +const areMsdInquiryStringsValid = computed(() => + !config.value.msd_enabled + || ( + isValidInquiryString(config.value.msd_flash_inquiry_string) + && isValidInquiryString(config.value.msd_cdrom_inquiry_string) + ) +) + const otgVendorIdHex = ref('1d6b') const otgProductIdHex = ref('0104') const otgManufacturer = ref('One-KVM') @@ -1144,6 +1158,7 @@ const isCh9329DescriptorDirty = computed(() => { const isHidSettingsValid = computed(() => isHidFunctionSelectionValid.value && isCh9329DescriptorValid.value + && areMsdInquiryStringsValid.value ) watch(bindMode, (mode) => { @@ -1471,6 +1486,8 @@ async function saveConfig() { msd: { enabled: otgEnabled && config.value.msd_enabled, msd_dir: config.value.msd_dir || undefined, + flash_inquiry_string: config.value.msd_flash_inquiry_string, + cdrom_inquiry_string: config.value.msd_cdrom_inquiry_string, }, network: { enabled: otgEnabled && config.value.otg_network_enabled, @@ -1541,6 +1558,8 @@ async function loadConfig() { hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false, msd_enabled: msd.enabled || false, msd_dir: msd.msd_dir || '', + msd_flash_inquiry_string: msd.flash_inquiry_string || 'One-KVM Virtual Flash', + msd_cdrom_inquiry_string: msd.cdrom_inquiry_string || 'One-KVM Virtual CD-ROM', otg_network_enabled: otgNetwork.enabled, otg_network_driver: otgNetwork.driver_mode, uac_enabled: uac.enabled, @@ -3299,41 +3318,48 @@ watch(isWindows, () => {

{{ t('settings.otgHidProfile') }}

-
-
-
- +
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
-
- -
-
- +
+
+
+ +
+
- -
-
-
-
-
- + +
+
+ +
+
- -
- -
-
- + +
+ +
- -
- -
-
- -
-
@@ -3349,6 +3375,24 @@ watch(isWindows, () => {
+
+ + +
+
+ + +
@@ -3412,12 +3456,6 @@ watch(isWindows, () => { {{ t('settings.otgRuntimeDegraded') }}: {{ otgNetworkStatus.error || t('common.error') }}

-
-
- - -
-

{{ t('settings.otgProfileWarning') }}