Files
market_intel/deploy/lib/common.sh
T
2026-08-01 10:33:19 +08:00

359 lines
10 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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/tty || true
else
IFS= read -r __line || true
fi
printf -v "${__var}" '%s' "${__line}"
}
confirm_yes() {
local msg="$1"
local ans=""
cm_read ans "${msg} [y/N] "
[[ "${ans}" == [yY] || "${ans}" == [yY][eE][sS] ]]
}
repo_ready() {
[[ -f "${1}/deploy/manage.sh" && -f "${1}/docker-compose.yml" && -d "${1}/deploy/lib" ]]
}
resolve_repo_root() {
if repo_ready "${INSTALL_ROOT}"; then
echo "${INSTALL_ROOT}"
return 0
fi
if [[ -n "${REPO_ROOT:-}" ]] && repo_ready "${REPO_ROOT}"; then
echo "${REPO_ROOT}"
return 0
fi
return 1
}
read_env_value() {
local file="$1"
local key="$2"
local val=""
if [[ -f "${file}" ]]; then
val="$(grep -E "^${key}=" "${file}" 2>/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 ""
}