diff --git a/Cargo.lock b/Cargo.lock index f0cca73..4b58137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.14.35" +version = "0.14.36" dependencies = [ "anyhow", "async-stream", @@ -1676,7 +1676,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.14.35" +version = "0.14.36" dependencies = [ "anyhow", "base64", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.14.35" +version = "0.14.36" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index dffadec..58873ea 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.14.35" +version = "0.14.36" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 883ff7e..a01c6a4 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.14.35" +version = "0.14.36" edition = "2024" build = "build.rs" diff --git a/scripts/install/server.sh b/scripts/install/server.sh index b7af1cb..43e3178 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -128,7 +128,59 @@ server_bind_port() { list_server_listener_pids() { local port port=$(server_bind_port) || return 1 - sudo lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | sort -u + { + sudo lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true + list_server_listener_pids_proc "$port" + } | sed '/^$/d' | sort -u +} + +list_server_listener_inodes_proc() { + local port=$1 hex_port + hex_port=$(printf '%04X' "$port") + sudo awk -v target="$hex_port" ' + FNR == 1 { next } + $4 == "0A" { + split($2, local, ":") + if (toupper(local[2]) == target) print $10 + } + ' /proc/net/tcp /proc/net/tcp6 2>/dev/null | sed '/^$/d' | sort -u +} + +list_server_listener_pids_proc() { + local port=$1 + local -a inodes=() + mapfile -t inodes < <(list_server_listener_inodes_proc "$port") + if (( ${#inodes[@]} == 0 )); then + return 0 + fi + + sudo bash -s -- "${inodes[@]}" <<'EOF' +set -euo pipefail +inodes=("$@") +shopt -s nullglob +for fd in /proc/[0-9]*/fd/*; do + target=$(readlink "$fd" 2>/dev/null || true) + for inode in "${inodes[@]}"; do + if [[ "$target" == "socket:[$inode]" ]]; then + pid=${fd#/proc/} + pid=${pid%%/*} + printf '%s\n' "$pid" + break + fi + done +done | sort -u +EOF +} + +server_listener_presence_detail() { + local port=$1 + local -a inodes=() + mapfile -t inodes < <(list_server_listener_inodes_proc "$port") + if (( ${#inodes[@]} == 0 )); then + printf 'no /proc listener entries for :%s' "$port" + return 0 + fi + printf 'listener inodes on :%s => %s' "$port" "${inodes[*]}" } clear_stale_server_listener() { @@ -155,6 +207,10 @@ clear_stale_server_listener() { return 1 fi if [[ "$found" == "0" ]]; then + if list_server_listener_inodes_proc "$port" | grep -q .; then + echo "❌ TCP :$port is listening but no owning PID could be identified; $(server_listener_presence_detail "$port")." >&2 + return 1 + fi return 0 fi @@ -186,6 +242,11 @@ clear_stale_server_listener() { sleep 0.1 done + if list_server_listener_inodes_proc "$port" | grep -q .; then + echo "❌ TCP :$port still appears to be listening after cleanup; $(server_listener_presence_detail "$port")." >&2 + return 1 + fi + echo "❌ lesavka-server listener on :$port survived cleanup; refusing to start a duplicate server." >&2 return 1 } diff --git a/server/Cargo.toml b/server/Cargo.toml index 04f0db0..91c7dd8 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.14.35" +version = "0.14.36" edition = "2024" autobins = false diff --git a/testing/tests/server_install_script_contract.rs b/testing/tests/server_install_script_contract.rs index 8cde308..4ea6625 100644 --- a/testing/tests/server_install_script_contract.rs +++ b/testing/tests/server_install_script_contract.rs @@ -114,10 +114,22 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { SERVER_INSTALL.contains("lsof -tiTCP:"), "install script should inspect the bind port before starting a fresh server" ); + assert!( + SERVER_INSTALL.contains("/proc/net/tcp"), + "install script should fall back to procfs listener detection when lsof misses the owner" + ); + assert!( + SERVER_INSTALL.contains("socket:["), + "install script should map listening socket inodes back to owning PIDs" + ); 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("no owning PID could be identified"), + "install script should fail loud when a port is listening but procfs cannot identify the owner" + ); assert!( SERVER_INSTALL.contains("validate_server_ready"), "install script should verify that lesavka-server reaches a running state"