server: guard gadget cycling

This commit is contained in:
Brad Stein 2026-01-08 23:58:19 -03:00
parent 0da8fbb5f7
commit 4e96c271ed
4 changed files with 97 additions and 17 deletions

View File

@ -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
#──────────────────────────────────────────────────

View File

@ -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..."

View File

@ -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}");

View File

@ -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<Self> {
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 …");