|
|
#!/usr/bin/env bash |
|
|
set -euo pipefail |
|
|
|
|
|
usage() { |
|
|
cat <<'EOF' |
|
|
Usage: |
|
|
./close_api_server.sh [--port PORT] [--pid PID] [--force] |
|
|
|
|
|
Defaults: |
|
|
PORT: 8001 |
|
|
|
|
|
Behavior: |
|
|
- If --pid is provided, stops that PID. |
|
|
- Otherwise, finds the listening PID(s) on --port and stops them. |
|
|
- By default, only stops processes whose cmdline contains "uvicorn" or "acestep.api_server". |
|
|
Use --force to skip this safety check. |
|
|
EOF |
|
|
} |
|
|
|
|
|
PORT="8001" |
|
|
PID="" |
|
|
FORCE="0" |
|
|
|
|
|
while [[ $# -gt 0 ]]; do |
|
|
case "$1" in |
|
|
--port) |
|
|
PORT="${2:-}"; shift 2 ;; |
|
|
--pid) |
|
|
PID="${2:-}"; shift 2 ;; |
|
|
--force) |
|
|
FORCE="1"; shift ;; |
|
|
-h|--help) |
|
|
usage; exit 0 ;; |
|
|
*) |
|
|
echo "Unknown argument: $1" >&2 |
|
|
usage |
|
|
exit 2 |
|
|
;; |
|
|
esac |
|
|
done |
|
|
|
|
|
if [[ -n "$PORT" ]] && ! [[ "$PORT" =~ ^[0-9]+$ ]]; then |
|
|
echo "Invalid --port: $PORT" >&2 |
|
|
exit 2 |
|
|
fi |
|
|
if [[ -n "$PID" ]] && ! [[ "$PID" =~ ^[0-9]+$ ]]; then |
|
|
echo "Invalid --pid: $PID" >&2 |
|
|
exit 2 |
|
|
fi |
|
|
|
|
|
_cmdline() { |
|
|
local pid="$1" |
|
|
if [[ -r "/proc/${pid}/cmdline" ]]; then |
|
|
tr '\0' ' ' < "/proc/${pid}/cmdline" | sed 's/[[:space:]]\+/ /g' || true |
|
|
else |
|
|
echo "" |
|
|
fi |
|
|
} |
|
|
|
|
|
_is_target_process() { |
|
|
local pid="$1" |
|
|
local cmd |
|
|
cmd="$(_cmdline "$pid")" |
|
|
[[ "$cmd" == *"uvicorn"* || "$cmd" == *"acestep.api_server"* ]] |
|
|
} |
|
|
|
|
|
_find_pids_by_port() { |
|
|
local port="$1" |
|
|
local pids="" |
|
|
|
|
|
if command -v lsof >/dev/null 2>&1; then |
|
|
pids="$(lsof -nP -t -iTCP:"$port" -sTCP:LISTEN 2>/dev/null | tr '\n' ' ' || true)" |
|
|
elif command -v ss >/dev/null 2>&1; then |
|
|
|
|
|
pids="$(ss -lptn "sport = :$port" 2>/dev/null | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' | sort -u | tr '\n' ' ' || true)" |
|
|
elif command -v netstat >/dev/null 2>&1; then |
|
|
|
|
|
pids="$(netstat -lntp 2>/dev/null | awk -v p=":${port}" '$4 ~ p && $6=="LISTEN" {split($7,a,"/"); if (a[1] ~ /^[0-9]+$/) print a[1]}' | sort -u | tr '\n' ' ' || true)" |
|
|
elif command -v fuser >/dev/null 2>&1; then |
|
|
pids="$(fuser -n tcp "$port" 2>/dev/null | tr '\n' ' ' || true)" |
|
|
fi |
|
|
|
|
|
echo "$pids" |
|
|
} |
|
|
|
|
|
_stop_pid() { |
|
|
local pid="$1" |
|
|
|
|
|
if ! kill -0 "$pid" 2>/dev/null; then |
|
|
echo "PID $pid not running." |
|
|
return 0 |
|
|
fi |
|
|
|
|
|
if [[ "$FORCE" != "1" ]] && ! _is_target_process "$pid"; then |
|
|
echo "Skip PID $pid (cmdline does not look like uvicorn/acestep.api_server). Use --force to stop anyway." >&2 |
|
|
echo "cmdline: $(_cmdline "$pid")" >&2 |
|
|
return 3 |
|
|
fi |
|
|
|
|
|
echo "Stopping PID $pid..." |
|
|
kill -TERM "$pid" 2>/dev/null || true |
|
|
|
|
|
for _ in $(seq 1 30); do |
|
|
if ! kill -0 "$pid" 2>/dev/null; then |
|
|
echo "Stopped PID $pid." |
|
|
return 0 |
|
|
fi |
|
|
sleep 0.2 |
|
|
done |
|
|
|
|
|
echo "PID $pid did not exit; sending SIGKILL..." >&2 |
|
|
kill -KILL "$pid" 2>/dev/null || true |
|
|
sleep 0.1 |
|
|
if kill -0 "$pid" 2>/dev/null; then |
|
|
echo "Failed to kill PID $pid." >&2 |
|
|
return 1 |
|
|
fi |
|
|
echo "Killed PID $pid." |
|
|
return 0 |
|
|
} |
|
|
|
|
|
if [[ -n "$PID" ]]; then |
|
|
_stop_pid "$PID" |
|
|
exit $? |
|
|
fi |
|
|
|
|
|
pids="$(_find_pids_by_port "$PORT")" |
|
|
if [[ -z "${pids// }" ]]; then |
|
|
echo "No listening process found on port $PORT." |
|
|
exit 0 |
|
|
fi |
|
|
|
|
|
rc=0 |
|
|
for pid in $pids; do |
|
|
if [[ -n "$pid" ]]; then |
|
|
_stop_pid "$pid" || rc=$? |
|
|
fi |
|
|
done |
|
|
|
|
|
exit "$rc" |
|
|
|