lesavka/scripts/install/server.sh

319 lines
11 KiB
Bash
Executable File

#!/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)
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
}
run_as_user() {
sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@"
}
while [[ $# -gt 0 ]]; do
case $1 in
-r|--ref) REF="$2"; shift 2 ;;
-h|--help)
echo "Usage: $0 [--ref <branch|commit>]"; 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 "==> 2a. Kernel-driver tweaks"
cat <<'EOF' | sudo tee /etc/modprobe.d/gc311-stream.conf >/dev/null
options uvcvideo quirks=0x200 timeout=10000
EOF
echo "==> 2b. Predictable /dev names for each capture card"
# ensure relay (GPIO power) is on if present
if systemctl list-unit-files | grep -q '^relay.service'; then
sudo systemctl enable --now relay.service
sleep 2
fi
# probe v4l2 devices for GC311s (07ca:3311)
mapfile -t GC_VIDEOS < <(
sudo v4l2-ctl --list-devices 2>/dev/null |
awk '/Live Gamer MINI/{getline; print $1}'
)
# fallback via udev if v4l2-ctl output is empty/partial
if [ "${#GC_VIDEOS[@]}" -ne 2 ]; then
mapfile -t GC_VIDEOS < <(
for dev in /dev/video*; do
props=$(sudo udevadm info -q property -n "$dev" 2>/dev/null || true)
if echo "$props" | grep -q 'ID_VENDOR_ID=07ca' && echo "$props" | grep -q 'ID_MODEL_ID=3311'; then
echo "$dev"
fi
done | sort -u
)
fi
if [ "${#GC_VIDEOS[@]}" -ne 2 ]; then
echo "⚠️ GC311 capture cards not fully present; skipping udev eye-link refresh." >&2
if [ "${#GC_VIDEOS[@]}" -eq 0 ]; then
echo " Detected: none" >&2
else
printf ' Detected: %s\n' "${GC_VIDEOS[@]}" >&2
fi
echo " The server install will continue, and existing /dev/lesavka_* links stay untouched." >&2
else
mapfile -t TAGS < <(
for v in "${GC_VIDEOS[@]}"; do
sudo udevadm info -q property -n "$v" |
awk -F= '/^ID_PATH_TAG=/{print $2}'
done
)
if [ -z "${TAGS[0]:-}" ] || [ -z "${TAGS[1]:-}" ]; then
echo "⚠️ GC311 cards were detected, but ID_PATH_TAG lookup was incomplete." >&2
echo " Skipping udev eye-link refresh and preserving any existing /dev/lesavka_* links." >&2
else
printf ' ↪ Left card: %s (%s)\n' "${GC_VIDEOS[0]}" "${TAGS[0]}"
printf ' ↪ Right card: %s (%s)\n' "${GC_VIDEOS[1]}" "${TAGS[1]}"
LEFT_TAG=${TAGS[0]}
RIGHT_TAG=${TAGS[1]}
sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null <<EOF
# auto-generated by lesavka/scripts/daemon/install-server.sh - DO NOT EDIT
SUBSYSTEM=="video4linux", ATTR{index}=="0", ENV{ID_PATH_TAG}=="$LEFT_TAG", SYMLINK+="lesavka_l_eye"
SUBSYSTEM=="video4linux", ATTR{index}=="0", ENV{ID_PATH_TAG}=="$RIGHT_TAG", SYMLINK+="lesavka_r_eye"
EOF
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=video4linux
sudo udevadm settle
fi
fi
echo "==> 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
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
Environment=LESAVKA_UVC_CODEC=mjpeg
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=process
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_CODEC=mjpeg
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
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)
if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || ! 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=1 \
LESAVKA_UVC_CODEC=mjpeg \
/usr/local/bin/lesavka-core.sh
sudo systemctl restart lesavka-core
echo "✅ lesavka-core installed and restarted..."
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
RefuseManualStop=yes
[Service]
ExecStart=/usr/local/bin/lesavka-uvc.sh
Restart=always
RestartSec=2
KillSignal=SIGTERM
KillMode=process
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 systemctl is-active --quiet lesavka-uvc; then
echo "✅ lesavka-uvc is active (dependency-managed; manual restart disabled)."
else
echo "⚠️ lesavka-uvc is not active; start via lesavka-core dependency path."
fi
sudo systemctl restart lesavka-server
echo "✅ lesavka-server installed and restarted..."
echo "➡️ Status: sudo systemctl status lesavka-server --no-pager"
echo "➡️ Logs: sudo journalctl -u lesavka-server -f --no-pager"