install: defer attached uvc runtime changes

This commit is contained in:
Brad Stein 2026-05-11 01:40:11 -03:00
parent 7c797e6248
commit 4bc5264513
6 changed files with 183 additions and 10 deletions

6
Cargo.lock generated
View File

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

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.22.2"
version = "0.22.3"
edition = "2024"
[dependencies]

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.22.2"
version = "0.22.3"
edition = "2024"
build = "build.rs"

View File

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

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.22.2"
version = "0.22.3"
edition = "2024"
autobins = false

View File

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