From e89271e46cec01aa0fda0df8c9fbd05b0ff49755 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Mon, 27 Apr 2026 23:04:18 -0300 Subject: [PATCH] fix(sync): rebuild incomplete uvc gadgets --- Cargo.lock | 6 ++--- client/Cargo.toml | 2 +- common/Cargo.toml | 2 +- scripts/daemon/lesavka-core.sh | 9 +++++-- scripts/install/server.sh | 4 +-- server/Cargo.toml | 2 +- testing/tests/server_core_script_contract.rs | 25 +++++++++++++++++++ .../tests/server_install_script_contract.rs | 8 ++++++ 8 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 testing/tests/server_core_script_contract.rs diff --git a/Cargo.lock b/Cargo.lock index d8c2b4a..7204e32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.14.33" +version = "0.14.34" dependencies = [ "anyhow", "async-stream", @@ -1676,7 +1676,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.14.33" +version = "0.14.34" dependencies = [ "anyhow", "base64", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.14.33" +version = "0.14.34" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 2111a71..71865ef 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.14.33" +version = "0.14.34" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 3f9b957..9c23066 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.14.33" +version = "0.14.34" edition = "2024" build = "build.rs" diff --git a/scripts/daemon/lesavka-core.sh b/scripts/daemon/lesavka-core.sh index 37f1600..6377eec 100755 --- a/scripts/daemon/lesavka-core.sh +++ b/scripts/daemon/lesavka-core.sh @@ -324,6 +324,7 @@ fi log "βœ… UDC detected: $UDC" # If a gadget is already configured, avoid tearing it down unless forced. +GADGET_NEEDS_REBUILD=0 if [[ -d $G && -z $ALLOW_RESET ]]; then if [[ -s $G/UDC || -d $G/configs/c.1 ]]; then if gadget_has_expected_functions; then @@ -333,6 +334,7 @@ if [[ -d $G && -z $ALLOW_RESET ]]; then exit 0 fi log "⚠️ gadget is configured but missing expected functions; continuing toward rebuild." + GADGET_NEEDS_REBUILD=1 fi fi @@ -341,7 +343,7 @@ BOUND_UDC="" if [[ -r $G/UDC ]]; then BOUND_UDC=$(cat "$G/UDC" 2>/dev/null || true) fi -if [[ -n $BOUND_UDC && -z $ALLOW_RESET ]]; then +if [[ -n $BOUND_UDC && -z $ALLOW_RESET && "$GADGET_NEEDS_REBUILD" != "1" ]]; then log "πŸ”’ gadget already bound to '$BOUND_UDC' - refusing reset." log " Set LESAVKA_ALLOW_GADGET_RESET=1 to force." exit 0 @@ -349,11 +351,14 @@ fi # Guard against lockups: don't reset gadget while host is attached unless forced. UDC_STATE="$(udc_state "$UDC")" -if [[ -z $ALLOW_RESET ]] && is_attached_state "$UDC_STATE"; then +if [[ -z $ALLOW_RESET ]] && is_attached_state "$UDC_STATE" && [[ "$GADGET_NEEDS_REBUILD" != "1" ]]; 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 +if [[ "$GADGET_NEEDS_REBUILD" == "1" ]]; then + log "♻️ incomplete gadget detected; rebuilding even though the old gadget is still bound." +fi #────────────────────────────────────────────────── # 3. (Re‑)create gadget diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 07455ac..b7af1cb 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -774,8 +774,8 @@ if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || [[ "$FORCE_GADGET_REBUILD" == "1" LESAVKA_UVC_FALLBACK=0 \ LESAVKA_UVC_CODEC="${INSTALL_UVC_CODEC}" \ /usr/local/bin/lesavka-core.sh - sudo systemctl restart lesavka-core - echo "βœ… lesavka-core installed and restarted..." + sudo systemctl reset-failed lesavka-core >/dev/null 2>&1 || true + echo "βœ… lesavka-core gadget rebuilt directly." else echo "⚠️ UDC state is '$UDC_STATE' - skipping lesavka-core restart." echo " Set LESAVKA_ALLOW_GADGET_RESET=1 to force." diff --git a/server/Cargo.toml b/server/Cargo.toml index 22c0d2b..0c081b3 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.14.33" +version = "0.14.34" edition = "2024" autobins = false diff --git a/testing/tests/server_core_script_contract.rs b/testing/tests/server_core_script_contract.rs new file mode 100644 index 0000000..f49d6b4 --- /dev/null +++ b/testing/tests/server_core_script_contract.rs @@ -0,0 +1,25 @@ +//! Contract tests for lesavka-core gadget rebuild guardrails. +//! +//! Scope: statically guard the shell logic in `scripts/daemon/lesavka-core.sh`. +//! Targets: incomplete gadget recovery behavior. +//! Why: the gadget can be bound but half-applied, and the core script must +//! rebuild that state instead of refusing reset forever. + +const CORE_SCRIPT: &str = include_str!("../../scripts/daemon/lesavka-core.sh"); + +#[test] +fn core_script_rebuilds_incomplete_bound_gadgets() { + for expected in [ + "GADGET_NEEDS_REBUILD=0", + "gadget is configured but missing expected functions; continuing toward rebuild.", + "GADGET_NEEDS_REBUILD=1", + "if [[ -n $BOUND_UDC && -z $ALLOW_RESET && \"$GADGET_NEEDS_REBUILD\" != \"1\" ]]; then", + "if [[ -z $ALLOW_RESET ]] && is_attached_state \"$UDC_STATE\" && [[ \"$GADGET_NEEDS_REBUILD\" != \"1\" ]]; then", + "incomplete gadget detected; rebuilding even though the old gadget is still bound.", + ] { + assert!( + CORE_SCRIPT.contains(expected), + "lesavka-core guardrail missing: {expected}" + ); + } +} diff --git a/testing/tests/server_install_script_contract.rs b/testing/tests/server_install_script_contract.rs index cb4d18c..8cde308 100644 --- a/testing/tests/server_install_script_contract.rs +++ b/testing/tests/server_install_script_contract.rs @@ -78,6 +78,14 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { SERVER_INSTALL.contains("UVC function is missing from the live gadget; forcing a rebuild before server start."), "install script should force a rebuild when the live gadget is attached but missing UVC" ); + assert!( + SERVER_INSTALL.contains("lesavka-core gadget rebuilt directly."), + "install script should trust the direct forced rebuild instead of immediately rerunning the oneshot core unit" + ); + assert!( + !SERVER_INSTALL.contains("sudo systemctl restart lesavka-core"), + "install script should not immediately rerun lesavka-core after a successful direct forced rebuild" + ); assert!( SERVER_INSTALL.contains("video-output node did not appear after rebuild"), "install script should explain why it refuses a half-applied UVC install"