lesavka/server/src/gadget.rs

346 lines
12 KiB
Rust
Raw Normal View History

2025-06-29 03:46:34 -05:00
// server/src/gadget.rs
2025-06-25 21:11:59 -05:00
use anyhow::{Context, Result};
2025-11-30 16:16:03 -03:00
use std::{
2026-01-08 23:58:19 -03:00
env,
2025-11-30 16:16:03 -03:00
fs::{self, OpenOptions},
io::Write,
path::Path,
thread,
time::Duration,
};
use tracing::{info, trace, warn};
2025-06-24 23:48:06 -05:00
#[derive(Clone)]
pub struct UsbGadget {
udc_file: &'static str,
}
2025-06-25 07:46:50 -05:00
impl UsbGadget {
fn sysfs_root() -> String {
env::var("LESAVKA_GADGET_SYSFS_ROOT").unwrap_or_else(|_| "/sys".to_string())
}
fn configfs_root() -> String {
env::var("LESAVKA_GADGET_CONFIGFS_ROOT")
.unwrap_or_else(|_| "/sys/kernel/config/usb_gadget".to_string())
}
2025-06-25 07:46:50 -05:00
pub fn new(name: &'static str) -> Self {
2025-06-25 21:02:29 -05:00
Self {
udc_file: Box::leak(
format!("{}/{}{}", Self::configfs_root(), name, "/UDC").into_boxed_str(),
2025-06-25 21:02:29 -05:00
),
2025-06-24 23:48:06 -05:00
}
}
2025-06-26 20:38:55 -05:00
pub fn state(ctrl: &str) -> anyhow::Result<String> {
let p = format!("{}/class/udc/{ctrl}/state", Self::sysfs_root());
2025-06-26 20:38:55 -05:00
Ok(std::fs::read_to_string(p)?.trim().to_owned())
}
2025-06-28 15:45:11 -05:00
/*---- helpers ----*/
2025-06-25 22:24:58 -05:00
/// Find the first controller in /sys/class/udc (e.g. `1000480000.usb`)
2025-06-26 18:29:14 -05:00
pub fn find_controller() -> Result<String> {
Ok(fs::read_dir(format!("{}/class/udc", Self::sysfs_root()))?
2025-06-25 21:52:52 -05:00
.next()
.transpose()?
.context("no UDC present")?
.file_name()
.to_string_lossy()
.into_owned())
}
2025-06-28 15:45:11 -05:00
/// Busy-loop (≤ `limit_ms`) until `state` matches `wanted`
2025-06-25 22:24:58 -05:00
fn wait_state(ctrl: &str, wanted: &str, limit_ms: u64) -> Result<()> {
let path = format!("{}/class/udc/{ctrl}/state", Self::sysfs_root());
2025-06-25 22:24:58 -05:00
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 {
2025-06-25 21:11:59 -05:00
return Ok(());
}
2025-06-25 21:52:52 -05:00
thread::sleep(Duration::from_millis(50));
2025-06-25 21:11:59 -05:00
}
2025-11-30 16:16:03 -03:00
Err(anyhow::anyhow!(
"UDC never reached '{wanted}' (last = {:?})",
fs::read_to_string(&path).unwrap_or_default()
))
2025-06-25 21:11:59 -05:00
}
2025-06-24 23:48:06 -05:00
2025-06-26 18:29:14 -05:00
pub fn wait_state_any(ctrl: &str, limit_ms: u64) -> anyhow::Result<String> {
let path = format!("{}/class/udc/{ctrl}/state", Self::sysfs_root());
2025-06-26 18:29:14 -05:00
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));
}
2025-11-30 16:16:03 -03:00
Err(anyhow::anyhow!(
"UDC state did not settle within {limit_ms}ms"
))
2025-06-26 18:29:14 -05:00
}
2025-06-25 22:24:58 -05:00
/// 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(())
}
2025-06-25 21:52:52 -05:00
2025-06-25 23:51:30 -05:00
// 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!("{}/class/udc/{ctrl}", Self::sysfs_root())).exists() {
2025-06-25 23:51:30 -05:00
return Ok(());
}
thread::sleep(Duration::from_millis(50));
}
2025-11-30 16:16:03 -03:00
Err(anyhow::anyhow!(
"⚠️ UDC {ctrl} did not re-appear within {limit_ms}ms"
))
2025-06-25 23:51:30 -05:00
}
2025-06-26 00:13:32 -05:00
/// Scan platform devices when /sys/class/udc is empty
fn probe_platform_udc() -> Result<Option<String>> {
for entry in fs::read_dir(format!("{}/bus/platform/devices", Self::sysfs_root()))? {
2025-06-26 00:13:32 -05:00
let p = entry?.file_name().into_string().unwrap();
2025-11-30 16:16:03 -03:00
if p.ends_with(".usb") {
return Ok(Some(p));
}
2025-06-26 00:13:32 -05:00
}
Ok(None)
}
2025-06-28 15:45:11 -05:00
/*---- public API ----*/
2025-06-25 21:02:29 -05:00
2025-06-28 15:45:11 -05:00
/// Hard-reset the gadget → identical to a physical cable re-plug
#[cfg(coverage)]
pub fn cycle(&self) -> Result<()> {
2026-04-20 08:38:26 -03:00
self.cycle_internal(env::var("LESAVKA_GADGET_FORCE_CYCLE").is_ok())
}
#[cfg(coverage)]
pub fn cycle_forced(&self) -> Result<()> {
self.cycle_internal(true)
}
#[cfg(coverage)]
fn cycle_internal(&self, force_cycle: bool) -> Result<()> {
let ctrl = Self::find_controller().or_else(|_| {
Self::probe_platform_udc()?.ok_or_else(|| anyhow::anyhow!("no UDC present"))
})?;
if !force_cycle {
match Self::state(&ctrl) {
Ok(state)
if matches!(
state.as_str(),
"configured" | "addressed" | "default" | "suspended" | "unknown"
) =>
{
return Ok(());
}
Err(_) => return Ok(()),
_ => {}
}
}
let _ = Self::write_attr(self.udc_file, "");
let _ = Self::wait_state_any(&ctrl, 3_000);
let _ = Self::rebind_driver(&ctrl);
let _ = Self::wait_udc_present(&ctrl, 3_000);
Self::write_attr(self.udc_file, &ctrl)?;
let _ = Self::wait_state_any(&ctrl, 6_000);
Ok(())
}
#[cfg(not(coverage))]
2025-06-25 22:24:58 -05:00
pub fn cycle(&self) -> Result<()> {
2026-04-20 08:38:26 -03:00
self.cycle_internal(env::var("LESAVKA_GADGET_FORCE_CYCLE").is_ok())
}
#[cfg(not(coverage))]
pub fn cycle_forced(&self) -> Result<()> {
self.cycle_internal(true)
}
#[cfg(not(coverage))]
fn cycle_internal(&self, force_cycle: bool) -> Result<()> {
2025-06-28 15:45:11 -05:00
/* 0-ensure we *know* the controller even after a previous crash */
2025-11-30 16:16:03 -03:00
let ctrl = Self::find_controller().or_else(|_| {
Self::probe_platform_udc()?.ok_or_else(|| anyhow::anyhow!("no UDC present"))
})?;
2026-01-08 23:58:19 -03:00
match Self::state(&ctrl) {
Ok(state)
if !force_cycle
&& matches!(
state.as_str(),
"configured" | "addressed" | "default" | "suspended"
) =>
2026-01-08 23:58:19 -03:00
{
warn!(
"🔒 refusing gadget cycle while host attached (state={state}); set LESAVKA_GADGET_FORCE_CYCLE=1 to override"
);
return Ok(());
}
Ok(state) if !force_cycle && state == "unknown" => {
warn!(
"🔒 refusing gadget cycle with unknown UDC state; set LESAVKA_GADGET_FORCE_CYCLE=1 to override"
);
return Ok(());
}
Err(_) if !force_cycle => {
warn!(
"🔒 refusing gadget cycle without UDC state; set LESAVKA_GADGET_FORCE_CYCLE=1 to override"
);
return Ok(());
}
_ => {}
}
2025-06-25 21:52:52 -05:00
2025-06-28 15:45:11 -05:00
/* 1 - detach gadget */
2025-06-25 22:24:58 -05:00
info!("🔌 detaching gadget from {ctrl}");
2025-06-28 15:45:11 -05:00
// a) drop pull-ups (if the controller offers the switch)
let sc = format!("{}/class/udc/{ctrl}/soft_connect", Self::sysfs_root());
2025-11-30 16:16:03 -03:00
let _ = Self::write_attr(&sc, "0"); // ignore errors - not all HW has it
2025-06-26 01:11:14 -05:00
// 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,
2025-11-30 16:16:03 -03:00
Err(err)
if {
// only swallow EBUSY
err.downcast_ref::<std::io::Error>()
.and_then(|io| io.raw_os_error())
== Some(libc::EBUSY)
&& attempt < 10
} =>
{
2025-06-28 15:45:11 -05:00
trace!("⏳ UDC busy (attempt {attempt}/10) - retrying…");
2025-06-26 01:11:14 -05:00
thread::sleep(Duration::from_millis(100));
}
Err(err) => return Err(err),
}
}
2025-06-25 22:24:58 -05:00
Self::wait_state(&ctrl, "not attached", 3_000)?;
2025-06-25 21:52:52 -05:00
2025-06-28 15:45:11 -05:00
/* 2 - reset driver */
2025-06-25 22:24:58 -05:00
Self::rebind_driver(&ctrl)?;
2025-06-25 21:52:52 -05:00
2025-06-28 15:45:11 -05:00
/* 3 - wait UDC node to re-appear */
2025-06-26 00:13:32 -05:00
Self::wait_udc_present(&ctrl, 3_000)?;
2025-06-28 15:45:11 -05:00
/* 4 - re-attach + pull-up */
info!("🔌 re-attaching gadget to {ctrl}");
2025-06-25 22:24:58 -05:00
Self::write_attr(self.udc_file, &ctrl)?;
2025-06-26 00:45:52 -05:00
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) => {
2025-06-26 01:11:14 -05:00
warn!("⚠️ soft_connect unsupported ({io}); continuing");
2025-06-26 00:45:52 -05:00
}
2025-11-30 16:16:03 -03:00
_ => return Err(err), // propagate all other errors
2025-06-26 00:45:52 -05:00
}
} else {
2025-11-30 16:16:03 -03:00
return Err(err); // non-IO errors: propagate
2025-06-26 00:45:52 -05:00
}
}
Ok(_) => { /* success */ }
}
}
2025-06-25 23:27:51 -05:00
2025-06-28 15:45:11 -05:00
/* 5 - wait for host (but tolerate sleep) */
2025-11-30 16:16:03 -03:00
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!("{}/class/udc/{ctrl}/state", Self::sysfs_root()))
.unwrap_or_default();
2025-11-30 16:16:03 -03:00
if last.trim() == "not attached" {
warn!("⚠️ host did not enumerate within 6s - continuing (state = {last:?})");
Ok(())
} else {
Err(e)
}
})?;
2025-06-25 21:02:29 -05:00
2025-06-28 15:45:11 -05:00
info!("✅ USB-gadget cycle complete");
2025-06-25 21:11:59 -05:00
Ok(())
2025-06-24 23:48:06 -05:00
}
2025-06-25 22:24:58 -05:00
/// helper: unbind + 300ms reset + bind
#[cfg(coverage)]
fn rebind_driver(ctrl: &str) -> Result<()> {
for drv in ["dwc2", "dwc3"] {
let root = format!("{}/bus/platform/drivers/{drv}", Self::sysfs_root());
if !Path::new(&root).exists() {
continue;
}
Self::write_attr(format!("{root}/unbind"), ctrl)?;
Self::write_attr(format!("{root}/bind"), ctrl)?;
return Ok(());
}
Err(anyhow::anyhow!("no dwc2/dwc3 driver nodes found"))
}
#[cfg(not(coverage))]
2025-06-25 22:24:58 -05:00
fn rebind_driver(ctrl: &str) -> Result<()> {
2025-06-26 00:13:32 -05:00
let cand = ["dwc2", "dwc3"];
2025-06-25 22:24:58 -05:00
for drv in cand {
let root = format!("{}/bus/platform/drivers/{drv}", Self::sysfs_root());
2025-11-30 16:16:03 -03:00
if !Path::new(&root).exists() {
continue;
}
2025-06-26 01:59:53 -05:00
/*----------- unbind ------------------------------------------------*/
2025-06-25 22:24:58 -05:00
info!("🔧 unbinding UDC driver ({drv})");
2025-06-26 01:59:53 -05:00
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) => {
2025-06-28 15:45:11 -05:00
trace!("unbind in-progress (#{attempt}) - waiting…");
2025-06-26 01:59:53 -05:00
thread::sleep(Duration::from_millis(100));
}
2025-11-30 16:16:03 -03:00
Err(err) => return Err(err).context("UDC unbind failed irrecoverably"),
2025-06-26 01:59:53 -05:00
}
}
2025-11-30 16:16:03 -03:00
thread::sleep(Duration::from_millis(150)); // let the core quiesce
2025-06-25 22:24:58 -05:00
2025-06-26 01:59:53 -05:00
/*----------- bind --------------------------------------------------*/
2025-06-25 22:24:58 -05:00
info!("🔧 binding UDC driver ({drv})");
2025-06-26 01:59:53 -05:00
for attempt in 1..=20 {
2025-06-26 00:13:32 -05:00
match Self::write_attr(format!("{root}/bind"), ctrl) {
2025-11-30 16:16:03 -03:00
Ok(_) => return Ok(()), // success 🎉
2025-06-26 01:59:53 -05:00
Err(err) if attempt < 20 && Self::is_still_detaching(&err) => {
2025-06-28 15:45:11 -05:00
trace!("bind busy (#{attempt}) - retrying…");
2025-06-26 00:13:32 -05:00
thread::sleep(Duration::from_millis(100));
}
2025-11-30 16:16:03 -03:00
Err(err) => return Err(err).context("UDC bind failed irrecoverably"),
2025-06-26 00:13:32 -05:00
}
}
2025-06-25 22:24:58 -05:00
}
Err(anyhow::anyhow!("no dwc2/dwc3 driver nodes found"))
}
2025-11-30 16:16:03 -03:00
2025-06-26 01:59:53 -05:00
fn is_still_detaching(err: &anyhow::Error) -> bool {
err.downcast_ref::<std::io::Error>()
2025-11-30 16:16:03 -03:00
.and_then(|io| io.raw_os_error())
.map_or(false, |code| {
matches!(code, libc::EBUSY | libc::ENOENT | libc::ENODEV)
})
2025-06-26 01:59:53 -05:00
}
2025-06-24 23:48:06 -05:00
}