Files
eth_hedge_sim/deploy/lib/common.sh
T
dekun 467203b61d Detect ensurepip and auto-install python3-venv in one-click deploy.
Avoid false OK on bare python3; repair broken .venv; re-check deps after git pull.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 21:33:18 +08:00

371 lines
11 KiB
Bash
Executable File
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 — 比特骆驼自动化对冲系统 部署公共函数
# 目标系统: Ubuntu 22.04 LTS
set -e
set -u
if [ -n "${BASH_VERSION:-}" ]; then
set -o pipefail
fi
INSTALL_ROOT="${INSTALL_ROOT:-/opt/eth_hedge_sim}"
GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/eth_hedge_sim.git}"
GIT_BRANCH="${GIT_BRANCH:-main}"
BACKUP_ROOT="${BACKUP_ROOT:-/root/backups/eth_hedge_sim}"
TZ_NAME="${EHS_TZ:-Asia/Shanghai}"
NODE_MAJOR="${NODE_MAJOR:-20}"
PM2_APP_NAME="${PM2_APP_NAME:-eth-hedge-api}"
API_PORT="${API_PORT:-5155}"
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 的进程,便于排查
_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
# 若仍卡死且 APT_FORCE_UNLOCK=1,才强杀(默认不杀,避免打断半截升级)
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
}
# 等待 apt/dpkg 锁(常见:unattended-upgrades 正在跑)
wait_for_apt_lock() {
local max_wait="${1:-600}"
local waited=0
local lock_frontend="/var/lib/dpkg/lock-frontend"
local lock_dpkg="/var/lib/dpkg/lock"
local lock_lists="/var/lib/apt/lists/lock"
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
# fuser 在有进程占用锁文件时返回 0
if command -v fuser >/dev/null 2>&1; then
if fuser "${lock_frontend}" >/dev/null 2>&1 \
|| fuser "${lock_dpkg}" >/dev/null 2>&1 \
|| fuser "${lock_lists}" >/dev/null 2>&1; then
busy=1
fi
fi
if [[ "${busy}" -eq 0 ]]; then
if [[ "${waited}" -gt 0 ]]; then
log "apt 锁已释放,继续安装"
fi
return 0
fi
if [[ "${waited}" -eq 0 ]]; then
log "检测到 apt/dpkg 正被占用(多为重启后的 unattended-upgrades),等待释放…"
log "占用进程:"
_apt_lock_holders | while IFS= read -r line; do log " ${line}"; done
log "可另开终端: ps aux | grep -E 'unattended|apt|dpkg'"
elif [[ "${tried_stop}" -eq 0 && "${waited}" -ge 15 ]]; then
# 等约 15s 仍占用:停自动升级服务(不杀半截 dpkg,除非 APT_FORCE_UNLOCK=1
tried_stop=1
_stop_auto_apt_for_deploy
elif [[ $((waited % 60)) -eq 0 ]]; then
log "仍在等待 apt 锁…已等 ${waited}s / ${max_wait}s"
_apt_lock_holders | while IFS= read -r line; do log " ${line}"; done
fi
if [[ "${waited}" -ge "${max_wait}" ]]; then
log "超时前占用进程:"
_apt_lock_holders | while IFS= read -r line; do log " ${line}"; done
die "等待 apt 锁超时(${max_wait}s)。可手动: systemctl stop unattended-upgrades; 或 APT_FORCE_UNLOCK=1 bash manage.sh"
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
if [[ -z "${ip}" ]]; then
ip="127.0.0.1"
fi
echo "${ip}"
}
# curl|bash 时 stdin 可能是管道,交互从 /dev/tty 读
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}/deploy/pull_and_restart.sh" ]]
}
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
}
# ---------- 运行时检测(有则跳过,无则安装) ----------
python_runtime_ok() {
command -v python3 >/dev/null 2>&1 || return 1
# Ubuntu 常有 python3 且 import venv 成功,但缺 python3.X-venv 时 ensurepip 不可用,
# python3 -m venv 会失败。必须以 ensurepip 为准。
python3 -c "import ensurepip" >/dev/null 2>&1 || return 1
return 0
}
node_runtime_ok() {
command -v node >/dev/null 2>&1 || return 1
command -v npm >/dev/null 2>&1 || return 1
return 0
}
pm2_runtime_ok() {
command -v pm2 >/dev/null 2>&1
}
print_runtime_probe() {
echo "环境检测:"
if python_runtime_ok; then
echo " [OK] Python3 + venv → $(python3 --version 2>&1)"
else
echo " [缺] Python3 / venv → 将自动安装"
fi
if node_runtime_ok; then
echo " [OK] Node.js + npm → $(node --version 2>&1) / npm $(npm --version 2>&1)"
else
echo " [缺] Node.js / npm → 将自动安装 Node ${NODE_MAJOR}"
fi
if pm2_runtime_ok; then
echo " [OK] PM2 → $(pm2 -v 2>&1)"
else
echo " [缺] PM2 → 将自动安装"
fi
}
install_python_runtime() {
step "安装 Python3 环境 (python3 / venv / pip / ensurepip)"
export DEBIAN_FRONTEND=noninteractive
if ! command -v apt-get >/dev/null 2>&1; then
die "非 apt 系统,请手动安装 python3 python3-venv python3-pip"
fi
apt_update
apt_install python3 python3-venv python3-pip
# Debian/Ubuntu:版本化包(如 python3.10-venv)才真正提供 ensurepip
local pyver=""
pyver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)"
if [[ -n "${pyver}" ]]; then
log "安装 python${pyver}-venv(提供 ensurepip)…"
apt_install "python${pyver}-venv" || true
fi
if ! python_runtime_ok; then
die "Python 环境安装后仍不可用(缺 ensurepip)。请检查: apt install python3-venv python${pyver:-3}-venv"
fi
log "Python 已就绪: $(python3 --version 2>&1)"
}
install_node_runtime() {
step "安装 Node.js ${NODE_MAJOR}"
export DEBIAN_FRONTEND=noninteractive
if ! command -v apt-get >/dev/null 2>&1; then
die "非 apt 系统,请手动安装 Node.js ${NODE_MAJOR}+"
fi
apt_update
apt_install ca-certificates curl
wait_for_apt_lock
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
apt_install nodejs
if ! node_runtime_ok; then
die "Node.js 安装后仍不可用"
fi
log "Node 已就绪: $(node --version 2>&1)"
}
install_pm2_runtime() {
step "安装 PM2"
if ! command -v npm >/dev/null 2>&1; then
die "安装 PM2 需要先有 npm"
fi
npm install -g pm2
if ! pm2_runtime_ok; then
die "PM2 安装后仍不可用"
fi
log "PM2 已就绪: $(pm2 -v 2>&1)"
}
ensure_base_packages() {
local need=()
command -v git >/dev/null 2>&1 || need+=(git)
command -v curl >/dev/null 2>&1 || need+=(curl)
if [[ ${#need[@]} -eq 0 ]]; then
log "基础包 git/curl 已存在,跳过"
return 0
fi
if ! command -v apt-get >/dev/null 2>&1; then
die "缺少依赖: ${need[*]},且无 apt-get"
fi
step "安装基础包: ${need[*]}"
export DEBIAN_FRONTEND=noninteractive
apt_update
apt_install ca-certificates "${need[@]}"
}
ensure_system_deps() {
step "环境检测 (Python / Node / PM2)"
print_runtime_probe
ensure_base_packages
if python_runtime_ok; then
log "Python 环境已存在,跳过安装"
else
install_python_runtime
fi
if node_runtime_ok; then
log "Node.js 环境已存在,跳过安装"
else
install_node_runtime
fi
if pm2_runtime_ok; then
log "PM2 已存在,跳过安装"
else
install_pm2_runtime
fi
step "环境确认"
print_runtime_probe
}
pm2_stop_project() {
if command -v pm2 >/dev/null 2>&1; then
pm2 stop "${PM2_APP_NAME}" 2>/dev/null || true
fi
}
pm2_delete_project() {
if command -v pm2 >/dev/null 2>&1; then
pm2 delete "${PM2_APP_NAME}" 2>/dev/null || true
pm2 save 2>/dev/null || true
fi
}
pm2_save_startup() {
if command -v pm2 >/dev/null 2>&1; then
pm2 save 2>/dev/null || true
pm2 startup systemd -u root --hp /root 2>/dev/null || true
fi
}
verify_health() {
local port="${1:-${API_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; do
if curl -fsS "${url}" >/dev/null 2>&1; then
log "health OK"
curl -fsS "${url}" || true
echo ""
return 0
fi
sleep 1
done
log "警告: health 暂未就绪,请稍后检查 pm2 logs ${PM2_APP_NAME}"
return 1
}
print_post_install_guide() {
local ip
ip="$(detect_server_ip)"
echo ""
echo "══════════════════════════════════════"
echo " 比特骆驼自动化对冲系统 部署完成"
echo " 目录: ${INSTALL_ROOT}"
echo " 本机: http://${ip}:${API_PORT}/health"
echo " 进程: pm2 status → ${PM2_APP_NAME}"
echo " 管理: bash ${INSTALL_ROOT}/deploy/manage.sh"
echo " 配置: ${INSTALL_ROOT}/.env"
echo "══════════════════════════════════════"
echo ""
}