diff --git a/Cargo.lock b/Cargo.lock index 4b58137..10720b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.14.36" +version = "0.14.37" dependencies = [ "anyhow", "async-stream", @@ -1676,7 +1676,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.14.36" +version = "0.14.37" dependencies = [ "anyhow", "base64", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.14.36" +version = "0.14.37" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 58873ea..ec8934a 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.14.36" +version = "0.14.37" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index a01c6a4..f6e9a00 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.14.36" +version = "0.14.37" edition = "2024" build = "build.rs" diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 43e3178..0d53449 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -134,6 +134,11 @@ list_server_listener_pids() { } | sed '/^$/d' | sort -u } +list_server_bound_inactive_lines() { + local port=$1 + sudo ss -H -B -tn "sport = :$port" 2>/dev/null | sed '/^$/d' || true +} + list_server_listener_inodes_proc() { local port=$1 hex_port hex_port=$(printf '%04X' "$port") @@ -183,6 +188,39 @@ server_listener_presence_detail() { printf 'listener inodes on :%s => %s' "$port" "${inodes[*]}" } +server_bound_inactive_detail() { + local port=$1 + local detail + detail=$(list_server_bound_inactive_lines "$port" | paste -sd ';' -) + if [[ -z $detail ]]; then + printf 'no bound-inactive tcp sockets for :%s' "$port" + return 0 + fi + printf 'bound-inactive tcp sockets on :%s => %s' "$port" "$detail" +} + +destroy_hidden_server_listener() { + local port=$1 + echo "⚠️ TCP :$port is listening without a visible owning PID; asking the kernel to drop the stale socket state." + sudo ss -K state connected "sport = :$port or dport = :$port" >/dev/null 2>&1 || true + sudo ss -K state listening "sport = :$port" >/dev/null 2>&1 || true + + for _ in {1..30}; do + if ! list_server_listener_inodes_proc "$port" | grep -q .; then + if list_server_bound_inactive_lines "$port" | grep -q .; then + echo "❌ TCP :$port stopped listening but remains bound after kernel cleanup; $(server_bound_inactive_detail "$port")." >&2 + return 1 + fi + echo "✅ kernel dropped the hidden TCP :$port socket state." + return 0 + fi + sleep 0.1 + done + + echo "❌ TCP :$port still appears to be listening after kernel socket cleanup; $(server_listener_presence_detail "$port")." >&2 + return 1 +} + clear_stale_server_listener() { local port pid cmdline found=0 unexpected=0 port=$(server_bind_port) || { @@ -208,9 +246,16 @@ clear_stale_server_listener() { fi if [[ "$found" == "0" ]]; then if list_server_listener_inodes_proc "$port" | grep -q .; then + if destroy_hidden_server_listener "$port"; then + return 0 + fi echo "❌ TCP :$port is listening but no owning PID could be identified; $(server_listener_presence_detail "$port")." >&2 return 1 fi + if list_server_bound_inactive_lines "$port" | grep -q .; then + echo "❌ TCP :$port is not listening but remains bound; $(server_bound_inactive_detail "$port")." >&2 + return 1 + fi return 0 fi @@ -246,6 +291,10 @@ clear_stale_server_listener() { echo "❌ TCP :$port still appears to be listening after cleanup; $(server_listener_presence_detail "$port")." >&2 return 1 fi + if list_server_bound_inactive_lines "$port" | grep -q .; then + echo "❌ TCP :$port stopped listening but remains bound after cleanup; $(server_bound_inactive_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 91c7dd8..89aa04c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.14.36" +version = "0.14.37" edition = "2024" autobins = false diff --git a/testing/tests/server_install_script_contract.rs b/testing/tests/server_install_script_contract.rs index 4ea6625..6210520 100644 --- a/testing/tests/server_install_script_contract.rs +++ b/testing/tests/server_install_script_contract.rs @@ -122,10 +122,30 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { SERVER_INSTALL.contains("socket:["), "install script should map listening socket inodes back to owning PIDs" ); + assert!( + SERVER_INSTALL.contains("ss -K state connected"), + "install script should clear hidden connected socket state before retrying the server port" + ); + assert!( + SERVER_INSTALL.contains("ss -K state listening"), + "install script should ask the kernel to drop a hidden listening socket when no PID is visible" + ); + assert!( + SERVER_INSTALL.contains("list_server_bound_inactive_lines"), + "install script should check for sockets that remain bound after listener teardown" + ); + assert!( + SERVER_INSTALL.contains("bound-inactive tcp sockets"), + "install script should explain when a hidden socket stops listening but still blocks the port" + ); 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("kernel dropped the hidden TCP"), + "install script should report when kernel-level stale-socket cleanup succeeds" + ); 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"