diff --git a/scripts/daemon/lesavka-core.sh b/scripts/daemon/lesavka-core.sh index db75b32..0cb2e51 100644 --- a/scripts/daemon/lesavka-core.sh +++ b/scripts/daemon/lesavka-core.sh @@ -22,6 +22,15 @@ udc_state() { cat "/sys/class/udc/$udc/state" 2>/dev/null || echo "unknown" } +is_attached_state() { + case "$1" in + configured|addressed|default|suspended) + return 0 + ;; + esac + return 1 +} + detach_gadget() { local udc="" udc="$(find_udc)" @@ -177,6 +186,14 @@ fi [[ -n $UDC ]] || { log "❌ UDC not present after manual bind"; exit 1; } log "βœ… UDC detected: $UDC" +# Guard against lockups: don't reset gadget while host is attached unless forced. +UDC_STATE="$(udc_state "$UDC")" +if [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]] && is_attached_state "$UDC_STATE"; then + log "πŸ”’ UDC state is '$UDC_STATE' - refusing gadget reset while host attached." + log " Set LESAVKA_ALLOW_GADGET_RESET=1 to force." + exit 0 +fi + #────────────────────────────────────────────────── # 3. (Re‑)create gadget #────────────────────────────────────────────────── diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 8a310ed..e99bfc1 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -5,6 +5,25 @@ ORIG_USER=${SUDO_USER:-$(id -un)} REF=${LESAVKA_REF:-master} # fallback +udc_state() { + local udc="" + udc=$(ls /sys/class/udc 2>/dev/null | head -n1 || true) + if [[ -z $udc ]]; then + echo "unknown" + return 0 + fi + cat "/sys/class/udc/$udc/state" 2>/dev/null || echo "unknown" +} + +is_attached_state() { + case "$1" in + configured|addressed|default|suspended|unknown) + return 0 + ;; + esac + return 1 +} + while [[ $# -gt 0 ]]; do case $1 in -r|--ref) REF="$2"; shift 2 ;; @@ -192,9 +211,16 @@ UNIT echo "==> 6c. Systemd units - initialization" sudo truncate -s 0 /tmp/lesavka-server.log sudo systemctl daemon-reload -sudo systemctl enable --now lesavka-core -sudo systemctl restart lesavka-core -echo "βœ… lesavka-core installed and restarted..." +sudo systemctl enable lesavka-core lesavka-uvc lesavka-server + +UDC_STATE=$(udc_state) +if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || ! is_attached_state "$UDC_STATE"; then + sudo systemctl restart lesavka-core + echo "βœ… lesavka-core installed and restarted..." +else + echo "⚠️ UDC state is '$UDC_STATE' - skipping lesavka-core restart." + echo " Set LESAVKA_ALLOW_GADGET_RESET=1 to force." +fi cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-uvc.service >/dev/null [Unit] @@ -217,10 +243,12 @@ User=root WantedBy=multi-user.target UNIT -sudo systemctl enable --now lesavka-uvc -sudo systemctl restart lesavka-uvc -echo "βœ… lesavka-uvc installed and restarted..." +if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || ! is_attached_state "$UDC_STATE"; then + sudo systemctl restart lesavka-uvc + echo "βœ… lesavka-uvc installed and restarted..." +else + echo "⚠️ UDC state is '$UDC_STATE' - skipping lesavka-uvc restart." +fi -sudo systemctl enable --now lesavka-server sudo systemctl restart lesavka-server echo "βœ… lesavka-server installed and restarted..." diff --git a/server/src/gadget.rs b/server/src/gadget.rs index c7f4fbc..ea0d631 100644 --- a/server/src/gadget.rs +++ b/server/src/gadget.rs @@ -1,6 +1,7 @@ // server/src/gadget.rs use anyhow::{Context, Result}; use std::{ + env, fs::{self, OpenOptions}, io::Write, path::Path, @@ -115,6 +116,31 @@ impl UsbGadget { let ctrl = Self::find_controller().or_else(|_| { Self::probe_platform_udc()?.ok_or_else(|| anyhow::anyhow!("no UDC present")) })?; + let force_cycle = env::var("LESAVKA_GADGET_FORCE_CYCLE").is_ok(); + match Self::state(&ctrl) { + Ok(state) + if !force_cycle + && matches!(state.as_str(), "configured" | "addressed" | "default" | "suspended") => + { + 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(()); + } + _ => {} + } /* 1 - detach gadget */ info!("πŸ”Œ detaching gadget from {ctrl}"); diff --git a/server/src/main.rs b/server/src/main.rs index a7f2e5e..4372c9e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,4 +1,4 @@ -//! lesavka-server - **auto-cycle disabled** +//! lesavka-server - gadget cycle guarded by env // server/src/main.rs #![forbid(unsafe_code)] @@ -26,8 +26,6 @@ use lesavka_common::lesavka::{ use lesavka_server::{audio, gadget::UsbGadget, handshake::HandshakeSvc, video}; /*──────────────── constants ────────────────*/ -/// **false**Β = never reset automatically. -const AUTO_CYCLE: bool = false; const VERSION: &str = env!("CARGO_PKG_VERSION"); const PKG_NAME: &str = env!("CARGO_PKG_NAME"); @@ -90,6 +88,10 @@ fn next_minute() -> SystemTime { UNIX_EPOCH + Duration::from_secs(next) } +fn allow_gadget_cycle() -> bool { + std::env::var("LESAVKA_ALLOW_GADGET_CYCLE").is_ok() +} + async fn recover_hid_if_needed( err: &std::io::Error, gadget: UsbGadget, @@ -109,12 +111,19 @@ async fn recover_hid_if_needed( return; } + let allow_cycle = allow_gadget_cycle(); tokio::spawn(async move { - warn!("πŸ” HID transport down (errno={code:?}) - cycling gadget"); - match tokio::task::spawn_blocking(move || gadget.cycle()).await { - Ok(Ok(())) => info!("βœ… USB gadget cycle complete (auto-recover)"), - Ok(Err(e)) => error!("πŸ’₯ USB gadget cycle failed: {e:#}"), - Err(e) => error!("πŸ’₯ USB gadget cycle task panicked: {e:#}"), + if allow_cycle { + warn!("πŸ” HID transport down (errno={code:?}) - cycling gadget"); + match tokio::task::spawn_blocking(move || gadget.cycle()).await { + Ok(Ok(())) => info!("βœ… USB gadget cycle complete (auto-recover)"), + Ok(Err(e)) => error!("πŸ’₯ USB gadget cycle failed: {e:#}"), + Err(e) => error!("πŸ’₯ USB gadget cycle task panicked: {e:#}"), + } + } else { + warn!( + "πŸ”’ HID transport down (errno={code:?}) - gadget cycle disabled; set LESAVKA_ALLOW_GADGET_CYCLE=1 to enable" + ); } if let Err(e) = async { @@ -258,11 +267,11 @@ struct Handler { impl Handler { async fn new(gadget: UsbGadget) -> anyhow::Result { - if AUTO_CYCLE { + if allow_gadget_cycle() { info!("πŸ› οΈ Initial USB reset…"); let _ = gadget.cycle(); // ignore failure - may boot without host } else { - info!("πŸ› οΈ AUTO_CYCLE disabled -Β no initial reset"); + info!("πŸ”’ gadget cycle disabled at startup (set LESAVKA_ALLOW_GADGET_CYCLE=1 to enable)"); } info!("πŸ› οΈ opening HID endpoints …");