#!/usr/bin/env bash # deploy/lib/common.sh — crypto_okx 部署公共函数 set -e set -u if [ -n "${BASH_VERSION:-}" ]; then set -o pipefail fi INSTALL_ROOT="${INSTALL_ROOT:-/opt/crypto_okx}" GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/crypto_okx.git}" GIT_BRANCH="${GIT_BRANCH:-main}" BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}" TZ_NAME="${CM_TZ:-Asia/Shanghai}" NODE_MAJOR="${NODE_MAJOR:-20}" APP_PORT="${APP_PORT:-5004}" PM2_APP_NAME="${PM2_APP_NAME:-crypto_okx}" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_DIR="$(cd "${LIB_DIR}/.." && pwd)" REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" PM2_APPS=( crypto_okx ) # 历史进程名(仅 stop/delete 时兼容) PM2_APPS_LEGACY=( crypto-monitor-okx crypto-monitor ) CONFIG_PATHS=( .env ) log() { printf '[%s] %s\n' "$(TZ="${TZ_NAME}" date '+%Y-%m-%d %H:%M:%S')" "$*"; } step() { echo ""; log "==> $*"; } die() { echo "错误: $*" >&2 exit 1 } require_root() { if [[ "$(id -u)" -ne 0 ]]; then die "请使用 root 执行(推荐: sudo -i 后运行)" fi } require_ubuntu() { if [[ ! -f /etc/os-release ]]; then log "警告: 未检测到 /etc/os-release,跳过 Ubuntu 版本检查" return 0 fi # shellcheck source=/dev/null source /etc/os-release if [[ "${ID:-}" != "ubuntu" ]]; then log "警告: 当前系统为 ${ID:-unknown},官方仅测试 Ubuntu 22.04/24.04" return 0 fi local ver="${VERSION_ID:-}" if [[ "${ver}" != "22.04" && "${ver}" != "24.04" ]]; then log "警告: Ubuntu ${ver} 未在文档中明确测试,继续执行" fi } detect_server_ip() { local ip="" if command -v hostname >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | awk '{print $1}')" fi if [[ -z "${ip}" ]]; then ip="127.0.0.1" fi echo "${ip}" } confirm_yes() { local msg="$1" local ans="" cm_read ans "${msg} [y/N] " [[ "${ans}" == [yY] || "${ans}" == [yY][eE][sS] ]] } confirm_uninstall() { local ans="" echo "此操作将停止 crypto_okx PM2 进程并移走安装目录." cm_read ans "输入 UNINSTALL 确认卸载: " [[ "${ans}" == "UNINSTALL" ]] } # curl | bash 时 stdin 是管道,须从 /dev/tty 读取用户输入 cm_read() { local __var="$1" local __prompt="$2" local __val="" if [[ -r /dev/tty ]]; then IFS= read -r -p "${__prompt}" __val /dev/null 2>&1; then die "未检测到 apt-get,请手动安装 python3-venv git curl" fi export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y python3 python3-pip python3-venv curl git ca-certificates local pyver="" if command -v python3 >/dev/null 2>&1; then pyver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv fi } install_node_pm2() { step "检查 Node.js 与 PM2" if command -v pm2 >/dev/null 2>&1; then log "PM2 已安装: $(pm2 -v)" return 0 fi if ! command -v node >/dev/null 2>&1; then log "安装 Node.js ${NODE_MAJOR}.x ..." curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - apt-get install -y nodejs fi log "安装 PM2 ..." npm install -g pm2 log "PM2: $(pm2 -v)" } pip_progress_bar_arg() { echo "on" } pip_upgrade_tools() { local pip_bin="$1" local bar bar="$(pip_progress_bar_arg)" echo " 升级 pip ..." "${pip_bin}" install -U pip setuptools wheel \ --disable-pip-version-check \ --progress-bar "${bar}" } pip_install_requirements() { local pip_bin="$1" local req_file="$2" local label="${3:-依赖}" local bar bar="$(pip_progress_bar_arg)" echo " 安装${label} (下方为 pip 进度) ..." [[ -f "${req_file}" ]] || die "缺少依赖文件: ${req_file}" "${pip_bin}" install -r "${req_file}" \ --disable-pip-version-check \ --retries 5 \ --progress-bar "${bar}" echo " ${label}安装完成" } install_backup_cron() { step "安装每日备份 cron" local inst="${REPO_ROOT}/scripts/install_backup_cron.sh" if [[ -f "${inst}" ]]; then chmod +x "${inst}" bash "${inst}" || log "警告: cron 安装失败" else log "未找到 scripts/install_backup_cron.sh,跳过" fi } remove_backup_cron() { step "移除备份 cron" local tmp removed=0 tmp="$(mktemp)" if ! crontab -l 2>/dev/null >"${tmp}"; then rm -f "${tmp}" return 0 fi local filtered filtered="$(grep -vF "backup_data.sh" "${tmp}" || true)" if [[ "${filtered}" != "$(cat "${tmp}")" ]]; then printf '%s\n' "${filtered}" | awk ' BEGIN { tz = 0 } /^CRON_TZ=Asia\/Shanghai$/ { if (tz++) next } { print } ' | crontab - removed=1 fi rm -f "${tmp}" if [[ "${removed}" -eq 1 ]]; then log "已移除 backup_data.sh 相关 cron" fi } pm2_app_exists() { local name="$1" # pm2 pid 在部分版本对不存在的进程仍返回 0;改用 jlist 精确匹配 name if ! command -v pm2 >/dev/null 2>&1; then return 1 fi if ! command -v python3 >/dev/null 2>&1; then pm2 describe "${name}" 2>/dev/null | grep -q "status" return $? fi pm2 jlist 2>/dev/null | python3 -c ' import json,sys name=sys.argv[1] try: apps=json.load(sys.stdin) except Exception: sys.exit(1) sys.exit(0 if any(isinstance(a,dict) and a.get("name")==name for a in (apps or [])) else 1) ' "${name}" } pm2_stop_project_apps() { if ! command -v pm2 >/dev/null 2>&1; then log "未安装 pm2,跳过" return 0 fi local name stopped=0 for name in "${PM2_APPS[@]}" "${PM2_APPS_LEGACY[@]}"; do if pm2_app_exists "${name}"; then log "pm2 stop ${name}" pm2 stop "${name}" 2>/dev/null || true stopped=$((stopped + 1)) fi done if [[ "${stopped}" -eq 0 ]]; then log "未发现本项目 PM2 进程(其它 PM2 不受影响)" fi } pm2_delete_project_apps() { if ! command -v pm2 >/dev/null 2>&1; then return 0 fi local name deleted=0 for name in "${PM2_APPS[@]}" "${PM2_APPS_LEGACY[@]}"; do if pm2_app_exists "${name}"; then log "pm2 delete ${name}" pm2 delete "${name}" 2>/dev/null || true deleted=$((deleted + 1)) fi done if [[ "${deleted}" -gt 0 ]]; then pm2 save 2>/dev/null || true fi } pm2_start_app() { step "启动 PM2: ${PM2_APP_NAME}" cd "${REPO_ROOT}" [[ -f "${REPO_ROOT}/ecosystem.config.cjs" ]] || die "缺少 ecosystem.config.cjs" [[ -x "${REPO_ROOT}/.venv/bin/python" ]] || die "缺少 .venv,请先运行 setup_env.sh" if pm2_app_exists "${PM2_APP_NAME}"; then log "已存在,执行 restart --update-env" if ! pm2 restart "${PM2_APP_NAME}" --update-env; then log "restart 失败,改为 start ecosystem.config.cjs" pm2 start ecosystem.config.cjs fi else log "首次启动: pm2 start ecosystem.config.cjs" pm2 start ecosystem.config.cjs fi } pm2_save_startup() { step "PM2 save & startup" pm2 save 2>/dev/null || true if pm2 startup systemd -u root --hp /root 2>/dev/null | grep -q "sudo"; then pm2 startup systemd -u root --hp /root 2>/dev/null | grep "^sudo" | bash || true else pm2 startup 2>/dev/null || true fi } is_deployed() { local root="$1" [[ -x "${root}/.venv/bin/python" ]] } read_app_port() { local env_file="${REPO_ROOT}/.env" local port="${APP_PORT}" if [[ -f "${env_file}" ]]; then local line line="$(grep -E '^[[:space:]]*APP_PORT=' "${env_file}" | tail -n1 || true)" if [[ -n "${line}" ]]; then port="${line#*=}" port="$(echo "${port}" | tr -d '"' | tr -d "'" | xargs)" fi fi echo "${port:-5004}" } check_http() { local url="$1" local code code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 8 "${url}" 2>/dev/null || echo "000")" [[ "${code}" == "200" || "${code}" == "302" || "${code}" == "301" || "${code}" == "401" || "${code}" == "403" ]] } verify_deployment() { local ip="${1:-$(detect_server_ip)}" local port local ok=1 local attempt max_attempts=20 port="$(read_app_port)" step "部署验收" if command -v pm2 >/dev/null 2>&1 && pm2_app_exists "${PM2_APP_NAME}"; then echo " [✓] PM2 ${PM2_APP_NAME} 已注册" else echo " [✗] PM2 缺少进程 ${PM2_APP_NAME}" ok=0 pm2 list 2>/dev/null || true fi # Flask 冷启动常需数秒;立刻 curl 易误报 echo " ... 等待页面就绪 (最多约 ${max_attempts}s,端口 ${port})" local http_ok=0 for attempt in $(seq 1 "${max_attempts}"); do if check_http "http://127.0.0.1:${port}/"; then http_ok=1 break fi # 进程已挂掉则提前失败 if command -v pm2 >/dev/null 2>&1 && ! pm2_app_exists "${PM2_APP_NAME}"; then echo " [✗] 等待期间 PM2 进程消失" ok=0 break fi local status="" status="$(pm2 jlist 2>/dev/null | python3 -c ' import json,sys name="'"${PM2_APP_NAME}"'" try: apps=json.load(sys.stdin) except Exception: print(""); sys.exit(0) for a in apps or []: if isinstance(a,dict) and a.get("name")==name: print((a.get("pm2_env") or {}).get("status") or "") break ' 2>/dev/null || true)" if [[ "${status}" == "errored" || "${status}" == "stopped" ]]; then echo " [✗] PM2 状态为 ${status}" ok=0 break fi sleep 1 done if [[ "${http_ok}" -eq 1 ]]; then echo " [✓] 页面可访问 (http://127.0.0.1:${port}/)" else echo " [✗] 页面不可访问 (http://127.0.0.1:${port}/)" ok=0 echo " 排查: pm2 logs ${PM2_APP_NAME} --lines 50" echo " 确认端口: grep APP_PORT ${REPO_ROOT}/.env" fi if [[ "${ok}" -eq 1 ]]; then echo "" log "验收通过: 进程在线 + 页面可打开" return 0 fi echo "" log "验收未完全通过,可执行: pm2 logs ${PM2_APP_NAME} --lines 30" return 1 } print_post_install_guide() { local ip="${1:-$(detect_server_ip)}" local port port="$(read_app_port)" cat </dev/null || echo '?')" echo " IP: $(detect_server_ip)" echo " 安装根: ${INSTALL_ROOT}" echo " 仓库: ${GIT_URL} (${GIT_BRANCH})" step "依赖检测" local item cmd for item in "python3:python3" "git:git" "curl:curl" "node:node" "npm:npm" "pm2:pm2"; do cmd="${item#*:}" if command -v "${cmd}" >/dev/null 2>&1; then case "${cmd}" in python3) echo " [✓] python3: $(python3 --version 2>&1)" ;; node) echo " [✓] node: $(node -v 2>&1)" ;; npm) echo " [✓] npm: $(npm -v 2>&1)" ;; pm2) echo " [✓] pm2: $(pm2 -v 2>&1)" ;; *) echo " [✓] ${cmd}: $(command -v "${cmd}")" ;; esac else echo " [✗] ${cmd}: 未安装" fi done if command -v python3 >/dev/null 2>&1; then local ver major minor ver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" major="${ver%%.*}" minor="${ver#*.}" if [[ "${major}" -gt 3 ]] || [[ "${major}" -eq 3 && "${minor}" -ge 10 ]]; then echo " [✓] Python 版本满足 3.10+ (${ver})" else echo " [✗] Python 需要 3.10+,当前 ${ver}" fi fi step "项目状态" local root="" if root="$(resolve_repo_root 2>/dev/null)"; then echo " [✓] 仓库就绪: ${root}" [[ -f "${root}/requirements.txt" ]] && echo " [✓] requirements.txt" || echo " [✗] requirements.txt" [[ -f "${root}/ecosystem.config.cjs" ]] && echo " [✓] ecosystem.config.cjs" || echo " [✗] ecosystem.config.cjs" [[ -f "${root}/.env" ]] && echo " [✓] .env 已存在" || echo " [!] .env 尚未生成" if [[ -x "${root}/.venv/bin/python" ]]; then echo " [✓] .venv: $("${root}/.venv/bin/python" --version 2>&1)" else echo " [!] .venv 未创建" fi else echo " [!] 尚未安装到 ${INSTALL_ROOT}" fi if command -v pm2 >/dev/null 2>&1; then step "PM2" pm2 list 2>/dev/null | head -n 40 || true fi }