#!/usr/bin/env bash # scripts/install/server.sh - install and setup all server related apps and environments set -euo pipefail ORIG_USER=${SUDO_USER:-$(id -un)} SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) SCRIPT_REPO_ROOT=$(cd -- "$SCRIPT_DIR/../.." && pwd) DEFAULT_REPO_URL=ssh://git@scm.bstein.dev:2242/bstein/lesavka.git export TMPDIR=${TMPDIR:-/var/tmp} REF=${LESAVKA_REF:-master} # fallback REPO_URL=${LESAVKA_REPO_URL:-} USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6) INSTALL_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-mjpeg} manifest_package_version() { local manifest=$1 [[ -f $manifest ]] || return 1 awk -F'"' ' $0 ~ /^\[package\]/ { in_package=1; next } in_package && $0 ~ /^\[/ { exit } in_package && $0 ~ /^[[:space:]]*version[[:space:]]*=/ { print $2; exit } ' "$manifest" } render_uvc_env_file() { cat </dev/null | head -n1 || true) if [[ -n $ctrl && -e "$by_path_root/platform-$ctrl-video-index0" ]]; then printf '%s\n' "$by_path_root/platform-$ctrl-video-index0" return 0 fi local candidate shopt -s nullglob for candidate in "$by_path_root"/platform-*-video-index0; do printf '%s\n' "$candidate" shopt -u nullglob return 0 done shopt -u nullglob return 1 } wait_for_uvc_output_node() { local node="" for _ in {1..50}; do if node=$(find_uvc_output_node); then printf '%s\n' "$node" return 0 fi sleep 0.1 done return 1 } validate_uvc_gadget_ready() { if [[ -n ${LESAVKA_DISABLE_UVC:-} ]]; then return 0 fi if [[ ! -d /sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 ]]; then echo "❌ UVC gadget function is missing after rebuild; refusing to continue with a half-applied install." >&2 return 1 fi local node="" if ! node=$(wait_for_uvc_output_node); then echo "❌ UVC gadget video-output node did not appear after rebuild; refusing to continue." >&2 return 1 fi echo "✅ UVC gadget output ready at ${node}" } uvc_gadget_present() { [[ -d /sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 ]] } udc_state() { local udc="" udc=$(ls /sys/class/udc 2>/dev/null | head -n1 || true) if [[ -z $udc ]]; then echo "unknown" return 0 fi cat "/sys/class/udc/$udc/state" 2>/dev/null || echo "unknown" } is_attached_state() { case "$1" in configured|addressed|default|suspended|unknown) return 0 ;; esac return 1 } server_bind_addr() { printf '%s\n' "${LESAVKA_SERVER_BIND_ADDR:-0.0.0.0:50051}" } server_bind_port() { local bind_addr port bind_addr=$(server_bind_addr) port=${bind_addr##*:} [[ $port =~ ^[0-9]+$ ]] || return 1 printf '%s\n' "$port" } list_server_listener_pids() { local port port=$(server_bind_port) || return 1 sudo lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | sort -u } clear_stale_server_listener() { local port pid cmdline found=0 unexpected=0 port=$(server_bind_port) || { echo "⚠️ could not parse LESAVKA_SERVER_BIND_ADDR='$(server_bind_addr)'; skipping stale-listener cleanup." return 0 } while read -r pid; do [[ -n $pid ]] || continue found=1 cmdline=$(sudo ps -o args= -p "$pid" 2>/dev/null || true) if [[ $cmdline == *"/usr/local/bin/lesavka-server"* ]] || [[ $cmdline == *"lesavka-server"* ]]; then echo "⚠️ clearing stale lesavka-server listener on :$port (pid $pid)." sudo kill -TERM "$pid" 2>/dev/null || true else echo "❌ TCP :$port is already owned by an unexpected process (pid $pid): ${cmdline:-}." >&2 unexpected=1 fi done < <(list_server_listener_pids || true) if [[ "$unexpected" != "0" ]]; then return 1 fi if [[ "$found" == "0" ]]; then return 0 fi for _ in {1..30}; do if ! list_server_listener_pids | grep -q .; then echo "✅ cleared stale lesavka-server listeners on :$port." return 0 fi sleep 0.1 done while read -r pid; do [[ -n $pid ]] || continue cmdline=$(sudo ps -o args= -p "$pid" 2>/dev/null || true) if [[ $cmdline == *"/usr/local/bin/lesavka-server"* ]] || [[ $cmdline == *"lesavka-server"* ]]; then echo "⚠️ stale lesavka-server listener on :$port survived SIGTERM; sending SIGKILL to pid $pid." sudo kill -KILL "$pid" 2>/dev/null || true else echo "❌ TCP :$port remained busy after cleanup and is owned by an unexpected process (pid $pid): ${cmdline:-}." >&2 return 1 fi done < <(list_server_listener_pids || true) for _ in {1..30}; do if ! list_server_listener_pids | grep -q .; then echo "✅ cleared stale lesavka-server listeners on :$port." return 0 fi sleep 0.1 done echo "❌ lesavka-server listener on :$port survived cleanup; refusing to start a duplicate server." >&2 return 1 } wait_for_unit_running() { local unit=$1 for _ in {1..50}; do if systemctl is-active --quiet "$unit"; then if [[ $(systemctl show "$unit" -p SubState --value 2>/dev/null || true) == "running" ]]; then return 0 fi fi sleep 0.2 done return 1 } validate_server_ready() { local bind_addr bind_addr=$(server_bind_addr) if wait_for_unit_running lesavka-server; then echo "✅ lesavka-server is active and running on ${bind_addr}." return 0 fi echo "❌ lesavka-server failed to reach active/running state on ${bind_addr}." >&2 sudo systemctl status lesavka-server --no-pager >&2 || true if [[ -s /tmp/lesavka-server.stderr ]]; then echo "---- /tmp/lesavka-server.stderr (tail) ----" >&2 sudo tail -n 40 /tmp/lesavka-server.stderr >&2 || true echo "------------------------------------------" >&2 fi return 1 } normalize_hdmi_connector() { local name="$1" if [[ $name =~ (HDMI-A-[0-9]+)$ ]]; then printf '%s\n' "${BASH_REMATCH[1]}" else printf '%s\n' "$name" fi } read_existing_hdmi_connector() { local line value line=$(grep -E '^LESAVKA_HDMI_CONNECTOR=' /etc/lesavka/server.env 2>/dev/null | tail -n1 || true) [[ -n $line ]] || return 0 value=${line#*=} value=${value%\"} value=${value#\"} value=${value%\'} value=${value#\'} normalize_hdmi_connector "$value" } boot_config_path() { for path in /boot/config.txt /boot/firmware/config.txt; do if [[ -e $path ]]; then printf '%s\n' "$path" return 0 fi done return 0 } boot_cmdline_path() { for path in /boot/cmdline.txt /boot/firmware/cmdline.txt; do if [[ -e $path ]]; then printf '%s\n' "$path" return 0 fi done return 0 } runtime_forced_hdmi_connectors() { [[ -r /proc/cmdline ]] || return 0 tr ' ' '\n' "$tmp" sudo install -m 0644 "$tmp" "$cfg" rm -f "$tmp" } write_hdmi_cmdline_force() { local connector="${1:-}" cmd token tmp line cmd=$(boot_cmdline_path) [[ -n $cmd ]] || return 0 tmp=$(mktemp) line=$(cat "$cmd") for token in $line; do case "$token" in video=HDMI-A-*:1920x1080@60e) continue ;; esac printf '%s ' "$token" >>"$tmp" done if [[ -n $connector ]]; then printf 'video=%s:1920x1080@60e ' "$connector" >>"$tmp" fi sed -i 's/[[:space:]]*$//' "$tmp" printf '\n' >>"$tmp" sudo install -m 0644 "$tmp" "$cmd" rm -f "$tmp" } clear_lesavka_hdmi_boot_force() { remove_lesavka_hdmi_config_force write_hdmi_cmdline_force "" } write_hdmi_boot_force() { local connector="$1" idx cfg connector=$(normalize_hdmi_connector "$connector") if [[ ! $connector =~ ^HDMI-A-([0-9]+)$ ]]; then echo "⚠️ cannot write HDMI boot force for unexpected connector '$connector'." >&2 return 0 fi idx=$((BASH_REMATCH[1] - 1)) cfg=$(boot_config_path) remove_lesavka_hdmi_config_force if [[ -n $cfg ]]; then { echo echo "# lesavka: force HDMI mode for capture dongle ($connector)" printf 'hdmi_force_hotplug:%s=1\n' "$idx" printf 'hdmi_group:%s=2\n' "$idx" printf 'hdmi_mode:%s=82\n' "$idx" } | sudo tee -a "$cfg" >/dev/null fi write_hdmi_cmdline_force "$connector" } prepare_hdmi_detection_boot_state() { local -a forced=() if [[ -n ${LESAVKA_HDMI_CONNECTOR:-} ]]; then return 0 fi mapfile -t forced < <(runtime_forced_hdmi_connectors) if (( ${#forced[@]} <= 1 )); then return 0 fi echo "⚠️ current boot forces multiple HDMI connectors: ${forced[*]}" >&2 echo " Linux will report every forced connector as connected, so auto-detection cannot be trusted yet." >&2 clear_lesavka_hdmi_boot_force echo "✅ removed Lesavka-managed dual-HDMI force settings from boot config." >&2 echo " Reboot Theia, then rerun this installer so it can see the real connected HDMI port." >&2 echo " If both ports are physically connected after reboot, rerun once with LESAVKA_HDMI_CONNECTOR=HDMI-A-N." >&2 exit 1 } describe_hdmi_connectors() { local dev name status id for dev in /sys/class/drm/card*-HDMI-A-*; do [[ -e $dev/status ]] || continue name=$(basename "$dev") status=$(cat "$dev/status" 2>/dev/null || true) id=$(cat "$dev/connector_id" 2>/dev/null || true) printf ' %s (%s): %s\n' "$(normalize_hdmi_connector "$name")" "${id:-no-id}" "${status:-unknown}" >&2 done } detect_connected_hdmi_connector() { local dev name stable_name status score local best="" best_score=0 best_count=0 declare -A scores=() # HDMI status can briefly flap during gadget/display bring-up, so sample a # few times. Store the logical connector suffix, not the DRM card prefix, so # this stays valid if Linux renumbers cardN across boots. for _ in 1 2 3 4 5; do for dev in /sys/class/drm/card*-HDMI-A-*; do [[ -e $dev/status ]] || continue name=$(basename "$dev") stable_name=$(normalize_hdmi_connector "$name") status=$(cat "$dev/status" 2>/dev/null || true) if [[ $status == connected ]]; then scores[$stable_name]=$(( ${scores[$stable_name]:-0} + 1 )) fi done sleep 0.2 done for name in "${!scores[@]}"; do score=${scores[$name]} if (( score > best_score )); then best=$name best_score=$score fi done (( best_score > 0 )) || return 0 for name in "${!scores[@]}"; do if (( scores[$name] == best_score )); then best_count=$((best_count + 1)) fi done if (( best_count == 1 )); then printf '%s\n' "$best" return 0 fi echo "❌ multiple HDMI connectors are equally connected; refusing to guess." >&2 describe_hdmi_connectors echo " Disconnect the non-target HDMI output or run once with LESAVKA_HDMI_CONNECTOR=HDMI-A-N." >&2 return 1 } resolve_hdmi_connector() { local detected existing if [[ -n ${LESAVKA_HDMI_CONNECTOR:-} ]]; then normalize_hdmi_connector "$LESAVKA_HDMI_CONNECTOR" return 0 fi if ! detected=$(detect_connected_hdmi_connector); then return 1 fi if [[ -n $detected ]]; then printf '%s\n' "$detected" return 0 fi existing=$(read_existing_hdmi_connector) if [[ -n $existing ]]; then echo "⚠️ no connected HDMI connector detected; preserving existing LESAVKA_HDMI_CONNECTOR=$existing." >&2 printf '%s\n' "$existing" fi } run_as_user() { sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@" } CAPTURE_DISCOVERY_RELAY_PRESENT=0 CAPTURE_DISCOVERY_RELAY_WAS_ACTIVE=0 CAPTURE_DISCOVERY_POWER_BORROWED=0 prepare_capture_power_for_discovery() { if ! systemctl list-unit-files | grep -q '^relay.service'; then return 0 fi CAPTURE_DISCOVERY_RELAY_PRESENT=1 if systemctl is-active --quiet relay.service; then CAPTURE_DISCOVERY_RELAY_WAS_ACTIVE=1 return 0 fi echo " ↪ borrowing relay GPIO power for capture discovery" sudo systemctl start relay.service CAPTURE_DISCOVERY_POWER_BORROWED=1 sudo udevadm settle --timeout=10 || true sleep 2 } restore_capture_power_after_discovery() { if [ "${CAPTURE_DISCOVERY_RELAY_PRESENT:-0}" -eq 0 ]; then return 0 fi if [ "${CAPTURE_DISCOVERY_POWER_BORROWED:-0}" -eq 0 ]; then return 0 fi if [ "${CAPTURE_DISCOVERY_RELAY_WAS_ACTIVE:-0}" -ne 0 ]; then return 0 fi echo " ↪ returning relay GPIO power control to Lesavka auto mode" sudo systemctl stop relay.service || true CAPTURE_DISCOVERY_POWER_BORROWED=0 } while [[ $# -gt 0 ]]; do case $1 in -r|--ref) REF="$2"; shift 2 ;; -h|--help) echo "Usage: $0 [--ref ]"; exit 0 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done echo "==> Using git ref: $REF" mkdir -p "$TMPDIR" if [[ -z $REPO_URL ]] && [[ -d $SCRIPT_REPO_ROOT/.git ]]; then REPO_URL=$(git -C "$SCRIPT_REPO_ROOT" config --get remote.origin.url || true) fi REPO_URL=${REPO_URL:-$DEFAULT_REPO_URL} echo "==> 1a. Base packages" sudo pacman -Sq --needed --noconfirm git \ rustup \ protobuf \ abseil-cpp \ gcc \ alsa-utils \ pipewire \ pipewire-pulse \ tailscale \ base-devel \ v4l-utils \ gstreamer \ gst-plugins-base \ gst-plugins-base-libs \ gst-plugins-good \ gst-plugins-bad \ gst-plugins-bad-libs \ gst-plugins-ugly \ gst-libav \ tcpdump \ lsof if ! command -v yay >/dev/null 2>&1; then echo "==> 1b. installing yay from AUR ..." run_as_user env TMPDIR="$TMPDIR" bash -c ' rm -rf "$TMPDIR/yay" && cd "$TMPDIR" && git clone --depth 1 https://aur.archlinux.org/yay.git && cd yay && makepkg -si --noconfirm' fi # yay -S --noconfirm grpcurl-bin echo "==> 1c. GPIO permissions for relay" echo 'z /dev/gpiochip* 0660 root gpio -' | sudo tee /etc/tmpfiles.d/gpiochip.conf >/dev/null sudo systemd-tmpfiles --create /etc/tmpfiles.d/gpiochip.conf || true echo "==> 1d. Audio permissions for diagnostics" if getent group audio >/dev/null 2>&1 && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then sudo usermod -aG audio "${SUDO_USER}" || true fi echo "==> 2a. Kernel-driver tweaks" cat <<'EOF' | sudo tee /etc/modprobe.d/gc311-stream.conf >/dev/null options uvcvideo quirks=0x200 timeout=10000 EOF prepare_hdmi_detection_boot_state echo "==> 2b. Predictable /dev names for each capture card" prepare_capture_power_for_discovery trap restore_capture_power_after_discovery EXIT discover_gc_capture_pairs() { for dev in /dev/video*; do props=$(sudo udevadm info -q property -n "$dev" 2>/dev/null || true) index=$(cat "/sys/class/video4linux/$(basename "$dev")/index" 2>/dev/null || true) if echo "$props" | grep -q 'ID_VENDOR_ID=07ca' \ && echo "$props" | grep -q 'ID_MODEL_ID=3311' \ && [ "$index" = "0" ]; then tag=$(printf '%s\n' "$props" | awk -F= '/^ID_PATH_TAG=/{print $2}') if [ -n "$tag" ]; then printf '%s %s\n' "$tag" "$dev" fi fi done | sort -u } mapfile -t GC_CAPTURE_PAIRS < <(discover_gc_capture_pairs) if [ "${#GC_CAPTURE_PAIRS[@]}" -ne 2 ]; then for _ in {1..20}; do sleep 0.5 mapfile -t GC_CAPTURE_PAIRS < <(discover_gc_capture_pairs) [ "${#GC_CAPTURE_PAIRS[@]}" -eq 2 ] && break done fi if [ "${#GC_CAPTURE_PAIRS[@]}" -ne 2 ]; then echo "⚠️ GC311 capture cards not fully present; skipping udev eye-link refresh." >&2 if [ "${#GC_CAPTURE_PAIRS[@]}" -eq 0 ]; then echo " Detected: none" >&2 else printf ' Detected: %s\n' "${GC_CAPTURE_PAIRS[@]}" >&2 fi echo " The server install will continue, and existing /dev/lesavka_* links stay untouched." >&2 else LEFT_TAG=${GC_CAPTURE_PAIRS[0]%% *} LEFT_DEV=${GC_CAPTURE_PAIRS[0]#* } RIGHT_TAG=${GC_CAPTURE_PAIRS[1]%% *} RIGHT_DEV=${GC_CAPTURE_PAIRS[1]#* } if [ -z "$LEFT_TAG" ] || [ -z "$RIGHT_TAG" ] || [ "$LEFT_TAG" = "$RIGHT_TAG" ]; then echo "⚠️ GC311 cards were detected, but the capture path tags are incomplete or duplicated." >&2 printf ' Left candidate: %s\n' "${GC_CAPTURE_PAIRS[0]:-missing}" >&2 printf ' Right candidate: %s\n' "${GC_CAPTURE_PAIRS[1]:-missing}" >&2 echo " Skipping udev eye-link refresh and preserving any existing /dev/lesavka_* links." >&2 else printf ' ↪ Left card: %s (%s)\n' "$LEFT_DEV" "$LEFT_TAG" printf ' ↪ Right card: %s (%s)\n' "$RIGHT_DEV" "$RIGHT_TAG" sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null < 3. Rust toolchain" sudo rustup default stable run_as_user rustup default stable echo "==> 4a. Source checkout" SRC_DIR=/var/src/lesavka if [[ ! -d $SRC_DIR ]]; then sudo mkdir -p /var/src sudo chown "$ORIG_USER":"$ORIG_USER" /var/src fi if [[ -d $SRC_DIR/.git ]]; then run_as_user git -C "$SRC_DIR" fetch --all --tags --prune else run_as_user git clone "$REPO_URL" "$SRC_DIR" fi if run_as_user git -C "$SRC_DIR" rev-parse --verify --quiet "origin/$REF" >/dev/null; then run_as_user git -C "$SRC_DIR" checkout -B "$REF" "origin/$REF" else run_as_user git -C "$SRC_DIR" checkout --force "$REF" fi echo "==> 4b. Kernel upgrade (optional)" if [[ "${LESAVKA_KERNEL_UPDATE:-0}" != "0" ]]; then sudo LESAVKA_KERNEL_BUILD_USER="$ORIG_USER" bash "$SRC_DIR/scripts/kernel/build-linux-rpi.sh" else echo "⚠️ skipping kernel upgrade (LESAVKA_KERNEL_UPDATE=0)" fi echo "==> 4c. Source build" run_as_user env TMPDIR="$TMPDIR" bash -c "cd '$SRC_DIR/server' && cargo clean && cargo build --release --bins" echo "==> 5. Install binaries" sudo install -Dm755 "$SRC_DIR/target/release/lesavka-server" /usr/local/bin/lesavka-server sudo install -Dm755 "$SRC_DIR/target/release/lesavka-uvc" /usr/local/bin/lesavka-uvc sudo install -Dm755 "$SRC_DIR/scripts/daemon/lesavka-core.sh" /usr/local/bin/lesavka-core.sh sudo install -Dm755 "$SRC_DIR/scripts/daemon/lesavka-uvc.sh" /usr/local/bin/lesavka-uvc.sh sudo install -Dm755 "$SRC_DIR/scripts/manual/run_uac_output_sanity.sh" /usr/local/bin/lesavka-uac-sanity echo "==> 5b. Runtime environment defaults" sudo install -d -m 0755 /etc/lesavka HDMI_CONNECTOR=$(resolve_hdmi_connector) if [[ -n $HDMI_CONNECTOR ]]; then echo " ↪ HDMI connector: $HDMI_CONNECTOR" write_hdmi_boot_force "$HDMI_CONNECTOR" else echo "⚠️ no connected HDMI connector detected; leaving LESAVKA_HDMI_CONNECTOR unset." >&2 fi { echo "# generated by lesavka/scripts/install/server.sh" echo "# Edit only for local hardware overrides; rerunning the installer refreshes defaults." if [[ -n $HDMI_CONNECTOR ]]; then printf 'LESAVKA_HDMI_CONNECTOR=%s\n' "$HDMI_CONNECTOR" fi printf 'LESAVKA_CAM_OUTPUT=%s\n' "${LESAVKA_INSTALL_CAM_OUTPUT:-uvc}" printf 'LESAVKA_CAM_WIDTH=%s\n' "${LESAVKA_CAM_WIDTH:-1920}" printf 'LESAVKA_CAM_HEIGHT=%s\n' "${LESAVKA_CAM_HEIGHT:-1080}" printf 'LESAVKA_CAM_FPS=%s\n' "${LESAVKA_CAM_FPS:-30}" printf 'LESAVKA_HDMI_WIDTH=%s\n' "${LESAVKA_HDMI_WIDTH:-1920}" printf 'LESAVKA_HDMI_HEIGHT=%s\n' "${LESAVKA_HDMI_HEIGHT:-1080}" printf 'LESAVKA_HDMI_SINK=%s\n' "${LESAVKA_HDMI_SINK:-fbdevsink}" printf 'LESAVKA_HDMI_FBDEV=%s\n' "${LESAVKA_HDMI_FBDEV:-/dev/fb0}" printf 'LESAVKA_HDMI_DRIVER=%s\n' "${LESAVKA_HDMI_DRIVER:-vc4}" printf 'LESAVKA_HDMI_PRESENTATION_DELAY_US=%s\n' "${LESAVKA_HDMI_PRESENTATION_DELAY_US:-180000}" printf 'LESAVKA_UAC_DEV=%s\n' "${LESAVKA_UAC_DEV:-hw:UAC2Gadget,0}" printf 'LESAVKA_ALSA_DEV=%s\n' "${LESAVKA_ALSA_DEV:-hw:UAC2Gadget,0}" printf 'LESAVKA_UAC_HDMI_COMPENSATION_US=%s\n' "${LESAVKA_UAC_HDMI_COMPENSATION_US:-205000}" printf 'LESAVKA_UAC_SESSION_CLOCK_ALIGN=%s\n' "${LESAVKA_UAC_SESSION_CLOCK_ALIGN:-0}" printf 'LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS=%s\n' "${LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS:-1000}" printf 'LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-0}" printf 'LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-0}" printf 'LESAVKA_UPSTREAM_PAIR_SLACK_US=%s\n' "${LESAVKA_UPSTREAM_PAIR_SLACK_US:-20000}" printf 'LESAVKA_UPSTREAM_STALE_DROP_MS=%s\n' "${LESAVKA_UPSTREAM_STALE_DROP_MS:-80}" printf 'LESAVKA_UVC_CODEC=%s\n' "${INSTALL_UVC_CODEC}" } | sudo tee /etc/lesavka/server.env >/dev/null UVC_ENV_TMP=$(mktemp) render_uvc_env_file >"$UVC_ENV_TMP" 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 sudo install -m 0644 "$UVC_ENV_TMP" /etc/lesavka/uvc.env rm -f "$UVC_ENV_TMP" echo "==> 6a. Systemd units - lesavka-core" cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-core.service >/dev/null [Unit] Description=lesavka USB gadget bring-up After=sys-kernel-config.mount Requires=sys-kernel-config.mount [Service] Type=oneshot ExecStart=/usr/local/bin/lesavka-core.sh RemainAfterExit=yes Environment=LESAVKA_UVC_FALLBACK=0 EnvironmentFile=-/etc/lesavka/server.env CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE AmbientCapabilities=CAP_SYS_MODULE MountFlags=slave [Install] WantedBy=multi-user.target UNIT echo "==> 6b. Systemd units - lesavka-server" cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-server.service >/dev/null [Unit] Description=lesavka gRPC relay After=network.target lesavka-core.service StartLimitIntervalSec=30 StartLimitBurst=10 [Service] ExecStartPre=/usr/local/bin/lesavka-core.sh --attach ExecStart=/usr/local/bin/lesavka-server TimeoutStopSec=10 KillSignal=SIGTERM KillMode=control-group Restart=always Environment=RUST_LOG=lesavka_server=info,lesavka_server::audio=info,lesavka_server::video=debug,lesavka_server::gadget=info Environment=RUST_BACKTRACE=1 Environment=GST_DEBUG="*:2,alsasink:6,alsasrc:6" Environment=LESAVKA_UVC_EXTERNAL=1 Environment=LESAVKA_EYE_ADAPTIVE=1 Environment=LESAVKA_EYE_MIN_FPS=12 Environment=LESAVKA_EYE_FPS=20 Environment=LESAVKA_MIC_INIT_ATTEMPTS=5 Environment=LESAVKA_MIC_INIT_DELAY_MS=250 Environment=LESAVKA_ALLOW_GADGET_CYCLE=1 EnvironmentFile=-/etc/lesavka/uvc.env EnvironmentFile=-/etc/lesavka/server.env Restart=always RestartSec=5 StandardError=append:/tmp/lesavka-server.stderr User=root [Install] WantedBy=multi-user.target UNIT echo "==> 6c. Systemd units - initialization" sudo truncate -s 0 /tmp/lesavka-server.log sudo systemctl daemon-reload sudo systemctl enable lesavka-core lesavka-server UDC_STATE=$(udc_state) FORCE_GADGET_REBUILD=0 if [[ -z ${LESAVKA_DISABLE_UVC:-} ]] && ! uvc_gadget_present; then FORCE_GADGET_REBUILD=1 echo "⚠️ UVC function is missing from the live gadget; forcing a rebuild before server start." fi if [[ "$UVC_ENV_CHANGED" == "1" ]] && is_attached_state "$UDC_STATE"; then FORCE_GADGET_REBUILD=1 echo "⚠️ UVC runtime settings changed while the host is attached; forcing a gadget rebuild so the new descriptors take effect." fi if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || [[ "$FORCE_GADGET_REBUILD" == "1" ]] || ! is_attached_state "$UDC_STATE"; then echo "⚠️ UDC state is '$UDC_STATE' - forcing a Lesavka gadget rebuild before server start." sudo env \ LESAVKA_ALLOW_GADGET_RESET=1 \ LESAVKA_ATTACH_WRITE_UDC=1 \ LESAVKA_DETACH_CLEAR_UDC=1 \ LESAVKA_RELOAD_UVCVIDEO=1 \ LESAVKA_UVC_FALLBACK=0 \ LESAVKA_UVC_CODEC="${INSTALL_UVC_CODEC}" \ /usr/local/bin/lesavka-core.sh 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." fi cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-uvc.service >/dev/null [Unit] Description=lesavka UVC control helper After=lesavka-core.service Requires=lesavka-core.service [Service] ExecStart=/usr/local/bin/lesavka-uvc.sh Restart=always RestartSec=2 KillSignal=SIGTERM KillMode=control-group TimeoutStopSec=10 StandardError=append:/tmp/lesavka-uvc.stderr User=root EnvironmentFile=-/etc/lesavka/uvc.env [Install] WantedBy=multi-user.target UNIT sudo systemctl daemon-reload sudo systemctl enable lesavka-uvc echo "==> 6d. Systemd units - remove legacy reboot watchdog" sudo systemctl stop lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true sudo systemctl disable lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true sudo systemctl unmask lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true sudo rm -f /etc/systemd/system/lesavka-watchdog.timer \ /etc/systemd/system/lesavka-watchdog.service \ /usr/local/bin/lesavka-watchdog.sh \ /etc/lesavka/watchdog.touch sudo systemctl daemon-reload if [[ "$UVC_ENV_CHANGED" == "1" ]] && systemctl is-active --quiet lesavka-uvc; then sudo systemctl restart lesavka-uvc echo "✅ lesavka-uvc restarted with the refreshed UVC runtime settings." elif systemctl is-active --quiet lesavka-uvc; then echo "✅ lesavka-uvc already active; runtime settings unchanged." else echo "⚠️ lesavka-uvc is not active; start via lesavka-core dependency path." fi validate_uvc_gadget_ready sudo truncate -s 0 /tmp/lesavka-server.stderr sudo systemctl stop lesavka-server >/dev/null 2>&1 || true clear_stale_server_listener sudo systemctl reset-failed lesavka-server >/dev/null 2>&1 || true sudo systemctl restart lesavka-server validate_server_ready 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) PERSISTED_CAM_OUTPUT=$(grep '^LESAVKA_CAM_OUTPUT=' /etc/lesavka/server.env 2>/dev/null | tail -n1 | cut -d= -f2- || true) PERSISTED_UVC_CODEC=$(grep '^LESAVKA_UVC_CODEC=' /etc/lesavka/uvc.env 2>/dev/null | tail -n1 | cut -d= -f2- || true) echo "✅ lesavka-server installed and restarted..." if [[ -n $INSTALLED_VERSION || -n $INSTALLED_SHA ]]; then echo "➡️ Installed: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}" fi if [[ -n $PERSISTED_CAM_OUTPUT ]]; then echo "➡️ Camera output: ${PERSISTED_CAM_OUTPUT}" fi if [[ -n $PERSISTED_UVC_CODEC ]]; then echo "➡️ UVC codec: ${PERSISTED_UVC_CODEC}" fi echo "➡️ Status: sudo systemctl status lesavka-server --no-pager" echo "➡️ Logs: sudo journalctl -u lesavka-server -f --no-pager" echo "✅ Installed version: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"