deploy.sh 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. #!/bin/bash
  2. set -e # 出错时终止脚本
  3. # 配置信息
  4. APP_DIR="${APP_DIR:-/root/AutoScraperX}"
  5. LOG_FILE="${APP_DIR}/logs/autoscraperx_deploy.log" # 部署日志路径(统一到项目logs目录)
  6. VENV_DIR="${VENV_DIR:-${APP_DIR}/venv}" # 可通过环境变量覆盖
  7. PYTHON="${VENV_DIR}/bin/python" # 虚拟环境Python解释器
  8. PIP="${VENV_DIR}/bin/pip" # 虚拟环境pip工具
  9. REQUIREMENTS="${APP_DIR}/requirements.txt"
  10. RUNTIME_DIR="${APP_DIR}/run"
  11. MAIN_STARTUP_LOG="${APP_DIR}/logs/main_startup.log"
  12. API_STARTUP_LOG="${APP_DIR}/logs/api_startup.log"
  13. MAIN_PID_FILE="${RUNTIME_DIR}/main.pid"
  14. API_PID_FILE="${RUNTIME_DIR}/api.pid"
  15. # 日志函数
  16. log() {
  17. local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
  18. echo "$msg"
  19. echo "$msg" >> "$LOG_FILE"
  20. }
  21. # 错误处理函数
  22. handle_error() {
  23. log "部署失败: $1"
  24. exit 1
  25. }
  26. # 确保目录存在
  27. ensure_dir() {
  28. if [ ! -d "$1" ]; then
  29. log "创建目录: $1"
  30. mkdir -p "$1" || handle_error "无法创建目录 $1"
  31. fi
  32. }
  33. # 检查文件权限
  34. check_permission() {
  35. if [ ! -x "$1" ]; then
  36. log "修复文件权限: $1"
  37. chmod +x "$1" || handle_error "无法修复 $1 权限"
  38. fi
  39. }
  40. # 优先根据PID文件停止进程,并兼容旧版deploy.sh没有PID文件的进程。
  41. stop_service() {
  42. local service_name="$1"
  43. local pid_file="$2"
  44. local process_pattern="$3"
  45. local pid=""
  46. if [ -f "$pid_file" ]; then
  47. pid="$(tr -cd '0-9' < "$pid_file")"
  48. if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
  49. if ps -p "$pid" -o command= | grep -F "$process_pattern" > /dev/null; then
  50. log "停止${service_name}: pid=${pid}"
  51. kill "$pid" || true
  52. for _ in {1..10}; do
  53. if ! kill -0 "$pid" 2>/dev/null; then
  54. break
  55. fi
  56. sleep 1
  57. done
  58. fi
  59. fi
  60. rm -f "$pid_file"
  61. fi
  62. # 首次升级时清理旧部署脚本直接启动、但没有记录PID的进程。
  63. pkill -f "$process_pattern" || true
  64. }
  65. show_startup_error() {
  66. local service_name="$1"
  67. local startup_log="$2"
  68. log "${service_name}启动失败,启动日志最后20行:"
  69. tail -n 20 "$startup_log" >> "$LOG_FILE" 2>/dev/null || true
  70. }
  71. wait_api_ready() {
  72. local api_pid="$1"
  73. for _ in {1..15}; do
  74. if ! kill -0 "$api_pid" 2>/dev/null; then
  75. return 1
  76. fi
  77. 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
  78. return 0
  79. fi
  80. sleep 1
  81. done
  82. return 1
  83. }
  84. # 主函数
  85. main() {
  86. # 先创建基础目录,确保后续所有步骤都能写部署日志。
  87. mkdir -p "${APP_DIR}/logs" "$RUNTIME_DIR" || exit 1
  88. # 前置检查:确保项目目录存在
  89. ensure_dir "$APP_DIR"
  90. ensure_dir "${APP_DIR}/logs" # 确保日志目录存在
  91. ensure_dir "$RUNTIME_DIR"
  92. check_permission "${APP_DIR}/run.sh" # 修复run.sh权限
  93. check_permission "${APP_DIR}/run_api.sh"
  94. log "===== 开始部署 AutoScraperX ====="
  95. cd "$APP_DIR" || handle_error "应用目录不存在: $APP_DIR"
  96. # 拉取最新代码(如果是Git仓库)
  97. log "拉取最新代码..."
  98. if [ -d ".git" ]; then
  99. git pull origin master || handle_error "Git拉取失败"
  100. else
  101. log "警告: 当前目录不是Git仓库,跳过拉取"
  102. fi
  103. # 创建或更新虚拟环境
  104. log "检查虚拟环境..."
  105. if [ ! -d "$VENV_DIR" ]; then
  106. log "创建新的虚拟环境..."
  107. python3 -m venv "$VENV_DIR" || handle_error "创建虚拟环境失败"
  108. fi
  109. # 安装依赖(使用虚拟环境pip)
  110. log "安装Python依赖..."
  111. "$PIP" install --upgrade pip || handle_error "更新pip失败"
  112. "$PIP" install -r "$REQUIREMENTS" || handle_error "安装依赖失败"
  113. # 在停止旧服务前验证全部配置,避免配置错误扩大停机时间。
  114. log "检查API配置和Token..."
  115. "$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" \
  116. || handle_error "API配置检查失败,请检查${APP_DIR}/.env"
  117. API_PORT="$("$PYTHON" -c "from config import settings; print(settings.API_PORT)")" \
  118. || handle_error "无法读取API_PORT"
  119. log "停止现有API和主服务..."
  120. stop_service "垂直视频API" "$API_PID_FILE" "${PYTHON} -m api.app"
  121. stop_service "主爬虫服务" "$MAIN_PID_FILE" "${PYTHON} main.py"
  122. # API先启动并通过数据库就绪检查,主爬虫随后启动。
  123. log "启动垂直视频API...(日志: ${API_STARTUP_LOG})"
  124. nohup "${PYTHON}" -m api.app > "$API_STARTUP_LOG" 2>&1 &
  125. API_PID=$!
  126. echo "$API_PID" > "$API_PID_FILE"
  127. if ! wait_api_ready "$API_PID"; then
  128. show_startup_error "垂直视频API" "$API_STARTUP_LOG"
  129. stop_service "垂直视频API" "$API_PID_FILE" "${PYTHON} -m api.app"
  130. handle_error "垂直视频API启动或数据库就绪检查失败"
  131. fi
  132. log "垂直视频API已就绪: pid=${API_PID}"
  133. log "启动主爬虫服务...(日志: ${MAIN_STARTUP_LOG})"
  134. nohup "${PYTHON}" main.py > "$MAIN_STARTUP_LOG" 2>&1 &
  135. MAIN_PID=$!
  136. echo "$MAIN_PID" > "$MAIN_PID_FILE"
  137. sleep 3
  138. if ! kill -0 "$MAIN_PID" 2>/dev/null; then
  139. show_startup_error "主爬虫服务" "$MAIN_STARTUP_LOG"
  140. stop_service "主爬虫服务" "$MAIN_PID_FILE" "${PYTHON} main.py"
  141. stop_service "垂直视频API" "$API_PID_FILE" "${PYTHON} -m api.app"
  142. handle_error "主爬虫服务启动失败"
  143. fi
  144. log "主爬虫服务已启动: pid=${MAIN_PID}"
  145. log "===== 部署完成:主爬虫和垂直视频API均已启动 ====="
  146. }
  147. # 执行主函数
  148. main