#!/usr/bin/env bash set -uo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RUN_DIR="$ROOT_DIR/.run" PID_FILE="$RUN_DIR/dev-all.pids" # Processes actually spawned by dev-all.sh look like this on the command # line (dev-all.sh always `cd`s into the target dir first, so the binary is # invoked with a *relative* path — never anchor these on an absolute path). # Env-var assignments (e.g. HAIXUN_NODE_WORKER_TYPE=...) must never be used # as a match pattern: once `env FOO=bar cmd` execs `cmd`, the assignment is # gone from the process's argv/cmdline and can never match again. PATTERNS=( "go workers|bin/worker -f etc/gateway.worker.yaml" "gateway|bin/gateway -f etc/gateway.yaml" "node style-8d worker|style-8d-worker.ts" "keyword-search tool|keyword-search-server.ts" "resolve-media tool|resolve-media-server.ts" "web dev server|vite.*--strictPort" ) port_in_use() { local port="$1" ss -tlnp 2>/dev/null | grep -qE ":${port} " } free_port() { local port="$1" if ! port_in_use "$port"; then return 0 fi echo "[dev-all-stop] freeing port :$port" if command -v fuser >/dev/null 2>&1; then fuser -k "${port}/tcp" 2>/dev/null || true else local line pid while IFS= read -r line; do for pid in $(grep -oE 'pid=[0-9]+' <<<"$line" | cut -d= -f2); do kill "$pid" 2>/dev/null || true done done < <(ss -tlnp 2>/dev/null | grep -E ":${port} " || true) fi sleep 0.3 } # Stop everything dev-all.sh recorded in its pidfile. Each recorded pid is a # process-group leader (dev-all.sh launches jobs via `setsid`), so signalling # the negative pid takes down the whole subtree (npm -> tsx -> node, etc.), # not just the immediate child. This is the reliable path and also covers # runs where dev-all.sh itself was killed abruptly (e.g. `kill -9`, closed # terminal) and never got to run its own cleanup trap. stop_from_pidfile() { if [[ ! -f "$PID_FILE" ]]; then return 0 fi echo "[dev-all-stop] stopping processes recorded in $PID_FILE" local pid label while read -r pid label; do [[ -n "${pid:-}" ]] || continue if kill -0 "$pid" 2>/dev/null; then echo "[dev-all-stop] -> ${label:-pgid $pid} (pgid $pid): TERM" kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true fi done < "$PID_FILE" sleep 1 while read -r pid label; do [[ -n "${pid:-}" ]] || continue if kill -0 "$pid" 2>/dev/null; then echo "[dev-all-stop] -> ${label:-pgid $pid} (pgid $pid) 仍在執行,改用 KILL" kill -KILL -- "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true fi done < "$PID_FILE" rm -f "$PID_FILE" } # Fallback for anything the pidfile doesn't know about: processes started # before this pidfile mechanism existed, started outside dev-all.sh, or left # over from a crashed run on a previous checkout. Matches the *actual* # observed command line, not env vars or absolute paths (see PATTERNS above). stop_pattern() { local label="$1" local pattern="$2" if pgrep -f "$pattern" >/dev/null 2>&1; then echo "[dev-all-stop] stopping leftover: $label" pkill -f "$pattern" 2>/dev/null || true sleep 0.3 pkill -9 -f "$pattern" 2>/dev/null || true fi } stop_from_pidfile for port in 8890 9101 9102; do free_port "$port" done for port in $(seq 5173 5199); do free_port "$port" done for entry in "${PATTERNS[@]}"; do stop_pattern "${entry%%|*}" "${entry#*|}" done sleep 0.5 # Verify instead of just hoping: print anything that survived so it's # obvious when a leftover truly needs manual attention. all_patterns="$(printf '%s|' "${PATTERNS[@]#*|}")" all_patterns="${all_patterns%|}" remaining="$(pgrep -f "$all_patterns" 2>/dev/null || true)" if [[ -n "$remaining" ]]; then echo "[dev-all-stop] 警告:仍有殘留程序未關閉:" >&2 # shellcheck disable=SC2086 ps -o pid,ppid,cmd -p $(echo "$remaining" | paste -sd, -) 2>/dev/null >&2 || true echo "[dev-all-stop] 可手動執行: kill -9 $(echo "$remaining" | paste -sd' ' -)" >&2 else echo "[dev-all-stop] 已確認沒有殘留的 dev-all 相關程序" fi echo "[dev-all-stop] done"