imbajin commented on code in PR #3105:
URL: https://github.com/apache/hugegraph/pull/3105#discussion_r3650486632


##########
hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh:
##########
@@ -79,15 +79,245 @@ function process_id() {
     return "$pid"
 }
 
-# check the port of rest server is occupied
+# Run a command with a hard deadline via background watchdog.
+# Returns the command's exit code if it finishes in time.
+# If the deadline expires, the command is killed (exit code reflects signal).
+# Works without the timeout command — uses sleep + kill -9 pattern.
+function run_with_deadline() {
+    local cmd="$1"
+    local deadline="$2"
+    shift 2
+
+    bash -c "$cmd" bash "$@" &
+    local child_pid=$!
+    (
+        sleep "$deadline"
+        kill -9 "$child_pid" 2>/dev/null
+    ) 2>/dev/null &
+    local watchdog_pid=$!
+
+    wait "$child_pid" 2>/dev/null
+    local rc=$?
+    kill -9 "$watchdog_pid" 2>/dev/null || true
+    wait "$watchdog_pid" 2>/dev/null || true
+    return $rc
+}
+
+# check whether the REST server port is occupied
 function check_port() {
-    local port=$(echo "$1" | sed 's|.*:||' | sed 's|/.*||')
-    if ! command_available "lsof"; then
-        echo "Required lsof but it is unavailable"
-        exit 1
+    local url="$1"
+    local host
+    local port
+
+    # Strip leading/trailing whitespace from URL (handles whitespace from 
ServerOptions)
+    url="${url#"${url%%[![:space:]]*}"}"
+    url="${url%"${url##*[![:space:]]}"}"
+
+    # Extract authority: strip scheme (http://, https://, or none) and path.
+    # This ensures port is extracted from host:port, not from a colon in the 
path.
+    local authority
+    if [[ "$url" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then
+        authority=$(echo "$url" | sed 's|^[^/]*://||; s|/.*||')
+    else
+        authority=$(echo "$url" | sed 's|/.*||')
     fi
-    lsof -i :"$port" >/dev/null
-    if [ $? -eq 0 ]; then
+
+    # Extract host and port from authority.
+    # Assumes IPv6 is bracketed (e.g. [::1]:8080). Unbracketed IPv6 like
+    # ::1:8080 would misparse as host=::, port=1. ServerOptions does not
+    # enforce bracketing at the config layer; if this ever fires on bad input
+    # the downstream bind would fail with a clearer error than the preflight.
+    if [[ "$authority" =~ ^\[([^\]]*)\]:([0-9]+)$ ]]; then
+        # IPv6 with port: [::1]:8080
+        host="${BASH_REMATCH[1]}"
+        port="${BASH_REMATCH[2]}"
+    elif [[ "$authority" =~ :([0-9]+)$ ]]; then
+        # IPv4 or hostname with port: 127.0.0.1:8080
+        port="${BASH_REMATCH[1]}"
+        host="${authority%:*}"
+    else
+        # No explicit port in authority
+        host="$authority"
+        port=""
+    fi
+
+    # Handle default ports from scheme when no explicit port given
+    if [[ -z "$port" ]]; then
+        # Reject invalid port: authority has exactly one colon 
(host:non-digits).
+        # IPv6 addresses (2+ colons, e.g. [::1] or ::1) are not caught by this.
+        if [[ "$authority" != *:*:* && "$authority" == *:* ]]; then
+            return 0
+        fi
+        if [[ "$url" == https://* ]]; then
+            port="443"
+        elif [[ "$url" == http://* ]]; then
+            port="80"
+        fi
+    fi
+
+    if [[ -z "$port" ]]; then
+        return 0
+    fi
+
+    if ! [[ "$port" =~ ^[0-9]+$ ]]; then
+        return 0
+    fi
+    port=$((10#$port))
+    if (( port < 1 || port > 65535 )); then
+        return 0
+    fi
+
+    # Strip any leading/trailing whitespace from host
+    host="${host#"${host%%[![:space:]]*}"}"
+    host="${host%"${host##*[![:space:]]}"}"
+
+    # Resolve hostname → numeric IPs so ss/netstat (which use -n) can match 
them.
+    # A "hostname" is anything that is not blank, a wildcard, or already 
numeric.
+    # Resolution runs with a deadline to prevent hangs from slow/stuck 
DNS/LDAP.
+    # If timeout is unavailable, skip resolution and fall through to bounded 
/dev/tcp.
+    local resolved_addrs=""
+    local is_hostname=0
+    if [[ -n "$host" && "$host" != "0.0.0.0" && "$host" != "::" && "$host" != 
"*" ]] \
+       && ! [[ "$host" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] \
+       && ! [[ "$host" =~ ^[0-9a-fA-F:]*:[0-9a-fA-F:]*$ ]]; then
+        is_hostname=1
+        if command_available "getent" && command_available "timeout"; then
+            resolved_addrs=$(timeout 2 getent hosts "$host" 2>/dev/null | awk 
'{print $1}')
+        elif command_available "dscacheutil" && command_available "timeout"; 
then
+            resolved_addrs=$(timeout 2 dscacheutil -q host -a name "$host" 
2>/dev/null \
+                             | awk '/ip_address:/{print $2}')
+        fi
+        # If timeout unavailable or resolution times out, resolved_addrs stays 
empty
+        # and we fall through to the bounded /dev/tcp probe (2s watchdog) 
instead.
+    fi
+
+    local linux_pattern
+    local bsd_pattern
+    if [[ -z "$host" || "$host" == "0.0.0.0" || "$host" == "::" || "$host" == 
"*" ]]; then
+        # Wildcard patterns match any address family (IPv4 or IPv6) on the port
+        # Note: This treats IPv4 and IPv6 wildcard listeners as conflicts, 
which is conservative
+        # but may be overly strict for dual-stack systems where they can 
coexist
+        linux_pattern=":${port}([[:space:]]|$)"
+        bsd_pattern="(\.|:)${port}([[:space:]]|$)"
+    else
+        # For hostnames, only use resolved numeric addresses (ss/netstat 
report numeric).
+        # Keep wildcard matching family-aware to avoid cross-family false 
conflicts.
+        local addr_alts=""
+        local match_ipv4=0
+        local match_ipv6=0
+        if [[ $is_hostname -eq 1 ]]; then
+            # Hostname: use resolved addresses and infer address family set
+            while IFS= read -r addr; do
+                [[ -z "$addr" ]] && continue
+                local esc_addr="${addr//./\\.}"
+                if [[ "$addr" == *":"* ]]; then
+                    esc_addr="${esc_addr//:/\\:}"
+                    match_ipv6=1
+                else
+                    match_ipv4=1
+                fi
+                if [[ -z "$addr_alts" ]]; then
+                    addr_alts="${esc_addr}|\[${esc_addr}\]"
+                else
+                    addr_alts="${addr_alts}|${esc_addr}|\[${esc_addr}\]"
+                fi
+            done <<< "$resolved_addrs"
+        else
+            # Numeric address: use it directly and infer address family
+            local esc_host="${host//./\\.}"
+            if [[ "$host" == *":"* ]]; then
+                esc_host="${esc_host//:/\\:}"
+                match_ipv6=1
+            else
+                match_ipv4=1
+            fi
+            if [[ -z "$addr_alts" ]]; then
+                addr_alts="${esc_host}|\[${esc_host}\]"
+            else
+                addr_alts="${addr_alts}|${esc_host}|\[${esc_host}\]"
+            fi
+        fi
+        if [[ $match_ipv4 -eq 1 ]]; then
+            if [[ -z "$addr_alts" ]]; then
+                addr_alts="0\.0\.0\.0|\*"
+            else
+                addr_alts="${addr_alts}|0\.0\.0\.0|\*"
+            fi
+        fi
+        if [[ $match_ipv6 -eq 1 ]]; then
+            if [[ -z "$addr_alts" ]]; then
+                addr_alts="\[::\]|::"
+            else
+                addr_alts="${addr_alts}|\[::\]|::"
+            fi
+        fi
+        # Pattern: addresses followed by colon and port, with whitespace/end 
anchor
+        # (avoids IPv6 hextet false positives without GNU-specific \b)
+        linux_pattern="(${addr_alts}):${port}([[:space:]]|$)"
+        bsd_pattern="(${addr_alts})(\\.|:)${port}([[:space:]]|$)"
+    fi
+
+    local in_use=0
+    local port_checked=0
+    local out=""
+
+    if command_available "ss"; then
+        if out=$(ss -ltn 2>/dev/null); then
+            # Only mark as checked if we can actually match the configured 
endpoint.
+            # For unresolved hostnames, fall through to the /dev/tcp probe 
instead.
+            if [[ $is_hostname -eq 0 || -n "$resolved_addrs" ]]; then
+                port_checked=1
+                if echo "$out" | grep -qE "$linux_pattern"; then
+                    in_use=1
+                fi
+            fi
+        else
+            # ss command failed (tool present but exited non-zero), try next 
tool
+            port_checked=0
+        fi
+    fi
+
+    if [[ $port_checked -eq 0 ]] && command_available "netstat"; then
+        if out=$(netstat -ltn 2>/dev/null) && echo "$out" | grep -qi "listen"; 
then
+            if [[ $is_hostname -eq 0 || -n "$resolved_addrs" ]]; then
+                port_checked=1
+                if echo "$out" | grep -qE "$linux_pattern" || echo "$out" | 
grep -qE "$bsd_pattern"; then
+                    in_use=1
+                fi
+            fi
+        elif out=$(netstat -an 2>/dev/null) && [[ -n "$out" ]]; then
+            if [[ $is_hostname -eq 0 || -n "$resolved_addrs" ]]; then
+                port_checked=1
+                if echo "$out" | grep -i "listen" | grep -qE "$bsd_pattern"; 
then
+                    in_use=1
+                fi
+            fi
+        fi
+    fi
+
+    if [[ $port_checked -eq 0 ]]; then
+        # Wildcard binds include loopback: normalize to loopback for the probe
+        local probe_host="$host"
+        if [[ -z "$probe_host" || "$probe_host" == "0.0.0.0" || "$probe_host" 
== "*" ]]; then
+            probe_host="127.0.0.1"
+        elif [[ "$probe_host" == "::" ]]; then
+            probe_host="::1"
+        fi
+
+        if command_available "timeout"; then
+            if timeout 1 bash -c ': >/dev/tcp/"$1"/"$2"' _ "$probe_host" 
"$port" 2>/dev/null; then
+                # 1-second deadline prevents hangs on dropped SYN packets or 
unresponsive hosts
+                in_use=1
+            fi
+        else
+            # Deadline-bounded probe without the timeout command
+            if run_with_deadline ': >/dev/tcp/"$1"/"$2" 2>/dev/null' 
"$probe_host" "$port" 2; then

Review Comment:
   ‼️ `run_with_deadline()` expects `(cmd, deadline, args...)`, but this call 
passes `(cmd, probe_host, port, 2)`. A controlled exact-head invocation showed 
the child receives `8080|2`, while `sleep "$deadline"` receives the host. On 
systems without `timeout`, the fallback therefore probes the wrong endpoint and 
can report an occupied configured port as free. Please call it as 
`run_with_deadline '...' 2 "$probe_host" "$port"` and cover occupied, refused, 
and deadline cases through the actual `check_port` branch.



##########
hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh:
##########
@@ -0,0 +1,706 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# test-check-port.sh — Unit tests for check_port() in util.sh
+#
+# Strategy: each test runs check_port in a subshell that overrides
+#   command_available() to control which probe branch is taken, and
+#   overrides the tool functions (ss, netstat, timeout) to control
+#   what they return — no real network connections needed.
+#
+# check_port calls `exit 1` when the port is in use, so the subshell
+# exits 1; it returns normally (exit 0) when the port is free.
+#
+# Usage: ./test-check-port.sh [path-to-hugegraph-static-dir]
+#   path-to-hugegraph-static-dir: directory containing bin/util.sh
+#   Defaults to current directory.
+#   In CI: $TRAVIS_DIR/test-check-port.sh 
hugegraph-server/hugegraph-dist/src/assembly/static
+
+set -uo pipefail
+# -u: fail on undefined variables (catches typos in test assertions)
+# -o pipefail: pipeline exit status is the last non-zero component
+# shellcheck disable=SC1090,SC1091  # UTIL_SH / PD_UTIL_SH sourced dynamically 
at runtime
+
+STATIC_DIR="${1:-$(pwd)}"
+UTIL_SH="$STATIC_DIR/bin/util.sh"
+
+REPO_ROOT="$(cd "$(dirname "$0")/../../../../.." && pwd)"
+PD_UTIL_SH="$REPO_ROOT/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh"
+STORE_UTIL_SH="$REPO_ROOT/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh"
+
+PASS=0
+FAIL=0
+ERRORS=()
+
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+pass() { echo -e "${GREEN}  PASS${NC} $1"; PASS=$((PASS + 1)); }
+fail() { echo -e "${RED}  FAIL${NC} $1"; ERRORS+=("$1"); FAIL=$((FAIL + 1)); }
+section() { echo ""; echo "── $1 ──"; }
+
+echo ""
+echo "check_port() unit test suite"
+echo "util.sh: $UTIL_SH"
+echo ""
+
+if [[ ! -f "$UTIL_SH" ]]; then
+    echo -e "${RED}ERROR:${NC} $UTIL_SH not found."
+    echo "       Pass the HugeGraph static assembly dir as \$1"
+    exit 1
+fi
+
+# ── ss branch 
─────────────────────────────────────────────────────────────────
+
+section "ss branch — IPv4"
+
+(
+    # shellcheck source=/dev/null
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*"; }
+    check_port "http://127.0.0.1:8080";
+)
+[[ $? -eq 1 ]] \
+    && pass "ss: IPv4 port occupied → exit 1" \
+    || fail "ss: IPv4 port occupied → expected exit 1, got 0"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*"; }
+    check_port "http://127.0.0.1:8080";
+)
+[[ $? -eq 0 ]] \
+    && pass "ss: IPv4 port free → exit 0" \
+    || fail "ss: IPv4 port free → expected exit 0, got 1"
+
+section "ss branch — IPv6 URL with scheme (http://[::1]:8080)"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 [::]:8080 [::]:*"; }
+    check_port "http://[::1]:8080";
+)
+[[ $? -eq 1 ]] \
+    && pass "ss: http://[::1]:8080 occupied → exit 1" \
+    || fail "ss: http://[::1]:8080 occupied → expected exit 1, got 0"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 [::]:9090 [::]:*"; }
+    check_port "http://[::1]:8080";
+)
+[[ $? -eq 0 ]] \
+    && pass "ss: http://[::1]:8080 free → exit 0" \
+    || fail "ss: http://[::1]:8080 free → expected exit 0, got 1"
+
+section "ss branch — IPv6 URL without scheme ([::1]:8080)"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 [::]:8080 [::]:*"; }
+    check_port "[::1]:8080"
+)
+[[ $? -eq 1 ]] \
+    && pass "ss: [::1]:8080 (no scheme) occupied → exit 1" \
+    || fail "ss: [::1]:8080 (no scheme) occupied → expected exit 1, got 0"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 [::]:9090 [::]:*"; }
+    check_port "[::1]:8080"
+)
+[[ $? -eq 0 ]] \
+    && pass "ss: [::1]:8080 (no scheme) free → exit 0" \
+    || fail "ss: [::1]:8080 (no scheme) free → expected exit 0, got 1"
+
+section "ss branch — wildcard 0.0.0.0"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*"; }
+    check_port "http://0.0.0.0:8080";
+)
+[[ $? -eq 1 ]] \
+    && pass "ss: 0.0.0.0:8080 occupied → exit 1" \
+    || fail "ss: 0.0.0.0:8080 occupied → expected exit 1, got 0"
+
+section "ss branch — wildcard ::"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 [::]:8080 [::]:*"; }
+    check_port "http://[::]:8080";
+)
+[[ $? -eq 1 ]] \
+    && pass "ss: [::]:8080 occupied → exit 1" \
+    || fail "ss: [::]:8080 occupied → expected exit 1, got 0"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 [::]:9090 [::]:*"; }
+    check_port "http://[::]:8080";
+)
+[[ $? -eq 0 ]] \
+    && pass "ss: [::]:8080 free → exit 0" \
+    || fail "ss: [::]:8080 free → expected exit 0, got 1"
+
+# ── netstat branch 
────────────────────────────────────────────────────────────
+
+section "netstat branch — Linux format (-ltn), occupied"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "netstat" ]]; }
+    netstat() { echo "tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN"; }
+    check_port "http://127.0.0.1:8080";
+)
+[[ $? -eq 1 ]] \
+    && pass "netstat -ltn: port 8080 occupied → exit 1" \
+    || fail "netstat -ltn: port 8080 occupied → expected exit 1, got 0"
+
+section "netstat branch — Linux format (-ltn), free"
+
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "netstat" ]]; }
+    netstat() { echo "tcp 0 0 0.0.0.0:9090 0.0.0.0:* LISTEN"; }
+    check_port "http://127.0.0.1:8080";
+)
+[[ $? -eq 0 ]] \
+    && pass "netstat -ltn: port 8080 free → exit 0" \
+    || fail "netstat -ltn: port 8080 free → expected exit 0, got 1"
+
+section "netstat branch — BSD/macOS fallback (-an), occupied"
+
+# Simulate netstat that produces no output for -ltn (Linux flag unsupported)
+# but outputs BSD-format lines for -an
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "netstat" ]]; }
+    netstat() {
+        if [[ "$1" == "-ltn" ]]; then
+            return 1  # flag not supported on BSD
+        fi
+        echo "tcp4 0 0 *.8080 *.* LISTEN"
+    }
+    check_port "http://127.0.0.1:8080";
+)
+[[ $? -eq 1 ]] \
+    && pass "netstat -an BSD: port 8080 occupied → exit 1" \
+    || fail "netstat -an BSD: port 8080 occupied → expected exit 1, got 0"
+
+section "netstat branch — IP octet false-positive guard"
+
+# Port 80 check; netstat output contains 192.168.80.1:443
+# The .80 in the IP address must NOT match port 80
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "netstat" ]]; }
+    netstat() { echo "tcp 0 0 192.168.80.1:443 0.0.0.0:* LISTEN"; }
+    check_port "http://127.0.0.1:80";
+)
+[[ $? -eq 0 ]] \
+    && pass "netstat: IP octet .80 does not false-positive for port 80 → exit 
0" \
+    || fail "netstat: IP octet .80 false-positived for port 80 → expected exit 
0, got 1"
+
+section "ss branch — host dot-escaping guard"
+
+# Host 127.0.0.1 must be matched literally: a listener whose address merely
+# matches the pattern with '.' as a regex wildcard (127a0b0c1) must NOT count
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "ss" ]]; }
+    ss() { echo "tcp LISTEN 0 128 127a0b0c1:8080 0.0.0.0:*"; }
+    check_port "http://127.0.0.1:8080";
+)
+[[ $? -eq 0 ]] \
+    && pass "ss: unescaped-dot lookalike 127a0b0c1 does not false-positive → 
exit 0" \
+    || fail "ss: unescaped-dot lookalike 127a0b0c1 false-positived → expected 
exit 0, got 1"
+
+# ── /dev/tcp fallback branch 
──────────────────────────────────────────────────
+
+section "/dev/tcp fallback — timeout available, port occupied"
+
+# timeout exits 0 → connection succeeded → port in use
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "timeout" ]]; }
+    timeout() {
+        # Assert correct invocation: timeout 1 bash -c SCRIPT _ HOST PORT
+        [[ "$1" == "1" && "$2" == "bash" && "$3" == "-c" && "$5" == "_" ]] \
+            || { echo "timeout mock: unexpected argv: $*"; return 2; }
+        return 0
+    }
+    check_port "http://127.0.0.1:8080";
+)
+[[ $? -eq 1 ]] \
+    && pass "/dev/tcp+timeout: connection succeeded (exit 0) → port occupied → 
exit 1" \
+    || fail "/dev/tcp+timeout: connection succeeded → expected exit 1, got 0"
+
+section "/dev/tcp fallback — timeout available, port free"
+
+# timeout exits 1 → connection refused → port free
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "timeout" ]]; }
+    timeout() {
+        [[ "$1" == "1" && "$2" == "bash" && "$3" == "-c" && "$5" == "_" ]] \
+            || { echo "timeout mock: unexpected argv: $*"; return 2; }
+        return 1
+    }
+    check_port "http://127.0.0.1:8080";
+)
+[[ $? -eq 0 ]] \
+    && pass "/dev/tcp+timeout: connection refused (exit 1) → port free → exit 
0" \
+    || fail "/dev/tcp+timeout: connection refused → expected exit 0, got 1"
+
+section "/dev/tcp fallback — real loopback (ephemeral port)"
+
+# Start Python server on ephemeral port 0, capture actual port from child 
stdout
+(
+    source "$UTIL_SH"
+    command_available() { [[ "$1" == "timeout" ]]; }
+    # Mock timeout: on hosts without timeout (macOS), run the probe directly.
+    # The probe args are: timeout 1 bash -c SCRIPT _ HOST PORT
+    timeout() {
+        [[ "$1" == "1" && "$2" == "bash" && "$3" == "-c" && "$5" == "_" ]] \
+            || { echo "timeout mock: unexpected argv: $*" >&2; return 2; }
+        # Run the probe with a 2-second hard deadline via background + watchdog
+        bash -c "$4" "$5" "$6" "$7" 2>/dev/null &
+        local probe_pid=$!
+        (sleep 2; kill -9 "$probe_pid" 2>/dev/null) &
+        local watchdog_pid=$!
+        wait "$probe_pid" 2>/dev/null
+        local rc=$?
+        kill -9 "$watchdog_pid" 2>/dev/null
+        wait "$watchdog_pid" 2>/dev/null
+        return $rc
+    }
+    # Use a temp file to capture the bound port from child
+    port_file=$(mktemp)
+    
+    # Start Python server that prints the bound port to stdout
+    python3 -c "
+import socket
+s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+s.bind(('127.0.0.1', 0))
+s.listen(1)  # actually listen so /dev/tcp can connect
+port = s.getsockname()[1]
+print(port, flush=True)
+import time
+time.sleep(30)  # keep server alive
+" > "$port_file" 2>/dev/null &
+    PY_PID=$!
+    # Poll for port file up to ~4s (Python cold-start can exceed 0.5s on busy 
CI)
+    bound_port=""
+    for _ in $(seq 1 40); do
+        bound_port=$(head -1 "$port_file" 2>/dev/null)
+        [[ -n "$bound_port" ]] && break
+        kill -0 $PY_PID 2>/dev/null || break
+        sleep 0.1
+    done
+    if [[ -z "$bound_port" || ! "$bound_port" =~ ^[0-9]+$ ]]; then
+        echo "SKIP: failed to get bound port"
+        kill -9 $PY_PID 2>/dev/null || true
+        exit 77
+    fi
+    # Verify child is alive
+    if ! kill -0 $PY_PID 2>/dev/null; then
+        echo "SKIP: Python child died"
+        exit 77
+    fi
+    # Register cleanup in EXIT trap — check_port calls exit 1 on occupied port,
+    # so cleanup after check_port is unreachable on that path.
+    trap "rm -f $port_file; kill -9 $PY_PID 2>/dev/null; wait $PY_PID 
2>/dev/null" EXIT
+    check_port "http://127.0.0.1:$bound_port";
+)
+_rc=$?
+if [[ $_rc -eq 77 ]]; then
+    echo "  SKIP /dev/tcp real ephemeral port (setup failed)"
+elif [[ $_rc -eq 1 ]]; then
+    pass "/dev/tcp real ephemeral port → detects occupation → exit 1"
+else
+    fail "/dev/tcp real ephemeral port → expected exit 1, got $_rc"
+fi
+
+section "/dev/tcp fallback — no-timeout watchdog kills stuck probe"
+
+# run_with_deadline "sleep 5" 2 should return in ~2s, not ~5s.
+# Proves the watchdog actually kills a stuck child, not just that the code 
path is taken.
+(
+    source "$UTIL_SH"
+    local start=$(date +%s)

Review Comment:
   ⚠️ `local` is only valid inside a function, but lines 358, 360, and 361 use 
it directly in a subshell. The exact-head Linux job reports `local: can only be 
used in a function` and `start: unbound variable`, then fails with 34 passed 
and 1 failed; the macOS job can false-pass the same errors. Please use ordinary 
assignments or wrap this assertion in a function, make unexpected shell errors 
fail the case, and exercise the production no-`timeout` `check_port` path 
rather than only the helper.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to