- FastAPI backend with build queue, WebSocket logs, task management - Vue 3 frontend with build/config/history views - Xcode project build automation with IPA export - Fix: initialize build_dir before try block to ensure cleanup on early failure
460 lines
13 KiB
Bash
Executable File
460 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
||
# iOS 自动打包服务 — 部署与管理脚本
|
||
# 用法: ./deploy.sh {build|start|stop|restart|status|watchdog|watchdog-stop}
|
||
|
||
set -e
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
cd "$SCRIPT_DIR"
|
||
|
||
PID_FILE="$SCRIPT_DIR/.server.pid"
|
||
WATCHDOG_PID_FILE="$SCRIPT_DIR/.watchdog.pid"
|
||
LOG_DIR="$SCRIPT_DIR/logs"
|
||
VENV_DIR="$SCRIPT_DIR/.venv"
|
||
|
||
# 加载 .env
|
||
if [ -f .env ]; then
|
||
set -a; source .env; set +a
|
||
fi
|
||
|
||
BACKEND_PORT="${BACKEND_PORT:-8000}"
|
||
WATCHDOG_INTERVAL="${WATCHDOG_INTERVAL:-30}" # 健康检查间隔(秒)
|
||
WATCHDOG_TIMEOUT="${WATCHDOG_TIMEOUT:-10}" # 健康检查超时(秒)
|
||
MAX_RESTART_ATTEMPTS="${MAX_RESTART_ATTEMPTS:-3}" # 连续重启上限,超过则冷却
|
||
COOLDOWN_SECONDS="${COOLDOWN_SECONDS:-300}" # 冷却时间(秒)
|
||
|
||
# ========== 颜色输出 ==========
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[0;33m'
|
||
NC='\033[0m'
|
||
|
||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||
|
||
# 日志同时写文件和终端
|
||
log_to_file() {
|
||
local level="$1"; shift
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >> "$LOG_DIR/watchdog.log"
|
||
}
|
||
|
||
# ========== 环境检查 ==========
|
||
check_env() {
|
||
if [ ! -f .env ]; then
|
||
error ".env 文件不存在"
|
||
echo " 请执行: cp .env.example .env && vim .env"
|
||
exit 1
|
||
fi
|
||
|
||
if ! command -v python3 &>/dev/null; then
|
||
error "未找到 python3"
|
||
exit 1
|
||
fi
|
||
|
||
if ! command -v node &>/dev/null; then
|
||
error "未找到 node"
|
||
exit 1
|
||
fi
|
||
|
||
# 检查 Xcode
|
||
if ! command -v xcodebuild &>/dev/null; then
|
||
warn "未找到 xcodebuild,打包功能将不可用"
|
||
fi
|
||
}
|
||
|
||
# ========== Python 虚拟环境 ==========
|
||
setup_venv() {
|
||
if [ ! -d "$VENV_DIR" ]; then
|
||
info "创建 Python 虚拟环境..."
|
||
python3 -m venv "$VENV_DIR"
|
||
fi
|
||
source "$VENV_DIR/bin/activate"
|
||
pip install -r requirements.txt -q
|
||
}
|
||
|
||
# ========== 构建前端 ==========
|
||
do_build() {
|
||
# build 只需要 Python 和 Node,不要求 .env
|
||
if ! command -v python3 &>/dev/null; then
|
||
error "未找到 python3"; exit 1
|
||
fi
|
||
if ! command -v node &>/dev/null; then
|
||
error "未找到 node"; exit 1
|
||
fi
|
||
|
||
# 首次部署时自动创建 .env
|
||
if [ ! -f .env ]; then
|
||
info "首次部署,创建 .env 配置文件..."
|
||
cp .env.example .env
|
||
warn "已创建 .env,请按需修改配置(当前为默认值)"
|
||
fi
|
||
|
||
info "安装前端依赖..."
|
||
cd "$SCRIPT_DIR/frontend"
|
||
npm install --silent
|
||
|
||
info "构建前端..."
|
||
npm run build
|
||
cd "$SCRIPT_DIR"
|
||
|
||
setup_venv
|
||
|
||
info "构建完成"
|
||
}
|
||
|
||
# ========== 启动服务 ==========
|
||
do_start() {
|
||
check_env
|
||
|
||
if [ -f "$PID_FILE" ]; then
|
||
local pid
|
||
pid=$(cat "$PID_FILE")
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
warn "服务已在运行 (PID: $pid)"
|
||
return 0
|
||
fi
|
||
rm -f "$PID_FILE"
|
||
fi
|
||
|
||
# 确保前端已构建
|
||
if [ ! -d "$SCRIPT_DIR/backend/static" ]; then
|
||
warn "前端未构建,先执行 build..."
|
||
do_build
|
||
fi
|
||
|
||
setup_venv
|
||
|
||
mkdir -p "$LOG_DIR"
|
||
|
||
info "启动后端服务 (端口: $BACKEND_PORT)..."
|
||
nohup python3 -m uvicorn backend.main:app \
|
||
--host 0.0.0.0 \
|
||
--port "$BACKEND_PORT" \
|
||
--workers 1 \
|
||
> "$LOG_DIR/server.log" 2>&1 &
|
||
|
||
echo $! > "$PID_FILE"
|
||
sleep 1
|
||
|
||
if kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||
info "服务已启动 (PID: $(cat "$PID_FILE"))"
|
||
info "访问地址: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo 'localhost'):$BACKEND_PORT"
|
||
else
|
||
error "启动失败,请查看日志: $LOG_DIR/server.log"
|
||
rm -f "$PID_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ========== 停止服务 ==========
|
||
do_stop() {
|
||
if [ ! -f "$PID_FILE" ]; then
|
||
warn "服务未在运行"
|
||
return 0
|
||
fi
|
||
|
||
local pid
|
||
pid=$(cat "$PID_FILE")
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
info "停止服务 (PID: $pid)..."
|
||
kill "$pid"
|
||
# 等待进程退出
|
||
for i in $(seq 1 10); do
|
||
kill -0 "$pid" 2>/dev/null || break
|
||
sleep 0.5
|
||
done
|
||
# 还没退出就强杀
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
warn "进程未响应,强制终止..."
|
||
kill -9 "$pid"
|
||
fi
|
||
info "服务已停止"
|
||
else
|
||
warn "进程已不存在"
|
||
fi
|
||
rm -f "$PID_FILE"
|
||
}
|
||
|
||
# ========== 查看状态 ==========
|
||
do_status() {
|
||
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||
info "服务运行中 (PID: $(cat "$PID_FILE")), 端口: $BACKEND_PORT"
|
||
else
|
||
warn "服务未运行"
|
||
rm -f "$PID_FILE" 2>/dev/null
|
||
fi
|
||
|
||
if [ -f "$WATCHDOG_PID_FILE" ] && kill -0 "$(cat "$WATCHDOG_PID_FILE")" 2>/dev/null; then
|
||
info "Watchdog 运行中 (PID: $(cat "$WATCHDOG_PID_FILE")), 检查间隔: ${WATCHDOG_INTERVAL}s"
|
||
else
|
||
warn "Watchdog 未运行"
|
||
rm -f "$WATCHDOG_PID_FILE" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# ========== 健康检查 ==========
|
||
check_health() {
|
||
# 检查进程是否存在
|
||
if [ ! -f "$PID_FILE" ] || ! kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||
echo "process_down"
|
||
return
|
||
fi
|
||
|
||
# 检查 HTTP 是否响应
|
||
local http_code
|
||
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
--connect-timeout "$WATCHDOG_TIMEOUT" \
|
||
--max-time "$WATCHDOG_TIMEOUT" \
|
||
"http://127.0.0.1:$BACKEND_PORT/api/health" 2>/dev/null || echo "000")
|
||
|
||
if [ "$http_code" != "200" ]; then
|
||
echo "http_$http_code"
|
||
return
|
||
fi
|
||
|
||
echo "ok"
|
||
}
|
||
|
||
# ========== Watchdog(前台模式,用于调试) ==========
|
||
do_watchdog_fg() {
|
||
info "Watchdog 启动(前台模式),检查间隔: ${WATCHDOG_INTERVAL}s"
|
||
info "按 Ctrl+C 停止"
|
||
|
||
local restart_count=0
|
||
local last_restart_time=0
|
||
|
||
while true; do
|
||
local result
|
||
result=$(check_health)
|
||
|
||
if [ "$result" = "ok" ]; then
|
||
restart_count=0
|
||
else
|
||
local now
|
||
now=$(date +%s)
|
||
warn "健康检查失败: $result"
|
||
|
||
# 冷却判断:连续重启超过上限,暂停一段时间
|
||
if [ "$restart_count" -ge "$MAX_RESTART_ATTEMPTS" ]; then
|
||
local elapsed=$((now - last_restart_time))
|
||
if [ "$elapsed" -lt "$COOLDOWN_SECONDS" ]; then
|
||
local wait=$((COOLDOWN_SECONDS - elapsed))
|
||
warn "连续重启 ${restart_count} 次,冷却中,${wait}s 后重试..."
|
||
log_to_file "WARN" "Cooldown: ${restart_count} restarts, waiting ${wait}s"
|
||
sleep "$wait"
|
||
continue
|
||
else
|
||
warn "冷却结束,重新尝试..."
|
||
restart_count=0
|
||
fi
|
||
fi
|
||
|
||
warn "正在重启服务..."
|
||
log_to_file "WARN" "Restarting service (cause: $result, attempt: $((restart_count+1)))"
|
||
|
||
do_stop
|
||
sleep 2
|
||
do_start
|
||
|
||
restart_count=$((restart_count + 1))
|
||
last_restart_time=$(date +%s)
|
||
|
||
if [ "$restart_count" -ge "$MAX_RESTART_ATTEMPTS" ]; then
|
||
error "已连续重启 ${restart_count} 次,进入冷却..."
|
||
log_to_file "ERROR" "Entered cooldown after $restart_count restarts"
|
||
fi
|
||
fi
|
||
|
||
sleep "$WATCHDOG_INTERVAL"
|
||
done
|
||
}
|
||
|
||
# ========== Watchdog(后台守护) ==========
|
||
do_watchdog() {
|
||
mkdir -p "$LOG_DIR"
|
||
|
||
if [ -f "$WATCHDOG_PID_FILE" ] && kill -0 "$(cat "$WATCHDOG_PID_FILE")" 2>/dev/null; then
|
||
warn "Watchdog 已在运行 (PID: $(cat "$WATCHDOG_PID_FILE"))"
|
||
return 0
|
||
fi
|
||
|
||
info "启动 Watchdog(后台模式),检查间隔: ${WATCHDOG_INTERVAL}s"
|
||
|
||
nohup bash -c "
|
||
cd '$SCRIPT_DIR'
|
||
source .env 2>/dev/null
|
||
exec bash '$(realpath "$0")' _watchdog_loop
|
||
" >> "$LOG_DIR/watchdog.log" 2>&1 &
|
||
|
||
echo $! > "$WATCHDOG_PID_FILE"
|
||
sleep 1
|
||
|
||
if kill -0 "$(cat "$WATCHDOG_PID_FILE")" 2>/dev/null; then
|
||
info "Watchdog 已启动 (PID: $(cat "$WATCHDOG_PID_FILE"))"
|
||
else
|
||
error "Watchdog 启动失败"
|
||
rm -f "$WATCHDOG_PID_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 内部循环入口(由后台 watchdog 进程调用)
|
||
do_watchdog_loop() {
|
||
local restart_count=0
|
||
local last_restart_time=0
|
||
|
||
log_to_file "INFO" "Watchdog started, interval=${WATCHDOG_INTERVAL}s"
|
||
|
||
while true; do
|
||
local result
|
||
result=$(check_health)
|
||
|
||
if [ "$result" = "ok" ]; then
|
||
restart_count=0
|
||
else
|
||
local now
|
||
now=$(date +%s)
|
||
log_to_file "WARN" "Health check failed: $result"
|
||
|
||
if [ "$restart_count" -ge "$MAX_RESTART_ATTEMPTS" ]; then
|
||
local elapsed=$((now - last_restart_time))
|
||
if [ "$elapsed" -lt "$COOLDOWN_SECONDS" ]; then
|
||
local wait=$((COOLDOWN_SECONDS - elapsed))
|
||
log_to_file "WARN" "Cooldown: ${restart_count} restarts, waiting ${wait}s"
|
||
sleep "$wait"
|
||
continue
|
||
else
|
||
log_to_file "INFO" "Cooldown ended, resetting counter"
|
||
restart_count=0
|
||
fi
|
||
fi
|
||
|
||
restart_count=$((restart_count + 1))
|
||
last_restart_time=$(date +%s)
|
||
log_to_file "WARN" "Restarting service (attempt: $restart_count)"
|
||
|
||
# 停止旧进程
|
||
if [ -f "$PID_FILE" ]; then
|
||
local old_pid
|
||
old_pid=$(cat "$PID_FILE")
|
||
kill "$old_pid" 2>/dev/null
|
||
sleep 2
|
||
kill -9 "$old_pid" 2>/dev/null || true
|
||
rm -f "$PID_FILE"
|
||
fi
|
||
|
||
# 重新启动
|
||
source "$VENV_DIR/bin/activate" 2>/dev/null
|
||
mkdir -p "$LOG_DIR"
|
||
nohup python3 -m uvicorn backend.main:app \
|
||
--host 0.0.0.0 \
|
||
--port "$BACKEND_PORT" \
|
||
--workers 1 \
|
||
>> "$LOG_DIR/server.log" 2>&1 &
|
||
echo $! > "$PID_FILE"
|
||
|
||
sleep 3
|
||
if kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||
log_to_file "INFO" "Service restarted successfully (PID: $(cat "$PID_FILE"))"
|
||
else
|
||
log_to_file "ERROR" "Service restart failed"
|
||
fi
|
||
fi
|
||
|
||
sleep "$WATCHDOG_INTERVAL"
|
||
done
|
||
}
|
||
|
||
# ========== 停止 Watchdog ==========
|
||
do_watchdog_stop() {
|
||
if [ ! -f "$WATCHDOG_PID_FILE" ]; then
|
||
warn "Watchdog 未在运行"
|
||
return 0
|
||
fi
|
||
|
||
local pid
|
||
pid=$(cat "$WATCHDOG_PID_FILE")
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
info "停止 Watchdog (PID: $pid)..."
|
||
kill "$pid"
|
||
sleep 1
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
info "Watchdog 已停止"
|
||
else
|
||
warn "Watchdog 进程已不存在"
|
||
fi
|
||
rm -f "$WATCHDOG_PID_FILE"
|
||
}
|
||
|
||
# ========== 运行测试 ==========
|
||
do_test() {
|
||
local failed=0
|
||
|
||
# 确保依赖已安装
|
||
if [ ! -d "$VENV_DIR" ]; then
|
||
setup_venv
|
||
else
|
||
source "$VENV_DIR/bin/activate"
|
||
fi
|
||
|
||
if [ ! -d "frontend/node_modules" ]; then
|
||
cd frontend && npm install --silent && cd ..
|
||
fi
|
||
|
||
# 后端测试
|
||
info "运行后端测试..."
|
||
echo "────────────────────────────────────────"
|
||
python3 -m pytest tests/ -v "$@" || failed=1
|
||
echo ""
|
||
|
||
# 前端测试
|
||
info "运行前端测试..."
|
||
echo "────────────────────────────────────────"
|
||
cd frontend && npx vitest run "$@" || failed=1
|
||
cd "$SCRIPT_DIR"
|
||
echo ""
|
||
|
||
# 结果
|
||
if [ "$failed" -eq 0 ]; then
|
||
info "全部测试通过"
|
||
else
|
||
error "存在失败的测试"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ========== 主入口 ==========
|
||
case "${1:-}" in
|
||
build) do_build ;;
|
||
start) do_start ;;
|
||
stop) do_stop; do_watchdog_stop ;;
|
||
restart) do_stop; do_start ;;
|
||
status) do_status ;;
|
||
test) shift; do_test "$@" ;;
|
||
watchdog) do_watchdog ;;
|
||
watchdog-stop) do_watchdog_stop ;;
|
||
watchdog-fg) do_watchdog_fg ;;
|
||
_watchdog_loop) do_watchdog_loop ;;
|
||
*)
|
||
echo "用法: $0 {build|start|stop|restart|status|test|watchdog|watchdog-stop}"
|
||
echo ""
|
||
echo " build 构建前端、安装依赖"
|
||
echo " start 启动服务(后台运行)"
|
||
echo " stop 停止服务(同时停止 watchdog)"
|
||
echo " restart 重启服务"
|
||
echo " status 查看运行状态"
|
||
echo " test 运行全部测试(后端 + 前端)"
|
||
echo " watchdog 启动健康监测(后台守护,自动重启故障服务)"
|
||
echo " watchdog-stop 停止健康监测"
|
||
echo " watchdog-fg 前台运行健康监测(调试用)"
|
||
echo ""
|
||
echo "环境变量(可在 .env 中配置):"
|
||
echo " WATCHDOG_INTERVAL 健康检查间隔(秒,默认 30)"
|
||
echo " WATCHDOG_TIMEOUT 健康检查超时(秒,默认 10)"
|
||
echo " MAX_RESTART_ATTEMPTS 连续重启上限(默认 3)"
|
||
echo " COOLDOWN_SECONDS 超过重启上限后冷却时间(秒,默认 300)"
|
||
exit 1
|
||
;;
|
||
esac
|