From b47627ad5b6c4c691909ef1a81f62db09a6c21f9 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Fri, 15 May 2026 01:22:56 -0300 Subject: [PATCH] install: refresh active uvc helper safely --- Cargo.lock | 6 +-- client/Cargo.toml | 2 +- common/Cargo.toml | 2 +- scripts/install/server.sh | 48 ++++++++++++------- server/Cargo.toml | 2 +- .../install/server_install_script_contract.rs | 33 +++++++------ 6 files changed, 56 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24321e5..c2ca7ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.22.33" +version = "0.22.34" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.22.33" +version = "0.22.34" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.22.33" +version = "0.22.34" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 20a518a..97a3c0e 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.22.33" +version = "0.22.34" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 97786d9..d25722e 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.22.33" +version = "0.22.34" edition = "2024" build = "build.rs" diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 870724a..554b9f8 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -976,6 +976,28 @@ restart_lesavka_server_only() { validate_server_ready } +restart_lesavka_uvc_helper_only() { + if [[ -n ${LESAVKA_DISABLE_UVC:-} ]]; then + echo "⚠️ UVC is disabled; skipping lesavka-uvc helper refresh." >&2 + return 0 + fi + if ! uvc_gadget_present; then + echo "⚠️ live UVC function is missing; skipping helper-only refresh until the next safe gadget rebuild." >&2 + return 0 + fi + if ! systemctl list-unit-files lesavka-uvc.service --no-pager --no-legend 2>/dev/null \ + | grep -q '^lesavka-uvc\.service'; then + echo "⚠️ lesavka-uvc unit is not installed yet; skipping helper-only refresh." >&2 + return 0 + fi + + sudo install -d -m 0755 /var/log/lesavka + sudo truncate -s 0 /var/log/lesavka/uvc.stderr + sudo systemctl reset-failed lesavka-uvc >/dev/null 2>&1 || true + sudo systemctl restart lesavka-uvc + echo "✅ lesavka-uvc helper restarted with updated binaries without cycling lesavka-core or the USB gadget." +} + normalize_hdmi_connector() { local name="$1" if [[ $name =~ (HDMI-A-[0-9]+)$ ]]; then @@ -1664,7 +1686,7 @@ if [[ "$HOST_GADGET_PROTECTED" == "1" ]] \ fi if [[ "$ATTACHED_UVC_CHANGE_DEFERRED" == "1" ]]; then echo "⚠️ UVC/UAC runtime settings or live descriptors differ while the host is attached." >&2 - echo " The installer will not restart lesavka-core or lesavka-uvc because that can wedge the Pi USB controller." >&2 + echo " The installer will not restart lesavka-core or rewrite attached USB descriptors because that can wedge the Pi USB controller." >&2 echo " To apply descriptor-changing UVC settings, use a maintenance window with LESAVKA_ALLOW_GADGET_RESET=1 LESAVKA_FORCE_GADGET_REBUILD=1." >&2 if lesavka_server_has_active_clients && [[ "${LESAVKA_INSTALL_RESTART_WITH_CLIENTS:-0}" != "1" ]]; then echo "⚠️ Active gRPC clients are connected; leaving the running server untouched for this install." >&2 @@ -1682,8 +1704,8 @@ if [[ "$ATTACHED_UVC_CHANGE_DEFERRED" == "1" ]]; then exit 0 fi - echo "✅ No active gRPC clients detected; applying runtime env and restarting lesavka-server only." >&2 - echo " Preserving lesavka-core and lesavka-uvc so the attached USB gadget is not cycled." >&2 + echo "✅ No active gRPC clients detected; applying runtime env and restarting lesavka-server." >&2 + echo " Preserving lesavka-core and live USB descriptors; refreshing only the user-space UVC helper if the live UVC function exists." >&2 sudo install -m 0644 "$SERVER_ENV_TMP" /etc/lesavka/server.env sudo install -m 0644 "$UVC_ENV_TMP" /etc/lesavka/uvc.env rm -f "$SERVER_ENV_TMP" "$UVC_ENV_TMP" @@ -1693,11 +1715,12 @@ if [[ "$ATTACHED_UVC_CHANGE_DEFERRED" == "1" ]]; then sudo systemctl daemon-reload sudo systemctl enable lesavka-server >/dev/null 2>&1 || true restart_lesavka_server_only + restart_lesavka_uvc_helper_only sudo /usr/local/bin/lesavka-recovery-ladder snapshot || true 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) sudo systemctl start lesavka-recovery-ladder.timer >/dev/null 2>&1 || true - echo "✅ lesavka-server binaries, runtime env, and server unit installed; server-only restart completed." + echo "✅ lesavka-server binaries, runtime env, and server unit installed; server restart plus helper-only UVC refresh completed." echo "➡️ Installed and running: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}" echo "➡️ Recovery ladder: installed, enabled, and started in server-only mode" echo "➡️ UVC codec request persisted for the next safe gadget rebuild: ${INSTALL_UVC_CODEC}" @@ -1709,16 +1732,8 @@ sudo install -m 0644 "$UVC_ENV_TMP" /etc/lesavka/uvc.env 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) - sudo systemctl start lesavka-recovery-ladder.timer >/dev/null 2>&1 || 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 "➡️ Recovery ladder: installed, enabled, and started in server-only mode" - echo "➡️ Deferred restart UVC codec: ${INSTALL_UVC_CODEC}" - exit 0 + echo " The installer will not rebuild lesavka-core or attached USB descriptors." >&2 + echo " It will refresh the user-space lesavka-uvc helper after unit files are updated so helper-only fixes become live." >&2 fi echo "==> 6a. Systemd units - lesavka-core" @@ -1759,7 +1774,8 @@ if [[ -z ${LESAVKA_DISABLE_UVC:-} ]] && ! uvc_gadget_present; then GADGET_REBUILD_REASON="UVC function is missing from the live gadget" echo "⚠️ UVC function is missing from the live gadget; forcing a rebuild before server start." fi -if [[ "$UVC_ENV_CHANGED" == "1" ]] && [[ "$HOST_GADGET_PROTECTED" == "1" ]]; then +if [[ "$UVC_ENV_CHANGED" == "1" ]] && [[ "$HOST_GADGET_PROTECTED" == "1" ]] \ + && { [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" == "1" ]] || [[ "$LIVE_UAC_DESCRIPTOR_MISMATCH" == "1" ]] || [[ "$LIVE_UVC_FUNCTION_MISSING" == "1" ]]; }; then FORCE_GADGET_REBUILD=1 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." @@ -1854,7 +1870,7 @@ if [[ "$HOST_GADGET_PROTECTED" == "1" ]] \ CAN_TOUCH_UVC_SERVICE=0 fi if [[ "$CAN_TOUCH_UVC_SERVICE" != "1" ]] && systemctl is-active --quiet lesavka-uvc; then - echo "✅ lesavka-uvc already active; preserving it during attached-gadget version update." + restart_lesavka_uvc_helper_only elif [[ "$CAN_TOUCH_UVC_SERVICE" != "1" ]]; then echo "⚠️ lesavka-uvc is inactive, but the host/gadget is protected; not starting it during a version-only install." >&2 elif [[ "$UVC_ENV_CHANGED" == "1" ]] && systemctl is-active --quiet lesavka-uvc; then diff --git a/server/Cargo.toml b/server/Cargo.toml index 7c1a339..34c9da1 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.22.33" +version = "0.22.34" 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 d31a0c7..e0897b7 100644 --- a/tests/installer/scripts/install/server_install_script_contract.rs +++ b/tests/installer/scripts/install/server_install_script_contract.rs @@ -194,6 +194,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_WIDTH 1280")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_HEIGHT 720")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0")); + assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_QUEUE_PACING 1")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_BULK 1")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_FRAME_SIZE_GUARD 1")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_FRAME_MAX_BYTES 0")); @@ -314,16 +315,16 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { ); assert!( SERVER_INSTALL - .contains("No active gRPC clients detected; applying runtime env and restarting lesavka-server only") + .contains("No active gRPC clients detected; applying runtime env and restarting lesavka-server") && SERVER_INSTALL - .contains("Preserving lesavka-core and lesavka-uvc so the attached USB gadget is not cycled") + .contains("Preserving lesavka-core and live USB descriptors; refreshing only the user-space UVC helper") && SERVER_INSTALL.contains("restart_lesavka_server_only"), - "idle attached-gadget installs should leave the newest server running without cycling USB gadget services" + "idle attached-gadget installs should leave the newest server and UVC helper running without cycling USB gadget descriptors" ); assert!( - SERVER_INSTALL.contains("server-only restart completed") + SERVER_INSTALL.contains("server restart plus helper-only UVC refresh completed") && SERVER_INSTALL.contains("Live UVC descriptors may still differ"), - "idle attached-gadget installs should be explicit about server readiness and deferred descriptor rebuilds" + "idle attached-gadget installs should be explicit about server/helper readiness and deferred descriptor rebuilds" ); assert!( SERVER_INSTALL @@ -362,11 +363,13 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { ); assert!( SERVER_INSTALL.contains("ATTACHED_UVC_RESTART_DEFERRED=1"), - "attached UVC env changes that match the live descriptor should still defer service restarts" + "attached UVC env changes that match the live descriptor should take the helper-only refresh path" ); 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" + SERVER_INSTALL.contains( + "It will refresh the user-space lesavka-uvc helper after unit files are updated" + ), + "safe env repairs should refresh the helper without rebuilding the attached gadget" ); assert!( SERVER_INSTALL.contains("dwDefaultFrameInterval"), @@ -393,16 +396,16 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { < SERVER_INSTALL .find("if [[ \"$ATTACHED_UVC_RESTART_DEFERRED\" == \"1\" ]]") .unwrap(), - "safe env repair should install env files before exiting without service restarts" + "safe env repair should install env files before choosing the helper-only refresh path" ); assert!( SERVER_INSTALL .find("if [[ \"$ATTACHED_UVC_RESTART_DEFERRED\" == \"1\" ]]") .unwrap() < SERVER_INSTALL - .find("sudo systemctl restart lesavka-uvc") + .rfind("restart_lesavka_uvc_helper_only") .unwrap(), - "safe env repair must exit before live UVC helper restarts" + "safe env repair should reach the helper-only restart after unit files are refreshed" ); assert!( SERVER_INSTALL @@ -418,9 +421,9 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { .find("ATTACHED_UVC_CHANGE_DEFERRED=1") .unwrap() < SERVER_INSTALL - .find("sudo systemctl restart lesavka-uvc") + .rfind("restart_lesavka_uvc_helper_only") .unwrap(), - "attached UVC deferral must run before any live UVC helper restart" + "attached UVC deferral must run before any helper-only refresh" ); assert!( SERVER_INSTALL.contains("[[ \"$EXPLICIT_GADGET_REBUILD\" != \"1\" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]"), @@ -470,9 +473,9 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { ); assert!( SERVER_INSTALL.contains("CAN_TOUCH_UVC_SERVICE=0") - && SERVER_INSTALL.contains("preserving it during attached-gadget version update") + && SERVER_INSTALL.contains("restart_lesavka_uvc_helper_only") && SERVER_INSTALL.contains("not starting it during a version-only install"), - "ordinary attached-gadget version updates must not start or restart the UVC helper" + "ordinary attached-gadget version updates should refresh an active UVC helper without starting an inactive one" ); assert!( SERVER_INSTALL.contains("sudo systemctl start lesavka-uvc"),