From 4bc52645136a3c26a00996ce30ecdd622d6068af Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Mon, 11 May 2026 01:40:11 -0300 Subject: [PATCH] install: defer attached uvc runtime changes --- Cargo.lock | 6 +- client/Cargo.toml | 2 +- common/Cargo.toml | 2 +- scripts/install/server.sh | 107 +++++++++++++++++- server/Cargo.toml | 2 +- .../install/server_install_script_contract.rs | 74 ++++++++++++ 6 files changed, 183 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 877b2e0..d193161 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.22.2" +version = "0.22.3" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.22.2" +version = "0.22.3" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.22.2" +version = "0.22.3" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 3c0fd96..7414b9a 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.22.2" +version = "0.22.3" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 8e66d60..5ec5f48 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.22.2" +version = "0.22.3" edition = "2024" build = "build.rs" diff --git a/scripts/install/server.sh b/scripts/install/server.sh index c3e414b..120f78a 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -439,6 +439,49 @@ uvc_gadget_present() { [[ -d /sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 ]] } +live_uvc_descriptor_codec() { + local function_root=/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 + if [[ -e "$function_root/streaming/header/h/mjpeg" ]]; then + echo "mjpeg" + return 0 + fi + if [[ -e "$function_root/streaming/header/h/yuyv" ]]; then + echo "hevc" + return 0 + fi + if [[ -d "$function_root/streaming/mjpeg/m" ]]; then + echo "mjpeg" + return 0 + fi + if [[ -d "$function_root/streaming/uncompressed/yuyv" ]]; then + echo "hevc" + return 0 + fi + echo "unknown" +} + +live_uvc_descriptor_matches_request() { + local function_root=/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 + local frame_root="" + case "$(live_uvc_descriptor_codec)" in + mjpeg) + [[ "$INSTALL_UVC_CODEC" == "mjpeg" ]] || return 1 + frame_root="$function_root/streaming/mjpeg/m/720p" + ;; + hevc) + [[ "$INSTALL_UVC_CODEC" != "mjpeg" ]] || return 1 + frame_root="$function_root/streaming/uncompressed/yuyv/480p" + ;; + *) + return 1 + ;; + esac + [[ -r "$frame_root/wWidth" && -r "$frame_root/wHeight" && -r "$frame_root/dwDefaultFrameInterval" ]] || return 1 + [[ "$(cat "$frame_root/wWidth" 2>/dev/null || true)" == "${LESAVKA_UVC_WIDTH:-1280}" ]] || return 1 + [[ "$(cat "$frame_root/wHeight" 2>/dev/null || true)" == "${LESAVKA_UVC_HEIGHT:-720}" ]] || return 1 + [[ "$(cat "$frame_root/dwDefaultFrameInterval" 2>/dev/null || true)" == "${LESAVKA_UVC_INTERVAL:-333333}" ]] +} + udc_state() { local udc="" udc=$(ls /sys/class/udc 2>/dev/null | head -n1 || true) @@ -1204,6 +1247,7 @@ if [[ -n $HDMI_CONNECTOR ]]; then else echo "⚠️ no connected HDMI connector detected; leaving LESAVKA_HDMI_CONNECTOR unset." >&2 fi +SERVER_ENV_TMP=$(mktemp) { echo "# generated by lesavka/scripts/install/server.sh" echo "# Edit only for local hardware overrides; rerunning the installer refreshes defaults." @@ -1271,7 +1315,7 @@ fi printf 'LESAVKA_TLS_CERT=%s\n' "${LESAVKA_TLS_CERT:-$LESAVKA_TLS_DIR/server.crt}" printf 'LESAVKA_TLS_KEY=%s\n' "${LESAVKA_TLS_KEY:-$LESAVKA_TLS_DIR/server.key}" printf 'LESAVKA_TLS_CLIENT_CA=%s\n' "${LESAVKA_TLS_CLIENT_CA:-$LESAVKA_TLS_DIR/ca.crt}" -} | sudo tee /etc/lesavka/server.env >/dev/null +} >"$SERVER_ENV_TMP" UVC_ENV_TMP=$(mktemp) render_uvc_env_file >"$UVC_ENV_TMP" @@ -1279,8 +1323,60 @@ UVC_ENV_CHANGED=1 if sudo test -f /etc/lesavka/uvc.env && sudo cmp -s "$UVC_ENV_TMP" /etc/lesavka/uvc.env; then UVC_ENV_CHANGED=0 fi +UDC_STATE=$(udc_state) +EXPLICIT_GADGET_REBUILD=0 +if [[ -n ${LESAVKA_FORCE_GADGET_REBUILD:-} ]]; then + EXPLICIT_GADGET_REBUILD=1 +fi +LIVE_UVC_DESCRIPTOR_MISMATCH=0 +if is_attached_state "$UDC_STATE" && uvc_gadget_present && ! live_uvc_descriptor_matches_request; then + LIVE_UVC_DESCRIPTOR_MISMATCH=1 +fi +LIVE_UVC_FUNCTION_MISSING=0 +if [[ -z ${LESAVKA_DISABLE_UVC:-} ]] && is_attached_state "$UDC_STATE" && ! uvc_gadget_present; then + LIVE_UVC_FUNCTION_MISSING=1 +fi +ATTACHED_UVC_CHANGE_DEFERRED=0 +if is_attached_state "$UDC_STATE" \ + && { [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" == "1" ]] || [[ "$LIVE_UVC_FUNCTION_MISSING" == "1" ]]; } \ + && { [[ "$EXPLICIT_GADGET_REBUILD" != "1" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; }; then + ATTACHED_UVC_CHANGE_DEFERRED=1 +fi +ATTACHED_UVC_RESTART_DEFERRED=0 +if is_attached_state "$UDC_STATE" \ + && [[ "$UVC_ENV_CHANGED" == "1" ]] \ + && [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" != "1" ]] \ + && [[ "$LIVE_UVC_FUNCTION_MISSING" != "1" ]] \ + && { [[ "$EXPLICIT_GADGET_REBUILD" != "1" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; }; then + ATTACHED_UVC_RESTART_DEFERRED=1 +fi +if [[ "$ATTACHED_UVC_CHANGE_DEFERRED" == "1" ]]; then + echo "⚠️ UVC runtime settings or live descriptors differ while the host is attached." >&2 + echo " Refusing to rewrite UVC/server runtime env or restart Lesavka services because that can wedge the Pi USB controller." >&2 + echo " Leaving /etc/lesavka/server.env and /etc/lesavka/uvc.env unchanged; binaries were installed but running services were left untouched." >&2 + echo " To apply descriptor-changing UVC settings, use a maintenance window with LESAVKA_ALLOW_GADGET_RESET=1 LESAVKA_FORCE_GADGET_REBUILD=1." >&2 + rm -f "$SERVER_ENV_TMP" "$UVC_ENV_TMP" + INSTALLED_VERSION=$(manifest_package_version "$SRC_DIR/server/Cargo.toml" 2>/dev/null || true) + INSTALLED_SHA=$(git -C "$SCRIPT_REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true) + echo "✅ lesavka-server binaries installed; live service restart deferred for attached-gadget safety." + echo "➡️ Installed binaries: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}" + echo "➡️ Deferred UVC codec request: ${INSTALL_UVC_CODEC}" + exit 0 +fi +sudo install -m 0644 "$SERVER_ENV_TMP" /etc/lesavka/server.env sudo install -m 0644 "$UVC_ENV_TMP" /etc/lesavka/uvc.env -rm -f "$UVC_ENV_TMP" +rm -f "$SERVER_ENV_TMP" "$UVC_ENV_TMP" +if [[ "$ATTACHED_UVC_RESTART_DEFERRED" == "1" ]]; then + echo "⚠️ UVC runtime env changed while the host is attached, but the requested descriptor matches the live gadget." >&2 + echo " Updated /etc/lesavka/server.env and /etc/lesavka/uvc.env without restarting live services." >&2 + echo " Rerun the installer with the same UVC settings to restart services after the env is stable, or use LESAVKA_ALLOW_GADGET_RESET=1 LESAVKA_FORCE_GADGET_REBUILD=1 during a maintenance window." >&2 + INSTALLED_VERSION=$(manifest_package_version "$SRC_DIR/server/Cargo.toml" 2>/dev/null || true) + INSTALLED_SHA=$(git -C "$SCRIPT_REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true) + echo "✅ lesavka-server binaries and runtime env installed; live service restart deferred for attached-gadget safety." + echo "➡️ Installed binaries: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}" + echo "➡️ Deferred restart UVC codec: ${INSTALL_UVC_CODEC}" + exit 0 +fi echo "==> 6a. Systemd units - lesavka-core" cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-core.service >/dev/null @@ -1347,10 +1443,8 @@ sudo truncate -s 0 /var/log/lesavka/server.log sudo systemctl daemon-reload sudo systemctl enable lesavka-core lesavka-server -UDC_STATE=$(udc_state) FORCE_GADGET_REBUILD=0 GADGET_REBUILD_REASON="" -EXPLICIT_GADGET_REBUILD=0 if [[ -z ${LESAVKA_DISABLE_UVC:-} ]] && ! uvc_gadget_present; then FORCE_GADGET_REBUILD=1 GADGET_REBUILD_REASON="UVC function is missing from the live gadget" @@ -1361,6 +1455,11 @@ if [[ "$UVC_ENV_CHANGED" == "1" ]] && is_attached_state "$UDC_STATE"; then GADGET_REBUILD_REASON="UVC runtime settings changed while the host is attached" echo "⚠️ UVC runtime settings changed while the host is attached; forcing a gadget rebuild so the new descriptors take effect." fi +if [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" == "1" ]] && is_attached_state "$UDC_STATE"; then + FORCE_GADGET_REBUILD=1 + GADGET_REBUILD_REASON="live UVC descriptor does not match requested codec ${INSTALL_UVC_CODEC}" + echo "⚠️ live UVC descriptor does not match requested codec ${INSTALL_UVC_CODEC}; forcing a gadget rebuild so descriptors and runtime agree." +fi if [[ -n ${LESAVKA_FORCE_GADGET_REBUILD:-} ]]; then FORCE_GADGET_REBUILD=1 EXPLICIT_GADGET_REBUILD=1 diff --git a/server/Cargo.toml b/server/Cargo.toml index 4661d25..3c6ba3f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.22.2" +version = "0.22.3" edition = "2024" autobins = false diff --git a/tests/installer/scripts/install/server_install_script_contract.rs b/tests/installer/scripts/install/server_install_script_contract.rs index b8e6950..d8ddb98 100644 --- a/tests/installer/scripts/install/server_install_script_contract.rs +++ b/tests/installer/scripts/install/server_install_script_contract.rs @@ -209,6 +209,80 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { SERVER_INSTALL.contains("EXPLICIT_GADGET_REBUILD=1"), "installer should distinguish organic rebuild needs from an explicit operator hard-reset request" ); + assert!( + SERVER_INSTALL.contains("ATTACHED_UVC_CHANGE_DEFERRED=1"), + "attached UVC descriptor/runtime changes should be deferred instead of half-applied" + ); + assert!( + SERVER_INSTALL + .contains("Leaving /etc/lesavka/server.env and /etc/lesavka/uvc.env unchanged"), + "deferred attached-gadget installs should preserve the last known live runtime env" + ); + assert!( + SERVER_INSTALL.contains("binaries were installed but running services were left untouched"), + "deferred attached-gadget installs must not restart live services" + ); + assert!( + SERVER_INSTALL.contains("LIVE_UVC_DESCRIPTOR_MISMATCH"), + "installer should catch partial previous runs where env already changed but live descriptors did not" + ); + assert!( + SERVER_INSTALL.contains("LIVE_UVC_FUNCTION_MISSING"), + "installer should not rebuild a missing attached UVC function without explicit maintenance-window force" + ); + assert!( + SERVER_INSTALL.contains("ATTACHED_UVC_RESTART_DEFERRED=1"), + "attached UVC env changes that match the live descriptor should still defer service restarts" + ); + assert!( + SERVER_INSTALL.contains("Updated /etc/lesavka/server.env and /etc/lesavka/uvc.env without restarting live services"), + "safe env repairs should not immediately restart the attached gadget path" + ); + assert!( + SERVER_INSTALL.contains("dwDefaultFrameInterval"), + "live descriptor matching should include frame interval, not only codec labels" + ); + assert!( + SERVER_INSTALL.contains("streaming/header/h/mjpeg") + && SERVER_INSTALL.contains("streaming/header/h/yuyv"), + "live descriptor codec detection should prefer the active UVC header links" + ); + assert!( + SERVER_INSTALL + .find("sudo install -m 0644 \"$UVC_ENV_TMP\" /etc/lesavka/uvc.env") + .unwrap() + < SERVER_INSTALL + .find("if [[ \"$ATTACHED_UVC_RESTART_DEFERRED\" == \"1\" ]]") + .unwrap(), + "safe env repair should install env files before exiting without service restarts" + ); + assert!( + SERVER_INSTALL + .find("if [[ \"$ATTACHED_UVC_RESTART_DEFERRED\" == \"1\" ]]") + .unwrap() + < SERVER_INSTALL + .find("sudo systemctl restart lesavka-uvc") + .unwrap(), + "safe env repair must exit before live UVC helper restarts" + ); + assert!( + SERVER_INSTALL + .find("ATTACHED_UVC_CHANGE_DEFERRED=1") + .unwrap() + < SERVER_INSTALL + .find("sudo install -m 0644 \"$SERVER_ENV_TMP\" /etc/lesavka/server.env") + .unwrap(), + "attached UVC deferral must run before runtime env files are rewritten" + ); + assert!( + SERVER_INSTALL + .find("ATTACHED_UVC_CHANGE_DEFERRED=1") + .unwrap() + < SERVER_INSTALL + .find("sudo systemctl restart lesavka-uvc") + .unwrap(), + "attached UVC deferral must run before any live UVC helper restart" + ); assert!( SERVER_INSTALL.contains("[[ \"$EXPLICIT_GADGET_REBUILD\" != \"1\" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]"), "attached-host hard rebuilds should require both allow and explicit force flags"