diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7848ed4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Shell scripts must use LF (Linux deploy via curl | bash) +*.sh text eol=lf + +# Docker / compose +Dockerfile text eol=lf +docker-compose.yml text eol=lf + +# Default for text +* text=auto diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index f2985d7..42ac7e8 100644 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -1,5 +1,5 @@ -#!/usr/bin/env bash -# 兼容入口:转发到 manage.sh -set -euo pipefail -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -exec bash "${ROOT}/deploy/manage.sh" "$@" +#!/usr/bin/env bash +# 兼容入口:转发到 manage.sh +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +exec bash "${ROOT}/deploy/manage.sh" "$@" diff --git a/deploy/lib/common.sh b/deploy/lib/common.sh index 01025f8..446b198 100644 --- a/deploy/lib/common.sh +++ b/deploy/lib/common.sh @@ -1,358 +1,358 @@ -#!/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_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" - 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 "" -} +#!/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_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" + 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 "" +} diff --git a/deploy/lib/install.sh b/deploy/lib/install.sh index 2c6a1d4..f170371 100644 --- a/deploy/lib/install.sh +++ b/deploy/lib/install.sh @@ -1,79 +1,79 @@ -#!/usr/bin/env bash -# deploy/lib/install.sh — 一键部署 market_intel(Docker Compose) -set -e -set -u -if [ -n "${BASH_VERSION:-}" ]; then - set -o pipefail -fi - -LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=common.sh -source "${LIB_DIR}/common.sh" - -run_pipeline() { - local root="$1" - REPO_ROOT="${root}" - ensure_dotenv "${root}" - compose_up_build - verify_health || true - print_post_install_guide -} - -install_fresh() { - require_root - step "一键部署 — Docker 环境检测" - ensure_docker - # 基础包 - if ! command -v git >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then - export DEBIAN_FRONTEND=noninteractive - apt_update - apt_install ca-certificates git curl - fi - step "克隆仓库 → ${INSTALL_ROOT}" - if [[ -d "${INSTALL_ROOT}" ]]; then - die "目录已存在: ${INSTALL_ROOT},请先卸载或选修复" - fi - mkdir -p "$(dirname "${INSTALL_ROOT}")" - git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" - run_pipeline "${INSTALL_ROOT}" -} - -install_repair() { - require_root - step "修复部署 — Docker 环境检测" - ensure_docker - if ! REPO_ROOT="$(resolve_repo_root)"; then - die "未找到安装目录 ${INSTALL_ROOT}" - fi - step "修复环境(保留 .env 与 data volume)" - if [[ -d "${REPO_ROOT}/.git" ]]; then - git -C "${REPO_ROOT}" pull --ff-only origin "${GIT_BRANCH}" 2>/dev/null \ - || git -C "${REPO_ROOT}" pull --ff-only 2>/dev/null \ - || true - fi - run_pipeline "${REPO_ROOT}" -} - -handle_existing() { - echo "" - echo "检测到已部署: ${INSTALL_ROOT}" - echo " a) 取消" - echo " b) 修复/重装环境(保留 .env 与数据 volume)" - local choice="" - cm_read choice "请选择 [a/b]: " - case "${choice}" in - b|B) install_repair ;; - *) log "已取消" ;; - esac -} - -main_install() { - require_root - if repo_ready "${INSTALL_ROOT}"; then - handle_existing - else - install_fresh - fi -} - -main_install "$@" +#!/usr/bin/env bash +# deploy/lib/install.sh — 一键部署 market_intel(Docker Compose) +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +run_pipeline() { + local root="$1" + REPO_ROOT="${root}" + ensure_dotenv "${root}" + compose_up_build + verify_health || true + print_post_install_guide +} + +install_fresh() { + require_root + step "一键部署 — Docker 环境检测" + ensure_docker + # 基础包 + if ! command -v git >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt_update + apt_install ca-certificates git curl + fi + step "克隆仓库 → ${INSTALL_ROOT}" + if [[ -d "${INSTALL_ROOT}" ]]; then + die "目录已存在: ${INSTALL_ROOT},请先卸载或选修复" + fi + mkdir -p "$(dirname "${INSTALL_ROOT}")" + git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" + run_pipeline "${INSTALL_ROOT}" +} + +install_repair() { + require_root + step "修复部署 — Docker 环境检测" + ensure_docker + if ! REPO_ROOT="$(resolve_repo_root)"; then + die "未找到安装目录 ${INSTALL_ROOT}" + fi + step "修复环境(保留 .env 与 data volume)" + if [[ -d "${REPO_ROOT}/.git" ]]; then + git -C "${REPO_ROOT}" pull --ff-only origin "${GIT_BRANCH}" 2>/dev/null \ + || git -C "${REPO_ROOT}" pull --ff-only 2>/dev/null \ + || true + fi + run_pipeline "${REPO_ROOT}" +} + +handle_existing() { + echo "" + echo "检测到已部署: ${INSTALL_ROOT}" + echo " a) 取消" + echo " b) 修复/重装环境(保留 .env 与数据 volume)" + local choice="" + cm_read choice "请选择 [a/b]: " + case "${choice}" in + b|B) install_repair ;; + *) log "已取消" ;; + esac +} + +main_install() { + require_root + if repo_ready "${INSTALL_ROOT}"; then + handle_existing + else + install_fresh + fi +} + +main_install "$@" diff --git a/deploy/lib/uninstall.sh b/deploy/lib/uninstall.sh index dbaf3a6..930b90f 100644 --- a/deploy/lib/uninstall.sh +++ b/deploy/lib/uninstall.sh @@ -1,105 +1,105 @@ -#!/usr/bin/env bash -# deploy/lib/uninstall.sh — 停容器;可选删 volume;备份 .env;删除安装目录 -set -e -set -u -if [ -n "${BASH_VERSION:-}" ]; then - set -o pipefail -fi - -LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=common.sh -source "${LIB_DIR}/common.sh" - -main_uninstall() { - require_root - - local root="" - if ! root="$(resolve_repo_root)"; then - if [[ -d "${INSTALL_ROOT}" ]]; then - root="${INSTALL_ROOT}" - else - die "未找到安装目录 ${INSTALL_ROOT}" - fi - fi - REPO_ROOT="${root}" - - echo "" - echo "将卸载 比特骆驼行情采集分析 (market_intel):" - echo " - 停止并移除 Compose 容器" - echo " - 备份 .env 到 ${BACKUP_ROOT}" - echo " - 删除安装目录: ${INSTALL_ROOT}" - echo " - 可选:删除 data volume" - echo "" - if ! confirm_yes "确认卸载并删除 ${INSTALL_ROOT}?"; then - log "已取消卸载" - return 0 - fi - - local remove_volume=0 - if confirm_yes "是否同时删除 Docker data volume (mi_data)?"; then - remove_volume=1 - fi - - local stamp backup_dir - stamp="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)" - backup_dir="${BACKUP_ROOT}/pre-uninstall-${stamp}" - mkdir -p "${backup_dir}" - - step "备份 .env" - if [[ -f "${REPO_ROOT}/.env" ]]; then - cp -a "${REPO_ROOT}/.env" "${backup_dir}/.env" - fi - { - echo "created_at=${stamp}" - echo "install_root=${INSTALL_ROOT}" - echo "remove_volume=${remove_volume}" - echo "action=rm_rf_install_root" - } >"${backup_dir}/uninstall.manifest" - - step "停止并移除容器" - if command -v docker >/dev/null 2>&1 && [[ -f "${REPO_ROOT}/docker-compose.yml" ]]; then - if [[ "${remove_volume}" -eq 1 ]]; then - (cd "${REPO_ROOT}" && docker compose down -v) || true - else - (cd "${REPO_ROOT}" && docker compose down) || true - fi - fi - - step "删除安装目录 ${INSTALL_ROOT}" - if [[ -d "${INSTALL_ROOT}" ]]; then - case "${INSTALL_ROOT}" in - /opt/market_intel|/opt/market_intel/) - rm -rf "${INSTALL_ROOT}" - log "已删除: ${INSTALL_ROOT}" - ;; - *) - if [[ "${ALLOW_UNSAFE_UNINSTALL:-}" == "1" ]]; then - rm -rf "${INSTALL_ROOT}" - log "已删除(ALLOW_UNSAFE_UNINSTALL=1): ${INSTALL_ROOT}" - else - die "拒绝删除非默认路径 ${INSTALL_ROOT};若确认,设置 ALLOW_UNSAFE_UNINSTALL=1" - fi - ;; - esac - else - log "安装目录不存在,跳过删除" - fi - - local leftover - for leftover in /opt/market_intel.removed.* /opt/market_intel.old.*; do - if [[ -e "${leftover}" ]]; then - rm -rf "${leftover}" - log "已清理残留: ${leftover}" - fi - done - - echo "" - echo "卸载完成." - echo " 配置备份: ${backup_dir}" - echo " volume 已删除: $([[ ${remove_volume} -eq 1 ]] && echo yes || echo no)" - echo "" - echo "重新部署:" - echo " curl -fsSL https://git.bz121.com/dekun/market_intel/raw/branch/main/deploy/manage.sh | bash" -} - -main_uninstall "$@" +#!/usr/bin/env bash +# deploy/lib/uninstall.sh — 停容器;可选删 volume;备份 .env;删除安装目录 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +main_uninstall() { + require_root + + local root="" + if ! root="$(resolve_repo_root)"; then + if [[ -d "${INSTALL_ROOT}" ]]; then + root="${INSTALL_ROOT}" + else + die "未找到安装目录 ${INSTALL_ROOT}" + fi + fi + REPO_ROOT="${root}" + + echo "" + echo "将卸载 比特骆驼行情采集分析 (market_intel):" + echo " - 停止并移除 Compose 容器" + echo " - 备份 .env 到 ${BACKUP_ROOT}" + echo " - 删除安装目录: ${INSTALL_ROOT}" + echo " - 可选:删除 data volume" + echo "" + if ! confirm_yes "确认卸载并删除 ${INSTALL_ROOT}?"; then + log "已取消卸载" + return 0 + fi + + local remove_volume=0 + if confirm_yes "是否同时删除 Docker data volume (mi_data)?"; then + remove_volume=1 + fi + + local stamp backup_dir + stamp="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)" + backup_dir="${BACKUP_ROOT}/pre-uninstall-${stamp}" + mkdir -p "${backup_dir}" + + step "备份 .env" + if [[ -f "${REPO_ROOT}/.env" ]]; then + cp -a "${REPO_ROOT}/.env" "${backup_dir}/.env" + fi + { + echo "created_at=${stamp}" + echo "install_root=${INSTALL_ROOT}" + echo "remove_volume=${remove_volume}" + echo "action=rm_rf_install_root" + } >"${backup_dir}/uninstall.manifest" + + step "停止并移除容器" + if command -v docker >/dev/null 2>&1 && [[ -f "${REPO_ROOT}/docker-compose.yml" ]]; then + if [[ "${remove_volume}" -eq 1 ]]; then + (cd "${REPO_ROOT}" && docker compose down -v) || true + else + (cd "${REPO_ROOT}" && docker compose down) || true + fi + fi + + step "删除安装目录 ${INSTALL_ROOT}" + if [[ -d "${INSTALL_ROOT}" ]]; then + case "${INSTALL_ROOT}" in + /opt/market_intel|/opt/market_intel/) + rm -rf "${INSTALL_ROOT}" + log "已删除: ${INSTALL_ROOT}" + ;; + *) + if [[ "${ALLOW_UNSAFE_UNINSTALL:-}" == "1" ]]; then + rm -rf "${INSTALL_ROOT}" + log "已删除(ALLOW_UNSAFE_UNINSTALL=1): ${INSTALL_ROOT}" + else + die "拒绝删除非默认路径 ${INSTALL_ROOT};若确认,设置 ALLOW_UNSAFE_UNINSTALL=1" + fi + ;; + esac + else + log "安装目录不存在,跳过删除" + fi + + local leftover + for leftover in /opt/market_intel.removed.* /opt/market_intel.old.*; do + if [[ -e "${leftover}" ]]; then + rm -rf "${leftover}" + log "已清理残留: ${leftover}" + fi + done + + echo "" + echo "卸载完成." + echo " 配置备份: ${backup_dir}" + echo " volume 已删除: $([[ ${remove_volume} -eq 1 ]] && echo yes || echo no)" + echo "" + echo "重新部署:" + echo " curl -fsSL https://git.bz121.com/dekun/market_intel/raw/branch/main/deploy/manage.sh | bash" +} + +main_uninstall "$@" diff --git a/deploy/lib/update.sh b/deploy/lib/update.sh index 7f0c224..20b3fff 100644 --- a/deploy/lib/update.sh +++ b/deploy/lib/update.sh @@ -1,41 +1,41 @@ -#!/usr/bin/env bash -# deploy/lib/update.sh — git pull + compose build/up;保留 .env 与 volume -set -e -set -u -if [ -n "${BASH_VERSION:-}" ]; then - set -o pipefail -fi - -LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=common.sh -source "${LIB_DIR}/common.sh" - -main_update() { - require_root - if ! REPO_ROOT="$(resolve_repo_root)"; then - die "未找到安装目录 ${INSTALL_ROOT},请先执行「1) 一键部署」" - fi - if ! repo_ready "${REPO_ROOT}"; then - die "安装不完整,请先执行「1) 一键部署」" - fi - - step "更新 — Docker 环境检测" - ensure_docker - step "git pull" - if [[ -d "${REPO_ROOT}/.git" ]]; then - git -C "${REPO_ROOT}" fetch --all --prune - git -C "${REPO_ROOT}" checkout "${GIT_BRANCH}" 2>/dev/null || true - git -C "${REPO_ROOT}" pull --ff-only origin "${GIT_BRANCH}" \ - || git -C "${REPO_ROOT}" pull --ff-only - else - log "警告: 非 git 目录,跳过 pull" - fi - # 补全缺失 env key,不覆盖已有 - ensure_dotenv "${REPO_ROOT}" - compose_up_build - verify_health || true - echo "" - log "更新完成(.env 与 data volume 已保留)" -} - -main_update "$@" +#!/usr/bin/env bash +# deploy/lib/update.sh — git pull + compose build/up;保留 .env 与 volume +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +main_update() { + require_root + if ! REPO_ROOT="$(resolve_repo_root)"; then + die "未找到安装目录 ${INSTALL_ROOT},请先执行「1) 一键部署」" + fi + if ! repo_ready "${REPO_ROOT}"; then + die "安装不完整,请先执行「1) 一键部署」" + fi + + step "更新 — Docker 环境检测" + ensure_docker + step "git pull" + if [[ -d "${REPO_ROOT}/.git" ]]; then + git -C "${REPO_ROOT}" fetch --all --prune + git -C "${REPO_ROOT}" checkout "${GIT_BRANCH}" 2>/dev/null || true + git -C "${REPO_ROOT}" pull --ff-only origin "${GIT_BRANCH}" \ + || git -C "${REPO_ROOT}" pull --ff-only + else + log "警告: 非 git 目录,跳过 pull" + fi + # 补全缺失 env key,不覆盖已有 + ensure_dotenv "${REPO_ROOT}" + compose_up_build + verify_health || true + echo "" + log "更新完成(.env 与 data volume 已保留)" +} + +main_update "$@" diff --git a/deploy/manage.sh b/deploy/manage.sh index 1eaed1a..93c4430 100644 --- a/deploy/manage.sh +++ b/deploy/manage.sh @@ -1,228 +1,228 @@ -#!/usr/bin/env bash -# 比特骆驼行情采集分析 部署管理器(工程 market_intel) -# 部署环境: Ubuntu 22.04 LTS · Docker Compose -# -# 新服务器(免克隆): -# curl -fsSL https://git.bz121.com/dekun/market_intel/raw/branch/main/deploy/manage.sh | bash -# -# 已安装: -# bash /opt/market_intel/deploy/manage.sh -# -set -e -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}" - -_script_src="${BASH_SOURCE[0]:-}" -if [[ -n "${_script_src}" && -f "${_script_src}" ]]; then - DEPLOY_DIR="$(cd "$(dirname "${_script_src}")" && pwd)" - REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" - LIB_DIR="${DEPLOY_DIR}/lib" -else - DEPLOY_DIR="" - REPO_ROOT="" - LIB_DIR="" -fi -unset _script_src - -set -u - -repo_ready() { - [[ -f "${1}/deploy/manage.sh" && -f "${1}/docker-compose.yml" && -d "${1}/deploy/lib" ]] -} - -ensure_git_cli() { - if command -v git >/dev/null 2>&1; then - return 0 - fi - if command -v apt-get >/dev/null 2>&1; then - export DEBIAN_FRONTEND=noninteractive - local waited=0 - while 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 \ - || { command -v fuser >/dev/null 2>&1 && fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; }; do - if [[ "${waited}" -eq 0 ]]; then - echo "等待 apt 锁释放(unattended-upgrades)…" - fi - if [[ "${waited}" -ge 600 ]]; then - echo "错误: 等待 apt 锁超时,请稍后再试" >&2 - exit 1 - fi - sleep 5 - waited=$((waited + 5)) - done - apt-get update -qq - apt-get install -y git ca-certificates curl - else - echo "错误: 未找到 git" >&2 - exit 1 - fi -} - -sync_repo_if_present() { - local root="$1" - if [[ -d "${root}/.git" ]] && command -v git >/dev/null 2>&1; then - git -C "${root}" fetch --all --prune 2>/dev/null || true - git -C "${root}" checkout "${GIT_BRANCH}" 2>/dev/null || true - git -C "${root}" pull --ff-only origin "${GIT_BRANCH}" 2>/dev/null \ - || git -C "${root}" pull --ff-only 2>/dev/null \ - || true - fi -} - -heal_existing_install() { - local root="$1" - echo "检测到已有目录但缺少管理脚本: ${root}" - echo "尝试同步最新代码…" - ensure_git_cli - - if [[ -d "${root}/.git" ]]; then - sync_repo_if_present "${root}" - if repo_ready "${root}"; then - echo "同步成功,切换到仓库内 manage.sh" - exec bash "${root}/deploy/manage.sh" "$@" &2 - exit 1 - fi - heal_existing_install "${INSTALL_ROOT}" "$@" - fi - - echo "比特骆驼行情采集分析 部署管理器 — 首次自举" - echo "将克隆到: ${INSTALL_ROOT}" - if [[ "$(id -u)" -ne 0 ]]; then - echo "错误: 请使用 root 执行" >&2 - exit 1 - fi - ensure_git_cli - mkdir -p "$(dirname "${INSTALL_ROOT}")" - git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" - exec bash "${INSTALL_ROOT}/deploy/manage.sh" "$@" /dev/tty 2>/dev/null || printf '%s' "${__prompt}" - fi - if [[ -r /dev/tty ]]; then - IFS= read -r __line /dev/null 2>&1; then + return 0 + fi + if command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + local waited=0 + while 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 \ + || { command -v fuser >/dev/null 2>&1 && fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; }; do + if [[ "${waited}" -eq 0 ]]; then + echo "等待 apt 锁释放(unattended-upgrades)…" + fi + if [[ "${waited}" -ge 600 ]]; then + echo "错误: 等待 apt 锁超时,请稍后再试" >&2 + exit 1 + fi + sleep 5 + waited=$((waited + 5)) + done + apt-get update -qq + apt-get install -y git ca-certificates curl + else + echo "错误: 未找到 git" >&2 + exit 1 + fi +} + +sync_repo_if_present() { + local root="$1" + if [[ -d "${root}/.git" ]] && command -v git >/dev/null 2>&1; then + git -C "${root}" fetch --all --prune 2>/dev/null || true + git -C "${root}" checkout "${GIT_BRANCH}" 2>/dev/null || true + git -C "${root}" pull --ff-only origin "${GIT_BRANCH}" 2>/dev/null \ + || git -C "${root}" pull --ff-only 2>/dev/null \ + || true + fi +} + +heal_existing_install() { + local root="$1" + echo "检测到已有目录但缺少管理脚本: ${root}" + echo "尝试同步最新代码…" + ensure_git_cli + + if [[ -d "${root}/.git" ]]; then + sync_repo_if_present "${root}" + if repo_ready "${root}"; then + echo "同步成功,切换到仓库内 manage.sh" + exec bash "${root}/deploy/manage.sh" "$@" &2 + exit 1 + fi + heal_existing_install "${INSTALL_ROOT}" "$@" + fi + + echo "比特骆驼行情采集分析 部署管理器 — 首次自举" + echo "将克隆到: ${INSTALL_ROOT}" + if [[ "$(id -u)" -ne 0 ]]; then + echo "错误: 请使用 root 执行" >&2 + exit 1 + fi + ensure_git_cli + mkdir -p "$(dirname "${INSTALL_ROOT}")" + git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" + exec bash "${INSTALL_ROOT}/deploy/manage.sh" "$@" /dev/tty 2>/dev/null || printf '%s' "${__prompt}" + fi + if [[ -r /dev/tty ]]; then + IFS= read -r __line