Initial commit: iOS Build Server

- 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
This commit is contained in:
shen
2026-06-06 17:42:27 +08:00
commit 6f4f625c56
51 changed files with 9968 additions and 0 deletions
Executable
+459
View File
@@ -0,0 +1,459 @@
#!/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