aab0b9bdf6
Co-authored-by: Cursor <cursoragent@cursor.com>
535 lines
15 KiB
Bash
Executable File
535 lines
15 KiB
Bash
Executable File
#!/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}"
|
||
INSTANCE_SLOT="${INSTANCE_SLOT:-1}"
|
||
|
||
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
|
||
}
|
||
|
||
# ---------- 同机多套槽位(1/2/3) ----------
|
||
|
||
infer_slot_from_root() {
|
||
local root="${1%/}"
|
||
case "${root}" in
|
||
*/eth_hedge_sim_2) echo 2 ;;
|
||
*/eth_hedge_sim_3) echo 3 ;;
|
||
*) echo 1 ;;
|
||
esac
|
||
}
|
||
|
||
# 设置 INSTANCE_SLOT / INSTALL_ROOT / API_PORT / PM2_APP_NAME
|
||
resolve_slot() {
|
||
local slot="${1:-${INSTANCE_SLOT:-1}}"
|
||
case "${slot}" in
|
||
1|2|3) ;;
|
||
*) die "无效槽位: ${slot}(仅支持 1/2/3)" ;;
|
||
esac
|
||
INSTANCE_SLOT="${slot}"
|
||
case "${slot}" in
|
||
1)
|
||
INSTALL_ROOT="/opt/eth_hedge_sim"
|
||
API_PORT="5155"
|
||
PM2_APP_NAME="eth-hedge-api"
|
||
;;
|
||
2)
|
||
INSTALL_ROOT="/opt/eth_hedge_sim_2"
|
||
API_PORT="5156"
|
||
PM2_APP_NAME="eth-hedge-api-2"
|
||
;;
|
||
3)
|
||
INSTALL_ROOT="/opt/eth_hedge_sim_3"
|
||
API_PORT="5157"
|
||
PM2_APP_NAME="eth-hedge-api-3"
|
||
;;
|
||
esac
|
||
export INSTANCE_SLOT INSTALL_ROOT API_PORT PM2_APP_NAME
|
||
}
|
||
|
||
# 按实际仓库目录对齐槽位(pull/update 时以目录为准)
|
||
apply_slot_for_root() {
|
||
local root="${1%/}"
|
||
[[ -n "${root}" ]] || die "apply_slot_for_root: 空目录"
|
||
resolve_slot "$(infer_slot_from_root "${root}")"
|
||
INSTALL_ROOT="${root}"
|
||
REPO_ROOT="${root}"
|
||
# 已有 .env 时保留用户改过的端口
|
||
if [[ -f "${root}/.env" ]]; then
|
||
local p=""
|
||
p="$(grep -E '^API_PORT=' "${root}/.env" 2>/dev/null | head -n1 | cut -d= -f2- | tr -d '\r' || true)"
|
||
if [[ -n "${p}" && "${p}" =~ ^[0-9]+$ ]]; then
|
||
API_PORT="${p}"
|
||
fi
|
||
fi
|
||
export INSTANCE_SLOT INSTALL_ROOT API_PORT PM2_APP_NAME REPO_ROOT
|
||
}
|
||
|
||
is_allowed_install_root() {
|
||
local root="${1%/}"
|
||
case "${root}" in
|
||
/opt/eth_hedge_sim|/opt/eth_hedge_sim_2|/opt/eth_hedge_sim_3) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
port_is_listening() {
|
||
local port="$1"
|
||
if command -v ss >/dev/null 2>&1; then
|
||
ss -ltn 2>/dev/null | grep -qE ":${port}[[:space:]]" && return 0
|
||
fi
|
||
if command -v lsof >/dev/null 2>&1; then
|
||
lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# 全新安装前:端口已被占用且不是本槽位 PM2 → 失败
|
||
assert_port_free_for_install() {
|
||
local port="${1:-${API_PORT}}"
|
||
local name="${2:-${PM2_APP_NAME}}"
|
||
if command -v pm2 >/dev/null 2>&1; then
|
||
if pm2 describe "${name}" >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
fi
|
||
if port_is_listening "${port}"; then
|
||
die "端口 ${port} 已被占用(槽位 ${INSTANCE_SLOT} / ${name})。请释放端口或换槽位"
|
||
fi
|
||
}
|
||
|
||
write_ecosystem_config() {
|
||
local root="${1:-${INSTALL_ROOT}}"
|
||
local name="${2:-${PM2_APP_NAME}}"
|
||
local port="${3:-${API_PORT}}"
|
||
local out="${root}/deploy/ecosystem.config.cjs"
|
||
mkdir -p "${root}/deploy"
|
||
cat >"${out}" <<EOF
|
||
module.exports = {
|
||
apps: [
|
||
{
|
||
name: '${name}',
|
||
cwd: '${root}/backend',
|
||
script: '${root}/.venv/bin/uvicorn',
|
||
args: 'app.main:app --host 0.0.0.0 --port ${port}',
|
||
interpreter: 'none',
|
||
env: {
|
||
// MODE 以仓库根 .env 为准(设置页可切 SIM/LIVE),勿在此硬编码覆盖
|
||
ENV_NAME: 'test',
|
||
TZ: 'Asia/Shanghai',
|
||
},
|
||
// 密钥与本地配置一律读 ${root}/.env(勿写进本文件)
|
||
},
|
||
],
|
||
};
|
||
EOF
|
||
log "已写入 PM2: name=${name} port=${port} → ${out}"
|
||
}
|
||
|
||
random_auth_secret() {
|
||
if command -v openssl >/dev/null 2>&1; then
|
||
openssl rand -hex 24
|
||
return 0
|
||
fi
|
||
head -c 32 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48
|
||
echo ""
|
||
}
|
||
|
||
# SKIP_SLOT_PROMPT=1 时用环境变量 INSTANCE_SLOT(默认 1),供「全部更新」等非交互调用
|
||
prompt_instance_slot() {
|
||
local line=""
|
||
local slot=""
|
||
if [[ "${SKIP_SLOT_PROMPT:-0}" == "1" ]]; then
|
||
resolve_slot "${INSTANCE_SLOT:-1}"
|
||
log "槽位 ${INSTANCE_SLOT}: ${INSTALL_ROOT} port=${API_PORT} pm2=${PM2_APP_NAME}"
|
||
return 0
|
||
fi
|
||
echo ""
|
||
echo "策略机槽位(同机最多 3 套,互不影响):"
|
||
echo " 1 → /opt/eth_hedge_sim :5155 eth-hedge-api"
|
||
echo " 2 → /opt/eth_hedge_sim_2 :5156 eth-hedge-api-2"
|
||
echo " 3 → /opt/eth_hedge_sim_3 :5157 eth-hedge-api-3"
|
||
cm_read line "请选择槽位 [1/2/3,默认1]: "
|
||
slot="${line:-1}"
|
||
resolve_slot "${slot}"
|
||
log "槽位 ${INSTANCE_SLOT}: ${INSTALL_ROOT} port=${API_PORT} pm2=${PM2_APP_NAME}"
|
||
}
|
||
|
||
list_installed_slots() {
|
||
local s root
|
||
for s in 1 2 3; do
|
||
case "${s}" in
|
||
1) root="/opt/eth_hedge_sim" ;;
|
||
2) root="/opt/eth_hedge_sim_2" ;;
|
||
3) root="/opt/eth_hedge_sim_3" ;;
|
||
esac
|
||
if repo_ready "${root}"; then
|
||
echo "${s}"
|
||
fi
|
||
done
|
||
}
|
||
|
||
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 " 槽位: ${INSTANCE_SLOT:-1}"
|
||
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 " 同机多套: 菜单选槽位 2/3;实盘请只用槽位 1"
|
||
echo "══════════════════════════════════════"
|
||
echo ""
|
||
}
|