278 lines
7.2 KiB
Bash
278 lines
7.2 KiB
Bash
|
|
#!/usr/bin/env bash
|
|||
|
|
|
|||
|
|
set -Eeuo pipefail
|
|||
|
|
set -m
|
|||
|
|
|
|||
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||
|
|
BACKEND_DIR="$ROOT_DIR/backend"
|
|||
|
|
FRONTEND_DIR="$ROOT_DIR/frontend"
|
|||
|
|
BACKEND_VENV="$BACKEND_DIR/.venv"
|
|||
|
|
BACKEND_PYTHON="$BACKEND_VENV/bin/python"
|
|||
|
|
|
|||
|
|
DO_SETUP=false
|
|||
|
|
CHECK_ONLY=false
|
|||
|
|
BACKEND_PID=""
|
|||
|
|
FRONTEND_PID=""
|
|||
|
|
CONFIG_PATH=""
|
|||
|
|
FRONTEND_PORT=""
|
|||
|
|
BACKEND_PORT=""
|
|||
|
|
|
|||
|
|
usage() {
|
|||
|
|
cat <<'EOF'
|
|||
|
|
用法:
|
|||
|
|
./scripts/start-dev.sh 启动前端和后端
|
|||
|
|
./scripts/start-dev.sh --setup 同步本地依赖后启动
|
|||
|
|
./scripts/start-dev.sh --check 只检查依赖、配置和数据库连接
|
|||
|
|
|
|||
|
|
环境变量:
|
|||
|
|
PYTHON_BIN 创建虚拟环境时使用的 Python 3.12+ 可执行文件
|
|||
|
|
BACKEND_CONFIG_FILE 指定其他后端 YAML 配置文件
|
|||
|
|
FRONTEND_PORT 覆盖 YAML 中的前端端口
|
|||
|
|
BACKEND_PORT 覆盖 YAML 中的后端端口
|
|||
|
|
EOF
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fail() {
|
|||
|
|
echo "❌ $*" >&2
|
|||
|
|
exit 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
process_group_alive() {
|
|||
|
|
local pid="$1"
|
|||
|
|
[[ -n "$pid" ]] && kill -0 -- "-$pid" 2>/dev/null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
stop_process_group() {
|
|||
|
|
local pid="$1"
|
|||
|
|
local attempt=0
|
|||
|
|
[[ -n "$pid" ]] || return 0
|
|||
|
|
|
|||
|
|
if process_group_alive "$pid"; then
|
|||
|
|
kill -TERM -- "-$pid" 2>/dev/null || true
|
|||
|
|
while process_group_alive "$pid" && [[ $attempt -lt 50 ]]; do
|
|||
|
|
sleep 0.1
|
|||
|
|
attempt=$((attempt + 1))
|
|||
|
|
done
|
|||
|
|
if process_group_alive "$pid"; then
|
|||
|
|
kill -KILL -- "-$pid" 2>/dev/null || true
|
|||
|
|
fi
|
|||
|
|
elif kill -0 "$pid" 2>/dev/null; then
|
|||
|
|
kill -TERM "$pid" 2>/dev/null || true
|
|||
|
|
fi
|
|||
|
|
wait "$pid" 2>/dev/null || true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cleanup() {
|
|||
|
|
trap - EXIT
|
|||
|
|
stop_process_group "$FRONTEND_PID"
|
|||
|
|
stop_process_group "$BACKEND_PID"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handle_signal() {
|
|||
|
|
echo
|
|||
|
|
echo "🛑 正在停止前端和后端……"
|
|||
|
|
exit 130
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
port_in_use() {
|
|||
|
|
local port="$1"
|
|||
|
|
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
select_base_python() {
|
|||
|
|
if [[ -n "${PYTHON_BIN:-}" ]]; then
|
|||
|
|
printf '%s\n' "$PYTHON_BIN"
|
|||
|
|
elif [[ -x /opt/miniconda3/bin/python3 ]]; then
|
|||
|
|
printf '%s\n' /opt/miniconda3/bin/python3
|
|||
|
|
else
|
|||
|
|
command -v python3 || true
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setup_dependencies() {
|
|||
|
|
local base_python
|
|||
|
|
base_python="$(select_base_python)"
|
|||
|
|
[[ -n "$base_python" ]] || fail "未找到 Python 3,请通过 PYTHON_BIN 指定 Python 3.12+。"
|
|||
|
|
|
|||
|
|
"$base_python" -c \
|
|||
|
|
'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)' \
|
|||
|
|
|| fail "后端要求 Python 3.12+,当前为 $($base_python --version 2>&1)。"
|
|||
|
|
|
|||
|
|
if [[ ! -x "$BACKEND_PYTHON" ]]; then
|
|||
|
|
echo "📦 创建后端虚拟环境……"
|
|||
|
|
"$base_python" -m venv "$BACKEND_VENV"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo "📦 同步后端依赖……"
|
|||
|
|
"$BACKEND_PYTHON" -m pip install -r "$BACKEND_DIR/requirements.txt"
|
|||
|
|
|
|||
|
|
echo "📦 同步前端依赖……"
|
|||
|
|
(cd "$FRONTEND_DIR" && npm ci)
|
|||
|
|
|
|||
|
|
if [[ -z "${BACKEND_CONFIG_FILE:-}" && ! -f "$BACKEND_DIR/config.yaml" ]]; then
|
|||
|
|
cp "$BACKEND_DIR/config.example.yaml" "$BACKEND_DIR/config.yaml"
|
|||
|
|
echo "📝 已根据 config.example.yaml 创建本地 config.yaml,请填写数据库密码。"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resolve_and_check_config() {
|
|||
|
|
local port_values
|
|||
|
|
|
|||
|
|
CONFIG_PATH="$(
|
|||
|
|
cd "$BACKEND_DIR"
|
|||
|
|
"$BACKEND_PYTHON" -c \
|
|||
|
|
'from app.core.config import _resolve_config_path; print(_resolve_config_path()[0])'
|
|||
|
|
)" || fail "无法解析后端配置文件路径。"
|
|||
|
|
|
|||
|
|
[[ -f "$CONFIG_PATH" ]] || fail "配置文件不存在:$CONFIG_PATH"
|
|||
|
|
port_values="$(
|
|||
|
|
cd "$BACKEND_DIR"
|
|||
|
|
"$BACKEND_PYTHON" -c \
|
|||
|
|
'from app.core.config import get_settings; settings = get_settings(); print(settings.frontend_port, settings.backend_port)'
|
|||
|
|
)" || fail "配置文件无法加载:$CONFIG_PATH"
|
|||
|
|
|
|||
|
|
read -r FRONTEND_PORT BACKEND_PORT <<< "$port_values"
|
|||
|
|
[[ -n "$FRONTEND_PORT" && -n "$BACKEND_PORT" ]] \
|
|||
|
|
|| fail "无法从配置文件读取前端和后端端口:$CONFIG_PATH"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
check_dependencies() {
|
|||
|
|
command -v node >/dev/null 2>&1 || fail "未找到 Node.js。"
|
|||
|
|
command -v npm >/dev/null 2>&1 || fail "未找到 npm。"
|
|||
|
|
command -v curl >/dev/null 2>&1 || fail "未找到 curl。"
|
|||
|
|
command -v lsof >/dev/null 2>&1 || fail "未找到 lsof。"
|
|||
|
|
[[ -x "$FRONTEND_DIR/node_modules/.bin/vite" ]] \
|
|||
|
|
|| fail "前端依赖不完整,请先运行 ./scripts/start-dev.sh --setup。"
|
|||
|
|
[[ -x "$BACKEND_PYTHON" ]] \
|
|||
|
|
|| fail "后端虚拟环境不存在,请先运行 ./scripts/start-dev.sh --setup。"
|
|||
|
|
|
|||
|
|
"$BACKEND_PYTHON" -c 'import fastapi, psycopg, sqlalchemy, uvicorn, yaml' \
|
|||
|
|
|| fail "后端依赖不完整,请先运行 ./scripts/start-dev.sh --setup。"
|
|||
|
|
resolve_and_check_config
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
check_database() {
|
|||
|
|
echo "🔍 检查 PostgreSQL 连接:$CONFIG_PATH"
|
|||
|
|
(
|
|||
|
|
cd "$BACKEND_DIR"
|
|||
|
|
"$BACKEND_PYTHON" -c '
|
|||
|
|
import psycopg
|
|||
|
|
from app.core.config import get_settings
|
|||
|
|
|
|||
|
|
url = get_settings().database_url
|
|||
|
|
prefix = "postgresql+psycopg://"
|
|||
|
|
if url.startswith(prefix):
|
|||
|
|
url = "postgresql://" + url[len(prefix):]
|
|||
|
|
connection = psycopg.connect(url, connect_timeout=3)
|
|||
|
|
connection.close()
|
|||
|
|
'
|
|||
|
|
) || fail "PostgreSQL 无法连接,请检查配置文件中的地址、用户名和密码:$CONFIG_PATH"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
wait_for_url() {
|
|||
|
|
local url="$1"
|
|||
|
|
local service_name="$2"
|
|||
|
|
local pid="$3"
|
|||
|
|
local attempt=0
|
|||
|
|
local max_attempts=30
|
|||
|
|
|
|||
|
|
while [[ $attempt -lt $max_attempts ]]; do
|
|||
|
|
if curl -fsS --max-time 1 "$url" >/dev/null 2>&1; then
|
|||
|
|
return 0
|
|||
|
|
fi
|
|||
|
|
if ! kill -0 "$pid" 2>/dev/null; then
|
|||
|
|
echo "❌ $service_name 进程已提前退出。" >&2
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
sleep 1
|
|||
|
|
attempt=$((attempt + 1))
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
echo "❌ 等待 $service_name 就绪超时:$url" >&2
|
|||
|
|
return 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
while [[ $# -gt 0 ]]; do
|
|||
|
|
case "$1" in
|
|||
|
|
--setup)
|
|||
|
|
DO_SETUP=true
|
|||
|
|
;;
|
|||
|
|
--check)
|
|||
|
|
CHECK_ONLY=true
|
|||
|
|
;;
|
|||
|
|
-h|--help)
|
|||
|
|
usage
|
|||
|
|
exit 0
|
|||
|
|
;;
|
|||
|
|
*)
|
|||
|
|
usage >&2
|
|||
|
|
fail "未知参数:$1"
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
shift
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
if [[ "$DO_SETUP" == true ]]; then
|
|||
|
|
setup_dependencies
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
check_dependencies
|
|||
|
|
check_database
|
|||
|
|
|
|||
|
|
if [[ "$CHECK_ONLY" == true ]]; then
|
|||
|
|
echo "✅ 前端、后端依赖、配置和 PostgreSQL 连接均正常。"
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
port_in_use "$BACKEND_PORT" \
|
|||
|
|
&& fail "后端端口 $BACKEND_PORT 已被占用,后端可能已经启动。"
|
|||
|
|
port_in_use "$FRONTEND_PORT" \
|
|||
|
|
&& fail "前端端口 $FRONTEND_PORT 已被占用,前端可能已经启动。"
|
|||
|
|
|
|||
|
|
trap cleanup EXIT
|
|||
|
|
trap handle_signal INT TERM
|
|||
|
|
|
|||
|
|
echo "🚀 启动后端:http://127.0.0.1:$BACKEND_PORT"
|
|||
|
|
(
|
|||
|
|
cd "$BACKEND_DIR"
|
|||
|
|
exec "$BACKEND_PYTHON" -m uvicorn app.main:app \
|
|||
|
|
--host 0.0.0.0 \
|
|||
|
|
--port "$BACKEND_PORT" \
|
|||
|
|
--reload
|
|||
|
|
) &
|
|||
|
|
BACKEND_PID=$!
|
|||
|
|
|
|||
|
|
wait_for_url "http://127.0.0.1:$BACKEND_PORT/modelTF/health" "后端" "$BACKEND_PID" \
|
|||
|
|
|| fail "后端启动失败。"
|
|||
|
|
|
|||
|
|
echo "🚀 启动前端:http://127.0.0.1:$FRONTEND_PORT"
|
|||
|
|
(
|
|||
|
|
cd "$FRONTEND_DIR"
|
|||
|
|
export FRONTEND_PORT BACKEND_PORT
|
|||
|
|
exec npm run dev -- --host 0.0.0.0 --port "$FRONTEND_PORT" --strictPort
|
|||
|
|
) &
|
|||
|
|
FRONTEND_PID=$!
|
|||
|
|
|
|||
|
|
wait_for_url "http://127.0.0.1:$FRONTEND_PORT" "前端" "$FRONTEND_PID" \
|
|||
|
|
|| fail "前端启动失败。"
|
|||
|
|
|
|||
|
|
echo "✅ 前端和后端均已就绪,按 Ctrl+C 可同时停止。"
|
|||
|
|
|
|||
|
|
while kill -0 "$BACKEND_PID" 2>/dev/null && kill -0 "$FRONTEND_PID" 2>/dev/null; do
|
|||
|
|
sleep 1
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
set +e
|
|||
|
|
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
|
|||
|
|
wait "$BACKEND_PID"
|
|||
|
|
EXIT_CODE=$?
|
|||
|
|
echo "❌ 后端已退出,正在停止前端。" >&2
|
|||
|
|
else
|
|||
|
|
wait "$FRONTEND_PID"
|
|||
|
|
EXIT_CODE=$?
|
|||
|
|
echo "❌ 前端已退出,正在停止后端。" >&2
|
|||
|
|
fi
|
|||
|
|
set -e
|
|||
|
|
|
|||
|
|
exit "$EXIT_CODE"
|