d78e59c125
Co-authored-by: Cursor <cursoragent@cursor.com>
375 lines
11 KiB
Bash
375 lines
11 KiB
Bash
#!/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_USERNAME" "管理员用户名" "admin"
|
||
ensure_env_key "${envf}" "ADMIN_PASSWORD" "管理员密码" "admin123"
|
||
ensure_env_key "${envf}" "BACKUP_DIR" "" "/root/market_intel_backups"
|
||
ensure_env_key "${envf}" "BACKUP_AUTO_ENABLED" "自动备份 1/0" "1"
|
||
ensure_env_key "${envf}" "BACKUP_INTERVAL_HOURS" "自动备份间隔小时" "24"
|
||
ensure_env_key "${envf}" "BACKUP_KEEP_COUNT" "" "30"
|
||
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
|
||
# 给容器一点启动时间,再进入健康检查
|
||
sleep 3
|
||
}
|
||
|
||
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}"
|
||
# 先等 api 容器起来,避免 rebuild 窗口误报
|
||
local i
|
||
for i in $(seq 1 60); do
|
||
if compose ps --status running 2>/dev/null | grep -qE 'mi_api|api'; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
for i in $(seq 1 45); do
|
||
# -sS 静默;丢弃正文,避免 JSON 与后续菜单输出粘连
|
||
if curl -fsS --connect-timeout 2 --max-time 5 "${url}" >/dev/null 2>&1; then
|
||
log "health OK (${url})"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
log "警告: health 暂未就绪,请检查: docker compose -f ${REPO_ROOT}/docker-compose.yml logs api --tail 80"
|
||
log "也可手动验证: curl -fsS ${url}"
|
||
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)"
|
||
# 清掉可能残留的 \r,避免和 docker/curl 进度条粘行
|
||
printf '\n'
|
||
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 "══════════════════════════════════════"
|
||
printf '\n'
|
||
}
|