Files
qlib/scripts/dev.sh
T
Simon 55b684fbe1 feat(scripts): dev.sh 前后端一键 start/stop/restart/status/logs
- 启动后端 uvicorn(:8000, --reload, 0.0.0.0) + 前端 next dev(:3000),setsid 独立进程组
- 停止按 pidfile + 监听端口双定位,整进程组清理(无子进程残留,实测残留 0)
- 端口探测 ss 优先(lsof 在该环境对 socket 不可靠)
- status/logs/restart;支持 API_PORT/WEB_PORT/BACKEND_API_URL 覆盖
- 日志落 logs/,pid 落 .run/(均已 gitignore);USAGE §5 增加用法
2026-09-07 20:13:39 +08:00

185 lines
5.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================
# qlib-platform 前后端一键管理脚本
#
# 用法:
# scripts/dev.sh start 启动后端(8000) + 前端(3000)
# scripts/dev.sh stop 停止两者
# scripts/dev.sh restart 重启两者
# scripts/dev.sh status 查看状态
# scripts/dev.sh logs [api|web] 实时查看日志
#
# 环境变量(可选):
# API_PORT 后端端口(默认 8000)
# WEB_PORT 前端端口(默认 3000)
# BACKEND_API_URL 前端同源代理指向的后端地址(默认 http://127.0.0.1:8000,
# 局域网访问前端时改为 http://<本机IP>:8000)
#
# 进程管理说明:pidfile 记录启动进程;停止时同时按「监听端口」定位真实服务进程
# (pnpm/uv 包装进程可能提前退出),并对整进程组停止,避免子进程残留。
# ============================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
RUN_DIR="$ROOT/.run"
LOG_DIR="$ROOT/logs"
mkdir -p "$RUN_DIR" "$LOG_DIR"
API_PORT="${API_PORT:-8000}"
WEB_PORT="${WEB_PORT:-3000}"
BACKEND_API_URL="${BACKEND_API_URL:-http://127.0.0.1:8000}"
API_PID_FILE="$RUN_DIR/backend.pid"
WEB_PID_FILE="$RUN_DIR/frontend.pid"
API_LOG="$LOG_DIR/backend.log"
WEB_LOG="$LOG_DIR/frontend.log"
log() { echo "[dev.sh] $*"; }
die() { echo "[dev.sh] 错误: $*" >&2; exit 1; }
# ---------- 进程探测 ----------
# 监听指定端口的 pid(无则空)
listener_pids() { # $1 port —— ss 优先(lsof 在部分环境对 socket 不可靠)
if command -v ss >/dev/null 2>&1; then
ss -tlnp 2>/dev/null | awk -v p=":$1 " '
$0 ~ p && /LISTEN/ {
if (match($0, /pid=[0-9]+/)) print substr($0, RSTART+4, RLENGTH-4)
}'
elif command -v lsof >/dev/null 2>&1; then
lsof -t -iTCP:"$1" -sTCP:LISTEN 2>/dev/null || true
fi
}
port_in_use() { [[ -n "$(listener_pids "$1")" ]]; }
pidfile_alive() { [[ -f "$1" ]] && kill -0 "$(cat "$1")" 2>/dev/null; }
# 杀掉指定 pid 及其整进程组(uvicorn --reload / next dev 的子进程一起清理)
kill_tree() { # $1 pid
local pid pgid
pid="$1"
pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ' || true)"
[[ -n "${pgid:-}" ]] && kill -- "-$pgid" 2>/dev/null || true
kill "$pid" 2>/dev/null || true
pkill -P "$pid" 2>/dev/null || true
}
# ---------- 启动 ----------
start_api() {
if port_in_use "$API_PORT"; then
log "后端已在 :$API_PORT 运行(pid $(listener_pids "$API_PORT" | head -1))"
listener_pids "$API_PORT" | head -1 > "$API_PID_FILE"
return 0
fi
log "启动后端 :$API_PORT(日志 $API_LOG)"
( cd "$ROOT/backend"
setsid nohup uv run uvicorn app.main:app --host 0.0.0.0 --port "$API_PORT" --reload \
>>"$API_LOG" 2>&1 &
) || true
for _ in $(seq 1 40); do
if port_in_use "$API_PORT"; then
listener_pids "$API_PORT" | head -1 > "$API_PID_FILE"
return 0
fi
sleep 0.5
done
die "后端启动超时,请查看 $API_LOG"
}
start_web() {
if port_in_use "$WEB_PORT"; then
log "前端已在 :$WEB_PORT 运行(pid $(listener_pids "$WEB_PORT" | head -1))"
listener_pids "$WEB_PORT" | head -1 > "$WEB_PID_FILE"
return 0
fi
log "启动前端 :$WEB_PORT(日志 $WEB_LOG)"
( cd "$ROOT/frontend/web"
export BACKEND_API_URL
# pnpm exec next 直接运行,避免 package.json dev 脚本自带 -p 3000 与端口参数重复
setsid nohup pnpm exec next dev -p "$WEB_PORT" >>"$WEB_LOG" 2>&1 &
) || true
for _ in $(seq 1 80); do
if port_in_use "$WEB_PORT"; then
listener_pids "$WEB_PORT" | head -1 > "$WEB_PID_FILE"
return 0
fi
sleep 0.5
done
die "前端启动超时,请查看 $WEB_LOG"
}
# ---------- 停止 ----------
stop_service() { # $1 pidfile $2 port $3 名称
local killed=0
if pidfile_alive "$1"; then
local pid
pid="$(cat "$1")"
log "停止 $3 (pid $pid)"
kill_tree "$pid"
killed=1
fi
local lp
for lp in $(listener_pids "$2"); do
log "停止 $3 监听进程 (pid $lp)"
kill_tree "$lp"
killed=1
done
if [[ "$killed" == 0 ]]; then
log "$3 未在运行"
fi
for _ in $(seq 1 20); do
port_in_use "$2" || break
sleep 0.3
done
if pidfile_alive "$1"; then kill -9 "$(cat "$1")" 2>/dev/null || true; fi
for lp in $(listener_pids "$2"); do kill -9 "$lp" 2>/dev/null || true; done
rm -f "$1"
log "$3 已停止"
}
status() {
local api_pid web_pid
api_pid="$(listener_pids "$API_PORT" | head -1 | tr -d '\n ' || true)"
web_pid="$(listener_pids "$WEB_PORT" | head -1 | tr -d '\n ' || true)"
log "后端: ${api_pid:+运行中 pid $api_pid}${api_pid:-未运行}"
log "前端: ${web_pid:+运行中 pid $web_pid}${web_pid:-未运行}"
log "访问: 前端 http://127.0.0.1:$WEB_PORT | API http://127.0.0.1:$API_PORT/api/health"
}
# ---------- actions ----------
case "${1:-start}" in
start)
start_api
start_web
status
;;
stop)
stop_service "$API_PID_FILE" "$API_PORT" "后端"
stop_service "$WEB_PID_FILE" "$WEB_PORT" "前端"
;;
restart)
"$0" stop
sleep 1
"$0" start
;;
status)
status
;;
logs)
case "${2:-}" in
api) tail -f "$API_LOG" ;;
web) tail -f "$WEB_LOG" ;;
*) tail -f "$API_LOG" "$WEB_LOG" ;;
esac
;;
*)
echo "用法: $0 {start|stop|restart|status|logs [api|web]}" >&2
exit 1
;;
esac