Files
mirosociety/start.sh
T
2026-09-27 23:06:24 +08:00

134 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# MiroSociety 前台启动 (开发模式)
# 前置: 请先执行 ./deploy.sh 安装依赖
# 前端: http://localhost:31400 后端: http://localhost:31401
#
# 直接 ./start.sh 即可启动;日志会实时输出到当前终端。
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
FRONTEND_PORT=31400
BACKEND_PORT=31401
VENV_DIR="$SCRIPT_DIR/backend/.venv"
BACKEND_DIR="$SCRIPT_DIR/backend"
FRONTEND_DIR="$SCRIPT_DIR/frontend"
RUN_DIR="$SCRIPT_DIR/.run"
BACKEND_PID_FILE="$RUN_DIR/backend.pid"
FRONTEND_PID_FILE="$RUN_DIR/frontend.pid"
cd "$SCRIPT_DIR"
# --- 颜色 ---
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
log() { echo -e "${GREEN}[start.sh]${NC} $*"; }
warn() { echo -e "${YELLOW}[start.sh]${NC} $*"; }
err() { echo -e "${RED}[start.sh]${NC} $*" >&2; }
# --- 检查依赖 ---
for cmd in lsof npm node curl; do
command -v "$cmd" >/dev/null 2>&1 || { err "缺少依赖命令: $cmd"; exit 1; }
done
[[ -x "$VENV_DIR/bin/python3" ]] || { err "未检测到 $VENV_DIR/bin/python3,请先执行 ./deploy.sh"; exit 1; }
"$VENV_DIR/bin/python3" -c "import uvicorn" 2>/dev/null || { err "venv 缺少 uvicorn,请先执行 ./deploy.sh"; exit 1; }
[[ -d "$FRONTEND_DIR/node_modules" ]] || { err "未检测到 frontend/node_modules,请先执行 ./deploy.sh"; exit 1; }
mkdir -p "$RUN_DIR"
# --- 已在跑 → 先杀 ---
read_pid() { [[ -f "$1" ]] && cat "$1" || echo ""; }
kill_pid_file() {
local pid_file="$1" label="$2"
local pid
pid=$(read_pid "$pid_file")
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
for _ in $(seq 1 10); do
kill -0 "$pid" 2>/dev/null || break
sleep 0.3
done
kill -9 "$pid" 2>/dev/null || true
log "$label 已停止 (pid=$pid)"
fi
rm -f "$pid_file"
}
free_port() {
local port="$1"
local pids
pids=$(lsof -ti tcp:"$port" 2>/dev/null || true)
if [[ -n "$pids" ]]; then
warn "端口 $port 被占用 (pid: $pids),正在终止..."
# shellcheck disable=SC2086
kill -9 $pids 2>/dev/null || true
sleep 1
fi
}
# --- 健康检查 ---
wait_for() {
local url="$1" name="$2"
for _ in $(seq 1 30); do
if curl -sf "$url" >/dev/null 2>&1; then
log "$name 已启动 ✓ ($url)"
return 0
fi
sleep 1
done
err "$name 启动超时,请检查当前终端中的服务日志"
return 1
}
# --- 启动后端与前端 ---
# 先停止旧前端,避免它在后端启动期间继续转发请求并打印 ECONNREFUSED。
free_port "$FRONTEND_PORT"
kill_pid_file "$FRONTEND_PID_FILE" "前端"
free_port "$BACKEND_PORT"
kill_pid_file "$BACKEND_PID_FILE" "后端"
log "启动后端 FastAPI @ :$BACKEND_PORT (前台,日志输出到当前终端)"
(
cd "$BACKEND_DIR"
HOST=0.0.0.0 PORT="$BACKEND_PORT" \
"$VENV_DIR/bin/python3" -m uvicorn app.main:app \
--host 0.0.0.0 --port "$BACKEND_PORT"
) &
BACKEND_PID=$!
echo "$BACKEND_PID" >"$BACKEND_PID_FILE"
# 先确认后端已经开始监听,再启动前端,避免 Vite 代理在启动窗口内报 ECONNREFUSED。
if ! wait_for "http://localhost:$BACKEND_PORT/health" "后端"; then
kill "$BACKEND_PID" 2>/dev/null || true
rm -f "$BACKEND_PID_FILE"
exit 1
fi
log "启动前端 Vite @ :$FRONTEND_PORT (前台,日志输出到当前终端)"
(
cd "$FRONTEND_DIR"
exec npx vite
) &
FRONTEND_PID=$!
echo "$FRONTEND_PID" >"$FRONTEND_PID_FILE"
# Ctrl-C/退出时同时停止两个前台子进程,避免残留监听端口。
cleanup() {
trap - INT TERM EXIT
kill "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null || true
wait "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null || true
rm -f "$BACKEND_PID_FILE" "$FRONTEND_PID_FILE"
}
trap cleanup INT TERM EXIT
# 前端在后端就绪后才启动,因此这里只需检查前端本身。
wait_for "http://localhost:$FRONTEND_PORT" "前端" || true
echo
log "全部就绪 ✓"
echo -e " ${GREEN}前端:${NC} http://localhost:$FRONTEND_PORT"
echo -e " ${GREEN}后端:${NC} http://localhost:$BACKEND_PORT (健康检查: /health)"
echo -e " ${GREEN}日志:${NC} 当前终端 (按 Ctrl-C 停止)"
# 保持当前终端占用,实时显示两个服务的日志。
wait "$BACKEND_PID" "$FRONTEND_PID"