refactor: replace lsof port preflight with ss/netstat (#3105)

Fix a startup hang in kind/Kubernetes environments — the case that surfaced while bringing up the Helm chart. In a pod where `ulimit -n` is very large, `lsof -i :PORT` walks an enormous file descriptor table and `start-hugegraph.sh` stalls before the server ever binds.

The fix replaces that `lsof` call in the server's `check_port`, and removes the dead `check_port` copies from PD/Store.
- retain a bounded real-clock startup smoke test

---------

Co-authored-by: imbajin <jin@apache.org>
diff --git a/.github/workflows/docker-build-ci.yml b/.github/workflows/docker-build-ci.yml
index 8d31ff2..eb1f0d9 100644
--- a/.github/workflows/docker-build-ci.yml
+++ b/.github/workflows/docker-build-ci.yml
@@ -26,6 +26,7 @@
     paths:
       - '**/Dockerfile*'
       - '.dockerignore'
+      - 'hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh'
 
 jobs:
   docker-build:
@@ -47,6 +48,28 @@
         run: |
           IMAGE_ID=$(docker build -q -f ${{ matrix.dockerfile }} .)
           echo "Built: $IMAGE_ID"
+          echo "IMAGE_ID=$IMAGE_ID" >> "$GITHUB_ENV"
           HC=$(docker inspect --format='{{json .Config.Healthcheck}}' "$IMAGE_ID")
           echo "Healthcheck: $HC"
           [[ "$HC" != "null" ]] || { echo "ERROR: HEALTHCHECK missing in ${{ matrix.dockerfile }}"; exit 1; }
+
+      # The startup preflight needs a socket-table tool, and the base image
+      # ships none of its own.  Without one every start reports "unknown" and
+      # a duplicate start is no longer refused, so assert the image can
+      # actually answer.  Only the server images run check_port.
+      # TODO(docker-ci): this pins the probe dependency, not the behaviour it
+      # protects.  A full duplicate-start/stop check needs a booted server with
+      # a backend, which belongs with the e2e job rather than the image build.
+      - name: Port preflight can answer inside ${{ matrix.dockerfile }}
+        if: ${{ startsWith(matrix.dockerfile, 'hugegraph-server/') }}
+        run: |
+          STATE=$(docker run --rm "$IMAGE_ID" bash -c \
+            'source /hugegraph-server/bin/util.sh && port_listen_state 8080')
+          echo "port_listen_state 8080 -> $STATE"
+          # Assert a positive answer rather than "not unknown": an empty $STATE
+          # would otherwise satisfy the check.  The step's default `bash -e`
+          # already aborts on a failed `docker run`, so this only states intent.
+          [[ "$STATE" == "free" || "$STATE" == "busy" ]] || {
+            echo "ERROR: no usable socket-table tool (ss/netstat) in ${{ matrix.dockerfile }}"
+            exit 1
+          }
diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml
index 6200679..6f670e1 100644
--- a/.github/workflows/pd-store-ci.yml
+++ b/.github/workflows/pd-store-ci.yml
@@ -110,7 +110,9 @@
       - name: Check startup test prerequisites (PD)
         id: pd-preflight
         run: |
-          for tool in lsof curl java; do
+          # These jobs run on Linux, where the suites clean up ports with fuser.
+          # lsof is no longer required: check_port now uses ss/netstat.
+          for tool in fuser curl java; do
             if ! command -v "$tool" >/dev/null 2>&1; then
               echo "can_run=false" >> "$GITHUB_OUTPUT"
               echo "skip_reason=missing tool: $tool" >> "$GITHUB_OUTPUT"
@@ -190,7 +192,9 @@
       - name: Check startup test prerequisites (Store)
         id: store-preflight
         run: |
-          for tool in lsof curl java; do
+          # These jobs run on Linux, where the suites clean up ports with fuser.
+          # lsof is no longer required: check_port now uses ss/netstat.
+          for tool in fuser curl java; do
             if ! command -v "$tool" >/dev/null 2>&1; then
               echo "can_run=false" >> "$GITHUB_OUTPUT"
               echo "skip_reason=missing tool: $tool" >> "$GITHUB_OUTPUT"
diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml
index 72f28b6..7b98021 100644
--- a/.github/workflows/server-ci.yml
+++ b/.github/workflows/server-ci.yml
@@ -79,17 +79,33 @@
         run: |
           mvn clean compile -U -Dmaven.javadoc.skip=true -ntp
 
+      - name: Run check_port unit tests
+        if: ${{ env.BACKEND == 'rocksdb' }}
+        run: |
+          # Validates the ss/netstat port preflight that replaced lsof
+          $TRAVIS_DIR/test-check-port.sh \
+            hugegraph-server/hugegraph-dist/src/assembly/static \
+            hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh
+
       - name: Check startup test prerequisites
         id: server-preflight
         if: ${{ env.BACKEND == 'rocksdb' }}
         run: |
-          for tool in lsof crontab curl java; do
+          # Note: lsof removed from prerequisites; check_port now uses ss, falling back
+          # to netstat, and warns without blocking when neither can answer.
+          # This job always runs on Linux (ubuntu-22.04), which uses fuser for port cleanup.
+          for tool in crontab curl java; do
             if ! command -v "$tool" >/dev/null 2>&1; then
               echo "can_run=false" >> "$GITHUB_OUTPUT"
               echo "skip_reason=missing tool: $tool" >> "$GITHUB_OUTPUT"
               exit 0
             fi
           done
+          if ! command -v fuser >/dev/null 2>&1; then
+            echo "can_run=false" >> "$GITHUB_OUTPUT"
+            echo "skip_reason=missing tool: fuser (required for Linux port cleanup)" >> "$GITHUB_OUTPUT"
+            exit 0
+          fi
           echo "can_run=true" >> "$GITHUB_OUTPUT"
 
       - name: Run start-hugegraph.sh foreground mode tests
@@ -187,6 +203,13 @@
         run: |
           mvn clean compile -pl hugegraph-server/hugegraph-test -am -U -Dmaven.javadoc.skip=true -ntp
 
+      - name: Run check_port unit tests
+        # Validates the ss/netstat port preflight that replaced lsof
+        run: |
+          $TRAVIS_DIR/test-check-port.sh \
+            hugegraph-server/hugegraph-dist/src/assembly/static \
+            hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh
+
       - name: Run RocksDB core test
         run: |
           $TRAVIS_DIR/run-core-test.sh $BACKEND
diff --git a/hugegraph-pd/Dockerfile b/hugegraph-pd/Dockerfile
index d55ac18..68e6e25 100644
--- a/hugegraph-pd/Dockerfile
+++ b/hugegraph-pd/Dockerfile
@@ -45,12 +45,14 @@
 WORKDIR /hugegraph-pd/
 
 # 1. Install runtime dependencies
+# Note: lsof is gone because its only consumer was the dead check_port removed
+# from hg-pd-dist bin/util.sh.  PD runs no port preflight, so unlike the server
+# images this needs no socket-table tool in its place.
 RUN apt-get -q update \
     && apt-get -q install -y --no-install-recommends --no-install-suggests \
        dumb-init \
        procps \
        curl \
-       lsof \
        vim \
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/*
diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh
index 0b7ad0f..b476d79 100644
--- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh
+++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh
@@ -20,20 +20,21 @@
 # TODO: consider reuse it with server-dist module (almost same as it)
 function command_available() {
     local cmd=$1
-    if [ $(command -v $cmd >/dev/null 2>&1) ]; then
-        return 1
-    else
+    if command -v "$cmd" >/dev/null 2>&1; then
         return 0
     fi
+    return 1
 }
 
 # read a property from .properties file
 function read_property() {
     # file path
+    local file_name
+    local property_name
     file_name=$1
     # replace "." to "\."
-    property_name=$(echo $2 | sed 's/\./\\\./g')
-    cat $file_name | sed -n -e "s/^[ ]*//g;/^#/d;s/^$property_name=//p" | tail -1
+    property_name=$(echo "$2" | sed 's/\./\\\./g')
+    cat "$file_name" | sed -n -e "s/^[ ]*//g;/^#/d;s/^$property_name=//p" | tail -1
 }
 
 function write_property() {
@@ -56,7 +57,7 @@
     local version=$2
     local module=$3
 
-    cat $file | tr -d '\n {}'| awk -F',+|:' '''{
+    cat "$file" | tr -d '\n {}'| awk -F',+|:' '''{
         pre="";
         for(i=1; i<=NF; ) {
             if(match($i, /version/)) {
@@ -72,27 +73,13 @@
 }
 
 function process_num() {
-    num=`ps -ef | grep $1 | grep -v grep | wc -l`
-    return $num
+    num=$(ps -ef | grep "$1" | grep -v grep | wc -l)
+    return "$num"
 }
 
 function process_id() {
-    pid=`ps -ef | grep $1 | grep -v grep | awk '{print $2}'`
-    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
-    fi
-    lsof -i :$port >/dev/null
-    if [ $? -eq 0 ]; then
-        echo "The port $port has already been used"
-        exit 1
-    fi
+    pid=$(ps -ef | grep "$1" | grep -v grep | awk '{print $2}')
+    return "$pid"
 }
 
 function crontab_append() {
@@ -130,13 +117,14 @@
     local server_url="$3"
     local timeout_s="$4"
 
-    local now_s=`date '+%s'`
-    local stop_s=$(( $now_s + $timeout_s ))
+    local now_s
+    now_s=$(date '+%s')
+    local stop_s=$(( now_s + timeout_s ))
 
     local status
 
     echo -n "Connecting to $server_name ($server_url)"
-    while [ $now_s -le $stop_s ]; do
+    while [ "$now_s" -le "$stop_s" ]; do
         echo -n .
         process_status "$server_name" "$pid" >/dev/null
         if [ $? -eq 1 ]; then
@@ -144,14 +132,14 @@
             return 1
         fi
 
-        status=`curl -o /dev/null -s -k -w %{http_code} $server_url`
-        if [[ $status -eq 200 || $status -eq 401 ]]; then
+        status=$(curl -o /dev/null -s -k -w '%{http_code}' "$server_url")
+        if [[ "$status" -eq 200 || "$status" -eq 401 ]]; then
             echo "OK"
             echo "Started [pid $pid]"
             return 0
         fi
         sleep 2
-        now_s=`date '+%s'`
+        now_s=$(date '+%s')
     done
 
     echo "The operation timed out when attempting to connect to $server_url" >&2
@@ -160,22 +148,28 @@
 
 function free_memory() {
     local free=""
-    local os=`uname`
+    local os=$(uname)
     if [ "$os" == "Linux" ]; then
-        local mem_free=`cat /proc/meminfo | grep -w "MemFree" | awk '{print $2}'`
-        local mem_buffer=`cat /proc/meminfo | grep -w "Buffers" | awk '{print $2}'`
-        local mem_cached=`cat /proc/meminfo | grep -w "Cached" | awk '{print $2}'`
+        local mem_free=$(cat /proc/meminfo | grep -w "MemFree" | awk '{print $2}')
+        local mem_buffer=$(cat /proc/meminfo | grep -w "Buffers" | awk '{print $2}')
+        local mem_cached=$(cat /proc/meminfo | grep -w "Cached" | awk '{print $2}')
         if [[ "$mem_free" == "" || "$mem_buffer" == "" || "$mem_cached" == "" ]]; then
             echo "Failed to get free memory"
             exit 1
         fi
-        free=`expr $mem_free + $mem_buffer + $mem_cached`
-        free=`expr $free / 1024`
+        free=$(expr "$mem_free" + "$mem_buffer" + "$mem_cached")
+        free=$(expr "$free" / 1024)
     elif [ "$os" == "Darwin" ]; then
-        local pages_free=`vm_stat | awk '/Pages free/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " "`
-        local pages_inactive=`vm_stat | awk '/Pages inactive/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " "`
-        local pages_available=`expr $pages_free + $pages_inactive`
-        free=`expr $pages_available \* 4096 / 1024 / 1024`
+        local pages_free pages_inactive
+        pages_free=$(vm_stat | awk '/Pages free/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " ")
+        pages_inactive=$(vm_stat | awk '/Pages inactive/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " ")
+        if [[ -z "$pages_free" || -z "$pages_inactive" ]]; then
+            echo "Failed to get free memory"
+            exit 1
+        fi
+        local pages_available
+        pages_available=$(expr "$pages_free" + "$pages_inactive")
+        free=$(expr "$pages_available" \* 4096 / 1024 / 1024)
     else
         echo "Unsupported operating system $os"
         exit 1
@@ -187,8 +181,8 @@
     local min_mem=$1
     local max_mem=$2
     # Get machine available memory
-    local free=`free_memory`
-    local half_free=$[free/2]
+    local free=$(free_memory)
+    local half_free=$((free/2))
 
     local xmx=$min_mem
     if [[ "$free" -lt "$min_mem" ]]; then
@@ -236,47 +230,78 @@
 }
 
 function get_ip() {
-    local os=`uname`
+    local os=$(uname)
     local loopback="127.0.0.1"
     local ip=""
     case $os in
         Linux)
             if command_available "ifconfig"; then
-                ip=`ifconfig | grep 'inet addr:' | grep -v "$loopback" | cut -d: -f2 | awk '{ print $1}'`
+                ip=$(ifconfig | grep 'inet addr:' | grep -v "$loopback" | cut -d: -f2 | awk '{ print $1}')
             elif command_available "ip"; then
-                ip=`ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | awk -F"/" '{print $1}'`
+                ip=$(ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | awk -F"/" '{print $1}')
             else
                 ip=$loopback
             fi
             ;;
         FreeBSD|OpenBSD|Darwin)
             if command_available "ifconfig"; then
-                ip=`ifconfig | grep -E 'inet.[0-9]' | grep -v "$loopback" | awk '{ print $2}'`
+                ip=$(ifconfig | grep -E 'inet.[0-9]' | grep -v "$loopback" | awk '{ print $2}')
             else
                 ip=$loopback
             fi
             ;;
         SunOS)
             if command_available "ifconfig"; then
-                ip=`ifconfig -a | grep inet | grep -v "$loopback" | awk '{ print $2} '`
+                ip=$(ifconfig -a | grep inet | grep -v "$loopback" | awk '{ print $2} ')
             else
                 ip=$loopback
             fi
             ;;
         *) ip=$loopback;;
     esac
-    echo $ip
+    [[ -z "$ip" ]] && ip=$loopback
+    echo "$ip"
 }
 
 function download() {
     local path=$1
     local link_url=$2
 
-    if command_available "wget"; then
-        wget --help | grep -q '\--show-progress' && progress_opt="-q --show-progress" || progress_opt=""
-        wget ${link_url} -P ${path} $progress_opt
-    elif command_available "curl"; then
-        curl ${link_url} -o ${path}/${link_url}
+    if [ ! -d "$path" ]; then
+        mkdir -p "$path" || {
+            echo "Failed to create directory: $path"
+            exit 1
+        }
+    fi
+
+    local filename tmp
+    filename=$(basename "${link_url%%[?#]*}")
+    local dest="${path}/${filename}"
+    # mktemp, not $$: a PID is shared by concurrent background subshells.
+    tmp=$(mktemp -- "${path}/.${filename}.XXXXXX") || {
+        echo "Failed to create a temporary file in $path"
+        return 1
+    }
+
+    if command_available "curl"; then
+        # -o must appear before -- so it is parsed as an option, not an extra URL.
+        if curl -fL -o "$tmp" -- "${link_url}"; then
+            mv -f -- "$tmp" "$dest" || { rm -f -- "$tmp"; return 1; }
+        else
+            rm -f -- "$tmp"
+            return 1
+        fi
+    elif command_available "wget"; then
+        local -a progress_opt=()
+        if wget --help 2>&1 | grep -q -- '--show-progress'; then
+            progress_opt=(-q --show-progress)
+        fi
+        if wget ${progress_opt[@]+"${progress_opt[@]}"} -O "$tmp" -- "${link_url}"; then
+            mv -f -- "$tmp" "$dest" || { rm -f -- "$tmp"; return 1; }
+        else
+            rm -f -- "$tmp"
+            return 1
+        fi
     else
         echo "Required wget or curl but they are unavailable"
         exit 1
@@ -289,19 +314,17 @@
     local tar=$3
     local link=$4
 
-    if [ ! -d ${path}/${dir} ]; then
-        if [ ! -f ${path}/${tar} ]; then
+    if [ ! -d "${path}/${dir}" ]; then
+        if [ ! -f "${path}/${tar}" ]; then
             echo "Downloading the compressed package '${tar}'"
-            download ${path} ${link}
-            if [ $? -ne 0 ]; then
+            if ! download "${path}" "${link}"; then
                 echo "Failed to download, please ensure the network is available and link is valid"
                 exit 1
             fi
             echo "[OK] Finished download"
         fi
         echo "Unzip the compressed package '$tar'"
-        tar -zxvf ${path}/${tar} -C ${path} >/dev/null 2>&1
-        if [ $? -ne 0 ]; then
+        if ! tar -zxvf "${path}/${tar}" -C "${path}" >/dev/null 2>&1; then
             echo "Failed to unzip, please check the compressed package"
             exit 1
         fi
@@ -316,7 +339,7 @@
     local pid="$2"
     local timeout_s="$3"
 
-    local now_s=`date '+%s'`
+    local now_s=$(date '+%s')
     local stop_s=$(( $now_s + $timeout_s ))
 
     echo -n "Killing $process_name(pid $pid)" >&2
@@ -328,7 +351,7 @@
             return 0
         fi
         sleep 2
-        now_s=`date '+%s'`
+        now_s=$(date '+%s')
     done
     echo "$process_name shutdown timeout(exceeded $timeout_s seconds)" >&2
     return 1
@@ -357,7 +380,7 @@
         return 0
     fi
 
-    case "`uname`" in
+    case "$(uname)" in
         CYGWIN*) taskkill /F /PID "$pid" ;;
         *)       kill "$pid" ;;
     esac
diff --git a/hugegraph-server/Dockerfile b/hugegraph-server/Dockerfile
index 2ca946e..5caadd2 100644
--- a/hugegraph-server/Dockerfile
+++ b/hugegraph-server/Dockerfile
@@ -46,12 +46,16 @@
 WORKDIR /hugegraph-server/
 
 # 1. Install runtime dependencies and configure server
+# Note: iproute2 provides `ss`, which the bin/util.sh port preflight needs.  The
+# jammy base image ships neither ss nor netstat, so without it the preflight is
+# permanently inconclusive and a duplicate start is no longer caught.  It
+# replaces lsof, which no shipped script calls any more.
 RUN apt-get -q update \
     && apt-get -q install -y --no-install-recommends --no-install-suggests \
        dumb-init \
        procps \
        curl \
-       lsof \
+       iproute2 \
        vim \
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/* \
diff --git a/hugegraph-server/Dockerfile-hstore b/hugegraph-server/Dockerfile-hstore
index 06d2d4b..7cd64e8 100644
--- a/hugegraph-server/Dockerfile-hstore
+++ b/hugegraph-server/Dockerfile-hstore
@@ -48,12 +48,16 @@
 WORKDIR /hugegraph-server/
 
 # 1. Install runtime dependencies and configure server
+# Note: iproute2 provides `ss`, which the bin/util.sh port preflight needs.  The
+# jammy base image ships neither ss nor netstat, so without it the preflight is
+# permanently inconclusive and a duplicate start is no longer caught.  It
+# replaces lsof, which no shipped script calls any more.
 RUN apt-get -q update \
     && apt-get -q install -y --no-install-recommends --no-install-suggests \
        dumb-init \
        procps \
        curl \
-       lsof \
+       iproute2 \
        vim \
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/* \
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh
index 91c0c3e..8ae46e8 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh
@@ -18,11 +18,10 @@
 
 function command_available() {
     local cmd=$1
-    if [[ -x "$(command -v "$cmd")" ]]; then
+    if command -v "$cmd" >/dev/null 2>&1; then
         return 0
-    else
-        return 1
     fi
+    return 1
 }
 
 function configure_riscv64_libatomic() {
@@ -67,6 +66,8 @@
 # read a property from .properties file
 function read_property() {
     # file path
+    local file_name
+    local property_name
     file_name=$1
     # replace "." to "\."
     property_name=$(echo "$2" | sed 's/\./\\\./g')
@@ -118,18 +119,197 @@
     return "$pid"
 }
 
-# check the port of rest server is occupied
+# 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
+
+    # 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.
+#
+# The port is compared as text against the listener table, so the argument must
+# already be normalised - parse_port_from_url() strips leading zeroes for this
+# reason, and "08080" passed directly here would not match a listener on 8080.
+#
+# 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", and the next probe is
+# tried; the sole exception is `ss -H` succeeding with no output at all, which
+# says positively that the host has no TCP listeners.
+#
+# 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): only the `ss` branch can tell "no listeners at all" from
+# "table unreadable", because -H removes the header and leaves a zero exit with
+# empty output as a positive signal.  Both netstat branches print headers that
+# the parser drops, so an empty result there stays "unknown" and warns.
+function port_listen_state() {
+    local port="$1"
+    local out
+    local state
+    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"
+        }'
+
+    # Each probe answers only when it produced a usable listener table; an
+    # inconclusive one falls through to the next rather than ending the search.
+    if [[ "$os" == "Darwin" || "$os" == *BSD* ]]; then
+        if command_available "netstat" && out=$(netstat -an -p tcp 2>/dev/null) \
+           && [[ -n "$out" ]]; then
+            state=$(echo "$out" | awk -v port="$port" -v sep="." -v want_listen=1 "$parser")
+            case "$state" in
+                busy|free) echo "$state"; return 0 ;;
+            esac
+        fi
+    else
+        # `ss -H -ltn` already restricts output to listening sockets, and -H
+        # drops the header, so a zero exit with no output means "nothing is
+        # listening" - the one case where empty is an answer, not a failure.
+        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
+        if command_available "netstat" && out=$(netstat -ltn 2>/dev/null) \
+           && [[ -n "$out" ]]; then
+            state=$(echo "$out" | awk -v port="$port" -v sep=":" -v want_listen=1 "$parser")
+            case "$state" in
+                busy|free) echo "$state"; return 0 ;;
+            esac
+        fi
+    fi
+
+    # TODO(check_port): with neither ss nor netstat present there is no probe
+    # left, so the preflight is permanently "unknown" and never detects a busy
+    # port.  The published server images carry iproute2 for this reason, but a
+    # hand-built minimal image can still ship neither.  A dependency-free
+    # fallback would need a bounded connect, which was deliberately removed
+    # here; adding one back means re-solving the hang this PR fixes.
+    echo "unknown"
+}
+
+# Best-effort startup preflight.  Exits 1 when the configured port is already
+# in use.  The server's own bind stays authoritative, so an inconclusive
+# result only warns and lets startup proceed.
 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
-    fi
-    lsof -i :"$port" >/dev/null
-    if [ $? -eq 0 ]; then
-        echo "The port $port has already been used"
-        exit 1
-    fi
+    local url="$1"
+    local port
+    local state
+
+    port=$(parse_port_from_url "$url") || return 0
+
+    state=$(port_listen_state "$port")
+    case "$state" in
+        busy)
+            echo "The port $port has already been used"
+            exit 1
+            ;;
+        unknown)
+            echo "WARN: could not determine whether port $port is free;" \
+                 "continuing and letting the server bind decide." >&2
+            ;;
+    esac
+
+    return 0
 }
 
 function crontab_append() {
@@ -167,14 +347,15 @@
     local server_url="$3"
     local timeout_s="$4"
 
-    local now_s=$(date '+%s')
+    local now_s
+    now_s=$(date '+%s')
     local stop_s=$((now_s + timeout_s))
 
     local status
     local error_file_name="startup_error.txt"
 
     echo -n "Connecting to $server_name ($server_url)"
-    while [ "$now_s" -le $stop_s ]; do
+    while [ "$now_s" -lt "$stop_s" ]; do
         echo -n .
         process_status "$server_name" "$pid" >/dev/null
         if [ $? -eq 1 ]; then
@@ -185,8 +366,18 @@
             return 1
         fi
 
-        status=$(curl -I -sS -k -w "%{http_code}" -o /dev/null "$server_url" 2> "$error_file_name")
-        if [[ $status -eq 200 || $status -eq 401 ]]; then
+        # Bound each probe by the time left in the overall deadline: without
+        # --max-time a single blackholed request blocks past ${timeout_s}s.
+        # Refresh the clock after the process check and immediately before the
+        # probe.  A stale loop-entry time must not grant curl a new one-second
+        # budget when the overall deadline has already been reached.
+        now_s=$(date '+%s')
+        local remain_s=$((stop_s - now_s))
+        [ "$remain_s" -le 0 ] && break
+        local connect_s=$((remain_s < 5 ? remain_s : 5))
+        status=$(curl -I -sS -k --connect-timeout "$connect_s" --max-time "$remain_s" \
+                      -w "%{http_code}" -o /dev/null "$server_url" 2> "$error_file_name")
+        if [[ "$status" -eq 200 || "$status" -eq 401 ]]; then
             echo "OK"
             echo "Started [pid $pid]"
             if [ -e "$error_file_name" ]; then
@@ -194,13 +385,24 @@
             fi
             return 0
         fi
-        sleep 2
+
+        # Pause for the retry interval, but never past the deadline: sleeping a
+        # fixed 2s and only then re-reading the clock made a 1s timeout take 2s.
+        # Nothing left to wait for means the deadline is spent, so stop here
+        # rather than spin until the clock ticks past it.
+        now_s=$(date '+%s')
+        local sleep_s=$((stop_s - now_s))
+        [ "$sleep_s" -le 0 ] && break
+        [ "$sleep_s" -gt 2 ] && sleep_s=2
+        sleep "$sleep_s"
         now_s=$(date '+%s')
     done
 
     echo ""
-    cat "$error_file_name"
-    rm "$error_file_name"
+    if [ -e "$error_file_name" ]; then
+        cat "$error_file_name"
+        rm "$error_file_name"
+    fi
     echo "The operation timed out(${timeout_s}s) when attempting to connect to $server_url" >&2
     return 1
 }
@@ -318,17 +520,43 @@
 function download() {
     local path=$1
     local download_url=$2
+
+    if [ ! -d "$path" ]; then
+        mkdir -p "$path" || {
+            echo "Failed to create directory: $path"
+            exit 1
+        }
+    fi
+
+    # Strip query/fragment so the on-disk name matches the server-side artifact.
+    local filename tmp
+    filename=$(basename "${download_url%%[?#]*}")
+    local dest="${path}/${filename}"
+    # mktemp, not $$: a PID is shared by concurrent background subshells.
+    tmp=$(mktemp -- "${path}/.${filename}.XXXXXX") || {
+        echo "Failed to create a temporary file in $path"
+        return 1
+    }
+
     if command_available "curl"; then
-        if [ ! -d "$path" ]; then
-            mkdir -p "$path" || {
-                echo "Failed to create directory: $path"
-                exit 1
-            }
+        # -o must appear before -- so it is parsed as an option, not an extra URL.
+        if curl -fL -o "$tmp" -- "${download_url}"; then
+            mv -f -- "$tmp" "$dest" || { rm -f -- "$tmp"; return 1; }
+        else
+            rm -f -- "$tmp"
+            return 1
         fi
-        curl -L "${download_url}" -o "${path}/$(basename "${download_url}")"
     elif command_available "wget"; then
-        wget --help | grep -q '\--show-progress' && progress_opt="-q --show-progress" || progress_opt=""
-        wget "${download_url}" -P "${path}" $progress_opt
+        local -a progress_opt=()
+        if wget --help 2>&1 | grep -q -- '--show-progress'; then
+            progress_opt=(-q --show-progress)
+        fi
+        if wget ${progress_opt[@]+"${progress_opt[@]}"} -O "$tmp" -- "${download_url}"; then
+            mv -f -- "$tmp" "$dest" || { rm -f -- "$tmp"; return 1; }
+        else
+            rm -f -- "$tmp"
+            return 1
+        fi
     else
         echo "Required curl or wget but they are unavailable"
         exit 1
@@ -341,19 +569,17 @@
     local tar=$3
     local link=$4
 
-    if [ ! -d "${path}"/"${dir}" ]; then
-        if [ ! -f "${path}"/"${tar}" ]; then
+    if [ ! -d "${path}/${dir}" ]; then
+        if [ ! -f "${path}/${tar}" ]; then
             echo "Downloading the compressed package '${tar}'"
-            download "${path}" "${link}"
-            if [ $? -ne 0 ]; then
+            if ! download "${path}" "${link}"; then
                 echo "Failed to download, please ensure the network is available and link is valid"
                 exit 1
             fi
             echo "[OK] Finished download"
         fi
         echo "Unzip the compressed package '$tar'"
-        tar zxvf "${path}"/"${tar}" -C "${path}" >/dev/null 2>&1
-        if [ $? -ne 0 ]; then
+        if ! tar -zxvf "${path}/${tar}" -C "${path}" >/dev/null 2>&1; then
             echo "Failed to unzip, please check the compressed package"
             exit 1
         fi
@@ -363,6 +589,10 @@
 
 ###########################################################################
 
+# TODO(wait_for_shutdown): this loop has the same fixed-2s-then-check-the-clock
+# shape wait_for_startup had, so it can also overrun its timeout by roughly one
+# sleep interval.  Left alone here to keep this PR to the port preflight; the
+# fix is the same remaining-deadline cap used above.
 function wait_for_shutdown() {
     local process_name="$1"
     local pid="$2"
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh
new file mode 100755
index 0000000..e895035
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh
@@ -0,0 +1,677 @@
+#!/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.
+#
+
+# Contract tests for the startup port preflight in bin/util.sh.
+#
+# The preflight is best effort: the server's own bind is authoritative.  These
+# tests pin the three-state contract (busy / free / unknown) rather than the
+# internals of any one probe.
+#
+# TODO(test-check-port): the Linux and BSD detection branches are both driven
+# by mocked tool output, so on any one runner only the host's own branch is
+# ever exercised against a real kernel.  The single real-listener case covers
+# whichever OS the job runs on.  Closing this needs the suite to run on both
+# a Linux and a macOS runner, which CI already does for the server job.
+
+set -u
+
+STATIC_DIR="${1:-hugegraph-server/hugegraph-dist/src/assembly/static}"
+UTIL_SH="$STATIC_DIR/bin/util.sh"
+STORE_UTIL_SH="${2:-}"
+
+if [[ ! -f "$UTIL_SH" ]]; then
+    if [[ $# -gt 0 ]]; then
+        echo "ERROR: required util.sh not found at $UTIL_SH" >&2
+        exit 1
+    fi
+    # With no argument this script may be discovered outside the repository by
+    # an optional test runner.  CI always supplies STATIC_DIR explicitly, so a
+    # bad workflow path takes the required failure branch above.
+    echo "SKIP: default util.sh not found at $UTIL_SH"
+    exit 0
+fi
+
+if [[ -n "$STORE_UTIL_SH" && ! -f "$STORE_UTIL_SH" ]]; then
+    echo "ERROR: required Store util.sh not found at $STORE_UTIL_SH" >&2
+    exit 1
+fi
+
+# shellcheck source=/dev/null
+source "$UTIL_SH"
+
+# Sections run in subshells so their command overrides stay isolated, which
+# means results have to be tallied through a file rather than a variable.
+RESULTS=$(mktemp)
+# Clean the tally on any exit, not only the normal one.  Section 5 runs outside
+# a subshell and has to extend this trap, so it restores this handler rather
+# than clearing it.
+trap 'rm -f "$RESULTS"' EXIT
+
+pass() {
+    echo "  PASS  $1"
+    echo "P" >> "$RESULTS"
+}
+
+fail() {
+    echo "  FAIL  $1"
+    echo "        $2"
+    echo "F" >> "$RESULTS"
+}
+
+expect() {
+    # expect <name> <expected> <actual>
+    if [[ "$2" == "$3" ]]; then
+        pass "$1"
+    else
+        fail "$1" "expected '$2', got '$3'"
+    fi
+}
+
+echo ""
+echo "check_port contract tests ($UTIL_SH)"
+
+# --------------------------------------------------------------------------
+echo ""
+echo "1. URL to port"
+# "<url>|<expected>", where SKIP means "no usable port, preflight is skipped".
+url_cases=(
+    'http://127.0.0.1:8080|8080'
+    'HTTP://127.0.0.1:8080|8080'
+    'http://127.0.0.1|80'
+    'https://127.0.0.1|443'
+    '127.0.0.1:8080|8080'
+    'http://127.0.0.1:8080/path:9090|8080'
+    'http://127.0.0.1:8080?probe=x|8080'
+    'http://127.0.0.1:8080#frag|8080'
+    'http://user:pass@127.0.0.1:8080|8080'
+    'http://user@127.0.0.1:8080|8080'
+    'http://[::1]:8080|8080'
+    'http://user:pass@[::1]:8080|8080'
+    '[::1]:8080|8080'
+    'https://[::1]|443'
+    'http://127.0.0.1:08080|8080'
+    '::1:8080|SKIP'
+    'http://127.0.0.1:abc|SKIP'
+    'http://127.0.0.1:0|SKIP'
+    'http://127.0.0.1:70000|SKIP'
+    '127.0.0.1|SKIP'
+    # Oversized values must be rejected on their digits, not after a 64-bit
+    # arithmetic conversion that would wrap them back into range.
+    'http://127.0.0.1:18446744073709551617|SKIP'
+    'http://127.0.0.1:18446744073709559616|SKIP'
+    'http://127.0.0.1:0000000000000008080|8080'
+    'http://127.0.0.1:00000|SKIP'
+)
+for case in "${url_cases[@]}"; do
+    url="${case%|*}"
+    want="${case##*|}"
+    got=$(parse_port_from_url "  $url  " 2>/dev/null) || got="SKIP"
+    expect "$url" "$want" "$got"
+done
+
+# --------------------------------------------------------------------------
+echo ""
+echo "2. Linux: ss busy / free / failure, and the netstat fallback"
+(
+    uname() { echo "Linux"; }
+    command_available() { [[ "$1" == "ss" ]]; }
+    WORK=$(mktemp -d)
+    trap 'rm -rf "$WORK"' EXIT
+    SS_ARGS_FILE="$WORK/ss-args"
+    SS_OUT=""
+    SS_RC=0
+    SS_FILTER_LISTEN=0
+    ss() {
+        echo "$*" > "$SS_ARGS_FILE"
+        [[ $# -eq 2 && "$1" == "-H" && "$2" == "-ltn" ]] || return 64
+        if [[ "$SS_FILTER_LISTEN" -eq 1 ]]; then
+            echo "$SS_OUT" | awk '$1 == "LISTEN"'
+        else
+            printf '%s' "$SS_OUT"
+        fi
+        return "$SS_RC"
+    }
+
+    SS_OUT='LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:*'
+    expect "listener on 8080 is busy" "busy" "$(port_listen_state 8080)"
+    expect "ss receives the exact listener-table arguments" \
+           "-H -ltn" "$(cat "$SS_ARGS_FILE")"
+
+    # Model the filtering done by `ss -l`: an established connection may use
+    # the target as its local ephemeral port, but it is not a listener.  The
+    # mock refuses changed argv, so dropping -l makes this unknown, not free.
+    SS_FILTER_LISTEN=1
+    SS_OUT='ESTAB 0 0 127.0.0.1:8080 127.0.0.1:443'
+    expect "Linux non-LISTEN local port is free" "free" "$(port_listen_state 8080)"
+    SS_FILTER_LISTEN=0
+
+    SS_OUT='LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*'
+    expect "no listener on 8080 is free" "free" "$(port_listen_state 8080)"
+
+    # The port must come from the local-address field, not an IPv6 hextet.
+    SS_OUT='LISTEN 0 128 [2001:db8:8080::1]:9090 [::]:*'
+    expect "IPv6 hextet 8080 is not the port" "free" "$(port_listen_state 8080)"
+
+    SS_OUT='LISTEN 0 128 [::]:8080 [::]:*'
+    expect "IPv6 wildcard listener is busy" "busy" "$(port_listen_state 8080)"
+
+    # A tool that runs but fails proves nothing.
+    SS_OUT=''
+    SS_RC=1
+    expect "ss failure is unknown" "unknown" "$(port_listen_state 8080)"
+
+    # `-H` prints no header, so a zero exit with no output is a positive "this
+    # host has no TCP listeners" - the state a fresh container starts in.
+    SS_RC=0
+    SS_OUT=''
+    expect "ss empty output is free" "free" "$(port_listen_state 8080)"
+
+    # Success with an unusable table proves nothing.
+    SS_OUT='some diagnostic banner'
+    expect "ss unparseable output is unknown" "unknown" "$(port_listen_state 8080)"
+
+    # ...and it must not end the search either: netstat may still have a
+    # readable table.  Returning on the first probe left the port "unknown"
+    # even when the fallback could have answered.
+    command_available() { [[ "$1" == "ss" || "$1" == "netstat" ]]; }
+    netstat() {
+        echo 'Active Internet connections (only servers)'
+        echo 'Proto Recv-Q Send-Q Local Address  Foreign Address  State'
+        echo 'tcp        0      0 0.0.0.0:8080   0.0.0.0:*        LISTEN'
+    }
+
+    SS_OUT='some diagnostic banner'
+    expect "unparseable ss falls back to netstat" "busy" "$(port_listen_state 8080)"
+    expect "netstat fallback can also report free" "free" "$(port_listen_state 9999)"
+
+    SS_RC=1
+    SS_OUT=''
+    expect "failed ss falls back to netstat" "busy" "$(port_listen_state 8080)"
+
+)
+
+# --------------------------------------------------------------------------
+echo ""
+echo "3. macOS/BSD: LISTEN versus ESTABLISHED"
+(
+    uname() { echo "Darwin"; }
+    command_available() { [[ "$1" == "netstat" ]]; }
+    NS_OUT=""
+    netstat() { printf '%s' "$NS_OUT"; }
+
+    NS_OUT='Active Internet connections (including servers)
+Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
+tcp4       0      0  *.8080                 *.*                    LISTEN'
+    expect "BSD LISTEN on 8080 is busy" "busy" "$(port_listen_state 8080)"
+
+    # An outbound connection to :443 is not a local listener on 443.  This is
+    # the false positive that blocked startup on macOS.
+    NS_OUT='Active Internet connections (including servers)
+Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
+tcp4       0      0  192.168.1.3.60320      44.195.16.138.443      ESTABLISHED
+tcp4       0      0  *.22                   *.*                    LISTEN'
+    expect "BSD ESTABLISHED peer :443 is free" "free" "$(port_listen_state 443)"
+
+    # A socket lingering in TIME_WAIT holds the port in its *local* address but
+    # is not a listener, so only the connection state can tell them apart.
+    NS_OUT='Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
+tcp4       0      0  127.0.0.1.8080         127.0.0.1.51000        TIME_WAIT
+tcp4       0      0  *.22                   *.*                    LISTEN'
+    expect "BSD TIME_WAIT on local 8080 is free" "free" "$(port_listen_state 8080)"
+
+    NS_OUT='Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
+tcp6       0      0  ::1.8080               *.*                    LISTEN'
+    expect "BSD IPv6 LISTEN is busy" "busy" "$(port_listen_state 8080)"
+
+    NS_OUT='Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)'
+    expect "BSD header only is unknown" "unknown" "$(port_listen_state 8080)"
+
+)
+
+# --------------------------------------------------------------------------
+echo ""
+echo "4. Unknown never blocks startup"
+(
+    uname() { echo "Linux"; }
+    command_available() { return 1; }
+
+    expect "no probe tool is unknown" "unknown" "$(port_listen_state 8080)"
+
+    # check_port must warn and let the server perform the authoritative bind.
+    err=$( (check_port "http://127.0.0.1:8080") 2>&1 >/dev/null )
+    rc=$?
+    expect "unknown exits 0" "0" "$rc"
+    if [[ "$err" == *"could not determine"* ]]; then
+        pass "unknown warns on stderr"
+    else
+        fail "unknown warns on stderr" "stderr was: $err"
+    fi
+
+)
+
+# --------------------------------------------------------------------------
+echo ""
+echo "5. Real listener on an ephemeral port"
+if command -v python3 >/dev/null 2>&1; then
+    PORT_FILE=$(mktemp)
+    # Cover the temp file from the moment it exists, then widen the trap to the
+    # child as soon as there is a pid; registering only after the fork leaves a
+    # window where an interrupt orphans the listener for its full 30s sleep.
+    trap 'rm -f "$PORT_FILE" "$RESULTS"' EXIT
+    python3 -c '
+import socket, sys, time
+s = socket.socket()
+s.bind(("127.0.0.1", 0))
+s.listen(1)
+print(s.getsockname()[1])
+sys.stdout.flush()
+time.sleep(30)
+' > "$PORT_FILE" &
+    PY_PID=$!
+    trap 'kill "$PY_PID" 2>/dev/null; rm -f "$PORT_FILE" "$RESULTS"' EXIT
+
+    BOUND=""
+    for _ in 1 2 3 4 5 6 7 8 9 10; do
+        BOUND=$(head -1 "$PORT_FILE" 2>/dev/null)
+        [[ -n "$BOUND" ]] && break
+        # Whole seconds only: fractional sleep is not POSIX, and this suite runs
+        # with `set -u` but no `-e`, so a busybox sleep would fail silently and
+        # spin all ten iterations instantly.
+        sleep 1
+    done
+
+    if [[ -z "$BOUND" ]] || ! kill -0 "$PY_PID" 2>/dev/null; then
+        fail "listener bound an ephemeral port" "child did not report a port"
+    else
+        pass "listener bound an ephemeral port ($BOUND)"
+        expect "bound port is busy" "busy" "$(port_listen_state "$BOUND")"
+        (check_port "http://127.0.0.1:$BOUND" >/dev/null 2>&1)
+        expect "check_port exits 1 on the bound port" "1" "$?"
+
+        # A port that was bound and released must not read as busy.
+        FREE=$(python3 -c 'import socket
+s = socket.socket()
+s.bind(("127.0.0.1", 0))
+p = s.getsockname()[1]
+s.close()
+print(p)')
+        expect "released port is not busy" "0" "$([[ "$(port_listen_state "$FREE")" == "busy" ]] && echo 1 || echo 0)"
+    fi
+
+    kill "$PY_PID" 2>/dev/null
+    wait "$PY_PID" 2>/dev/null
+    rm -f "$PORT_FILE"
+    # Restore the script-level handler rather than clearing it, so the tally
+    # file stays covered for the remaining sections.
+    trap 'rm -f "$RESULTS"' EXIT
+else
+    echo "  SKIP  real listener test: python3 not available"
+fi
+
+# --------------------------------------------------------------------------
+echo ""
+echo "6. download() rename failure is reported"
+(
+    WORK=$(mktemp -d)
+    trap 'rm -rf "$WORK"' EXIT
+
+    command_available() { [[ "$1" == "curl" ]]; }
+    curl() {
+        # Write to the -o destination so a temp file exists to rename.
+        local out=""
+        while [[ $# -gt 0 ]]; do
+            [[ "$1" == "-o" ]] && { out="$2"; shift; }
+            shift
+        done
+        echo "payload" > "$out"
+        return 0
+    }
+    # Simulate a rename that fails (read-only target, cross-device, ...).
+    mv() { return 1; }
+
+    if download "$WORK" "https://example.com/pkg.tar.gz"; then
+        fail "download reports rename failure" "download returned 0 after mv failed"
+    else
+        pass "download reports rename failure"
+    fi
+
+    if [[ -e "$WORK/pkg.tar.gz" ]]; then
+        fail "failed rename leaves no destination" "$WORK/pkg.tar.gz exists"
+    else
+        pass "failed rename leaves no destination"
+    fi
+
+    leftover=$(find "$WORK" -name '.pkg.tar.gz.*' 2>/dev/null | wc -l | tr -d ' ')
+    expect "failed rename cleans its temp file" "0" "$leftover"
+
+)
+
+# --------------------------------------------------------------------------
+echo ""
+echo "7. Startup probe is bounded"
+(
+    WORK=$(mktemp -d)
+    cd "$WORK" || exit 1
+    trap 'cd /; rm -rf "$WORK"' EXIT
+
+    CLOCK_FILE="$WORK/clock"
+    ARGS_FILE="$WORK/curl-args"
+    SLEEP_FILE="$WORK/sleep-args"
+    echo 100 > "$CLOCK_FILE"
+    : > "$ARGS_FILE"
+    : > "$SLEEP_FILE"
+    curl() { echo "$*" >> "$ARGS_FILE"; echo "000"; return 28; }
+    # Keep the contract checks deterministic across whole-second boundaries.
+    # The retry pause consumes the remaining simulated second without making
+    # this half of the test depend on scheduler timing.
+    date() { cat "$CLOCK_FILE"; }
+    sleep() {
+        echo "$1" >> "$SLEEP_FILE"
+        echo 101 > "$CLOCK_FILE"
+    }
+    process_status() { return 0; }
+    ps() { return 0; }
+
+    TIMEOUT_S=1
+    if wait_for_startup "$$" "test-server" "http://127.0.0.1:1" "$TIMEOUT_S" \
+                        >/dev/null 2>&1; then
+        WAIT_STATUS=0
+    else
+        WAIT_STATUS=$?
+    fi
+
+    expect "failed startup probe returns failure" "1" "$WAIT_STATUS"
+
+    # The deterministic half: bounding each curl does not bound the loop, which
+    # used to sleep a flat 2s and only then re-read the clock.  A 1s timeout
+    # must never ask for more than 1s of sleeping.
+    TOTAL_SLEEP_S=$(awk '{ total += $1 } END { print total + 0 }' "$SLEEP_FILE")
+    if [[ "$TOTAL_SLEEP_S" -le "$TIMEOUT_S" ]]; then
+        pass "retry sleeps stay inside the ${TIMEOUT_S}s deadline"
+    else
+        fail "retry sleeps stay inside the ${TIMEOUT_S}s deadline" \
+             "slept ${TOTAL_SLEEP_S}s in total: $(tr '\n' ' ' < "$SLEEP_FILE")"
+    fi
+
+    # With a one-second overall deadline, every positive remaining budget is
+    # exactly one second.  Parse each curl call so option presence, values, and
+    # the deadline bound are all part of the contract rather than string-grep
+    # smoke checks.
+    CURL_CALLS=0
+    TIMEOUT_VALUES_VALID=1
+    TIMEOUT_ERROR=""
+    while IFS= read -r curl_args; do
+        [[ -z "$curl_args" ]] && continue
+        CURL_CALLS=$((CURL_CALLS + 1))
+        CONNECT_TIMEOUT=""
+        MAX_TIME=""
+        set -- $curl_args
+        while [[ $# -gt 0 ]]; do
+            case "$1" in
+                --connect-timeout)
+                    shift
+                    [[ $# -gt 0 ]] && CONNECT_TIMEOUT="$1"
+                    ;;
+                --max-time)
+                    shift
+                    [[ $# -gt 0 ]] && MAX_TIME="$1"
+                    ;;
+            esac
+            [[ $# -gt 0 ]] && shift
+        done
+
+        if [[ ! "$CONNECT_TIMEOUT" =~ ^[0-9]+$ ||
+              ! "$MAX_TIME" =~ ^[0-9]+$ ||
+              "$CONNECT_TIMEOUT" -le 0 || "$CONNECT_TIMEOUT" -gt "$TIMEOUT_S" ||
+              "$MAX_TIME" -le 0 || "$MAX_TIME" -gt "$TIMEOUT_S" ]]; then
+            TIMEOUT_VALUES_VALID=0
+            TIMEOUT_ERROR="invalid timeouts connect='$CONNECT_TIMEOUT' max='$MAX_TIME' in: $curl_args"
+            break
+        fi
+    done < "$ARGS_FILE"
+
+    if [[ "$CURL_CALLS" -gt 0 && "$TIMEOUT_VALUES_VALID" -eq 1 ]]; then
+        pass "startup probe timeouts are positive and within the remaining deadline"
+    else
+        [[ -n "$TIMEOUT_ERROR" ]] || TIMEOUT_ERROR="no curl calls recorded"
+        fail "startup probe timeouts are positive and within the remaining deadline" \
+             "$TIMEOUT_ERROR"
+    fi
+
+    # A probe may consume all of its assigned budget.  Refreshing the clock
+    # after curl must then suppress the retry pause rather than sleep beyond
+    # the overall deadline.
+    EXHAUST_SLEEP_FILE="$WORK/exhausted-sleep-calls"
+    echo 100 > "$CLOCK_FILE"
+    : > "$EXHAUST_SLEEP_FILE"
+    curl() {
+        echo 101 > "$CLOCK_FILE"
+        echo "000"
+        return 28
+    }
+    sleep() { echo "$1" >> "$EXHAUST_SLEEP_FILE"; }
+    if wait_for_startup "$$" "budget-server" "http://127.0.0.1:1" \
+                        "$TIMEOUT_S" >/dev/null 2>&1; then
+        EXHAUST_STATUS=0
+    else
+        EXHAUST_STATUS=$?
+    fi
+
+    expect "budget-exhausting probe returns failure" "1" "$EXHAUST_STATUS"
+    EXHAUST_SLEEP_CALLS=$(wc -l < "$EXHAUST_SLEEP_FILE" | tr -d ' ')
+    expect "budget-exhausting probe starts no retry sleep" \
+           "0" "$EXHAUST_SLEEP_CALLS"
+
+    # Keep a small real-clock smoke test for the end-to-end deadline.  Crossing
+    # an integer-second boundary between the initial and pre-probe clock reads
+    # may legitimately produce zero curl calls, so this half checks only the
+    # return status and wall-clock bound and does not require an args file.
+    date() { command date "$@"; }
+    curl() { echo "000"; return 28; }
+    sleep() { command sleep "$1"; }
+    START_S=$(date '+%s')
+    if wait_for_startup "$$" "wall-clock-server" "http://127.0.0.1:1" \
+                        "$TIMEOUT_S" >/dev/null 2>&1; then
+        WALL_STATUS=0
+    else
+        WALL_STATUS=$?
+    fi
+    ELAPSED_S=$(( $(date '+%s') - START_S ))
+
+    expect "real-clock failed startup probe returns failure" "1" "$WALL_STATUS"
+    # One second of slack accounts for the granularity of date '+%s'.
+    if [[ "$ELAPSED_S" -le $((TIMEOUT_S + 1)) ]]; then
+        pass "wait_for_startup returns within the deadline (${ELAPSED_S}s)"
+    else
+        fail "wait_for_startup returns within the deadline" \
+             "took ${ELAPSED_S}s for a ${TIMEOUT_S}s timeout"
+    fi
+
+)
+
+# --------------------------------------------------------------------------
+echo ""
+echo "8. Deadline boundary starts no probe"
+(
+    WORK=$(mktemp -d)
+    cd "$WORK" || exit 1
+    trap 'cd /; rm -rf "$WORK"' EXIT
+
+    CLOCK_FILE="$WORK/clock-calls"
+    CURL_FILE="$WORK/curl-calls"
+    echo 0 > "$CLOCK_FILE"
+    : > "$CURL_FILE"
+
+    # The first read establishes [100, 101) as the allowed interval.  The
+    # process check consumes that interval, so the mandatory pre-curl refresh
+    # observes the deadline exactly.  File-backed state works through the
+    # command substitutions used by wait_for_startup, including Bash 3.2.
+    date() {
+        local calls
+        calls=$(cat "$CLOCK_FILE")
+        calls=$((calls + 1))
+        echo "$calls" > "$CLOCK_FILE"
+        if [[ "$calls" -eq 1 ]]; then
+            echo 100
+        else
+            echo 101
+        fi
+    }
+    process_status() { return 0; }
+    curl() { echo called >> "$CURL_FILE"; echo "000"; return 28; }
+    sleep() { echo called >> "$WORK/sleep-calls"; }
+
+    wait_for_startup "$$" "boundary-server" "http://127.0.0.1:1" 1 \
+                     >/dev/null 2>&1
+
+    CLOCK_CALLS=$(cat "$CLOCK_FILE")
+    if [[ "$CLOCK_CALLS" -ge 2 ]]; then
+        pass "startup refreshes the clock at the probe boundary"
+    else
+        fail "startup refreshes the clock at the probe boundary" \
+             "clock was read only ${CLOCK_CALLS} time(s)"
+    fi
+
+    PROBE_CALLS=$(wc -l < "$CURL_FILE" | tr -d ' ')
+    expect "no startup probe begins at the deadline" "0" "$PROBE_CALLS"
+
+)
+
+if [[ -n "$STORE_UTIL_SH" ]]; then
+    # ----------------------------------------------------------------------
+    echo ""
+    echo "9. Store verified replacement survives a stale concurrent caller"
+    (
+        # Keep the Store helper overrides isolated from the server helpers used
+        # by every other section in this suite.
+        # shellcheck source=/dev/null
+        source "$STORE_UTIL_SH"
+
+        WORK=$(mktemp -d)
+        DEST="$WORK/lib.so"
+        HASHED="$WORK/stale-hashed"
+        RELEASE="$WORK/release-stale"
+        STALE_DOWNLOAD="$WORK/stale-download"
+        STALE_PID=""
+        trap '
+            touch "$RELEASE"
+            if [[ -n "$STALE_PID" ]]; then
+                kill "$STALE_PID" 2>/dev/null
+                wait "$STALE_PID" 2>/dev/null
+            fi
+            rm -rf "$WORK"
+        ' EXIT
+
+        echo "invalid" > "$DEST"
+
+        wait_for_file() {
+            local path="$1"
+            local _
+            for _ in 1 2 3 4 5 6 7 8 9 10; do
+                [[ -e "$path" ]] && return 0
+                command sleep 1
+            done
+            return 1
+        }
+
+        # Both callers first observe the invalid destination.  The stale caller
+        # then holds that checksum until the installer has atomically published
+        # valid bytes.  Reintroducing the old pre-download rm makes the stale
+        # caller delete that valid replacement before its own download fails.
+        md5sum() {
+            local target="${@: -1}"
+            if [[ "$target" == "$DEST" ]]; then
+                if [[ "$CALLER" == "stale" ]]; then
+                    touch "$HASHED"
+                    wait_for_file "$RELEASE" || return 1
+                fi
+                echo "bad  $target"
+            else
+                echo "good  $target"
+            fi
+        }
+
+        curl() {
+            [[ "$1" == "-fL" && "$2" == "-o" && "$4" == "--" ]] || return 64
+            if [[ "$CALLER" == "installer" ]]; then
+                echo "valid" > "$3"
+                return 0
+            fi
+            touch "$STALE_DOWNLOAD"
+            return 22
+        }
+
+        (
+            CALLER="stale"
+            download_and_verify "https://example.com/lib.so" "$DEST" "good" \
+                                >/dev/null 2>&1
+        ) &
+        STALE_PID=$!
+
+        if ! wait_for_file "$HASHED"; then
+            fail "stale caller captured the original checksum" \
+                 "caller did not reach the checksum barrier"
+            exit 0
+        fi
+        pass "stale caller captured the original checksum"
+
+        CALLER="installer"
+        if download_and_verify "https://example.com/lib.so" "$DEST" "good" \
+                               >/dev/null 2>&1; then
+            pass "concurrent installer succeeds"
+        else
+            fail "concurrent installer succeeds" "installer returned nonzero"
+        fi
+
+        touch "$RELEASE"
+        wait "$STALE_PID" 2>/dev/null
+        STALE_WAIT_RC=$?
+        STALE_PID=""
+        if [[ "$STALE_WAIT_RC" -ne 0 && -e "$STALE_DOWNLOAD" ]]; then
+            pass "stale caller reports its failed replacement download"
+        else
+            CURL_REACHED=$([[ -e "$STALE_DOWNLOAD" ]] && echo yes || echo no)
+            fail "stale caller reports its failed replacement download" \
+                 "rc=$STALE_WAIT_RC, curl reached=$CURL_REACHED"
+        fi
+
+        if [[ -f "$DEST" && "$(cat "$DEST")" == "valid" ]]; then
+            pass "failed stale caller preserves the valid replacement"
+        else
+            fail "failed stale caller preserves the valid replacement" \
+                 "destination is missing or no longer contains valid bytes"
+        fi
+
+        LEFTOVERS=$(find "$WORK" -name 'lib.so.*' -type f | wc -l | tr -d ' ')
+        expect "concurrent replacement cleans private temp files" "0" "$LEFTOVERS"
+    )
+fi
+
+# --------------------------------------------------------------------------
+echo ""
+echo "----------------------------------------"
+# grep -c prints 0 and exits 1 when there are no matches; the count is what
+# matters here, so the exit status is deliberately ignored.
+PASSED=$(grep -c '^P' "$RESULTS" 2>/dev/null)
+FAILED=$(grep -c '^F' "$RESULTS" 2>/dev/null)
+rm -f "$RESULTS"
+echo "passed: $PASSED   failed: $FAILED"
+if [[ "$FAILED" -gt 0 ]]; then
+    exit 1
+fi
+exit 0
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh
index 260e7f5..ab73255 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh
@@ -30,6 +30,7 @@
 PD_ROOT="${1:-$(pwd)}"
 BIN="$PD_ROOT/bin"
 START_SCRIPT="$BIN/start-hugegraph-pd.sh"
+STOP_SCRIPT="$BIN/stop-hugegraph-pd.sh"
 PID_FILE="$BIN/pid"
 PD_URL="http://localhost:8620"
 STARTUP_WAIT=60   # seconds to wait for PD HTTP to respond
@@ -67,14 +68,30 @@
 
 cleanup() {
     info "Cleaning up..."
+    "$STOP_SCRIPT" >/dev/null 2>&1 || true
     if [[ -s "$PID_FILE" ]]; then
         kill "$(cat "$PID_FILE")" 2>/dev/null || true
     fi
     rm -f "$PID_FILE"
     rm -rf "$PD_ROOT/logs/"
-    # kill anything still holding the PD port
-    lsof -ti :8620 | xargs kill -9 2>/dev/null || true
-    lsof -ti :8686 | xargs kill -9 2>/dev/null || true
+    # kill anything still holding the PD port: fuser on Linux, lsof on macOS,
+    # which ships no fuser.  Only the Linux path had to lose its lsof dependency.
+    if [[ "$(uname)" == "Darwin" ]]; then
+        local port pids pid
+        for port in 8620 8686; do
+            pids=$(lsof -ti ":$port" 2>/dev/null)
+            if [[ -n "$pids" ]]; then
+                for pid in $pids; do
+                    kill -9 "$pid" 2>/dev/null || true
+                done
+            fi
+        done
+    elif command -v fuser >/dev/null 2>&1; then
+        fuser -k 8620/tcp 2>/dev/null || true
+        fuser -k 8686/tcp 2>/dev/null || true
+    else
+        echo "Warning: Neither lsof nor fuser available for PD port cleanup"
+    fi
     sleep 3
 }
 
@@ -129,7 +146,12 @@
     exit 1
 fi
 
-for tool in lsof curl java; do
+if [[ "$(uname)" == "Darwin" ]]; then
+    _prereq_tools="lsof curl java"
+else
+    _prereq_tools="fuser curl java"
+fi
+for tool in $_prereq_tools; do
     if ! command -v "$tool" >/dev/null 2>&1; then
         echo "SKIP: required tool '$tool' not found — skipping test suite"
         exit 77
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-store.sh
index 7e11da2..9ff324a 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-store.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-store.sh
@@ -51,15 +51,30 @@
 
 cleanup() {
     info "Cleaning up..."
+    "$STOP_SCRIPT" >/dev/null 2>&1 || true
     if [[ -s "$PID_FILE" ]]; then
         kill "$(cat "$PID_FILE")" 2>/dev/null || true
     fi
     rm -f "$PID_FILE"
     rm -rf "$STORE_ROOT/logs/"
     # kill anything holding Store ports (8520 REST, 8510 raft, 8500 gRPC)
-    lsof -ti :8520 | xargs kill -9 2>/dev/null || true
-    lsof -ti :8510 | xargs kill -9 2>/dev/null || true
-    lsof -ti :8500 | xargs kill -9 2>/dev/null || true
+    if [[ "$(uname)" == "Darwin" ]]; then
+        local port pids pid
+        for port in 8520 8510 8500; do
+            pids=$(lsof -ti ":$port" 2>/dev/null)
+            if [[ -n "$pids" ]]; then
+                for pid in $pids; do
+                    kill -9 "$pid" 2>/dev/null || true
+                done
+            fi
+        done
+    elif command -v fuser >/dev/null 2>&1; then
+        fuser -k 8520/tcp 2>/dev/null || true
+        fuser -k 8510/tcp 2>/dev/null || true
+        fuser -k 8500/tcp 2>/dev/null || true
+    else
+        echo "Warning: Neither lsof nor fuser available for Store port cleanup"
+    fi
     sleep 3
 }
 
@@ -110,7 +125,12 @@
     exit 1
 fi
 
-for tool in lsof curl java; do
+if [[ "$(uname)" == "Darwin" ]]; then
+    _prereq_tools="lsof curl java"
+else
+    _prereq_tools="fuser curl java"
+fi
+for tool in $_prereq_tools; do
     if ! command -v "$tool" >/dev/null 2>&1; then
         echo "SKIP: required tool '$tool' not found — skipping test suite"
         exit 77
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh
index 353cab6..9f0bcfa 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh
@@ -76,10 +76,27 @@
         kill "$(cat "$PID_FILE")" 2>/dev/null || true
     fi
     rm -f "$PID_FILE"
-    # kill anything still holding server ports so check_port doesn't fail next test
-    lsof -ti :8080 | xargs kill -9 2>/dev/null || true
-    lsof -ti :8182 | xargs kill -9 2>/dev/null || true
-    lsof -ti :8088 | xargs kill -9 2>/dev/null || true
+    # kill anything still holding server ports: fuser on Linux, lsof on macOS,
+    # which ships no fuser.  Only the Linux path had to lose its lsof dependency.
+    if [[ "$(uname)" == "Darwin" ]]; then
+        local port pids pid
+        for port in 8080 8182 8088; do
+            pids=$(lsof -ti ":$port" 2>/dev/null)
+            if [[ -n "$pids" ]]; then
+                for pid in $pids; do
+                    kill -9 "$pid" 2>/dev/null || true
+                done
+            fi
+        done
+    elif command -v fuser >/dev/null 2>&1; then
+        # fuser exists and should work on Linux; suppress failures but log if debug needed
+        fuser -k 8080/tcp 2>/dev/null || true
+        fuser -k 8182/tcp 2>/dev/null || true
+        fuser -k 8088/tcp 2>/dev/null || true
+    else
+        # Neither lsof nor fuser available; ports may remain occupied
+        echo "Warning: Neither lsof nor fuser available for port cleanup"
+    fi
     sleep 3
     crontab -l 2>/dev/null | grep -v monitor-hugegraph | crontab - 2>/dev/null || true
 }
@@ -145,7 +162,12 @@
     exit 1
 fi
 
-for tool in lsof crontab curl java; do
+if [[ "$(uname)" == "Darwin" ]]; then
+    _prereq_tools="lsof crontab curl java"
+else
+    _prereq_tools="fuser crontab curl java"
+fi
+for tool in $_prereq_tools; do
     if ! command -v "$tool" >/dev/null 2>&1; then
         echo "SKIP: required tool '$tool' not found — skipping test suite"
         exit 77
@@ -155,7 +177,8 @@
 # start-monitor.sh requires JAVA_HOME
 if [[ -z "${JAVA_HOME:-}" ]]; then
     if command -v /usr/libexec/java_home >/dev/null 2>&1; then
-        export JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null)"
+        JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null)"
+        export JAVA_HOME
     fi
 fi
 
diff --git a/hugegraph-store/Dockerfile b/hugegraph-store/Dockerfile
index 49d41f6..f1b9db5 100644
--- a/hugegraph-store/Dockerfile
+++ b/hugegraph-store/Dockerfile
@@ -45,12 +45,14 @@
 WORKDIR /hugegraph-store/
 
 # 1. Install runtime dependencies
+# Note: lsof is gone because its only consumer was the dead check_port removed
+# from hg-store-dist bin/util.sh.  Store runs no port preflight, so unlike the
+# server images this needs no socket-table tool in its place.
 RUN apt-get -q update \
     && apt-get -q install -y --no-install-recommends --no-install-suggests \
        dumb-init \
        procps \
        curl \
-       lsof \
        vim \
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/*
diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
index 5a11eea..88165e4 100755
--- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
+++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
@@ -46,8 +46,8 @@
     lib_file="$TOP/bin/libjemalloc_aarch64.so"
     download_url="${GITHUB}/apache/hugegraph-doc/raw/binary-1.5/dist/server/libjemalloc_aarch64.so"
     expected_md5="2a631d2f81837f9d5864586761c5e380"
-    if download_and_verify $download_url $lib_file $expected_md5; then
-        export LD_PRELOAD=$lib_file
+    if download_and_verify "$download_url" "$lib_file" "$expected_md5"; then
+        export LD_PRELOAD="$lib_file"
     else
         echo "Failed to verify or download $lib_file, skip it"
     fi
@@ -55,8 +55,8 @@
     lib_file="$TOP/bin/libjemalloc.so"
     download_url="${GITHUB}/apache/hugegraph-doc/raw/binary-1.5/dist/server/libjemalloc.so"
     expected_md5="fd61765eec3bfea961b646c269f298df"
-    if download_and_verify $download_url $lib_file $expected_md5; then
-        export LD_PRELOAD=$lib_file
+    if download_and_verify "$download_url" "$lib_file" "$expected_md5"; then
+        export LD_PRELOAD="$lib_file"
     else
         echo "Failed to verify or download $lib_file, skip it"
     fi
@@ -73,7 +73,8 @@
 #export FILE_LIMITN=1024000
 
 function check_evn_limit() {
-    local limit_check=$(ulimit -n)
+    local limit_check
+    limit_check=$(ulimit -n)
     if [[ ${limit_check} != "unlimited" && ${limit_check} -lt ${FILE_LIMITN} ]]; then
         echo -e "${BASH_SOURCE[0]##*/}:${LINENO}:\E[1;32m ulimit -n can open too few maximum file descriptors, need (${FILE_LIMITN})!! \E[0m"
         return 1
@@ -218,7 +219,7 @@
 #  JAVA_OPTIONS="${JAVA_OPTIONS} -javaagent:${LIB}/jmx_prometheus_javaagent-0.16.1.jar=${JMX_EXPORT_PORT}:${CONF}/jmx_exporter.yml"
 #fi
 
-if [ $(ps -ef|grep -v grep| grep java|grep -cE ${CONF}) -ne 0 ]; then
+if [ "$(ps -ef | grep -v grep | grep java | grep -cE "${CONF}")" -ne 0 ]; then
    echo "HugeGraphStoreServer is already running..."
    exit 0
 fi
diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh
index 3b3d660..e45249a 100644
--- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh
+++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh
@@ -19,20 +19,21 @@
 
 function command_available() {
     local cmd=$1
-    if [ `command -v $cmd >/dev/null 2>&1` ]; then
-        return 1
-    else
+    if command -v "$cmd" >/dev/null 2>&1; then
         return 0
     fi
+    return 1
 }
 
 # read a property from .properties file
 function read_property() {
     # file path
+    local file_name
+    local property_name
     file_name=$1
     # replace "." to "\."
-    property_name=`echo $2 | sed 's/\./\\\./g'`
-    cat $file_name | sed -n -e "s/^[ ]*//g;/^#/d;s/^$property_name=//p" | tail -1
+    property_name=$(echo "$2" | sed 's/\./\\\./g')
+    cat "$file_name" | sed -n -e "s/^[ ]*//g;/^#/d;s/^$property_name=//p" | tail -1
 }
 
 function write_property() {
@@ -40,7 +41,7 @@
     local key=$2
     local value=$3
 
-    local os=`uname`
+    local os=$(uname)
     case $os in
         # Note: in mac os should use sed -i '' "xxx" to replace string,
         # otherwise prompt 'command c expects \ followed by text'.
@@ -55,7 +56,7 @@
     local version=$2
     local module=$3
 
-    cat $file | tr -d '\n {}'| awk -F',+|:' '''{
+    cat "$file" | tr -d '\n {}'| awk -F',+|:' '''{
         pre="";
         for(i=1; i<=NF; ) {
             if(match($i, /version/)) {
@@ -71,27 +72,13 @@
 }
 
 function process_num() {
-    num=`ps -ef | grep $1 | grep -v grep | wc -l`
-    return $num
+    num=$(ps -ef | grep "$1" | grep -v grep | wc -l)
+    return "$num"
 }
 
 function process_id() {
-    pid=`ps -ef | grep $1 | grep -v grep | awk '{print $2}'`
-    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
-    fi
-    lsof -i :$port >/dev/null
-    if [ $? -eq 0 ]; then
-        echo "The port $port has already been used"
-        exit 1
-    fi
+    pid=$(ps -ef | grep "$1" | grep -v grep | awk '{print $2}')
+    return "$pid"
 }
 
 function crontab_append() {
@@ -129,13 +116,14 @@
     local server_url="$3"
     local timeout_s="$4"
 
-    local now_s=`date '+%s'`
-    local stop_s=$(( $now_s + $timeout_s ))
+    local now_s
+    now_s=$(date '+%s')
+    local stop_s=$(( now_s + timeout_s ))
 
     local status
 
     echo -n "Connecting to $server_name ($server_url)"
-    while [ $now_s -le $stop_s ]; do
+    while [ "$now_s" -le "$stop_s" ]; do
         echo -n .
         process_status "$server_name" "$pid" >/dev/null
         if [ $? -eq 1 ]; then
@@ -143,14 +131,14 @@
             return 1
         fi
 
-        status=`curl -o /dev/null -s -k -w %{http_code} $server_url`
-        if [[ $status -eq 200 || $status -eq 401 ]]; then
+        status=$(curl -o /dev/null -s -k -w '%{http_code}' "$server_url")
+        if [[ "$status" -eq 200 || "$status" -eq 401 ]]; then
             echo "OK"
             echo "Started [pid $pid]"
             return 0
         fi
         sleep 2
-        now_s=`date '+%s'`
+        now_s=$(date '+%s')
     done
 
     echo "The operation timed out when attempting to connect to $server_url" >&2
@@ -159,22 +147,28 @@
 
 function free_memory() {
     local free=""
-    local os=`uname`
+    local os=$(uname)
     if [ "$os" == "Linux" ]; then
-        local mem_free=`cat /proc/meminfo | grep -w "MemFree" | awk '{print $2}'`
-        local mem_buffer=`cat /proc/meminfo | grep -w "Buffers" | awk '{print $2}'`
-        local mem_cached=`cat /proc/meminfo | grep -w "Cached" | awk '{print $2}'`
+        local mem_free=$(cat /proc/meminfo | grep -w "MemFree" | awk '{print $2}')
+        local mem_buffer=$(cat /proc/meminfo | grep -w "Buffers" | awk '{print $2}')
+        local mem_cached=$(cat /proc/meminfo | grep -w "Cached" | awk '{print $2}')
         if [[ "$mem_free" == "" || "$mem_buffer" == "" || "$mem_cached" == "" ]]; then
             echo "Failed to get free memory"
             exit 1
         fi
-        free=`expr $mem_free + $mem_buffer + $mem_cached`
-        free=`expr $free / 1024`
+        free=$(expr "$mem_free" + "$mem_buffer" + "$mem_cached")
+        free=$(expr "$free" / 1024)
     elif [ "$os" == "Darwin" ]; then
-        local pages_free=`vm_stat | awk '/Pages free/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " "`
-        local pages_inactive=`vm_stat | awk '/Pages inactive/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " "`
-        local pages_available=`expr $pages_free + $pages_inactive`
-        free=`expr $pages_available \* 4096 / 1024 / 1024`
+        local pages_free pages_inactive
+        pages_free=$(vm_stat | awk '/Pages free/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " ")
+        pages_inactive=$(vm_stat | awk '/Pages inactive/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " ")
+        if [[ -z "$pages_free" || -z "$pages_inactive" ]]; then
+            echo "Failed to get free memory"
+            exit 1
+        fi
+        local pages_available
+        pages_available=$(expr "$pages_free" + "$pages_inactive")
+        free=$(expr "$pages_available" \* 4096 / 1024 / 1024)
     else
         echo "Unsupported operating system $os"
         exit 1
@@ -186,8 +180,8 @@
     local min_mem=$1
     local max_mem=$2
     # Get machine available memory
-    local free=`free_memory`
-    local half_free=$[free/2]
+    local free=$(free_memory)
+    local half_free=$((free/2))
 
     local xmx=$min_mem
     if [[ "$free" -lt "$min_mem" ]]; then
@@ -235,47 +229,78 @@
 }
 
 function get_ip() {
-    local os=`uname`
+    local os=$(uname)
     local loopback="127.0.0.1"
     local ip=""
     case $os in
         Linux)
             if command_available "ifconfig"; then
-                ip=`ifconfig | grep 'inet addr:' | grep -v "$loopback" | cut -d: -f2 | awk '{ print $1}'`
+                ip=$(ifconfig | grep 'inet addr:' | grep -v "$loopback" | cut -d: -f2 | awk '{ print $1}')
             elif command_available "ip"; then
-                ip=`ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | awk -F"/" '{print $1}'`
+                ip=$(ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | awk -F"/" '{print $1}')
             else
                 ip=$loopback
             fi
             ;;
         FreeBSD|OpenBSD|Darwin)
             if command_available "ifconfig"; then
-                ip=`ifconfig | grep -E 'inet.[0-9]' | grep -v "$loopback" | awk '{ print $2}'`
+                ip=$(ifconfig | grep -E 'inet.[0-9]' | grep -v "$loopback" | awk '{ print $2}')
             else
                 ip=$loopback
             fi
             ;;
         SunOS)
             if command_available "ifconfig"; then
-                ip=`ifconfig -a | grep inet | grep -v "$loopback" | awk '{ print $2} '`
+                ip=$(ifconfig -a | grep inet | grep -v "$loopback" | awk '{ print $2} ')
             else
                 ip=$loopback
             fi
             ;;
         *) ip=$loopback;;
     esac
-    echo $ip
+    [[ -z "$ip" ]] && ip=$loopback
+    echo "$ip"
 }
 
 function download() {
     local path=$1
     local link_url=$2
 
-    if command_available "wget"; then
-        wget --help | grep -q '\--show-progress' && progress_opt="-q --show-progress" || progress_opt=""
-        wget ${link_url} -P ${path} $progress_opt
-    elif command_available "curl"; then
-        curl ${link_url} -o ${path}/${link_url}
+    if [ ! -d "$path" ]; then
+        mkdir -p "$path" || {
+            echo "Failed to create directory: $path"
+            exit 1
+        }
+    fi
+
+    local filename tmp
+    filename=$(basename "${link_url%%[?#]*}")
+    local dest="${path}/${filename}"
+    # mktemp, not $$: a PID is shared by concurrent background subshells.
+    tmp=$(mktemp -- "${path}/.${filename}.XXXXXX") || {
+        echo "Failed to create a temporary file in $path"
+        return 1
+    }
+
+    if command_available "curl"; then
+        # -o must appear before -- so it is parsed as an option, not an extra URL.
+        if curl -fL -o "$tmp" -- "${link_url}"; then
+            mv -f -- "$tmp" "$dest" || { rm -f -- "$tmp"; return 1; }
+        else
+            rm -f -- "$tmp"
+            return 1
+        fi
+    elif command_available "wget"; then
+        local -a progress_opt=()
+        if wget --help 2>&1 | grep -q -- '--show-progress'; then
+            progress_opt=(-q --show-progress)
+        fi
+        if wget ${progress_opt[@]+"${progress_opt[@]}"} -O "$tmp" -- "${link_url}"; then
+            mv -f -- "$tmp" "$dest" || { rm -f -- "$tmp"; return 1; }
+        else
+            rm -f -- "$tmp"
+            return 1
+        fi
     else
         echo "Required wget or curl but they are unavailable"
         exit 1
@@ -286,14 +311,15 @@
     local url=$1
     local filepath=$2
     local expected_md5=$3
+    local actual_md5
 
-    if [[ -f $filepath ]]; then
+    if [[ -f "$filepath" ]]; then
         echo "File $filepath exists. Verifying MD5 checksum..."
-        actual_md5=$(md5sum $filepath | awk '{ print $1 }')
-        if [[ $actual_md5 != $expected_md5 ]]; then
+        actual_md5=$(md5sum -- "$filepath" | awk '{ print $1 }')
+        if [[ "$actual_md5" != "$expected_md5" ]]; then
             echo "MD5 checksum verification failed for $filepath. Expected: $expected_md5, but got: $actual_md5"
-            echo "Deleting $filepath..."
-            rm -f $filepath
+            # Keep the shared destination until a verified temporary file is
+            # ready; a stale reader must not delete another caller's replacement.
         else
             echo "MD5 checksum verification succeeded for $filepath."
             return 0
@@ -301,11 +327,21 @@
     fi
 
     echo "Downloading $filepath..."
-    curl -L -o $filepath $url
-
-    actual_md5=$(md5sum $filepath | awk '{ print $1 }')
-    if [[ $actual_md5 != $expected_md5 ]]; then
-        echo "MD5 checksum verification failed for $filepath after download. Expected: $expected_md5, but got: $actual_md5"
+    local tmp
+    # mktemp, not $$: a PID is shared by concurrent background subshells.
+    tmp=$(mktemp -- "${filepath}.XXXXXX") || return 1
+    if curl -fL -o "$tmp" -- "$url"; then
+        actual_md5=$(md5sum -- "$tmp" | awk '{ print $1 }')
+        if [[ "$actual_md5" != "$expected_md5" ]]; then
+            echo "MD5 checksum verification failed for $filepath after download. Expected: $expected_md5, but got: $actual_md5"
+            rm -f -- "$tmp"
+            return 1
+        fi
+        # A failed rename must not report success: callers export LD_PRELOAD
+        # from $filepath and would otherwise use a library that never installed.
+        mv -f -- "$tmp" "$filepath" || { rm -f -- "$tmp"; return 1; }
+    else
+        rm -f -- "$tmp"
         return 1
     fi
 
@@ -318,19 +354,17 @@
     local tar=$3
     local link=$4
 
-    if [ ! -d ${path}/${dir} ]; then
-        if [ ! -f ${path}/${tar} ]; then
+    if [ ! -d "${path}/${dir}" ]; then
+        if [ ! -f "${path}/${tar}" ]; then
             echo "Downloading the compressed package '${tar}'"
-            download ${path} ${link}
-            if [ $? -ne 0 ]; then
+            if ! download "${path}" "${link}"; then
                 echo "Failed to download, please ensure the network is available and link is valid"
                 exit 1
             fi
             echo "[OK] Finished download"
         fi
         echo "Unzip the compressed package '$tar'"
-        tar -zxvf ${path}/${tar} -C ${path} >/dev/null 2>&1
-        if [ $? -ne 0 ]; then
+        if ! tar -zxvf "${path}/${tar}" -C "${path}" >/dev/null 2>&1; then
             echo "Failed to unzip, please check the compressed package"
             exit 1
         fi
@@ -345,7 +379,7 @@
     local pid="$2"
     local timeout_s="$3"
 
-    local now_s=`date '+%s'`
+    local now_s=$(date '+%s')
     local stop_s=$(( $now_s + $timeout_s ))
 
     echo -n "Killing $process_name(pid $pid)" >&2
@@ -357,7 +391,7 @@
             return 0
         fi
         sleep 2
-        now_s=`date '+%s'`
+        now_s=$(date '+%s')
     done
     echo "$process_name shutdown timeout(exceeded $timeout_s seconds)" >&2
     return 1
@@ -386,7 +420,7 @@
         return 0
     fi
 
-    case "`uname`" in
+    case "$(uname)" in
         CYGWIN*) taskkill /F /PID "$pid" ;;
         *)       kill "$pid" ;;
     esac