iOSBuildServer/deploy.sh

493 lines
14 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.

#!/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}"
BIND_HOST="${BIND_HOST:-127.0.0.1}"
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} $*"; }
# 在 PID 文件丢失或进程 PID 被复用时,仍能安全识别本服务。
is_server_process() {
local pid="$1"
local command
command=$(ps -p "$pid" -o command= 2>/dev/null || true)
[[ "$command" == *"uvicorn backend.main:app"* ]]
}
get_server_pid() {
local pid
if [ -f "$PID_FILE" ]; then
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null && is_server_process "$pid"; then
echo "$pid"
return 0
fi
rm -f "$PID_FILE"
fi
# 仅认领监听当前配置端口、且命令明确属于本项目的 uvicorn 进程。
for pid in $(lsof -tiTCP:"$BACKEND_PORT" -sTCP:LISTEN 2>/dev/null); do
if is_server_process "$pid"; then
echo "$pid" > "$PID_FILE"
echo "$pid"
return 0
fi
done
return 1
}
# 日志同时写文件和终端
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
# 运行已构建的前端不需要 Node仅在缺少静态资源时才需要它重新构建。
if [ ! -d "$SCRIPT_DIR/backend/static" ] && ! 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() {
# 项目目录移动后venv 内 pip 的 shebang 会指向旧路径,需要重建。
if [ -d "$VENV_DIR" ] && ! "$VENV_DIR/bin/pip" --version >/dev/null 2>&1; then
warn "检测到失效的 Python 虚拟环境,正在重建..."
rm -rf "$VENV_DIR"
fi
if [ ! -d "$VENV_DIR" ]; then
info "创建 Python 虚拟环境..."
python3 -m venv "$VENV_DIR"
fi
"$VENV_DIR/bin/python3" -m 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
local existing_pid
existing_pid=$(get_server_pid || true)
if [ -n "$existing_pid" ]; then
warn "服务已在运行 (PID: $existing_pid)"
return 0
fi
# 确保前端已构建
if [ ! -d "$SCRIPT_DIR/backend/static" ]; then
warn "前端未构建,先执行 build..."
do_build
fi
setup_venv
mkdir -p "$LOG_DIR"
info "启动后端服务 ($BIND_HOST:$BACKEND_PORT)..."
nohup "$VENV_DIR/bin/python3" -m uvicorn backend.main:app \
--host "$BIND_HOST" \
--port "$BACKEND_PORT" \
--proxy-headers \
--forwarded-allow-ips 127.0.0.1 \
--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://$BIND_HOST:$BACKEND_PORT"
else
error "启动失败,请查看日志: $LOG_DIR/server.log"
rm -f "$PID_FILE"
return 1
fi
}
# ========== 停止服务 ==========
do_stop() {
local pid
pid=$(get_server_pid || true)
if [ -z "$pid" ]; then
warn "服务未在运行"
return 0
fi
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 "服务已停止"
rm -f "$PID_FILE"
}
# ========== 查看状态 ==========
do_status() {
local pid
pid=$(get_server_pid || true)
if [ -n "$pid" ]; then
info "服务运行中 (PID: $pid), 端口: $BACKEND_PORT"
else
warn "服务未运行"
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() {
# 检查进程是否存在
local pid
pid=$(get_server_pid || true)
if [ -z "$pid" ]; 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
# 重新启动
setup_venv
mkdir -p "$LOG_DIR"
nohup "$VENV_DIR/bin/python3" -m uvicorn backend.main:app \
--host "$BIND_HOST" \
--port "$BACKEND_PORT" \
--proxy-headers \
--forwarded-allow-ips 127.0.0.1 \
--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
# 确保依赖已安装
setup_venv
"$VENV_DIR/bin/python3" -m pip install -r requirements-dev.txt -q
if [ ! -d "frontend/node_modules" ]; then
cd frontend && npm install --silent && cd ..
fi
# 后端测试
info "运行后端测试..."
echo "────────────────────────────────────────"
"$VENV_DIR/bin/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