imbajin commented on code in PR #3105:
URL: https://github.com/apache/hugegraph/pull/3105#discussion_r3649890156
##########
hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh:
##########
@@ -79,15 +79,229 @@ function process_id() {
return "$pid"
}
-# check the port of rest server is occupied
+# 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
+ 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); then
Review Comment:
⚠️ A zero exit status does not prove that `netstat -an` produced a usable
listener table. In restricted environments it can return success with empty or
diagnostic-only output; this branch then skips `/dev/tcp` and may report an
occupied endpoint as free. Please model this result as `unknown`: set
`port_checked` only after recognizing valid listener rows, otherwise continue
to the bounded fallback. Add regressions for exit 0 with empty and unparseable
output.
##########
hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh:
##########
@@ -0,0 +1,684 @@
+#!/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" ]]; }
Review Comment:
⚠️ Every `/dev/tcp` test reports `timeout` as available or reimplements it
with a mock, including this macOS-oriented case. The production no-`timeout`
watchdog is never exercised, although stock macOS depends on its
kill/wait/deadline behavior. Please test the actual branch with
`command_available timeout` returning false for occupied, refused, and deadline
cases, and assert that both child processes are reaped.
--
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]