| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- #!/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配置和Token..."
- "$PYTHON" -c "from config import settings; assert len(settings.CHUI_ZHI_API_TOKEN) >= 32, 'CHUI_ZHI_API_TOKEN至少需要32个字符'; 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和主服务..."
- 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
|