diff --git a/Cargo.lock b/Cargo.lock index f93ee70..d8c2b4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.14.32" +version = "0.14.33" dependencies = [ "anyhow", "async-stream", @@ -1676,7 +1676,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.14.32" +version = "0.14.33" dependencies = [ "anyhow", "base64", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.14.32" +version = "0.14.33" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 2cf8cc5..2111a71 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.14.32" +version = "0.14.33" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 4f2c34a..3f9b957 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.14.32" +version = "0.14.33" edition = "2024" build = "build.rs" diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 7e9e0ba..07455ac 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -113,6 +113,114 @@ is_attached_state() { 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 @@ -719,7 +827,12 @@ 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) diff --git a/server/Cargo.toml b/server/Cargo.toml index aede407..22c0d2b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.14.32" +version = "0.14.33" edition = "2024" autobins = false diff --git a/testing/tests/server_install_script_contract.rs b/testing/tests/server_install_script_contract.rs index cb7e14c..cb4d18c 100644 --- a/testing/tests/server_install_script_contract.rs +++ b/testing/tests/server_install_script_contract.rs @@ -98,6 +98,26 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { SERVER_INSTALL.contains("lesavka-uvc already active; runtime settings unchanged."), "install script should avoid unnecessary UVC restarts when nothing changed" ); + assert!( + SERVER_INSTALL.contains("clear_stale_server_listener"), + "install script should clear stale server listeners before restart" + ); + assert!( + SERVER_INSTALL.contains("lsof -tiTCP:"), + "install script should inspect the bind port before starting a fresh server" + ); + assert!( + SERVER_INSTALL.contains("unexpected process"), + "install script should fail loud instead of killing an unrelated process on the server port" + ); + assert!( + SERVER_INSTALL.contains("validate_server_ready"), + "install script should verify that lesavka-server reaches a running state" + ); + assert!( + SERVER_INSTALL.contains("failed to reach active/running state"), + "install script should explain server startup failures instead of claiming success" + ); } #[test]