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


##########
hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh:
##########
@@ -118,18 +119,175 @@ function process_id() {
     return "$pid"
 }
 
-# check the port of rest server 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
+# Extract a validated TCP port from a configured server URL.
+# Echoes the port on success.  Returns 1 when the value carries no usable port
+# or is ambiguous, in which case the caller skips the preflight.
+function parse_port_from_url() {
+    local url="$1"
+
+    # ServerOptions tolerates surrounding whitespace, so strip it first.
+    url="${url#"${url%%[![:space:]]*}"}"
+    url="${url%"${url##*[![:space:]]}"}"
+    [[ -z "$url" ]] && return 1
+
+    # The scheme is optional and case-insensitive.
+    local scheme="" rest="$url"
+    if [[ "$url" == *"://"* ]]; then
+        scheme=$(echo "${url%%://*}" | tr '[:upper:]' '[:lower:]')
+        rest="${url#*://}"
     fi
-    lsof -i :"$port" >/dev/null
-    if [ $? -eq 0 ]; then
-        echo "The port $port has already been used"
-        exit 1
+
+    # The authority ends at the first '/', '?' or '#'.
+    local authority="${rest%%[/?#]*}"
+    [[ -z "$authority" ]] && return 1
+
+    # Drop any userinfo prefix; its colon would otherwise look like an
+    # unbracketed IPv6 separator.
+    authority="${authority##*@}"
+    [[ -z "$authority" ]] && return 1
+
+    local port=""
+    if [[ "$authority" =~ ^\[[^]]*\](:([0-9]+))?$ ]]; then
+        # Bracketed IPv6, with or without a port: [::1] or [::1]:8080
+        port="${BASH_REMATCH[2]}"
+    elif [[ "$authority" == *:*:* ]]; then
+        # Unbracketed IPv6 is ambiguous: in "::1:8080" the trailing group may
+        # be a port or another hextet.  Refuse to guess.
+        # TODO(check_port): no preflight runs at all for this form.  If
+        # ServerOptions ever guarantees a normalized bracketed value here, this
+        # branch can resolve the port instead of skipping the check.
+        echo "WARN: ambiguous IPv6 authority '$authority' in server URL;" \
+             "use bracket notation such as [::1]:8080." >&2
+        return 1
+    elif [[ "$authority" == *:* ]]; then
+        port="${authority##*:}"
+    fi
+
+    # Fall back to the scheme's default port.
+    # TODO(check_port): a scheme-less value with no explicit port (e.g. plain
+    # "127.0.0.1") has no derivable port, so it is skipped rather than guessed.
+    # Reading the configured default from ServerOptions would close this gap.
+    if [[ -z "$port" ]]; then
+        case "$scheme" in
+            http)  port="80" ;;
+            https) port="443" ;;
+            *)     return 1 ;;
+        esac
+    fi
+
+    [[ "$port" =~ ^[0-9]+$ ]] || return 1
+    # Normalise leading-zero forms textually; Java reads 08080 as decimal 8080.
+    # Arithmetic conversion must not happen before the value is bounded: Bash
+    # evaluates in 64-bit and wraps silently, so 18446744073709559616 would
+    # otherwise pass the range check as port 8000.
+    port="${port#"${port%%[!0]*}"}"
+    [[ -z "$port" ]] && return 1
+    (( ${#port} <= 5 )) || return 1
+    (( port >= 1 && port <= 65535 )) || return 1
+
+    echo "$port"
+}
+
+# Echo "busy", "free" or "unknown" for the given TCP port.
+#
+# Detection is deliberately port-only, matching the conservative behaviour of
+# the `lsof -i :PORT` call this replaces.  Reproducing kernel socket semantics
+# in Bash - dual-stack IPV6_V6ONLY, wildcard versus specific binds, address
+# canonicalisation - produced more wrong answers than it prevented, so we only
+# ask "is anything already listening on this port?".
+#
+# Only LISTEN rows and only the local-address column are inspected, so an
+# unrelated outbound connection to the same port number is never mistaken for
+# a local listener.  A tool that is missing, fails, or yields no recognisable
+# listener row reports "unknown" rather than "free".
+#
+# TODO(check_port): port-only matching ignores the listener's address, so a
+# listener bound to one local address (127.0.0.1:8080) reports the port busy
+# even when the server would bind a different one (192.168.1.5:8080).  This is
+# deliberate - it is what `lsof -i :PORT` did, and it fails safe - but it can
+# refuse a bind that would have succeeded.  If that is reported in practice,
+# revisit by comparing the local-address column instead of only its port.
+#
+# TODO(check_port): a host with genuinely zero LISTEN sockets is indistinguish-
+# able from a restricted or unparseable table, so both report "unknown" and
+# warn on every start.  Distinguishing them needs a positive signal that the
+# table was readable (e.g. an exit status ss/netstat do not currently give).
+function port_listen_state() {
+    local port="$1"
+    local out
+    local os
+    os=$(uname)
+
+    # $4 is the local address for both `ss -ltn` and BSD `netstat -an`.
+    # Splitting on the last separator keeps IPv6 hextets (for example
+    # [2001:db8::80]:443) from being misread as the port.
+    local parser='
+        NF >= 4 && (!want_listen || $NF == "LISTEN") {
+            addr = $4
+            cut = 0
+            for (k = length(addr); k > 0; k--) {
+                if (substr(addr, k, 1) == sep) { cut = k; break }
+            }
+            if (cut == 0) next
+            rows++
+            if (substr(addr, cut + 1) == port) { found = 1; exit }
+        }
+        END {
+            if (found) print "busy"
+            else if (rows > 0) print "free"
+            else print "unknown"
+        }'
+
+    if [[ "$os" == "Darwin" || "$os" == *BSD* ]]; then
+        if command_available "netstat" && out=$(netstat -an -p tcp 
2>/dev/null) \
+           && [[ -n "$out" ]]; then
+            echo "$out" | awk -v port="$port" -v sep="." -v want_listen=1 
"$parser"
+            return 0
+        fi
+    else
+        # `ss -H -ltn` already restricts output to listening sockets.
+        if command_available "ss" && out=$(ss -H -ltn 2>/dev/null) && [[ -n 
"$out" ]]; then
+            echo "$out" | awk -v port="$port" -v sep=":" -v want_listen=0 
"$parser"
+            return 0

Review Comment:
   Correct — the branch returned unconditionally, so a `ss` run that produced 
unusable output ended the search with a working `netstat` sitting right behind 
it. Fixed in `d93b6f9d`: the parser result is captured and only an actual 
answer returns.
   
   ```bash
   if command_available "ss" && out=$(ss -H -ltn 2>/dev/null); then
       if [[ -z "$out" ]]; then
           echo "free"
           return 0
       fi
       state=$(echo "$out" | awk -v port="$port" -v sep=":" -v want_listen=0 
"$parser")
       case "$state" in
           busy|free) echo "$state"; return 0 ;;
       esac
   fi
   # netstat is tried next, same shape
   ```
   
   `case busy|free` rather than `!= unknown` so an awk that dies and prints 
nothing also falls through instead of echoing an empty state.
   
   One thing came out of your Docker comment that had to be fixed here too, or 
the image fix would only have been half of one. `ss -H -ltn` on a host with no 
listeners exits 0 and prints nothing, and a fresh container is exactly that. 
Treating empty as failure meant even an image with `iproute2` would warn on 
every clean start. `-H` suppresses the header, so there is no header to lose: a 
zero exit with no output is a positive "no TCP listeners", not an unreadable 
table. That is now the one case where empty is an answer, and it is the only 
reason `port_listen_state` returns `free` instead of `unknown` in the new 
image-build assertion.
   
   Both netstat branches still print headers the parser drops, so an empty 
result there stays `unknown`. That is narrowed in the `TODO(check_port)` above 
the function rather than claimed as solved.
   
   Regressions added (section 2 of the suite):
   
   ```
   PASS  ss empty output is free
   PASS  ss unparseable output is unknown
   PASS  unparseable ss falls back to netstat
   PASS  netstat fallback can also report free
   PASS  failed ss falls back to netstat
   ```
   
   Restoring the early `return 0` fails exactly the two fallback cases; 
restoring `unknown`-on-empty fails exactly one.
   



-- 
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