#!/usr/bin/env bash # deploy/lib/common.sh — market_intel 部署公共函数(Docker Compose) # 目标系统: Ubuntu 22.04 LTS set -e set -u if [ -n "${BASH_VERSION:-}" ]; then set -o pipefail fi INSTALL_ROOT="${INSTALL_ROOT:-/opt/market_intel}" GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/market_intel.git}" GIT_BRANCH="${GIT_BRANCH:-main}" BACKUP_ROOT="${BACKUP_ROOT:-/root/backups/market_intel}" TZ_NAME="${MI_TZ:-Asia/Shanghai}" MI_PORT_DEFAULT="${MI_PORT_DEFAULT:-5170}" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_DIR="$(cd "${LIB_DIR}/.." && pwd)" REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" 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 } _apt_lock_holders() { ps -eo pid,cmd 2>/dev/null | grep -E '[u]nattended-upgr|[a]pt-get|[a]pt |[d]pkg ' | head -n 8 || true if command -v fuser >/dev/null 2>&1; then fuser -v /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock 2>&1 | head -n 12 || true fi } _stop_auto_apt_for_deploy() { if [[ "${APT_STOP_AUTO:-1}" != "1" ]]; then return 0 fi if command -v systemctl >/dev/null 2>&1; then log "临时停止 unattended-upgrades / apt-daily,避免占锁…" systemctl stop unattended-upgrades.service 2>/dev/null || true systemctl stop apt-daily.service apt-daily-upgrade.service 2>/dev/null || true systemctl kill --kill-who=all unattended-upgrades.service 2>/dev/null || true fi if [[ "${APT_FORCE_UNLOCK:-0}" == "1" ]]; then log "APT_FORCE_UNLOCK=1:结束残留 apt/dpkg 进程…" pkill -9 -x unattended-upgr 2>/dev/null || true pkill -9 -x apt-get 2>/dev/null || true pkill -9 -x apt 2>/dev/null || true pkill -9 -x dpkg 2>/dev/null || true sleep 2 dpkg --configure -a 2>/dev/null || true fi } wait_for_apt_lock() { local max_wait="${1:-600}" local waited=0 local tried_stop=0 if ! command -v apt-get >/dev/null 2>&1; then return 0 fi while true; do local busy=0 if pgrep -x unattended-upgr >/dev/null 2>&1 \ || pgrep -x apt-get >/dev/null 2>&1 \ || pgrep -x apt >/dev/null 2>&1 \ || pgrep -x dpkg >/dev/null 2>&1; then busy=1 fi if command -v fuser >/dev/null 2>&1; then if fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \ || fuser /var/lib/dpkg/lock >/dev/null 2>&1 \ || fuser /var/lib/apt/lists/lock >/dev/null 2>&1; then busy=1 fi fi if [[ "${busy}" -eq 0 ]]; then [[ "${waited}" -gt 0 ]] && log "apt 锁已释放,继续安装" return 0 fi if [[ "${waited}" -eq 0 ]]; then log "检测到 apt/dpkg 正被占用,等待释放…" _apt_lock_holders | while IFS= read -r line; do log " ${line}"; done elif [[ "${tried_stop}" -eq 0 && "${waited}" -ge 15 ]]; then tried_stop=1 _stop_auto_apt_for_deploy elif [[ $((waited % 60)) -eq 0 ]]; then log "仍在等待 apt 锁…已等 ${waited}s / ${max_wait}s" fi if [[ "${waited}" -ge "${max_wait}" ]]; then die "等待 apt 锁超时(${max_wait}s)" fi sleep 5 waited=$((waited + 5)) done } apt_update() { wait_for_apt_lock apt-get update -qq } apt_install() { wait_for_apt_lock apt-get install -y "$@" } detect_server_ip() { local ip="" if command -v hostname >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | awk '{print $1}')" fi [[ -z "${ip}" ]] && ip="127.0.0.1" echo "${ip}" } cm_read() { local __var="$1" local __prompt="${2:-}" local __line="" if [[ -n "${__prompt}" ]]; then printf '%s' "${__prompt}" >/dev/tty 2>/dev/null || printf '%s' "${__prompt}" fi if [[ -r /dev/tty ]]; then IFS= read -r __line /dev/null | tail -n1 | cut -d= -f2- || true)" fi printf '%s' "${val}" } # 已有非空不覆盖;空或缺省则写入/询问 ensure_env_key() { local file="$1" local key="$2" local prompt="$3" local default="$4" local current current="$(read_env_value "${file}" "${key}")" if [[ -n "${current}" ]]; then log "保留 ${key}=***(已有非空值)" return 0 fi local input="" if [[ -n "${prompt}" ]]; then cm_read input "${prompt} [${default}]: " fi if [[ -z "${input}" ]]; then input="${default}" fi if grep -qE "^${key}=" "${file}" 2>/dev/null; then # 替换空值行 local tmp tmp="$(mktemp)" awk -v k="${key}" -v v="${input}" ' BEGIN{FS=OFS="="} $1==k {$0=k"="v} {print} ' "${file}" >"${tmp}" && mv "${tmp}" "${file}" else printf '%s=%s\n' "${key}" "${input}" >>"${file}" fi log "已设置 ${key}" } ensure_dotenv() { local root="$1" local envf="${root}/.env" if [[ ! -f "${envf}" ]]; then if [[ -f "${root}/.env.example" ]]; then cp -a "${root}/.env.example" "${envf}" log "已从 .env.example 生成 .env" else touch "${envf}" fi fi echo "" echo "配置 .env(回车采用默认;已有非空值不覆盖)" ensure_env_key "${envf}" "MI_PORT" "HTTP 端口" "${MI_PORT_DEFAULT}" ensure_env_key "${envf}" "SAMPLE_INTERVAL_SEC" "采样间隔秒" "30" ensure_env_key "${envf}" "MIN_OPTION_LEVERAGE" "杠杆达标线" "100" ensure_env_key "${envf}" "AUTH_SECRET" "鉴权密钥(disabled 关闭)" "change-me" ensure_env_key "${envf}" "ADMIN_USERNAME" "管理员用户名" "admin" ensure_env_key "${envf}" "ADMIN_PASSWORD" "管理员密码" "admin123" ensure_env_key "${envf}" "OKX_API_KEY" "OKX API Key(可空)" "" ensure_env_key "${envf}" "OKX_API_SECRET" "OKX API Secret(可空)" "" ensure_env_key "${envf}" "OKX_API_PASSPHRASE" "OKX Passphrase(可空)" "" ensure_env_key "${envf}" "WECOM_ENABLED" "企微告警 1/0" "0" ensure_env_key "${envf}" "WECOM_WEBHOOK_URL" "企微 Webhook(可空)" "" ensure_env_key "${envf}" "WECOM_MACHINE_NAME" "机器名(可空)" "" ensure_env_key "${envf}" "ALERT_FAIL_THRESHOLD" "连续失败告警阈值" "5" # 固定库路径(容器内) if [[ -z "$(read_env_value "${envf}" "MI_DB_PATH")" ]]; then ensure_env_key "${envf}" "MI_DB_PATH" "" "/app/data/market_intel.db" fi } docker_ok() { command -v docker >/dev/null 2>&1 || return 1 docker compose version >/dev/null 2>&1 || return 1 return 0 } ensure_docker() { step "环境检测 (Docker / Compose)" if docker_ok; then log "Docker 已就绪: $(docker --version 2>&1)" log "Compose: $(docker compose version 2>&1)" return 0 fi if ! command -v apt-get >/dev/null 2>&1; then die "未找到 Docker,且无 apt-get,请手动安装 Docker + Compose 插件" fi step "安装 Docker Engine + Compose 插件" export DEBIAN_FRONTEND=noninteractive apt_update apt_install ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg fi local codename codename="$(. /etc/os-release && echo "${VERSION_CODENAME}")" echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${codename} stable" \ >/etc/apt/sources.list.d/docker.list apt_update apt_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin systemctl enable --now docker 2>/dev/null || true if ! docker_ok; then die "Docker 安装后仍不可用" fi log "Docker 安装完成" } compose() { local root="${REPO_ROOT:-${INSTALL_ROOT}}" (cd "${root}" && docker compose "$@") } compose_up_build() { step "docker compose up -d --build" mkdir -p /root/market_intel_backups 2>/dev/null || true compose up -d --build } compose_stop() { require_root if ! REPO_ROOT="$(resolve_repo_root)"; then die "未找到安装目录 ${INSTALL_ROOT}" fi step "停止服务" compose stop log "已停止" } compose_start() { require_root if ! REPO_ROOT="$(resolve_repo_root)"; then die "未找到安装目录 ${INSTALL_ROOT}" fi step "启动服务" compose up -d verify_health || true } read_mi_port() { local root="${1:-${REPO_ROOT:-${INSTALL_ROOT}}}" local p p="$(read_env_value "${root}/.env" "MI_PORT")" if [[ -z "${p}" ]]; then p="${MI_PORT_DEFAULT}" fi echo "${p}" } verify_health() { local port port="$(read_mi_port)" local url="http://127.0.0.1:${port}/health" step "健康检查 ${url}" local i for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do if curl -fsS "${url}" >/dev/null 2>&1; then log "health OK" curl -fsS "${url}" || true echo "" return 0 fi sleep 2 done log "警告: health 暂未就绪,请检查: docker compose -f ${REPO_ROOT}/docker-compose.yml logs" return 1 } show_status() { if ! REPO_ROOT="$(resolve_repo_root)"; then die "未找到安装目录 ${INSTALL_ROOT}" fi step "容器状态" compose ps || true verify_health || true } print_post_install_guide() { local ip port ip="$(detect_server_ip)" port="$(read_mi_port)" echo "" echo "══════════════════════════════════════" echo " 比特骆驼行情采集分析 部署完成" echo " 目录: ${INSTALL_ROOT}" echo " 本机: http://${ip}:${port}/health" echo " 看板: http://${ip}:${port}/" echo " 管理: bash ${INSTALL_ROOT}/deploy/manage.sh" echo " 配置: ${INSTALL_ROOT}/.env" echo "══════════════════════════════════════" echo "" }