#!/bin/bash # 一键重启开发服务 —— 起完**核实到 200 才算数**。 # # ./start.sh 重启 api + web(默认) # ./start.sh api 只重启 api(改了后端用这个;前端有 HMR,一般不必重启) # ./start.sh web 只重启 web # ./start.sh status 只看现在什么状态,一个进程都不动 # ./start.sh stop 停掉两个 # # 为什么要有它 —— 手工重启踩过的坑,这里逐条堵上: # ① `lsof -ti:5173` 会把**浏览器那条已建立的连接**也算成端口占用者, # 按它 kill -9 就是杀用户的浏览器。所以一律 `-sTCP:LISTEN`,只认真属主。 # ② 只杀监听进程,**tsup 监视器留了下来** —— 它还活着,一秒后就重拉一个占回同一个端口。 # 所以顺进程链往上走,把本仓库的监视器与 pnpm 壳一起收;收完还要**再核端口是不是真空了**。 # ③ 拿旧 `ps` 快照里的 PID 去 kill,或者按名字 pkill —— 两样都误伤过。这里只从端口现查。 # ④ 起完不核实就说「好了」。实测有一次 macOS 没有 `setsid` 根本起不来, # 而失败发生在后台里,不核实根本发现不了。 set -u cd "$(dirname "$0")" || exit 1 REPO=$(pwd) LOGDIR="$REPO/.dev-logs" mkdir -p "$LOGDIR" WHAT=${1:-both} API_PORT=8787 WEB_PORT=5173 PG_NAME=jcai-pg say() { printf '%s\n' "$*"; } # 端口的**真属主** —— 只认 LISTEN。不加这个过滤会捞到浏览器(坑①) owner_of() { lsof -ti:"$1" -sTCP:LISTEN 2>/dev/null | head -1; } # 顺着进程链往上收:监听进程本身必收,再往上每一级**确认是本仓库的 dev 壳**才收。 # 链长这样:node dist/main.js ← node …/tsup/…/cli-default.js ← pnpm --filter @app/api dev # 只收监听那一个的话,tsup 一秒后就把端口抢回去了(坑②)。 kill_chain() { local pid=$1 depth=0 parent cmd KILLED='' while [ -n "$pid" ] && [ "$pid" != '1' ] && [ "$pid" != "$$" ] && [ "$depth" -lt 4 ]; do parent=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ') if [ "$depth" -gt 0 ]; then cmd=$(ps -o command= -p "$pid" 2>/dev/null) case "$cmd" in *"$REPO"* | *'@app/'*) : ;; # 本仓库的监视器 / pnpm 壳,收 *) break ;; # 到终端或别人家的进程了,就此打住 esac fi kill -9 "$pid" 2>/dev/null && KILLED="$KILLED $pid" pid=$parent depth=$((depth + 1)) done } # 停到端口**真的空出来**为止 —— 收完还有人占,就是还有监视器在抢,再收一轮。 stop_service() { local label=$1 port=$2 round owner all='' for round in 1 2 3 4; do owner=$(owner_of "$port") if [ -z "$owner" ]; then if [ -z "$all" ]; then [ "$round" = '1' ] && say " $label 本来就没在跑" return 0 fi sleep 1 if [ -z "$(owner_of "$port")" ]; then say " $label 已停 (pid$all)" return 0 fi continue fi kill_chain "$owner" all="$all$KILLED" sleep 1 done say " ! $label :$port 还被占着,收了几轮都有人抢回来 —— 手工看看 lsof -i:$port" return 1 } start_one() { local name=$1 port=$2 label=$3 if [ -n "$(owner_of "$port")" ]; then say " $label 已在跑,不动它" return 0 fi # # `exec` 不能省 —— 它让子 shell **被 pnpm 顶替掉**,进程链上不留 bash。 # # 少了它(三种写法都试过:`(cmd & disown)`、双重 fork、`disown` 提到顶层), # start.sh 自己会赖着不退出、一直攥着 stdout,于是 `./start.sh | tail` 这类管道用法 # **永远等不到结尾**(实测挂满 100 秒;服务其实早起好了,人只看见光标在闪)。 # 换成 `sleep` 当被起的命令就不复现 —— 是 pnpm 特有的,不是写法通病。 # ` "$LOGDIR/$name.log" 2>&1 ) & disown 2>/dev/null say " $label 启动中… (日志 .dev-logs/$name.log)" } # 轮询到 200 才算就绪 —— 不核实的「好了」等于没说(坑④) wait_ok() { local url=$1 label=$2 i code for i in $(seq 1 45); do code=$(curl -s -o /dev/null -m 3 -w '%{http_code}' "$url" 2>/dev/null) if [ "$code" = '200' ]; then say " ✓ $label 就绪 (${i}s)" return 0 fi sleep 1 done say " ✗ $label 没有应答 —— 看 .dev-logs/$label.log" return 1 } http_code() { curl -s -o /dev/null -m 3 -w '%{http_code}' "$1" 2>/dev/null; } check_db() { if ! docker ps --filter "name=$PG_NAME" --filter status=running --format '{{.Names}}' 2>/dev/null | grep -q "$PG_NAME"; then say " ! 数据库 $PG_NAME 没在跑 —— 先 pnpm db:up" return 1 fi say " ✓ 数据库 $PG_NAME 在跑" return 0 } case "$WHAT" in status) say '当前状态:' say " api :$API_PORT $(http_code "http://127.0.0.1:$API_PORT/api/health")" say " web :$WEB_PORT $(http_code "http://127.0.0.1:$WEB_PORT/")" say " 反代 /api $(http_code "http://127.0.0.1:$WEB_PORT/api/health")" check_db exit 0 ;; stop) say '停止服务:' stop_service api "$API_PORT" stop_service web "$WEB_PORT" exit 0 ;; api) say '重启 api:'; stop_service api "$API_PORT" ;; web) say '重启 web:'; stop_service web "$WEB_PORT" ;; both) say '重启 api + web:'; stop_service api "$API_PORT"; stop_service web "$WEB_PORT" ;; *) say "不认识的参数:$WHAT(可用:both / api / web / status / stop)"; exit 2 ;; esac check_db || say ' (数据库不在也照起,但一调接口就会报连不上)' # 两个都过一遍:刚停掉的那个会起来,另一个在跑就不动它、碰巧是停的就顺手带起来。 # 「重启 api」不该把一个半残的环境留给人 —— 反代不通,浏览器里看到的还是坏的。 start_one api "$API_PORT" "api :$API_PORT" start_one web "$WEB_PORT" "web :$WEB_PORT" RC=0 wait_ok "http://127.0.0.1:$API_PORT/api/health" 'api' || RC=1 wait_ok "http://127.0.0.1:$WEB_PORT/" 'web' || RC=1 # 反代要单独核:api 活着而反代不通,人在浏览器里看到的还是坏的 PROXY=$(http_code "http://127.0.0.1:$WEB_PORT/api/health") if [ "$PROXY" = '200' ]; then say ' ✓ 反代 /api → 200' else say " ✗ 反代 /api → ${PROXY:-连不上}" RC=1 fi say '' if [ "$RC" = '0' ]; then say " 都好了 → http://localhost:$WEB_PORT/" else say ' 有起不来的,日志在 .dev-logs/' fi exit $RC