#!/bin/bash set -e # 出错时终止脚本 # 配置信息 APP_DIR="${APP_DIR:-/root/AutoScraperX}" LOG_FILE="${APP_DIR}/logs/autoscraperx_deploy.log" # 部署日志路径(统一到项目logs目录) VENV_DIR="${VENV_DIR:-${APP_DIR}/venv}" # 可通过环境变量覆盖 PYTHON="${VENV_DIR}/bin/python" # 虚拟环境Python解释器 PIP="${VENV_DIR}/bin/pip" # 虚拟环境pip工具 REQUIREMENTS="${APP_DIR}/requirements.txt" RUNTIME_DIR="${APP_DIR}/run" MAIN_STARTUP_LOG="${APP_DIR}/logs/main_startup.log" API_STARTUP_LOG="${APP_DIR}/logs/api_startup.log" MAIN_PID_FILE="${RUNTIME_DIR}/main.pid" API_PID_FILE="${RUNTIME_DIR}/api.pid" # 日志函数 log() { local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1" echo "$msg" echo "$msg" >> "$LOG_FILE" } # 错误处理函数 handle_error() { log "部署失败: $1" exit 1 } # 确保目录存在 ensure_dir() { if [ ! -d "$1" ]; then log "创建目录: $1" mkdir -p "$1" || handle_error "无法创建目录 $1" fi } # 检查文件权限 check_permission() { if [ ! -x "$1" ]; then log "修复文件权限: $1" chmod +x "$1" || handle_error "无法修复 $1 权限" fi } # 优先根据PID文件停止进程,并兼容旧版deploy.sh没有PID文件的进程。 stop_service() { local service_name="$1" local pid_file="$2" local process_pattern="$3" local pid="" if [ -f "$pid_file" ]; then pid="$(tr -cd '0-9' < "$pid_file")" if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then if ps -p "$pid" -o command= | grep -F "$process_pattern" > /dev/null; then log "停止${service_name}: pid=${pid}" kill "$pid" || true for _ in {1..10}; do if ! kill -0 "$pid" 2>/dev/null; then break fi sleep 1 done fi fi rm -f "$pid_file" fi # 首次升级时清理旧部署脚本直接启动、但没有记录PID的进程。 pkill -f "$process_pattern" || true } show_startup_error() { local service_name="$1" local startup_log="$2" log "${service_name}启动失败,启动日志最后20行:" tail -n 20 "$startup_log" >> "$LOG_FILE" 2>/dev/null || true } wait_api_ready() { local api_pid="$1" for _ in {1..15}; do if ! kill -0 "$api_pid" 2>/dev/null; then return 1 fi if "$PYTHON" -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:${API_PORT}/ready', timeout=3).read()" > /dev/null 2>&1; then return 0 fi sleep 1 done return 1 } # 主函数 main() { # 先创建基础目录,确保后续所有步骤都能写部署日志。 mkdir -p "${APP_DIR}/logs" "$RUNTIME_DIR" || exit 1 # 前置检查:确保项目目录存在 ensure_dir "$APP_DIR" ensure_dir "${APP_DIR}/logs" # 确保日志目录存在 ensure_dir "$RUNTIME_DIR" check_permission "${APP_DIR}/run.sh" # 修复run.sh权限 check_permission "${APP_DIR}/run_api.sh" log "===== 开始部署 AutoScraperX =====" cd "$APP_DIR" || handle_error "应用目录不存在: $APP_DIR" # 拉取最新代码(如果是Git仓库) log "拉取最新代码..." if [ -d ".git" ]; then git pull origin master || handle_error "Git拉取失败" else log "警告: 当前目录不是Git仓库,跳过拉取" fi # 创建或更新虚拟环境 log "检查虚拟环境..." if [ ! -d "$VENV_DIR" ]; then log "创建新的虚拟环境..." python3 -m venv "$VENV_DIR" || handle_error "创建虚拟环境失败" fi # 安装依赖(使用虚拟环境pip) log "安装Python依赖..." "$PIP" install --upgrade pip || handle_error "更新pip失败" "$PIP" install -r "$REQUIREMENTS" || handle_error "安装依赖失败" # 在停止旧服务前验证全部配置,避免配置错误扩大停机时间。 log "检查API配置..." "$PYTHON" -c "from config import settings; print(f'API监听地址: {settings.API_HOST}:{settings.API_PORT}')" >> "$LOG_FILE" \ || handle_error "API配置检查失败,请检查${APP_DIR}/.env" API_PORT="$("$PYTHON" -c "from config import settings; print(settings.API_PORT)")" \ || handle_error "无法读取API_PORT" log "停止现有API和主服务..." # 兼容清理迁移期间直接从fastapi_app启动的进程,正式入口统一为api.app。 stop_service "迁移期垂直视频API" "$API_PID_FILE" "${PYTHON} -m api.fastapi_app" stop_service "垂直视频API" "$API_PID_FILE" "${PYTHON} -m api.app" stop_service "主爬虫服务" "$MAIN_PID_FILE" "${PYTHON} main.py" # API先启动并通过数据库就绪检查,主爬虫随后启动。 log "启动垂直视频API...(日志: ${API_STARTUP_LOG})" nohup "${PYTHON}" -m api.app > "$API_STARTUP_LOG" 2>&1 & API_PID=$! echo "$API_PID" > "$API_PID_FILE" if ! wait_api_ready "$API_PID"; then show_startup_error "垂直视频API" "$API_STARTUP_LOG" stop_service "垂直视频API" "$API_PID_FILE" "${PYTHON} -m api.app" handle_error "垂直视频API启动或数据库就绪检查失败" fi log "垂直视频API已就绪: pid=${API_PID}" log "启动主爬虫服务...(日志: ${MAIN_STARTUP_LOG})" nohup "${PYTHON}" main.py > "$MAIN_STARTUP_LOG" 2>&1 & MAIN_PID=$! echo "$MAIN_PID" > "$MAIN_PID_FILE" sleep 3 if ! kill -0 "$MAIN_PID" 2>/dev/null; then show_startup_error "主爬虫服务" "$MAIN_STARTUP_LOG" stop_service "主爬虫服务" "$MAIN_PID_FILE" "${PYTHON} main.py" stop_service "垂直视频API" "$API_PID_FILE" "${PYTHON} -m api.app" handle_error "主爬虫服务启动失败" fi log "主爬虫服务已启动: pid=${MAIN_PID}" log "===== 部署完成:主爬虫和垂直视频API均已启动 =====" } # 执行主函数 main