install: preserve live uvc profile on upgrades

This commit is contained in:
Brad Stein 2026-05-11 13:03:33 -03:00
parent 0f1fe89138
commit f9d23526c5
8 changed files with 121 additions and 43 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.22.4" version = "0.22.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.22.4" version = "0.22.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.22.4" version = "0.22.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

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

View File

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

View File

@ -7,11 +7,43 @@ SCRIPT_REPO_ROOT=$(cd -- "$SCRIPT_DIR/../.." && pwd)
DEFAULT_REPO_URL=ssh://git@scm.bstein.dev:2242/bstein/lesavka.git DEFAULT_REPO_URL=ssh://git@scm.bstein.dev:2242/bstein/lesavka.git
export TMPDIR=${TMPDIR:-/var/tmp} export TMPDIR=${TMPDIR:-/var/tmp}
persisted_env_value() {
local file=$1
local key=$2
[[ -r $file ]] || return 1
awk -F= -v key="$key" '
$0 ~ /^[[:space:]]*#/ { next }
$1 == key {
sub(/^[^=]*=/, "")
print
found=1
exit
}
END { if (!found) exit 1 }
' "$file"
}
persisted_uvc_value() {
persisted_env_value /etc/lesavka/uvc.env "$1"
}
uvc_env_value() {
local key=$1
local default=$2
if [[ -n ${!key+x} ]]; then
printf '%s\n' "${!key}"
return 0
fi
persisted_uvc_value "$key" && return 0
printf '%s\n' "$default"
}
REF=${LESAVKA_REF:-master} # fallback REF=${LESAVKA_REF:-master} # fallback
REPO_URL=${LESAVKA_REPO_URL:-} REPO_URL=${LESAVKA_REPO_URL:-}
INSTALL_SOURCE=${LESAVKA_INSTALL_SOURCE:-auto} INSTALL_SOURCE=${LESAVKA_INSTALL_SOURCE:-auto}
USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6) USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
INSTALL_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-mjpeg} PERSISTED_UVC_CODEC=$(persisted_uvc_value LESAVKA_UVC_CODEC || true)
INSTALL_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-${PERSISTED_UVC_CODEC:-mjpeg}}
INSTALL_CAM_CODEC=${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-hevc}} INSTALL_CAM_CODEC=${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-hevc}}
INSTALL_UPLINK_AUDIO_CODEC=${LESAVKA_INSTALL_UPLINK_AUDIO_CODEC:-${LESAVKA_UPLINK_AUDIO_CODEC:-pcm}} INSTALL_UPLINK_AUDIO_CODEC=${LESAVKA_INSTALL_UPLINK_AUDIO_CODEC:-${LESAVKA_UPLINK_AUDIO_CODEC:-pcm}}
INSTALL_UVC_FRAME_META=${LESAVKA_INSTALL_UVC_FRAME_META:-${LESAVKA_UVC_FRAME_META:-0}} INSTALL_UVC_FRAME_META=${LESAVKA_INSTALL_UVC_FRAME_META:-${LESAVKA_UVC_FRAME_META:-0}}
@ -238,19 +270,19 @@ render_uvc_env_file() {
cat <<EOF cat <<EOF
# generated by lesavka/scripts/install/server.sh # generated by lesavka/scripts/install/server.sh
# Edit only for local UVC hardware overrides; rerunning the installer refreshes defaults. # Edit only for local UVC hardware overrides; rerunning the installer refreshes defaults.
LESAVKA_UVC_DEBUG=${LESAVKA_UVC_DEBUG:-1} LESAVKA_UVC_DEBUG=$(uvc_env_value LESAVKA_UVC_DEBUG 1)
LESAVKA_UVC_MAXPACKET=${LESAVKA_UVC_MAXPACKET:-1024} LESAVKA_UVC_MAXPACKET=$(uvc_env_value LESAVKA_UVC_MAXPACKET 1024)
LESAVKA_UVC_LIMIT_PCT=${LESAVKA_UVC_LIMIT_PCT:-100} LESAVKA_UVC_LIMIT_PCT=$(uvc_env_value LESAVKA_UVC_LIMIT_PCT 100)
LESAVKA_UVC_FPS=${LESAVKA_UVC_FPS:-30} LESAVKA_UVC_FPS=$(uvc_env_value LESAVKA_UVC_FPS 30)
LESAVKA_UVC_INTERVAL=${LESAVKA_UVC_INTERVAL:-333333} LESAVKA_UVC_INTERVAL=$(uvc_env_value LESAVKA_UVC_INTERVAL 333333)
LESAVKA_UVC_WIDTH=${LESAVKA_UVC_WIDTH:-1280} LESAVKA_UVC_WIDTH=$(uvc_env_value LESAVKA_UVC_WIDTH 1280)
LESAVKA_UVC_HEIGHT=${LESAVKA_UVC_HEIGHT:-720} LESAVKA_UVC_HEIGHT=$(uvc_env_value LESAVKA_UVC_HEIGHT 720)
LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC} LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}
LESAVKA_UVC_BLOCKING=${LESAVKA_UVC_BLOCKING:-1} LESAVKA_UVC_BLOCKING=$(uvc_env_value LESAVKA_UVC_BLOCKING 1)
LESAVKA_UVC_CONTROL_READ_ONLY=${LESAVKA_UVC_CONTROL_READ_ONLY:-0} LESAVKA_UVC_CONTROL_READ_ONLY=$(uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0)
LESAVKA_UVC_MAXBURST=${LESAVKA_UVC_MAXBURST:-0} LESAVKA_UVC_MAXBURST=$(uvc_env_value LESAVKA_UVC_MAXBURST 0)
LESAVKA_UVC_FRAME_MAX_BYTES=${LESAVKA_UVC_FRAME_MAX_BYTES:-0} LESAVKA_UVC_FRAME_MAX_BYTES=$(uvc_env_value LESAVKA_UVC_FRAME_MAX_BYTES 0)
LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC=${LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC:-10000000} LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC=$(uvc_env_value LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC 10000000)
EOF EOF
} }
@ -439,6 +471,10 @@ uvc_gadget_present() {
[[ -d /sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 ]] [[ -d /sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 ]]
} }
live_gadget_bound() {
[[ -s /sys/kernel/config/usb_gadget/lesavka/UDC ]]
}
live_uvc_descriptor_codec() { live_uvc_descriptor_codec() {
local function_root=/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 local function_root=/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0
if [[ -e "$function_root/streaming/header/h/mjpeg" ]]; then if [[ -e "$function_root/streaming/header/h/mjpeg" ]]; then
@ -1365,26 +1401,34 @@ if sudo test -f /etc/lesavka/uvc.env && sudo cmp -s "$UVC_ENV_TMP" /etc/lesavka/
UVC_ENV_CHANGED=0 UVC_ENV_CHANGED=0
fi fi
UDC_STATE=$(udc_state) UDC_STATE=$(udc_state)
LIVE_GADGET_BOUND=0
if live_gadget_bound; then
LIVE_GADGET_BOUND=1
fi
HOST_GADGET_PROTECTED=0
if is_attached_state "$UDC_STATE" || [[ "$LIVE_GADGET_BOUND" == "1" ]]; then
HOST_GADGET_PROTECTED=1
fi
EXPLICIT_GADGET_REBUILD=0 EXPLICIT_GADGET_REBUILD=0
if [[ -n ${LESAVKA_FORCE_GADGET_REBUILD:-} ]]; then if [[ -n ${LESAVKA_FORCE_GADGET_REBUILD:-} ]]; then
EXPLICIT_GADGET_REBUILD=1 EXPLICIT_GADGET_REBUILD=1
fi fi
LIVE_UVC_DESCRIPTOR_MISMATCH=0 LIVE_UVC_DESCRIPTOR_MISMATCH=0
if is_attached_state "$UDC_STATE" && uvc_gadget_present && ! live_uvc_descriptor_matches_request; then if [[ "$HOST_GADGET_PROTECTED" == "1" ]] && uvc_gadget_present && ! live_uvc_descriptor_matches_request; then
LIVE_UVC_DESCRIPTOR_MISMATCH=1 LIVE_UVC_DESCRIPTOR_MISMATCH=1
fi fi
LIVE_UVC_FUNCTION_MISSING=0 LIVE_UVC_FUNCTION_MISSING=0
if [[ -z ${LESAVKA_DISABLE_UVC:-} ]] && is_attached_state "$UDC_STATE" && ! uvc_gadget_present; then if [[ -z ${LESAVKA_DISABLE_UVC:-} ]] && [[ "$HOST_GADGET_PROTECTED" == "1" ]] && ! uvc_gadget_present; then
LIVE_UVC_FUNCTION_MISSING=1 LIVE_UVC_FUNCTION_MISSING=1
fi fi
ATTACHED_UVC_CHANGE_DEFERRED=0 ATTACHED_UVC_CHANGE_DEFERRED=0
if is_attached_state "$UDC_STATE" \ if [[ "$HOST_GADGET_PROTECTED" == "1" ]] \
&& { [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" == "1" ]] || [[ "$LIVE_UVC_FUNCTION_MISSING" == "1" ]]; } \ && { [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" == "1" ]] || [[ "$LIVE_UVC_FUNCTION_MISSING" == "1" ]]; } \
&& { [[ "$EXPLICIT_GADGET_REBUILD" != "1" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; }; then && { [[ "$EXPLICIT_GADGET_REBUILD" != "1" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; }; then
ATTACHED_UVC_CHANGE_DEFERRED=1 ATTACHED_UVC_CHANGE_DEFERRED=1
fi fi
ATTACHED_UVC_RESTART_DEFERRED=0 ATTACHED_UVC_RESTART_DEFERRED=0
if is_attached_state "$UDC_STATE" \ if [[ "$HOST_GADGET_PROTECTED" == "1" ]] \
&& [[ "$UVC_ENV_CHANGED" == "1" ]] \ && [[ "$UVC_ENV_CHANGED" == "1" ]] \
&& [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" != "1" ]] \ && [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" != "1" ]] \
&& [[ "$LIVE_UVC_FUNCTION_MISSING" != "1" ]] \ && [[ "$LIVE_UVC_FUNCTION_MISSING" != "1" ]] \
@ -1449,12 +1493,10 @@ cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-server.service >/dev/null
[Unit] [Unit]
Description=lesavka gRPC relay Description=lesavka gRPC relay
After=network.target lesavka-core.service lesavka-uvc.service After=network.target lesavka-core.service lesavka-uvc.service
Wants=lesavka-uvc.service
StartLimitIntervalSec=30 StartLimitIntervalSec=30
StartLimitBurst=10 StartLimitBurst=10
[Service] [Service]
ExecStartPre=/usr/local/bin/lesavka-core.sh --attach
ExecStart=/usr/local/bin/lesavka-server ExecStart=/usr/local/bin/lesavka-server
TimeoutStopSec=10 TimeoutStopSec=10
KillSignal=SIGTERM KillSignal=SIGTERM
@ -1495,12 +1537,12 @@ if [[ -z ${LESAVKA_DISABLE_UVC:-} ]] && ! uvc_gadget_present; then
GADGET_REBUILD_REASON="UVC function is missing from the live gadget" 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." echo "⚠️ UVC function is missing from the live gadget; forcing a rebuild before server start."
fi fi
if [[ "$UVC_ENV_CHANGED" == "1" ]] && is_attached_state "$UDC_STATE"; then if [[ "$UVC_ENV_CHANGED" == "1" ]] && [[ "$HOST_GADGET_PROTECTED" == "1" ]]; then
FORCE_GADGET_REBUILD=1 FORCE_GADGET_REBUILD=1
GADGET_REBUILD_REASON="UVC runtime settings changed while the host is attached" 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." echo "⚠️ UVC runtime settings changed while the host is attached; forcing a gadget rebuild so the new descriptors take effect."
fi fi
if [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" == "1" ]] && is_attached_state "$UDC_STATE"; then if [[ "$LIVE_UVC_DESCRIPTOR_MISMATCH" == "1" ]] && [[ "$HOST_GADGET_PROTECTED" == "1" ]]; then
FORCE_GADGET_REBUILD=1 FORCE_GADGET_REBUILD=1
GADGET_REBUILD_REASON="live UVC descriptor does not match requested codec ${INSTALL_UVC_CODEC}" 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." echo "⚠️ live UVC descriptor does not match requested codec ${INSTALL_UVC_CODEC}; forcing a gadget rebuild so descriptors and runtime agree."
@ -1511,14 +1553,14 @@ if [[ -n ${LESAVKA_FORCE_GADGET_REBUILD:-} ]]; then
GADGET_REBUILD_REASON="explicit LESAVKA_FORCE_GADGET_REBUILD request" GADGET_REBUILD_REASON="explicit LESAVKA_FORCE_GADGET_REBUILD request"
echo "⚠️ explicit LESAVKA_FORCE_GADGET_REBUILD request; forcing a gadget rebuild before server start." echo "⚠️ explicit LESAVKA_FORCE_GADGET_REBUILD request; forcing a gadget rebuild before server start."
fi fi
if [[ "$FORCE_GADGET_REBUILD" == "1" ]] && is_attached_state "$UDC_STATE" \ if [[ "$FORCE_GADGET_REBUILD" == "1" ]] && [[ "$HOST_GADGET_PROTECTED" == "1" ]] \
&& { [[ "$EXPLICIT_GADGET_REBUILD" != "1" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; }; then && { [[ "$EXPLICIT_GADGET_REBUILD" != "1" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; }; then
echo "⚠️ ${GADGET_REBUILD_REASON:-Gadget state} requires a rebuild, but UDC state is '$UDC_STATE' and attached-host hard reset was not explicitly allowed." >&2 echo "⚠️ ${GADGET_REBUILD_REASON:-Gadget state} requires a rebuild, but UDC state is '$UDC_STATE' (bound=${LIVE_GADGET_BOUND}) and attached-host hard reset was not explicitly allowed." >&2
echo " Preserving the attached gadget to avoid wedging the Pi USB controller." >&2 echo " Preserving the attached gadget to avoid wedging the Pi USB controller." >&2
echo " Run during a maintenance window with LESAVKA_ALLOW_GADGET_RESET=1 LESAVKA_FORCE_GADGET_REBUILD=1 if a hard rebuild is required." >&2 echo " Run during a maintenance window with LESAVKA_ALLOW_GADGET_RESET=1 LESAVKA_FORCE_GADGET_REBUILD=1 if a hard rebuild is required." >&2
FORCE_GADGET_REBUILD=0 FORCE_GADGET_REBUILD=0
fi fi
if [[ "$FORCE_GADGET_REBUILD" == "1" ]] || ! is_attached_state "$UDC_STATE"; then if [[ "$FORCE_GADGET_REBUILD" == "1" ]] || [[ "$HOST_GADGET_PROTECTED" != "1" ]]; then
echo "⚠️ UDC state is '$UDC_STATE' - forcing a Lesavka gadget rebuild before server start." echo "⚠️ UDC state is '$UDC_STATE' - forcing a Lesavka gadget rebuild before server start."
sudo systemctl stop lesavka-server >/dev/null 2>&1 || true sudo systemctl stop lesavka-server >/dev/null 2>&1 || true
sudo systemctl stop lesavka-uvc >/dev/null 2>&1 || true sudo systemctl stop lesavka-uvc >/dev/null 2>&1 || true
@ -1539,7 +1581,7 @@ if [[ "$FORCE_GADGET_REBUILD" == "1" ]] || ! is_attached_state "$UDC_STATE"; the
elif [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; then elif [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; then
echo "✅ UDC state is '$UDC_STATE'; LESAVKA_ALLOW_GADGET_RESET permits recovery, but no hard gadget rebuild is needed." echo "✅ UDC state is '$UDC_STATE'; LESAVKA_ALLOW_GADGET_RESET permits recovery, but no hard gadget rebuild is needed."
else else
echo "⚠️ UDC state is '$UDC_STATE' - skipping lesavka-core restart." echo "⚠️ UDC state is '$UDC_STATE' (bound=${LIVE_GADGET_BOUND}) - skipping lesavka-core restart."
echo " Set LESAVKA_ALLOW_GADGET_RESET=1 LESAVKA_FORCE_GADGET_REBUILD=1 to force during a maintenance window." echo " Set LESAVKA_ALLOW_GADGET_RESET=1 LESAVKA_FORCE_GADGET_REBUILD=1 to force during a maintenance window."
fi fi
@ -1579,7 +1621,16 @@ sudo rm -f /etc/systemd/system/lesavka-watchdog.timer \
echo "==> 6e. Systemd units - recovery ladder" echo "==> 6e. Systemd units - recovery ladder"
echo "✅ recovery ladder unit files installed and timer enabled; activation waits for successful service verification." echo "✅ recovery ladder unit files installed and timer enabled; activation waits for successful service verification."
if [[ "$UVC_ENV_CHANGED" == "1" ]] && systemctl is-active --quiet lesavka-uvc; then CAN_TOUCH_UVC_SERVICE=1
if [[ "$HOST_GADGET_PROTECTED" == "1" ]] \
&& { [[ "$EXPLICIT_GADGET_REBUILD" != "1" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; }; then
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."
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
sudo systemctl restart lesavka-uvc sudo systemctl restart lesavka-uvc
echo "✅ lesavka-uvc restarted with the refreshed UVC runtime settings." echo "✅ lesavka-uvc restarted with the refreshed UVC runtime settings."
elif systemctl is-active --quiet lesavka-uvc; then elif systemctl is-active --quiet lesavka-uvc; then

View File

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

View File

@ -61,7 +61,12 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
SERVER_INSTALL SERVER_INSTALL
.contains("${LESAVKA_INSTALL_UPLINK_AUDIO_CODEC:-${LESAVKA_UPLINK_AUDIO_CODEC:-pcm}}") .contains("${LESAVKA_INSTALL_UPLINK_AUDIO_CODEC:-${LESAVKA_UPLINK_AUDIO_CODEC:-pcm}}")
); );
assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_UVC_CODEC:-mjpeg}")); assert!(
SERVER_INSTALL.contains(
"INSTALL_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-${PERSISTED_UVC_CODEC:-mjpeg}}"
),
"plain upgrade installs should preserve the already-installed UVC codec"
);
assert!( assert!(
SERVER_INSTALL.contains("${LESAVKA_INSTALL_UVC_FRAME_META:-${LESAVKA_UVC_FRAME_META:-0}}") SERVER_INSTALL.contains("${LESAVKA_INSTALL_UVC_FRAME_META:-${LESAVKA_UVC_FRAME_META:-0}}")
); );
@ -145,11 +150,11 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
"installer should persist HEVC-specific calibration maps" "installer should persist HEVC-specific calibration maps"
); );
assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}")); assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_MAXPACKET:-1024}")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_MAXPACKET 1024"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_INTERVAL:-333333}")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_INTERVAL 333333"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_WIDTH:-1280}")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_WIDTH 1280"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEIGHT:-720}")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_HEIGHT 720"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_CONTROL_READ_ONLY:-0}")); assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0"));
assert!( assert!(
!SERVER_INSTALL.contains("LESAVKA_UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}"), !SERVER_INSTALL.contains("LESAVKA_UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}"),
"install script should not let ambient LESAVKA_UVC_CODEC leak into persisted defaults" "install script should not let ambient LESAVKA_UVC_CODEC leak into persisted defaults"
@ -239,6 +244,12 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
SERVER_INSTALL.contains("LIVE_UVC_FUNCTION_MISSING"), SERVER_INSTALL.contains("LIVE_UVC_FUNCTION_MISSING"),
"installer should not rebuild a missing attached UVC function without explicit maintenance-window force" "installer should not rebuild a missing attached UVC function without explicit maintenance-window force"
); );
assert!(
SERVER_INSTALL.contains("live_gadget_bound")
&& SERVER_INSTALL.contains("HOST_GADGET_PROTECTED=1")
&& SERVER_INSTALL.contains("bound=${LIVE_GADGET_BOUND}"),
"installer should protect a bound gadget even when the UDC state is ambiguous"
);
assert!( assert!(
SERVER_INSTALL.contains("ATTACHED_UVC_RESTART_DEFERRED=1"), 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 still defer service restarts"
@ -338,6 +349,12 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
SERVER_INSTALL.contains("lesavka-uvc already active; runtime settings unchanged."), SERVER_INSTALL.contains("lesavka-uvc already active; runtime settings unchanged."),
"install script should avoid unnecessary UVC restarts when nothing changed" "install script should avoid unnecessary UVC restarts when nothing changed"
); );
assert!(
SERVER_INSTALL.contains("CAN_TOUCH_UVC_SERVICE=0")
&& SERVER_INSTALL.contains("preserving it during attached-gadget version update")
&& SERVER_INSTALL.contains("not starting it during a version-only install"),
"ordinary attached-gadget version updates must not start or restart the UVC helper"
);
assert!( assert!(
SERVER_INSTALL.contains("sudo systemctl start lesavka-uvc"), SERVER_INSTALL.contains("sudo systemctl start lesavka-uvc"),
"install script should start the UVC helper so the host enumerates the UVC function" "install script should start the UVC helper so the host enumerates the UVC function"
@ -347,8 +364,9 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
"install script should report when it starts the UVC helper for enumeration" "install script should report when it starts the UVC helper for enumeration"
); );
assert!( assert!(
SERVER_INSTALL.contains("Wants=lesavka-uvc.service"), !SERVER_INSTALL.contains("Wants=lesavka-uvc.service")
"server unit should pull in the external UVC helper on UVC installs" && !SERVER_INSTALL.contains("ExecStartPre=/usr/local/bin/lesavka-core.sh --attach"),
"server restarts during ordinary installs must not implicitly start UVC or touch the gadget"
); );
assert!( assert!(
!SERVER_INSTALL.contains("Environment=LESAVKA_ALLOW_GADGET_CYCLE=1"), !SERVER_INSTALL.contains("Environment=LESAVKA_ALLOW_GADGET_CYCLE=1"),

View File

@ -18,11 +18,13 @@ const CLIENT_CAMERA: &str = include_str!(concat!(
#[test] #[test]
fn server_install_defaults_to_hevc_ingress_and_mjpeg_uvc_output() { fn server_install_defaults_to_hevc_ingress_and_mjpeg_uvc_output() {
for marker in [ for marker in [
"INSTALL_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-mjpeg}", "PERSISTED_UVC_CODEC=$(persisted_uvc_value LESAVKA_UVC_CODEC || true)",
"INSTALL_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-${PERSISTED_UVC_CODEC:-mjpeg}}",
"INSTALL_CAM_CODEC=${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-hevc}}", "INSTALL_CAM_CODEC=${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-hevc}}",
"printf 'LESAVKA_CAM_CODEC=%s\\n' \"${INSTALL_CAM_CODEC}\"", "printf 'LESAVKA_CAM_CODEC=%s\\n' \"${INSTALL_CAM_CODEC}\"",
"printf 'LESAVKA_UVC_CODEC=%s\\n' \"${INSTALL_UVC_CODEC}\"", "printf 'LESAVKA_UVC_CODEC=%s\\n' \"${INSTALL_UVC_CODEC}\"",
"\"LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}\"", "\"LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}\"",
"uvc_env_value LESAVKA_UVC_WIDTH 1280",
] { ] {
assert!( assert!(
SERVER_INSTALL.contains(marker), SERVER_INSTALL.contains(marker),

View File

@ -16,10 +16,8 @@ fn generated_systemd_units_load_runtime_env_files_in_the_right_places() {
for marker in [ for marker in [
"EnvironmentFile=-/etc/lesavka/server.env", "EnvironmentFile=-/etc/lesavka/server.env",
"EnvironmentFile=-/etc/lesavka/uvc.env", "EnvironmentFile=-/etc/lesavka/uvc.env",
"ExecStartPre=/usr/local/bin/lesavka-core.sh --attach",
"ExecStart=/usr/local/bin/lesavka-server", "ExecStart=/usr/local/bin/lesavka-server",
"ExecStart=/usr/local/bin/lesavka-uvc.sh", "ExecStart=/usr/local/bin/lesavka-uvc.sh",
"Wants=lesavka-uvc.service",
"Requires=lesavka-core.service", "Requires=lesavka-core.service",
] { ] {
assert!( assert!(
@ -27,6 +25,15 @@ fn generated_systemd_units_load_runtime_env_files_in_the_right_places() {
"generated units should preserve marker {marker}" "generated units should preserve marker {marker}"
); );
} }
for marker in [
"ExecStartPre=/usr/local/bin/lesavka-core.sh --attach",
"Wants=lesavka-uvc.service",
] {
assert!(
!SERVER_INSTALL.contains(marker),
"server restarts should not implicitly touch gadget/UVC setup through {marker}"
);
}
} }
#[test] #[test]