mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
feat: 虚拟媒体支持同时挂载多个镜像
This commit is contained in:
@@ -135,8 +135,8 @@ impl OtgGadgetManager {
|
||||
Ok(device_path)
|
||||
}
|
||||
|
||||
pub fn add_msd(&mut self) -> Result<MsdFunction> {
|
||||
let func = MsdFunction::new(self.msd_instance);
|
||||
pub fn add_msd(&mut self, lun_capacity: u8) -> Result<MsdFunction> {
|
||||
let func = MsdFunction::new(self.msd_instance, lun_capacity)?;
|
||||
let func_clone = func.clone();
|
||||
self.add_function(Box::new(func))?;
|
||||
self.msd_instance += 1;
|
||||
|
||||
195
src/otg/msd.rs
195
src/otg/msd.rs
@@ -56,13 +56,21 @@ impl MsdLunConfig {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MsdFunction {
|
||||
name: String,
|
||||
lun_capacity: u8,
|
||||
}
|
||||
|
||||
impl MsdFunction {
|
||||
pub fn new(instance: u8) -> Self {
|
||||
Self {
|
||||
name: format!("mass_storage.usb{}", instance),
|
||||
pub fn new(instance: u8, lun_capacity: u8) -> Result<Self> {
|
||||
if lun_capacity != 1 && lun_capacity != 8 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"MSD LUN capacity must be 1 or 8, got {lun_capacity}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
name: format!("mass_storage.usb{}", instance),
|
||||
lun_capacity,
|
||||
})
|
||||
}
|
||||
|
||||
fn function_path(&self, gadget_path: &Path) -> PathBuf {
|
||||
@@ -73,6 +81,32 @@ impl MsdFunction {
|
||||
self.function_path(gadget_path).join(format!("lun.{}", lun))
|
||||
}
|
||||
|
||||
fn existing_lun_paths(&self, gadget_path: &Path) -> Result<Vec<(u16, PathBuf)>> {
|
||||
let func_path = self.function_path(gadget_path);
|
||||
if !func_path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let entries = fs::read_dir(&func_path).map_err(|e| {
|
||||
AppError::Internal(format!(
|
||||
"Failed to read MSD function directory {}: {}",
|
||||
func_path.display(),
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let mut luns = entries
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let name = entry.file_name();
|
||||
let name = name.to_str()?;
|
||||
let lun = name.strip_prefix("lun.")?.parse::<u16>().ok()?;
|
||||
Some((lun, entry.path()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
luns.sort_by_key(|(lun, _)| *lun);
|
||||
Ok(luns)
|
||||
}
|
||||
|
||||
pub async fn configure_lun_async(
|
||||
&self,
|
||||
gadget_path: &Path,
|
||||
@@ -88,11 +122,32 @@ impl MsdFunction {
|
||||
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
|
||||
}
|
||||
|
||||
fn clear_lun_unbound(&self, gadget_path: &Path, lun: u8) -> Result<()> {
|
||||
let lun_path = self.lun_path(gadget_path, lun);
|
||||
if !lun_path.exists() {
|
||||
create_dir(&lun_path)?;
|
||||
}
|
||||
write_file(&lun_path.join("file"), "")?;
|
||||
let _ = write_file(&lun_path.join("cdrom"), "0");
|
||||
let _ = write_file(&lun_path.join("ro"), "0");
|
||||
let _ = write_file(&lun_path.join("removable"), "1");
|
||||
let _ = write_file(&lun_path.join("nofua"), "1");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn configure_lun(&self, gadget_path: &Path, lun: u8, config: &MsdLunConfig) -> Result<()> {
|
||||
if lun >= self.lun_capacity {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"LUN {lun} is outside MSD capacity {}",
|
||||
self.lun_capacity
|
||||
)));
|
||||
}
|
||||
let lun_path = self.lun_path(gadget_path, lun);
|
||||
|
||||
if !lun_path.exists() {
|
||||
create_dir(&lun_path)?;
|
||||
return Err(AppError::Internal(format!(
|
||||
"Configured MSD LUN {lun} does not exist"
|
||||
)));
|
||||
}
|
||||
|
||||
let read_attr = |attr: &str| -> String {
|
||||
@@ -210,8 +265,18 @@ impl MsdFunction {
|
||||
}
|
||||
|
||||
pub fn disconnect_lun(&self, gadget_path: &Path, lun: u8) -> Result<()> {
|
||||
if lun >= self.lun_capacity {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"LUN {lun} is outside MSD capacity {}",
|
||||
self.lun_capacity
|
||||
)));
|
||||
}
|
||||
let lun_path = self.lun_path(gadget_path, lun);
|
||||
|
||||
self.disconnect_lun_path(&lun_path, lun as u16)
|
||||
}
|
||||
|
||||
fn disconnect_lun_path(&self, lun_path: &Path, lun: u16) -> Result<()> {
|
||||
if lun_path.exists() {
|
||||
let forced_eject_path = lun_path.join("forced_eject");
|
||||
if forced_eject_path.exists() {
|
||||
@@ -226,11 +291,17 @@ impl MsdFunction {
|
||||
"forced_eject write failed: {}, falling back to clearing file",
|
||||
e
|
||||
);
|
||||
write_file(&lun_path.join("file"), "")?;
|
||||
let file_path = lun_path.join("file");
|
||||
if file_path.exists() {
|
||||
write_file(&file_path, "")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
write_file(&lun_path.join("file"), "")?;
|
||||
let file_path = lun_path.join("file");
|
||||
if file_path.exists() {
|
||||
write_file(&file_path, "")?;
|
||||
}
|
||||
}
|
||||
info!("LUN {} disconnected", lun);
|
||||
}
|
||||
@@ -275,16 +346,10 @@ impl GadgetFunction for MsdFunction {
|
||||
let _ = write_file(&stall_path, "0");
|
||||
}
|
||||
|
||||
let lun0_path = func_path.join("lun.0");
|
||||
if !lun0_path.exists() {
|
||||
create_dir(&lun0_path)?;
|
||||
for lun in 0..self.lun_capacity {
|
||||
self.clear_lun_unbound(gadget_path, lun)?;
|
||||
}
|
||||
|
||||
let _ = write_file(&lun0_path.join("cdrom"), "0");
|
||||
let _ = write_file(&lun0_path.join("ro"), "0");
|
||||
let _ = write_file(&lun0_path.join("removable"), "1");
|
||||
let _ = write_file(&lun0_path.join("nofua"), "1");
|
||||
|
||||
debug!("Created MSD function: {}", self.name());
|
||||
Ok(())
|
||||
}
|
||||
@@ -311,8 +376,25 @@ impl GadgetFunction for MsdFunction {
|
||||
fn cleanup(&self, gadget_path: &Path) -> Result<()> {
|
||||
let func_path = self.function_path(gadget_path);
|
||||
|
||||
for lun in 0..8 {
|
||||
let _ = self.disconnect_lun(gadget_path, lun);
|
||||
let lun_paths = match self.existing_lun_paths(gadget_path) {
|
||||
Ok(luns) => luns,
|
||||
Err(e) => {
|
||||
warn!("Could not enumerate MSD LUN directories: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
for (lun, lun_path) in lun_paths {
|
||||
if let Err(e) = self.disconnect_lun_path(&lun_path, lun) {
|
||||
warn!("Could not disconnect LUN {} during cleanup: {}", lun, e);
|
||||
}
|
||||
// lun.0 is the mass-storage function's configfs default group. It
|
||||
// cannot be removed directly and is released with the function.
|
||||
if lun == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = remove_dir(&lun_path) {
|
||||
warn!("Could not remove LUN {} directory: {}", lun, e);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = remove_dir(&func_path) {
|
||||
@@ -327,6 +409,7 @@ impl GadgetFunction for MsdFunction {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_lun_config_cdrom() {
|
||||
@@ -346,8 +429,86 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_msd_function_name() {
|
||||
let msd = MsdFunction::new(0);
|
||||
let msd = MsdFunction::new(0, 1).unwrap();
|
||||
assert_eq!(msd.name(), "mass_storage.usb0");
|
||||
assert_eq!(msd.endpoints_required(), 2);
|
||||
assert_eq!(msd.lun_capacity, 1);
|
||||
|
||||
let multi = MsdFunction::new(0, 8).unwrap();
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_uses_configured_lun_capacity() {
|
||||
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();
|
||||
|
||||
msd.create(temp_dir.path()).unwrap();
|
||||
|
||||
for lun in 0..capacity {
|
||||
assert!(msd.lun_path(temp_dir.path(), lun).exists());
|
||||
}
|
||||
assert!(!msd.lun_path(temp_dir.path(), capacity).exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configure_lun_does_not_rebind_udc() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let lun_path = temp_dir.path().join("functions/mass_storage.usb0/lun.0");
|
||||
std::fs::create_dir_all(&lun_path).unwrap();
|
||||
for attr in ["file", "cdrom", "ro", "removable", "nofua"] {
|
||||
std::fs::write(lun_path.join(attr), b"0\n").unwrap();
|
||||
}
|
||||
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();
|
||||
|
||||
msd.configure_lun(temp_dir.path(), 0, &MsdLunConfig::disk(image_path, false))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(temp_dir.path().join("UDC")).unwrap(),
|
||||
"test.udc\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_removes_all_dynamic_luns_including_stale_capacity() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let func_path = temp_dir.path().join("functions/mass_storage.usb0");
|
||||
for lun in 1..8 {
|
||||
std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap();
|
||||
}
|
||||
let msd = MsdFunction::new(0, 1).unwrap();
|
||||
|
||||
msd.cleanup(temp_dir.path()).unwrap();
|
||||
|
||||
assert!(!func_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_leaves_default_lun_for_configfs_function_removal() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let func_path = temp_dir.path().join("functions/mass_storage.usb0");
|
||||
for lun in 0..2 {
|
||||
std::fs::create_dir_all(func_path.join(format!("lun.{lun}"))).unwrap();
|
||||
}
|
||||
let msd = MsdFunction::new(0, 1).unwrap();
|
||||
|
||||
msd.cleanup(temp_dir.path()).unwrap();
|
||||
|
||||
assert!(func_path.join("lun.0").exists());
|
||||
assert!(!func_path.join("lun.1").exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ pub(crate) struct OtgDesiredState {
|
||||
pub hid_functions: Option<OtgHidFunctions>,
|
||||
pub keyboard_leds: bool,
|
||||
pub msd_enabled: bool,
|
||||
pub msd_lun_capacity: u8,
|
||||
pub max_endpoints: u8,
|
||||
}
|
||||
|
||||
@@ -49,6 +50,7 @@ impl Default for OtgDesiredState {
|
||||
hid_functions: None,
|
||||
keyboard_leds: false,
|
||||
msd_enabled: false,
|
||||
msd_lun_capacity: 1,
|
||||
max_endpoints: super::endpoint::DEFAULT_MAX_ENDPOINTS,
|
||||
}
|
||||
}
|
||||
@@ -71,6 +73,7 @@ impl OtgDesiredState {
|
||||
hid_functions,
|
||||
keyboard_leds: hid.effective_otg_keyboard_leds(),
|
||||
msd_enabled: msd.enabled,
|
||||
msd_lun_capacity: 1,
|
||||
max_endpoints: hid
|
||||
.resolved_otg_endpoint_limit()
|
||||
.unwrap_or(super::endpoint::DEFAULT_MAX_ENDPOINTS),
|
||||
@@ -83,11 +86,12 @@ impl OtgDesiredState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct OtgServiceState {
|
||||
pub gadget_active: bool,
|
||||
pub hid_enabled: bool,
|
||||
pub msd_enabled: bool,
|
||||
pub msd_lun_capacity: u8,
|
||||
pub configured_udc: Option<String>,
|
||||
pub hid_paths: Option<HidDevicePaths>,
|
||||
pub hid_functions: Option<OtgHidFunctions>,
|
||||
@@ -97,6 +101,24 @@ struct OtgServiceState {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for OtgServiceState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
gadget_active: false,
|
||||
hid_enabled: false,
|
||||
msd_enabled: false,
|
||||
msd_lun_capacity: 1,
|
||||
configured_udc: None,
|
||||
hid_paths: None,
|
||||
hid_functions: None,
|
||||
keyboard_leds_enabled: false,
|
||||
max_endpoints: super::endpoint::DEFAULT_MAX_ENDPOINTS,
|
||||
descriptor: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OtgService {
|
||||
manager: Mutex<Option<OtgGadgetManager>>,
|
||||
state: RwLock<OtgServiceState>,
|
||||
@@ -131,8 +153,41 @@ impl OtgService {
|
||||
self.msd_function.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn msd_lun_capacity(&self) -> u8 {
|
||||
self.desired.read().await.msd_lun_capacity
|
||||
}
|
||||
|
||||
pub async fn apply_config(&self, hid: &HidConfig, msd: &MsdConfig) -> Result<()> {
|
||||
let desired = OtgDesiredState::from_config(hid, msd)?;
|
||||
let desired = self
|
||||
.desired_from_config_preserving_runtime(hid, msd)
|
||||
.await?;
|
||||
self.apply_desired_state(desired).await
|
||||
}
|
||||
|
||||
async fn desired_from_config_preserving_runtime(
|
||||
&self,
|
||||
hid: &HidConfig,
|
||||
msd: &MsdConfig,
|
||||
) -> Result<OtgDesiredState> {
|
||||
let mut desired = OtgDesiredState::from_config(hid, msd)?;
|
||||
desired.msd_lun_capacity = self.desired.read().await.msd_lun_capacity;
|
||||
Ok(desired)
|
||||
}
|
||||
|
||||
pub async fn set_msd_lun_capacity(&self, capacity: u8) -> Result<()> {
|
||||
if capacity != 1 && capacity != 8 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"MSD LUN capacity must be 1 or 8, got {capacity}"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut desired = self.desired.read().await.clone();
|
||||
if !desired.msd_enabled {
|
||||
return Err(AppError::Internal(
|
||||
"MSD is not enabled in the OTG gadget".to_string(),
|
||||
));
|
||||
}
|
||||
desired.msd_lun_capacity = capacity;
|
||||
self.apply_desired_state(desired).await
|
||||
}
|
||||
|
||||
@@ -160,6 +215,7 @@ impl OtgService {
|
||||
if state.gadget_active
|
||||
&& state.hid_enabled == desired.hid_enabled()
|
||||
&& state.msd_enabled == desired.msd_enabled
|
||||
&& state.msd_lun_capacity == desired.msd_lun_capacity
|
||||
&& state.configured_udc == desired.udc
|
||||
&& state.hid_functions == desired.hid_functions
|
||||
&& state.keyboard_leds_enabled == desired.keyboard_leds
|
||||
@@ -188,6 +244,7 @@ impl OtgService {
|
||||
state.gadget_active = false;
|
||||
state.hid_enabled = false;
|
||||
state.msd_enabled = false;
|
||||
state.msd_lun_capacity = 1;
|
||||
state.configured_udc = None;
|
||||
state.hid_paths = None;
|
||||
state.hid_functions = None;
|
||||
@@ -280,7 +337,7 @@ impl OtgService {
|
||||
}
|
||||
|
||||
let msd_func = if desired.msd_enabled {
|
||||
match manager.add_msd() {
|
||||
match manager.add_msd(desired.msd_lun_capacity) {
|
||||
Ok(func) => {
|
||||
debug!("MSD function added to gadget");
|
||||
Some(func)
|
||||
@@ -323,6 +380,7 @@ impl OtgService {
|
||||
state.gadget_active = true;
|
||||
state.hid_enabled = desired.hid_enabled();
|
||||
state.msd_enabled = desired.msd_enabled;
|
||||
state.msd_lun_capacity = desired.msd_lun_capacity;
|
||||
state.configured_udc = Some(udc);
|
||||
state.hid_paths = hid_paths;
|
||||
state.hid_functions = desired.hid_functions;
|
||||
@@ -393,4 +451,32 @@ mod tests {
|
||||
let _service = OtgService::new();
|
||||
let _ = OtgService::is_available();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn service_starts_with_single_lun_capacity() {
|
||||
let service = OtgService::new();
|
||||
assert_eq!(service.desired.read().await.msd_lun_capacity, 1);
|
||||
assert_eq!(service.state.read().await.msd_lun_capacity, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_updates_preserve_runtime_lun_capacity() {
|
||||
let service = OtgService::new();
|
||||
service.desired.write().await.msd_lun_capacity = 8;
|
||||
|
||||
let desired = service
|
||||
.desired_from_config_preserving_runtime(&HidConfig::default(), &MsdConfig::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(desired.msd_lun_capacity, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lun_capacity_participates_in_desired_state_equality() {
|
||||
let single = OtgDesiredState::default();
|
||||
let mut multi = single.clone();
|
||||
multi.msd_lun_capacity = 8;
|
||||
assert_ne!(single, multi);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user