lesavka/server/src/gadget.rs

227 lines
8.9 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// server/src/gadget.rs
use std::{fs::{self, OpenOptions}, io::Write, path::Path, thread, time::Duration};
use anyhow::{Context, Result};
use tracing::{info, warn, trace};
#[derive(Clone)]
pub struct UsbGadget {
udc_file: &'static str,
}
impl UsbGadget {
pub fn new(name: &'static str) -> Self {
Self {
udc_file: Box::leak(
format!("/sys/kernel/config/usb_gadget/{name}/UDC").into_boxed_str(),
),
}
}
pub fn state(ctrl: &str) -> anyhow::Result<String> {
let p = format!("/sys/class/udc/{ctrl}/state");
Ok(std::fs::read_to_string(p)?.trim().to_owned())
}
/*---- helpers ----*/
/// Find the first controller in /sys/class/udc (e.g. `1000480000.usb`)
pub fn find_controller() -> Result<String> {
Ok(fs::read_dir("/sys/class/udc")?
.next()
.transpose()?
.context("no UDC present")?
.file_name()
.to_string_lossy()
.into_owned())
}
/// Busy-loop (≤ `limit_ms`) until `state` matches `wanted`
fn wait_state(ctrl: &str, wanted: &str, limit_ms: u64) -> Result<()> {
let path = format!("/sys/class/udc/{ctrl}/state");
for _ in 0..=limit_ms / 50 {
let s = fs::read_to_string(&path).unwrap_or_default();
trace!("⏳ state={s:?}, want={wanted}");
if s.trim() == wanted {
return Ok(());
}
thread::sleep(Duration::from_millis(50));
}
Err(anyhow::anyhow!("UDC never reached '{wanted}' (last = {:?})",
fs::read_to_string(&path).unwrap_or_default()))
}
pub fn wait_state_any(ctrl: &str, limit_ms: u64) -> anyhow::Result<String> {
let path = format!("/sys/class/udc/{ctrl}/state");
for _ in 0..=limit_ms / 50 {
if let Ok(s) = std::fs::read_to_string(&path) {
let s = s.trim();
if matches!(s, "configured" | "not attached") {
return Ok(s.to_owned());
}
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(anyhow::anyhow!("UDC state did not settle within {limit_ms}ms"))
}
/// Write `value` (plus “\n”) into a sysfs attribute
fn write_attr<P: AsRef<Path>>(p: P, value: &str) -> Result<()> {
OpenOptions::new()
.write(true)
.open(p)?
.write_all(format!("{value}\n").as_bytes())?;
Ok(())
}
// Wait (≤ `limit_ms`) until `/sys/class/udc/<ctrl>` exists again.
fn wait_udc_present(ctrl: &str, limit_ms: u64) -> Result<()> {
for _ in 0..=limit_ms / 50 {
if Path::new(&format!("/sys/class/udc/{ctrl}")).exists() {
return Ok(());
}
thread::sleep(Duration::from_millis(50));
}
Err(anyhow::anyhow!("⚠️ UDC {ctrl} did not re-appear within {limit_ms}ms"))
}
/// Scan platform devices when /sys/class/udc is empty
fn probe_platform_udc() -> Result<Option<String>> {
for entry in fs::read_dir("/sys/bus/platform/devices")? {
let p = entry?.file_name().into_string().unwrap();
if p.ends_with(".usb") { return Ok(Some(p)); }
}
Ok(None)
}
/*---- public API ----*/
/// Hard-reset the gadget → identical to a physical cable re-plug
pub fn cycle(&self) -> Result<()> {
/* 0-ensure we *know* the controller even after a previous crash */
let ctrl = Self::find_controller()
.or_else(|_| Self::probe_platform_udc()?
.ok_or_else(|| anyhow::anyhow!("no UDC present")))?;
/* 1 - detach gadget */
info!("🔌 detaching gadget from {ctrl}");
// a) drop pull-ups (if the controller offers the switch)
let sc = format!("/sys/class/udc/{ctrl}/soft_connect");
let _ = Self::write_attr(&sc, "0"); // ignore errors - not all HW has it
// b) clear the UDC attribute; the kernel may transiently answer EBUSY
for attempt in 1..=10 {
match Self::write_attr(self.udc_file, "") {
Ok(_) => break,
Err(err) if {
// only swallow EBUSY
err.downcast_ref::<std::io::Error>()
.and_then(|io| io.raw_os_error())
== Some(libc::EBUSY) && attempt < 10
} => {
trace!("⏳ UDC busy (attempt {attempt}/10) - retrying…");
thread::sleep(Duration::from_millis(100));
}
Err(err) => return Err(err),
}
}
Self::wait_state(&ctrl, "not attached", 3_000)?;
/* 2 - reset driver */
Self::rebind_driver(&ctrl)?;
/* 3 - wait UDC node to re-appear */
Self::wait_udc_present(&ctrl, 3_000)?;
/* 4 - re-attach + pull-up */
info!("🔌 re-attaching gadget to {ctrl}");
Self::write_attr(self.udc_file, &ctrl)?;
if Path::new(&sc).exists() {
// try to set the pull-up; ignore if the kernel rejects it
match Self::write_attr(&sc, "1") {
Err(err) => {
// only swallow specific errno values
if let Some(io) = err.downcast_ref::<std::io::Error>() {
match io.raw_os_error() {
// EINVAL | EPERM | ENOENT
Some(libc::EINVAL) | Some(libc::EPERM) | Some(libc::ENOENT) => {
warn!("⚠️ soft_connect unsupported ({io}); continuing");
}
_ => return Err(err), // propagate all other errors
}
} else {
return Err(err); // non-IO errors: propagate
}
}
Ok(_) => { /* success */ }
}
}
/* 5 - wait for host (but tolerate sleep) */
Self::wait_state(&ctrl, "configured", 6_000)
.or_else(|e| {
// If the host is physically absent (sleep / KVM paused)
// we allow 'not attached' and continue - we can still
// accept keyboard/mouse data and the host will enumerate
// later without another reset.
let last = fs::read_to_string(format!("/sys/class/udc/{ctrl}/state"))
.unwrap_or_default();
if last.trim() == "not attached" {
warn!("⚠️ host did not enumerate within 6s - continuing (state = {last:?})");
Ok(())
} else {
Err(e)
}
})?;
info!("✅ USB-gadget cycle complete");
Ok(())
}
/// helper: unbind + 300ms reset + bind
fn rebind_driver(ctrl: &str) -> Result<()> {
let cand = ["dwc2", "dwc3"];
for drv in cand {
let root = format!("/sys/bus/platform/drivers/{drv}");
if !Path::new(&root).exists() { continue }
/*----------- unbind ------------------------------------------------*/
info!("🔧 unbinding UDC driver ({drv})");
for attempt in 1..=20 {
match Self::write_attr(format!("{root}/unbind"), ctrl) {
Ok(_) => break,
Err(err) if attempt < 20 && Self::is_still_detaching(&err) => {
trace!("unbind in-progress (#{attempt}) - waiting…");
thread::sleep(Duration::from_millis(100));
}
Err(err) => return Err(err)
.context("UDC unbind failed irrecoverably"),
}
}
thread::sleep(Duration::from_millis(150)); // let the core quiesce
/*----------- bind --------------------------------------------------*/
info!("🔧 binding UDC driver ({drv})");
for attempt in 1..=20 {
match Self::write_attr(format!("{root}/bind"), ctrl) {
Ok(_) => return Ok(()), // success 🎉
Err(err) if attempt < 20 && Self::is_still_detaching(&err) => {
trace!("bind busy (#{attempt}) - retrying…");
thread::sleep(Duration::from_millis(100));
}
Err(err) => return Err(err)
.context("UDC bind failed irrecoverably"),
}
}
}
Err(anyhow::anyhow!("no dwc2/dwc3 driver nodes found"))
}
fn is_still_detaching(err: &anyhow::Error) -> bool {
err.downcast_ref::<std::io::Error>()
.and_then(|io| io.raw_os_error())
.map_or(false, |code| {
matches!(code, libc::EBUSY | libc::ENOENT | libc::ENODEV)
})
}
}