Files
One-KVM/src/otg/manager.rs
2026-09-21 21:47:49 +08:00

547 lines
17 KiB
Rust

use std::fs;
use std::path::PathBuf;
use tracing::{debug, error, info, warn};
use super::configfs::{
configfs_path, create_dir, find_udc, is_configfs_available, remove_dir, 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, MsdInquiryStrings};
use super::network::NetworkFunction;
use crate::config::OtgNetworkConfig;
use crate::error::{AppError, Result};
const REBIND_DELAY_MS: u64 = 300;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GadgetDescriptor {
pub vendor_id: u16,
pub product_id: u16,
pub device_version: u16,
pub manufacturer: String,
pub product: String,
pub serial_number: String,
}
impl Default for GadgetDescriptor {
fn default() -> Self {
Self {
vendor_id: DEFAULT_USB_VENDOR_ID,
product_id: DEFAULT_USB_PRODUCT_ID,
device_version: DEFAULT_USB_BCD_DEVICE,
manufacturer: "One-KVM".to_string(),
product: "One-KVM USB Device".to_string(),
serial_number: "0123456789".to_string(),
}
}
}
pub struct OtgGadgetManager {
gadget_name: String,
gadget_path: PathBuf,
config_path: PathBuf,
descriptor: GadgetDescriptor,
hid_instance: u8,
msd_instance: u8,
network_instance: u8,
uac_instance: u8,
functions: Vec<Box<dyn GadgetFunction>>,
bound_udc: Option<String>,
created_by_us: bool,
}
impl OtgGadgetManager {
pub fn new() -> Self {
Self::with_config(DEFAULT_GADGET_NAME)
}
pub fn with_config(gadget_name: &str) -> Self {
Self::with_descriptor(gadget_name, GadgetDescriptor::default())
}
pub fn with_descriptor(gadget_name: &str, descriptor: GadgetDescriptor) -> Self {
let gadget_path = configfs_path().join(gadget_name);
let config_path = gadget_path.join("configs/c.1");
Self {
gadget_name: gadget_name.to_string(),
gadget_path,
config_path,
descriptor,
hid_instance: 0,
msd_instance: 0,
network_instance: 0,
uac_instance: 0,
functions: Vec::with_capacity(4),
bound_udc: None,
created_by_us: false,
}
}
pub fn is_available() -> bool {
is_configfs_available()
}
pub fn find_udc() -> Option<String> {
find_udc()
}
pub fn gadget_exists(&self) -> bool {
self.gadget_path.exists()
}
pub fn is_bound(&self) -> bool {
let udc_file = self.gadget_path.join("UDC");
if let Ok(content) = fs::read_to_string(&udc_file) {
!content.trim().is_empty()
} else {
false
}
}
pub fn add_keyboard(&mut self, keyboard_leds: bool) -> Result<PathBuf> {
let func = HidFunction::keyboard(self.hid_instance, keyboard_leds);
let device_path = func.device_path();
self.add_function(Box::new(func))?;
self.hid_instance += 1;
Ok(device_path)
}
pub fn add_mouse_relative(&mut self) -> Result<PathBuf> {
let func = HidFunction::mouse_relative(self.hid_instance);
let device_path = func.device_path();
self.add_function(Box::new(func))?;
self.hid_instance += 1;
Ok(device_path)
}
pub fn add_mouse_absolute(&mut self) -> Result<PathBuf> {
let func = HidFunction::mouse_absolute(self.hid_instance);
let device_path = func.device_path();
self.add_function(Box::new(func))?;
self.hid_instance += 1;
Ok(device_path)
}
pub fn add_consumer_control(&mut self) -> Result<PathBuf> {
let func = HidFunction::consumer_control(self.hid_instance);
let device_path = func.device_path();
self.add_function(Box::new(func))?;
self.hid_instance += 1;
Ok(device_path)
}
pub fn add_msd(
&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();
self.add_function(Box::new(func))?;
self.msd_instance += 1;
Ok(func_clone)
}
pub fn add_network(&mut self, config: &OtgNetworkConfig) -> Result<NetworkFunction> {
let func = NetworkFunction::new(self.network_instance, config)?;
let func_clone = func.clone();
self.add_function(Box::new(func))?;
self.network_instance += 1;
Ok(func_clone)
}
pub fn add_uac(&mut self, sample_rate: u32, channels: u8) -> Result<super::uac::UacFunction> {
let func = super::uac::UacFunction::new(self.uac_instance, sample_rate, channels)?;
let func_clone = func.clone();
self.add_function(Box::new(func))?;
self.uac_instance += 1;
Ok(func_clone)
}
fn add_function(&mut self, func: Box<dyn GadgetFunction>) -> Result<()> {
self.functions.push(func);
Ok(())
}
pub fn setup(&mut self) -> Result<()> {
debug!("Setting up OTG USB Gadget: {}", self.gadget_name);
if !Self::is_available() {
return Err(AppError::Internal(format!(
"ConfigFS not available at {}",
configfs_path().display()
)));
}
if self.gadget_exists() {
if self.is_bound() {
debug!("Gadget already exists and is bound, skipping setup");
return Ok(());
}
warn!("Gadget exists but not bound, will reconfigure");
self.cleanup()?;
}
create_dir(&self.gadget_path)?;
self.created_by_us = true;
self.set_device_descriptors()?;
self.create_strings()?;
self.create_configuration()?;
for func in &self.functions {
func.create(&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");
Ok(())
}
pub fn bind(&mut self, udc: &str) -> Result<()> {
self.recreate_config_links()?;
debug!("Binding gadget to UDC: {}", udc);
write_file(&self.gadget_path.join("UDC"), &udc)?;
self.bound_udc = Some(udc.to_string());
std::thread::sleep(std::time::Duration::from_millis(REBIND_DELAY_MS));
Ok(())
}
pub fn unbind(&mut self) -> Result<()> {
if self.is_bound() {
write_file(&self.gadget_path.join("UDC"), "")?;
self.bound_udc = None;
info!("Unbound gadget from UDC");
std::thread::sleep(std::time::Duration::from_millis(REBIND_DELAY_MS));
}
Ok(())
}
pub fn cleanup(&mut self) -> Result<()> {
if !self.gadget_exists() {
self.created_by_us = false;
return Ok(());
}
info!("Cleaning up OTG USB Gadget: {}", self.gadget_name);
let mut errors = Vec::new();
if let Err(error) = self.unbind() {
errors.push(format!("unbind failed: {error}"));
}
for func in self.functions.iter().rev() {
if let Err(error) = func.unlink(&self.config_path) {
errors.push(format!("unlink {} failed: {error}", func.name()));
}
}
let config_strings = self.config_path.join("strings/0x409");
if let Err(error) = remove_dir(&config_strings) {
errors.push(error.to_string());
}
if let Err(error) = remove_dir(&self.config_path) {
errors.push(error.to_string());
}
for func in self.functions.iter().rev() {
if let Err(error) = func.cleanup(&self.gadget_path) {
errors.push(format!("cleanup {} failed: {error}", func.name()));
}
}
let gadget_strings = self.gadget_path.join("strings/0x409");
if let Err(error) = remove_dir(&gadget_strings) {
errors.push(error.to_string());
}
if let Err(error) = remove_dir(&self.gadget_path) {
errors.push(error.to_string());
}
if !errors.is_empty() {
return Err(AppError::Config(format!(
"OTG gadget cleanup incomplete: {}",
errors.join("; ")
)));
}
self.created_by_us = false;
info!("OTG USB Gadget cleanup complete");
Ok(())
}
fn set_device_descriptors(&self) -> Result<()> {
write_file(
&self.gadget_path.join("idVendor"),
&format!("0x{:04x}", self.descriptor.vendor_id),
)?;
write_file(
&self.gadget_path.join("idProduct"),
&format!("0x{:04x}", self.descriptor.product_id),
)?;
write_file(
&self.gadget_path.join("bcdDevice"),
&format!("0x{:04x}", self.descriptor.device_version),
)?;
write_file(
&self.gadget_path.join("bcdUSB"),
&format!("0x{:04x}", USB_BCD_USB),
)?;
write_file(&self.gadget_path.join("bDeviceClass"), "0x00")?;
write_file(&self.gadget_path.join("bDeviceSubClass"), "0x00")?;
write_file(&self.gadget_path.join("bDeviceProtocol"), "0x00")?;
debug!("Set device descriptors");
Ok(())
}
fn create_strings(&self) -> Result<()> {
let strings_path = self.gadget_path.join("strings/0x409");
create_dir(&strings_path)?;
write_file(
&strings_path.join("serialnumber"),
&self.descriptor.serial_number,
)?;
write_file(
&strings_path.join("manufacturer"),
&self.descriptor.manufacturer,
)?;
write_file(&strings_path.join("product"), &self.descriptor.product)?;
debug!("Created USB strings");
Ok(())
}
fn create_configuration(&self) -> Result<()> {
create_dir(&self.config_path)?;
let strings_path = self.config_path.join("strings/0x409");
create_dir(&strings_path)?;
write_file(
&strings_path.join("configuration"),
self.configuration_label(),
)?;
write_file(&self.config_path.join("MaxPower"), "500")?;
debug!("Created configuration c.1");
Ok(())
}
fn configuration_label(&self) -> &'static str {
let has_msd = self
.functions
.iter()
.any(|func| func.name().starts_with("mass_storage."));
let has_network = self.functions.iter().any(|func| {
["ncm.", "ecm.", "rndis."]
.iter()
.any(|prefix| func.name().starts_with(prefix))
});
if has_msd && has_network {
"Config 1: HID + MSD + NET"
} else if has_msd {
"Config 1: HID + MSD"
} else if has_network {
"Config 1: HID + NET"
} else {
"Config 1: HID"
}
}
pub fn gadget_path(&self) -> &PathBuf {
&self.gadget_path
}
fn recreate_config_links(&self) -> Result<()> {
let functions_path = self.gadget_path.join("functions");
if !functions_path.exists() || !self.config_path.exists() {
return Ok(());
}
// ConfigFS binds functions in link insertion order. Preserve the
// setup order (UAC before HID), including on rebind: directory
// iteration order is unspecified and can change endpoint allocation.
for func in &self.functions {
let dest = self.config_path.join(func.name());
if dest.symlink_metadata().is_ok() {
fs::remove_file(&dest).map_err(|error| {
AppError::Internal(format!(
"Failed to remove config link {}: {error}",
dest.display()
))
})?;
}
}
for func in &self.functions {
func.link(&self.config_path, &self.gadget_path)?;
}
Ok(())
}
}
impl Default for OtgGadgetManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for OtgGadgetManager {
fn drop(&mut self) {
if self.created_by_us {
if let Err(e) = self.cleanup() {
error!("Failed to cleanup OTG gadget on drop: {}", e);
}
}
}
}
pub async fn wait_for_hid_devices(device_paths: &[PathBuf], timeout_ms: u64) -> bool {
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_millis(timeout_ms);
let mut delay_ms = 10u64;
const MAX_DELAY_MS: u64 = 100;
while start.elapsed() < timeout {
if device_paths.iter().all(|p| p.exists()) {
return true;
}
let remaining = timeout.saturating_sub(start.elapsed());
let sleep_duration = std::time::Duration::from_millis(delay_ms).min(remaining);
if sleep_duration.is_zero() {
break;
}
tokio::time::sleep(sleep_duration).await;
delay_ms = (delay_ms * 2).min(MAX_DELAY_MS);
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use std::sync::{Arc, Mutex};
struct RecordedFunction {
name: &'static str,
links: Arc<Mutex<Vec<String>>>,
}
impl GadgetFunction for RecordedFunction {
fn name(&self) -> &str {
self.name
}
fn create(&self, _: &Path) -> Result<()> {
Ok(())
}
fn link(&self, config: &Path, gadget: &Path) -> Result<()> {
super::super::configfs::create_symlink(
&gadget.join("functions").join(self.name),
&config.join(self.name),
)?;
self.links.lock().unwrap().push(self.name.into());
Ok(())
}
fn unlink(&self, _: &Path) -> Result<()> {
Ok(())
}
fn cleanup(&self, _: &Path) -> Result<()> {
Ok(())
}
}
#[test]
fn rebind_links_uac_before_hid_in_registration_order() {
let temp = tempfile::tempdir().unwrap();
let mut manager = OtgGadgetManager::new();
manager.gadget_path = temp.path().to_path_buf();
manager.config_path = temp.path().join("configs/c.1");
fs::create_dir_all(&manager.config_path).unwrap();
let links = Arc::new(Mutex::new(Vec::new()));
for name in ["uac1.usb0", "hid.usb0", "mass_storage.usb0"] {
fs::create_dir_all(temp.path().join("functions").join(name)).unwrap();
// Include a dangling pre-existing link, as well as testing rebind.
std::os::unix::fs::symlink("/nonexistent-uac-test", manager.config_path.join(name))
.unwrap();
manager
.add_function(Box::new(RecordedFunction {
name,
links: Arc::clone(&links),
}))
.unwrap();
}
for _ in 0..2 {
links.lock().unwrap().clear();
manager.recreate_config_links().unwrap();
assert_eq!(
*links.lock().unwrap(),
["uac1.usb0", "hid.usb0", "mass_storage.usb0"]
);
}
}
#[test]
fn link_failure_prevents_udc_binding() {
let temp = tempfile::tempdir().unwrap();
let mut manager = OtgGadgetManager::new();
manager.gadget_path = temp.path().to_path_buf();
manager.config_path = temp.path().join("configs/c.1");
fs::create_dir_all(temp.path().join("functions/hid.usb0")).unwrap();
// A directory occupying a config link cannot be removed as a file.
fs::create_dir_all(manager.config_path.join("hid.usb0")).unwrap();
fs::write(temp.path().join("UDC"), "").unwrap();
manager.add_keyboard(false).unwrap();
assert!(manager.bind("test-udc").is_err());
assert_eq!(fs::read_to_string(temp.path().join("UDC")).unwrap(), "");
}
#[test]
fn test_manager_creation() {
let manager = OtgGadgetManager::new();
assert_eq!(manager.gadget_name, DEFAULT_GADGET_NAME);
assert!(!manager.gadget_exists());
}
#[test]
fn test_function_selection_is_not_prevalidated() {
let mut manager = OtgGadgetManager::with_config("test");
assert!(manager.add_keyboard(false).is_ok());
assert!(manager.add_mouse_relative().is_ok());
assert!(manager.add_mouse_absolute().is_ok());
assert_eq!(manager.functions.len(), 3);
}
}