#!/usr/bin/env bash set -euo pipefail project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" frontend_pid="" backend_pid="" cleanup_done=0 run_for_seconds="${ZBT_DEV_RUN_SECONDS:-0}" startup_timeout_seconds="${ZBT_DEV_STARTUP_TIMEOUT_SECONDS:-120}" die() { echo "错误:$*" >&2 exit 1 } require_command() { local command_name="$1" command -v "$command_name" >/dev/null 2>&1 || \ die "未找到 $command_name,请先安装并确保它位于 PATH 中。" } assert_port_available() { local port="$1" local application_name="$2" local listeners listeners="$(lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)" if [[ -n "$listeners" ]]; then echo "$listeners" >&2 die "$application_name 端口 $port 已被占用,请先结束上方进程后重试。" fi } process_group_is_alive() { local group_id="$1" [[ -n "$group_id" ]] && kill -0 -- "-$group_id" 2>/dev/null } stop_process_group() { local group_id="$1" if process_group_is_alive "$group_id"; then kill -TERM -- "-$group_id" 2>/dev/null || true fi } cleanup() { local exit_status=$? local deadline if (( cleanup_done )); then return "$exit_status" fi cleanup_done=1 trap - INT TERM HUP if [[ -n "$frontend_pid" || -n "$backend_pid" ]]; then echo echo "正在停止智保通开发服务..." fi stop_process_group "$frontend_pid" stop_process_group "$backend_pid" deadline=$((SECONDS + 5)) while (( SECONDS < deadline )); do if ! process_group_is_alive "$frontend_pid" && \ ! process_group_is_alive "$backend_pid"; then break fi sleep 0.2 done if process_group_is_alive "$frontend_pid"; then kill -KILL -- "-$frontend_pid" 2>/dev/null || true fi if process_group_is_alive "$backend_pid"; then kill -KILL -- "-$backend_pid" 2>/dev/null || true fi if [[ -n "$frontend_pid" ]]; then wait "$frontend_pid" 2>/dev/null || true fi if [[ -n "$backend_pid" ]]; then wait "$backend_pid" 2>/dev/null || true fi if [[ -n "$frontend_pid" || -n "$backend_pid" ]]; then echo "智保通开发服务已停止。" fi return "$exit_status" } http_ready() { local url="$1" curl --fail --silent --show-error --max-time 2 "$url" >/dev/null 2>&1 } wait_for_services() { local deadline=$((SECONDS + startup_timeout_seconds)) while (( SECONDS < deadline )); do kill -0 "$frontend_pid" 2>/dev/null || \ die "前端开发服务器启动失败,请检查上方 pnpm 输出。" kill -0 "$backend_pid" 2>/dev/null || \ die "后端开发服务器启动失败,请检查上方 Uvicorn 输出。" if http_ready "http://127.0.0.1:5173/" && \ http_ready "http://127.0.0.1:5174/" && \ http_ready "http://127.0.0.1:8000/docs"; then return 0 fi sleep 0.3 done die "开发服务在 ${startup_timeout_seconds} 秒内未全部就绪,请检查上方启动日志。" } case "$run_for_seconds" in ''|*[!0-9]*) die "ZBT_DEV_RUN_SECONDS 必须是非负整数。" ;; esac case "$startup_timeout_seconds" in ''|*[!0-9]*) die "ZBT_DEV_STARTUP_TIMEOUT_SECONDS 必须是正整数。" ;; 0) die "ZBT_DEV_STARTUP_TIMEOUT_SECONDS 必须大于 0。" ;; esac trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM trap 'exit 129' HUP require_command pnpm require_command uv require_command curl require_command lsof assert_port_available 5173 "H5" assert_port_available 5174 "管理后台" assert_port_available 8000 "后端 API" echo "正在启动 H5、管理后台和后端 API..." # Bash 3.2(macOS 系统自带版本)没有 setsid。临时启用 job control, # 可让两个后台任务各自成为独立进程组,清理时连同 Vite/Uvicorn 子进程一起结束。 set -m ( cd "$project_root/frontend" exec pnpm dev ) & frontend_pid=$! ( cd "$project_root/backend" exec uv run uvicorn zbt.main:app \ --host 127.0.0.1 \ --port 8000 \ --reload ) & backend_pid=$! set +m wait_for_services echo echo "智保通开发服务已全部启动:" echo "H5:http://127.0.0.1:5173" echo "管理后台:http://127.0.0.1:5174" echo "后端 API:http://127.0.0.1:8000/docs" echo "按 Ctrl+C 可一次性停止全部服务。" ready_at=$SECONDS while true; do kill -0 "$frontend_pid" 2>/dev/null || \ die "前端开发服务器意外退出,请检查上方日志。" kill -0 "$backend_pid" 2>/dev/null || \ die "后端开发服务器意外退出,请检查上方日志。" if (( run_for_seconds > 0 && SECONDS - ready_at >= run_for_seconds )); then break fi sleep 0.5 done