feat: 增加 OTG HID 远程唤醒和 OTG MSD 设备名称配置

- 在补丁内核上启用 HID 写入唤醒及 USB 远程唤醒描述符
  - 支持全局配置 Flash 和 CD-ROM INQUIRY 字符串
  - 兼容普通内核的通用 INQUIRY 属性
  - 调整 OTG 功能布局并更新大容量 DVD 镜像提示
This commit is contained in:
mofeng-git
2026-07-29 13:56:52 +08:00
parent e0bddc2faa
commit 887f29096f
12 changed files with 347 additions and 67 deletions

View File

@@ -49,18 +49,51 @@ impl Default for VideoConfig {
pub struct MsdConfig { pub struct MsdConfig {
pub enabled: bool, pub enabled: bool,
pub msd_dir: String, 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 { impl Default for MsdConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
enabled: true, enabled: true,
msd_dir: String::new(), msd_dir: String::new(),
flash_inquiry_string: DEFAULT_FLASH_INQUIRY_STRING.to_string(),
cdrom_inquiry_string: DEFAULT_CDROM_INQUIRY_STRING.to_string(),
} }
} }
} }
impl MsdConfig { 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 { pub fn msd_dir_path(&self) -> std::path::PathBuf {
std::path::PathBuf::from(&self.msd_dir) 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());
}
}

View File

@@ -106,6 +106,16 @@ pub fn write_file(path: &Path, content: &str) -> Result<()> {
Ok(()) 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<bool> {
if !path.exists() {
return Ok(false);
}
write_file(path, content)?;
Ok(true)
}
pub fn write_bytes(path: &Path, data: &[u8]) -> Result<()> { pub fn write_bytes(path: &Path, data: &[u8]) -> Result<()> {
let mut file = File::create(path) let mut file = File::create(path)
.map_err(|e| AppError::Internal(format!("Failed to create {}: {}", path.display(), e)))?; .map_err(|e| AppError::Internal(format!("Failed to create {}: {}", path.display(), e)))?;

View File

@@ -3,6 +3,7 @@ use tracing::debug;
use super::configfs::{ use super::configfs::{
create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file, create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file,
write_file_if_exists,
}; };
use super::function::GadgetFunction; use super::function::GadgetFunction;
use super::report_desc::{ use super::report_desc::{
@@ -143,6 +144,10 @@ impl GadgetFunction for HidFunction {
self.func_type.report_desc(self.keyboard_leds), 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!( debug!(
"Created HID function: {} at {}", "Created HID function: {} at {}",
self.name(), self.name(),

View File

@@ -4,12 +4,12 @@ use tracing::{debug, error, info, warn};
use super::configfs::{ use super::configfs::{
configfs_path, create_dir, create_symlink, find_udc, is_configfs_available, remove_dir, 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, remove_file, write_file, write_file_if_exists, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE,
DEFAULT_USB_VENDOR_ID, USB_BCD_USB, DEFAULT_USB_PRODUCT_ID, DEFAULT_USB_VENDOR_ID, USB_BCD_USB,
}; };
use super::function::GadgetFunction; use super::function::GadgetFunction;
use super::hid::HidFunction; use super::hid::HidFunction;
use super::msd::MsdFunction; use super::msd::{MsdFunction, MsdInquiryStrings};
use super::network::NetworkFunction; use super::network::NetworkFunction;
use crate::config::OtgNetworkConfig; use crate::config::OtgNetworkConfig;
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
@@ -134,8 +134,12 @@ impl OtgGadgetManager {
Ok(device_path) Ok(device_path)
} }
pub fn add_msd(&mut self, lun_capacity: u8) -> Result<MsdFunction> { pub fn add_msd(
let func = MsdFunction::new(self.msd_instance, lun_capacity)?; &mut self,
lun_capacity: u8,
inquiry_strings: MsdInquiryStrings,
) -> Result<MsdFunction> {
let func = MsdFunction::new(self.msd_instance, lun_capacity, inquiry_strings)?;
let func_clone = func.clone(); let func_clone = func.clone();
self.add_function(Box::new(func))?; self.add_function(Box::new(func))?;
self.msd_instance += 1; self.msd_instance += 1;
@@ -196,6 +200,22 @@ impl OtgGadgetManager {
func.link(&self.config_path, &self.gadget_path)?; 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"); debug!("OTG USB Gadget setup complete");
Ok(()) Ok(())
} }

View File

@@ -5,6 +5,7 @@ use tracing::{debug, info, warn};
use super::configfs::{create_dir, create_symlink, remove_dir, remove_file, write_file}; use super::configfs::{create_dir, create_symlink, remove_dir, remove_file, write_file};
use super::function::GadgetFunction; use super::function::GadgetFunction;
use crate::config::{MsdConfig, DEFAULT_CDROM_INQUIRY_STRING, DEFAULT_FLASH_INQUIRY_STRING};
use crate::error::{AppError, MsdErrorCode, Result}; use crate::error::{AppError, MsdErrorCode, Result};
const MEDIA_TYPE_REBIND_DELAY_MS: u64 = 300; 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)] #[derive(Debug, Clone)]
pub struct MsdFunction { pub struct MsdFunction {
name: String, name: String,
lun_capacity: u8, lun_capacity: u8,
inquiry_strings: MsdInquiryStrings,
} }
impl MsdFunction { impl MsdFunction {
pub fn new(instance: u8, lun_capacity: u8) -> Result<Self> { pub fn new(instance: u8, lun_capacity: u8, inquiry_strings: MsdInquiryStrings) -> Result<Self> {
if lun_capacity != 1 && lun_capacity != 8 { if lun_capacity != 1 && lun_capacity != 8 {
return Err(AppError::BadRequest(format!( return Err(AppError::BadRequest(format!(
"MSD LUN capacity must be 1 or 8, got {lun_capacity}" "MSD LUN capacity must be 1 or 8, got {lun_capacity}"
@@ -73,6 +99,7 @@ impl MsdFunction {
Ok(Self { Ok(Self {
name: format!("mass_storage.usb{}", instance), name: format!("mass_storage.usb{}", instance),
lun_capacity, lun_capacity,
inquiry_strings,
}) })
} }
@@ -212,6 +239,23 @@ impl MsdFunction {
current_cdrom != if config.cdrom { "1" } else { "0" } current_cdrom != if config.cdrom { "1" } else { "0" }
} }
fn inquiry_string_path(lun_path: &Path, cdrom: bool) -> Option<PathBuf> {
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( fn configure_lun_attributes(
&self, &self,
lun_path: &Path, lun_path: &Path,
@@ -256,6 +300,7 @@ impl MsdFunction {
lun, current_cdrom, new_cdrom lun, current_cdrom, new_cdrom
); );
write_file(&lun_path.join("cdrom"), new_cdrom)?; write_file(&lun_path.join("cdrom"), new_cdrom)?;
self.write_inquiry_string(lun_path, config.cdrom)?;
} }
if current_ro != new_ro { if current_ro != new_ro {
debug!("Updating LUN {} ro: {} -> {}", lun, current_ro, new_ro); debug!("Updating LUN {} ro: {} -> {}", lun, current_ro, new_ro);
@@ -320,6 +365,26 @@ impl MsdFunction {
Ok(()) 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<()> { pub async fn disconnect_lun_async(&self, gadget_path: &Path, lun: u8) -> Result<()> {
let gadget_path = gadget_path.to_path_buf(); let gadget_path = gadget_path.to_path_buf();
let this = self.clone(); let this = self.clone();
@@ -455,6 +520,7 @@ impl GadgetFunction for MsdFunction {
for lun in 0..self.lun_capacity { for lun in 0..self.lun_capacity {
self.clear_lun_unbound(gadget_path, lun)?; self.clear_lun_unbound(gadget_path, lun)?;
self.write_inquiry_strings(&self.lun_path(gadget_path, lun))?;
} }
debug!("Created MSD function: {}", self.name()); debug!("Created MSD function: {}", self.name());
@@ -526,6 +592,10 @@ mod tests {
use super::*; use super::*;
use tempfile::TempDir; use tempfile::TempDir;
fn test_msd(capacity: u8) -> MsdFunction {
MsdFunction::new(0, capacity, MsdInquiryStrings::default()).unwrap()
}
#[test] #[test]
fn test_lun_config_cdrom() { fn test_lun_config_cdrom() {
let config = MsdLunConfig::cdrom(PathBuf::from("/tmp/test.iso")); let config = MsdLunConfig::cdrom(PathBuf::from("/tmp/test.iso"));
@@ -542,21 +612,62 @@ mod tests {
assert!(config.removable); 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] #[test]
fn test_msd_function_name() { 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.name(), "mass_storage.usb0");
assert_eq!(msd.lun_capacity, 1); assert_eq!(msd.lun_capacity, 1);
let multi = MsdFunction::new(0, 8).unwrap(); let multi = test_msd(8);
assert_eq!(multi.lun_capacity, 8); assert_eq!(multi.lun_capacity, 8);
} }
#[test] #[test]
fn test_msd_function_rejects_invalid_capacity() { fn test_msd_function_rejects_invalid_capacity() {
assert!(MsdFunction::new(0, 0).is_err()); assert!(MsdFunction::new(0, 0, MsdInquiryStrings::default()).is_err());
assert!(MsdFunction::new(0, 2).is_err()); assert!(MsdFunction::new(0, 2, MsdInquiryStrings::default()).is_err());
assert!(MsdFunction::new(0, 9).is_err()); assert!(MsdFunction::new(0, 9, MsdInquiryStrings::default()).is_err());
} }
#[test] #[test]
@@ -575,7 +686,7 @@ mod tests {
std::fs::create_dir_all(&lun_path).unwrap(); 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("file"), b"backing.img\n").unwrap();
std::fs::write(lun_path.join("forced_eject"), b"0\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(); 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"); let lun_path = temp_dir.path().join("functions/mass_storage.usb0/lun.0");
std::fs::create_dir_all(&lun_path).unwrap(); 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("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(); 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"); 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::create_dir_all(lun_path.join("forced_eject")).unwrap();
std::fs::write(lun_path.join("file"), b"backing.img\n").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(); 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("file"), format!("backing-{lun}.img\n")).unwrap();
std::fs::write(lun_path.join("forced_eject"), b"0\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(); msd.disconnect_lun(temp_dir.path(), 1).unwrap();
@@ -656,7 +767,7 @@ mod tests {
for capacity in [1, 8] { for capacity in [1, 8] {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
std::fs::create_dir_all(temp_dir.path().join("functions")).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(); 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(); std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
let image_path = temp_dir.path().join("test.img"); let image_path = temp_dir.path().join("test.img");
std::fs::write(&image_path, b"image").unwrap(); 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)) msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::disk(image_path, false))
.unwrap(); .unwrap();
@@ -711,7 +822,7 @@ mod tests {
std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap(); std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
let image_path = temp_dir.path().join("test.iso"); let image_path = temp_dir.path().join("test.iso");
std::fs::write(&image_path, b"iso").unwrap(); 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())) msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path.clone()))
.unwrap(); .unwrap();
@@ -751,7 +862,7 @@ mod tests {
std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap(); std::fs::write(temp_dir.path().join("UDC"), b"test.udc\n").unwrap();
let image_path = temp_dir.path().join("test.iso"); let image_path = temp_dir.path().join("test.iso");
std::fs::write(&image_path, b"iso").unwrap(); std::fs::write(&image_path, b"iso").unwrap();
let msd = MsdFunction::new(0, 1).unwrap(); let msd = test_msd(1);
assert!(msd assert!(msd
.configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path),) .configure_lun(temp_dir.path(), 0, &MsdLunConfig::cdrom(image_path),)
@@ -771,7 +882,7 @@ mod tests {
for lun in 1..8 { for lun in 1..8 {
std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap(); 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(); 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("file"), format!("backing-{lun}.img\n")).unwrap();
std::fs::write(lun_path.join("forced_eject"), b"0\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 // Ordinary files do not disappear with configfs groups, so cleanup is
// expected to report directory-removal failures in this test fixture. // expected to report directory-removal failures in this test fixture.
@@ -809,7 +920,7 @@ mod tests {
for lun in 0..2 { for lun in 0..2 {
std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap(); 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(); let error = msd.cleanup(temp_dir.path()).unwrap_err();

View File

@@ -6,7 +6,7 @@ use typeshare::typeshare;
use super::bridge::NetworkBridgeRuntime; use super::bridge::NetworkBridgeRuntime;
use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager}; use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager};
use super::msd::{MsdFunction, MsdLunConfig}; use super::msd::{MsdFunction, MsdInquiryStrings, MsdLunConfig};
use crate::config::{ use crate::config::{
HidBackend, HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions, OtgNetworkConfig, HidBackend, HidConfig, MsdConfig, OtgDescriptorConfig, OtgHidFunctions, OtgNetworkConfig,
UacConfig, UacConfig,
@@ -62,6 +62,7 @@ pub(crate) struct OtgDesiredState {
pub keyboard_leds: bool, pub keyboard_leds: bool,
pub msd_enabled: bool, pub msd_enabled: bool,
pub msd_lun_capacity: u8, pub msd_lun_capacity: u8,
pub msd_inquiry_strings: MsdInquiryStrings,
pub network: OtgNetworkConfig, pub network: OtgNetworkConfig,
pub uac: UacConfig, pub uac: UacConfig,
} }
@@ -75,6 +76,7 @@ impl Default for OtgDesiredState {
keyboard_leds: false, keyboard_leds: false,
msd_enabled: false, msd_enabled: false,
msd_lun_capacity: 1, msd_lun_capacity: 1,
msd_inquiry_strings: MsdInquiryStrings::default(),
network: OtgNetworkConfig::default(), network: OtgNetworkConfig::default(),
uac: UacConfig::default(), uac: UacConfig::default(),
} }
@@ -90,6 +92,7 @@ impl OtgDesiredState {
) -> Result<Self> { ) -> Result<Self> {
network.validate()?; network.validate()?;
uac.validate()?; uac.validate()?;
msd.validate()?;
let hid_functions = if hid.backend == HidBackend::Otg { let hid_functions = if hid.backend == HidBackend::Otg {
let functions = hid.constrained_otg_functions(); let functions = hid.constrained_otg_functions();
Some(functions) Some(functions)
@@ -115,6 +118,7 @@ impl OtgDesiredState {
keyboard_leds: hid.effective_otg_keyboard_leds(), keyboard_leds: hid.effective_otg_keyboard_leds(),
msd_enabled: msd.enabled, msd_enabled: msd.enabled,
msd_lun_capacity: 1, msd_lun_capacity: 1,
msd_inquiry_strings: MsdInquiryStrings::from(msd),
network: network.clone(), network: network.clone(),
uac: if uac.enabled { uac: if uac.enabled {
uac.clone() uac.clone()
@@ -142,6 +146,7 @@ struct OtgServiceState {
pub hid_enabled: bool, pub hid_enabled: bool,
pub msd_enabled: bool, pub msd_enabled: bool,
pub msd_lun_capacity: u8, pub msd_lun_capacity: u8,
pub msd_inquiry_strings: MsdInquiryStrings,
pub network: OtgNetworkConfig, pub network: OtgNetworkConfig,
pub uac: UacConfig, pub uac: UacConfig,
pub configured_udc: Option<String>, pub configured_udc: Option<String>,
@@ -160,6 +165,7 @@ impl Default for OtgServiceState {
hid_enabled: false, hid_enabled: false,
msd_enabled: false, msd_enabled: false,
msd_lun_capacity: 1, msd_lun_capacity: 1,
msd_inquiry_strings: MsdInquiryStrings::default(),
network: OtgNetworkConfig::default(), network: OtgNetworkConfig::default(),
uac: UacConfig::default(), uac: UacConfig::default(),
configured_udc: None, configured_udc: None,
@@ -355,6 +361,7 @@ impl OtgService {
&& state.hid_enabled == desired.hid_enabled() && state.hid_enabled == desired.hid_enabled()
&& state.msd_enabled == desired.msd_enabled && state.msd_enabled == desired.msd_enabled
&& state.msd_lun_capacity == desired.msd_lun_capacity && state.msd_lun_capacity == desired.msd_lun_capacity
&& state.msd_inquiry_strings == desired.msd_inquiry_strings
&& state.network == desired.network && state.network == desired.network
&& state.uac == desired.uac && state.uac == desired.uac
&& state.configured_udc == desired.udc && state.configured_udc == desired.udc
@@ -395,6 +402,7 @@ impl OtgService {
state.hid_enabled = false; state.hid_enabled = false;
state.msd_enabled = false; state.msd_enabled = false;
state.msd_lun_capacity = 1; state.msd_lun_capacity = 1;
state.msd_inquiry_strings = MsdInquiryStrings::default();
state.network = OtgNetworkConfig::default(); state.network = OtgNetworkConfig::default();
state.uac = UacConfig::default(); state.uac = UacConfig::default();
state.configured_udc = None; state.configured_udc = None;
@@ -505,7 +513,10 @@ impl OtgService {
} }
let msd_func = if desired.msd_enabled { 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) => { Ok(func) => {
debug!("MSD function added to gadget"); debug!("MSD function added to gadget");
Some(func) Some(func)
@@ -592,6 +603,7 @@ impl OtgService {
state.hid_enabled = desired.hid_enabled(); state.hid_enabled = desired.hid_enabled();
state.msd_enabled = desired.msd_enabled; state.msd_enabled = desired.msd_enabled;
state.msd_lun_capacity = desired.msd_lun_capacity; state.msd_lun_capacity = desired.msd_lun_capacity;
state.msd_inquiry_strings = desired.msd_inquiry_strings.clone();
state.network = desired.network.clone(); state.network = desired.network.clone();
state.uac = desired.uac.clone(); state.uac = desired.uac.clone();
state.configured_udc = Some(udc); state.configured_udc = Some(udc);
@@ -715,6 +727,14 @@ mod tests {
assert_ne!(single, multi); 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] #[test]
fn onecloud_full_composite_is_not_rejected_before_configfs() { fn onecloud_full_composite_is_not_rejected_before_configfs() {
let hid = HidConfig { let hid = HidConfig {

View File

@@ -277,6 +277,9 @@ pub async fn apply_msd_config(
let old_msd_enabled = old_config.enabled; let old_msd_enabled = old_config.enabled;
let new_msd_enabled = effective_new_msd_enabled; let new_msd_enabled = effective_new_msd_enabled;
let msd_dir_changed = old_config.msd_dir != new_config.msd_dir; 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!( tracing::info!(
"MSD enabled: old={}, new={}", "MSD enabled: old={}, new={}",
@@ -286,6 +289,9 @@ pub async fn apply_msd_config(
if msd_dir_changed { if msd_dir_changed {
tracing::info!("MSD directory changed: {}", new_config.msd_dir); 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(); let msd_dir = new_config.msd_dir_path();
if let Err(e) = std::fs::create_dir_all(msd_dir.join("images")) { 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); 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 { if !needs_reload {
tracing::info!( tracing::info!("MSD configuration unchanged, no reload needed");
"MSD enabled state unchanged ({}) and directory unchanged, no reload needed",
new_msd_enabled
);
return Ok(()); return Ok(());
} }

View File

@@ -476,6 +476,8 @@ impl OtgNetworkConfigUpdate {
pub struct MsdConfigUpdate { pub struct MsdConfigUpdate {
pub enabled: Option<bool>, pub enabled: Option<bool>,
pub msd_dir: Option<String>, pub msd_dir: Option<String>,
pub flash_inquiry_string: Option<String>,
pub cdrom_inquiry_string: Option<String>,
} }
#[cfg(unix)] #[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(()) Ok(())
} }
@@ -502,6 +510,12 @@ impl MsdConfigUpdate {
if let Some(ref dir) = self.msd_dir { if let Some(ref dir) = self.msd_dir {
config.msd_dir = dir.trim().to_string(); 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();
}
} }
} }

View File

@@ -409,7 +409,7 @@ export default {
download: 'Download', download: 'Download',
downloadComplete: 'Download complete', downloadComplete: 'Download complete',
largeFileWarning: '>2.2GB', 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', operationInProgress: 'Operation in progress, please wait',
selectDriveSize: 'Select virtual drive size', selectDriveSize: 'Select virtual drive size',
driveSpaceUnknown: 'Unable to read available space for the MSD directory filesystem', driveSpaceUnknown: 'Unable to read available space for the MSD directory filesystem',
@@ -644,6 +644,8 @@ export default {
username: 'Username', username: 'Username',
password: 'Password', password: 'Password',
msdDir: 'MSD directory', msdDir: 'MSD directory',
msdFlashInquiryString: 'Flash device name',
msdCdromInquiryString: 'CD-ROM device name',
atxPowerButton: 'Power Button', atxPowerButton: 'Power Button',
atxResetButton: 'Reset Button', atxResetButton: 'Reset Button',
atxPowerManagement: 'Power Management', atxPowerManagement: 'Power Management',

View File

@@ -408,7 +408,7 @@ export default {
download: '下载', download: '下载',
downloadComplete: '下载完成', downloadComplete: '下载完成',
largeFileWarning: '>2.2GB', largeFileWarning: '>2.2GB',
largeFileTooltip: '文件大于 2.2GB,请使用 Flash 模式挂载', largeFileTooltip: '若内核未启用 DVD 镜像支持,挂载后仅前 2.2 GB 可访问。',
operationInProgress: '操作进行中,请稍候', operationInProgress: '操作进行中,请稍候',
selectDriveSize: '选择虚拟驱动器大小', selectDriveSize: '选择虚拟驱动器大小',
driveSpaceUnknown: '无法读取 MSD 目录所在储存空间的剩余可用空间', driveSpaceUnknown: '无法读取 MSD 目录所在储存空间的剩余可用空间',
@@ -643,6 +643,8 @@ export default {
username: '用户名', username: '用户名',
password: '密码', password: '密码',
msdDir: 'MSD 目录', msdDir: 'MSD 目录',
msdFlashInquiryString: 'Flash 设备名称',
msdCdromInquiryString: 'CD-ROM 设备名称',
atxPowerButton: '电源按钮', atxPowerButton: '电源按钮',
atxResetButton: '重启按钮', atxResetButton: '重启按钮',
atxPowerManagement: '电源管理', atxPowerManagement: '电源管理',

View File

@@ -86,6 +86,8 @@ export interface OtgNetworkConfig {
export interface MsdConfig { export interface MsdConfig {
enabled: boolean; enabled: boolean;
msd_dir: string; msd_dir: string;
flash_inquiry_string: string;
cdrom_inquiry_string: string;
} }
export enum AtxDriverType { export enum AtxDriverType {
@@ -546,6 +548,8 @@ export interface HidConfigUpdate {
export interface MsdConfigUpdate { export interface MsdConfigUpdate {
enabled?: boolean; enabled?: boolean;
msd_dir?: string; msd_dir?: string;
flash_inquiry_string?: string;
cdrom_inquiry_string?: string;
} }
export interface NetworkInterfaceInfo { export interface NetworkInterfaceInfo {

View File

@@ -675,6 +675,8 @@ const config = ref({
hid_ch9329_hybrid_mouse: false, hid_ch9329_hybrid_mouse: false,
msd_enabled: false, msd_enabled: false,
msd_dir: '', 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_enabled: false,
otg_network_driver: 'ncm' as 'ncm' | 'ecm' | 'rndis', otg_network_driver: 'ncm' as 'ncm' | 'ecm' | 'rndis',
otg_network_interface: '', otg_network_interface: '',
@@ -1013,6 +1015,18 @@ const isHidFunctionSelectionValid = computed(() => {
return !!(f.keyboard || f.mouse_relative || f.mouse_absolute || f.consumer) 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 otgVendorIdHex = ref('1d6b')
const otgProductIdHex = ref('0104') const otgProductIdHex = ref('0104')
const otgManufacturer = ref('One-KVM') const otgManufacturer = ref('One-KVM')
@@ -1144,6 +1158,7 @@ const isCh9329DescriptorDirty = computed(() => {
const isHidSettingsValid = computed(() => const isHidSettingsValid = computed(() =>
isHidFunctionSelectionValid.value isHidFunctionSelectionValid.value
&& isCh9329DescriptorValid.value && isCh9329DescriptorValid.value
&& areMsdInquiryStringsValid.value
) )
watch(bindMode, (mode) => { watch(bindMode, (mode) => {
@@ -1471,6 +1486,8 @@ async function saveConfig() {
msd: { msd: {
enabled: otgEnabled && config.value.msd_enabled, enabled: otgEnabled && config.value.msd_enabled,
msd_dir: config.value.msd_dir || undefined, 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: { network: {
enabled: otgEnabled && config.value.otg_network_enabled, enabled: otgEnabled && config.value.otg_network_enabled,
@@ -1541,6 +1558,8 @@ async function loadConfig() {
hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false, hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false,
msd_enabled: msd.enabled || false, msd_enabled: msd.enabled || false,
msd_dir: msd.msd_dir || '', 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_enabled: otgNetwork.enabled,
otg_network_driver: otgNetwork.driver_mode, otg_network_driver: otgNetwork.driver_mode,
uac_enabled: uac.enabled, uac_enabled: uac.enabled,
@@ -3299,21 +3318,7 @@ watch(isWindows, () => {
<h4 class="text-sm font-medium">{{ t('settings.otgHidProfile') }}</h4> <h4 class="text-sm font-medium">{{ t('settings.otgHidProfile') }}</h4>
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
<div class="space-y-3 rounded-md border border-border/60 p-3"> <div class="grid gap-3 md:grid-cols-2">
<div class="flex items-center justify-between gap-4">
<div>
<Label>{{ t('settings.otgFunctionMouseRelative') }}</Label>
</div>
<Switch v-model="config.hid_otg_functions.mouse_relative" />
</div>
<Separator />
<div class="flex items-center justify-between gap-4">
<div>
<Label>{{ t('settings.otgFunctionMouseAbsolute') }}</Label>
</div>
<Switch v-model="config.hid_otg_functions.mouse_absolute" />
</div>
</div>
<div class="space-y-3 rounded-md border border-border/60 p-3"> <div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div> <div>
@@ -3336,6 +3341,27 @@ watch(isWindows, () => {
<Switch v-model="config.hid_otg_keyboard_leds" :disabled="isKeyboardLedToggleDisabled" /> <Switch v-model="config.hid_otg_keyboard_leds" :disabled="isKeyboardLedToggleDisabled" />
</div> </div>
</div> </div>
<div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between gap-4">
<div>
<Label>{{ t('settings.otgFunctionMouseRelative') }}</Label>
</div>
<Switch v-model="config.hid_otg_functions.mouse_relative" />
</div>
<Separator />
<div class="flex items-center justify-between gap-4">
<div>
<Label>{{ t('settings.otgFunctionMouseAbsolute') }}</Label>
</div>
<Switch v-model="config.hid_otg_functions.mouse_absolute" />
</div>
<Separator />
<div class="flex items-center justify-between gap-4">
<Label>{{ t('settings.uacMic') }}</Label>
<Switch v-model="config.uac_enabled" />
</div>
</div>
</div>
<div class="space-y-3 rounded-md border border-border/60 p-3"> <div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div> <div>
@@ -3349,6 +3375,24 @@ watch(isWindows, () => {
<Label for="msd-dir">{{ t('settings.msdDir') }}</Label> <Label for="msd-dir">{{ t('settings.msdDir') }}</Label>
<Input id="msd-dir" v-model="config.msd_dir" placeholder="/etc/one-kvm/msd" /> <Input id="msd-dir" v-model="config.msd_dir" placeholder="/etc/one-kvm/msd" />
</div> </div>
<div class="space-y-2">
<Label for="msd-flash-inquiry-string">{{ t('settings.msdFlashInquiryString') }}</Label>
<Input
id="msd-flash-inquiry-string"
v-model="config.msd_flash_inquiry_string"
placeholder="One-KVM Virtual Flash"
maxlength="28"
/>
</div>
<div class="space-y-2">
<Label for="msd-cdrom-inquiry-string">{{ t('settings.msdCdromInquiryString') }}</Label>
<Input
id="msd-cdrom-inquiry-string"
v-model="config.msd_cdrom_inquiry_string"
placeholder="One-KVM Virtual CD-ROM"
maxlength="28"
/>
</div>
</template> </template>
</div> </div>
<div class="space-y-3 rounded-md border border-border/60 p-3"> <div class="space-y-3 rounded-md border border-border/60 p-3">
@@ -3412,12 +3456,6 @@ watch(isWindows, () => {
{{ t('settings.otgRuntimeDegraded') }}: {{ otgNetworkStatus.error || t('common.error') }} {{ t('settings.otgRuntimeDegraded') }}: {{ otgNetworkStatus.error || t('common.error') }}
</p> </p>
</div> </div>
<div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between gap-4">
<Label>{{ t('settings.uacMic') }}</Label>
<Switch v-model="config.uac_enabled" />
</div>
</div>
</div> </div>
<p class="text-xs text-warning"> <p class="text-xs text-warning">
{{ t('settings.otgProfileWarning') }} {{ t('settings.otgProfileWarning') }}