Files
CoinOX/deploy/lib/common.sh
T
dekun 4a4de8f5fb Add Ubuntu interactive one-click deploy; default sim mode.
Ship deploy/manage.sh toolkit for /opt/coinox, rewrite Ubuntu-only deploy docs, drop Windows deploy guidance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 08:35:07 +08:00

514 lines
14 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 — CoinOX 部署公共函数
set -e
set -u
if [ -n "${BASH_VERSION:-}" ]; then
set -o pipefail
fi
INSTALL_ROOT="${INSTALL_ROOT:-/opt/coinox}"
GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/CoinOX.git}"
GIT_BRANCH="${GIT_BRANCH:-main}"
BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}"
TZ_NAME="${CM_TZ:-Asia/Shanghai}"
NODE_MAJOR="${NODE_MAJOR:-20}"
APP_PORT="${APP_PORT:-5010}"
PM2_APP_NAME="${PM2_APP_NAME:-coinox}"
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$(cd "${LIB_DIR}/.." && pwd)"
REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)"
PM2_APPS=(
coinox
)
# 历史进程名(仅 stop/delete 时兼容)
PM2_APPS_LEGACY=(
crypto-monitor-okx
crypto-monitor
)
CONFIG_PATHS=(
.env
)
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
}
require_ubuntu() {
if [[ ! -f /etc/os-release ]]; then
log "警告: 未检测到 /etc/os-release,跳过 Ubuntu 版本检查"
return 0
fi
# shellcheck source=/dev/null
source /etc/os-release
if [[ "${ID:-}" != "ubuntu" ]]; then
log "警告: 当前系统为 ${ID:-unknown},官方仅测试 Ubuntu 22.04/24.04"
return 0
fi
local ver="${VERSION_ID:-}"
if [[ "${ver}" != "22.04" && "${ver}" != "24.04" ]]; then
log "警告: Ubuntu ${ver} 未在文档中明确测试,继续执行"
fi
}
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}"
}
confirm_yes() {
local msg="$1"
local ans=""
cm_read ans "${msg} [y/N] "
[[ "${ans}" == [yY] || "${ans}" == [yY][eE][sS] ]]
}
confirm_uninstall() {
local ans=""
echo "此操作将停止 CoinOX PM2 进程并移走安装目录."
cm_read ans "输入 UNINSTALL 确认卸载: "
[[ "${ans}" == "UNINSTALL" ]]
}
# curl | bash 时 stdin 是管道,须从 /dev/tty 读取用户输入
cm_read() {
local __var="$1"
local __prompt="$2"
local __val=""
if [[ -r /dev/tty ]]; then
IFS= read -r -p "${__prompt}" __val </dev/tty || true
else
IFS= read -r -p "${__prompt}" __val || true
fi
printf -v "${__var}" '%s' "${__val}"
}
repo_ready() {
[[ -f "${1}/deploy/setup_env.sh" && -f "${1}/deploy/manage.sh" && -f "${1}/app.py" ]]
}
resolve_repo_root() {
if repo_ready "${INSTALL_ROOT}"; then
echo "${INSTALL_ROOT}"
return 0
fi
if repo_ready "${REPO_ROOT}"; then
echo "${REPO_ROOT}"
return 0
fi
return 1
}
backup_configs_to() {
local src_root="$1"
local dest="$2"
mkdir -p "${dest}"
local rel copied=0
for rel in "${CONFIG_PATHS[@]}"; do
local src="${src_root}/${rel}"
if [[ -f "${src}" ]]; then
mkdir -p "${dest}/$(dirname "${rel}")"
cp -a "${src}" "${dest}/${rel}"
log "backup ${rel}"
copied=$((copied + 1))
fi
done
if [[ "${copied}" -eq 0 ]]; then
log "提示: 未找到可备份的配置文件"
fi
}
install_system_packages() {
step "安装系统依赖"
if ! command -v apt-get >/dev/null 2>&1; then
die "未检测到 apt-get,请手动安装 python3-venv git curl"
fi
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y python3 python3-pip python3-venv curl git ca-certificates
local pyver=""
if command -v python3 >/dev/null 2>&1; then
pyver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv
fi
}
install_node_pm2() {
step "检查 Node.js 与 PM2"
if command -v pm2 >/dev/null 2>&1; then
log "PM2 已安装: $(pm2 -v)"
return 0
fi
if ! command -v node >/dev/null 2>&1; then
log "安装 Node.js ${NODE_MAJOR}.x ..."
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
apt-get install -y nodejs
fi
log "安装 PM2 ..."
npm install -g pm2
log "PM2: $(pm2 -v)"
}
pip_progress_bar_arg() {
echo "on"
}
pip_upgrade_tools() {
local pip_bin="$1"
local bar
bar="$(pip_progress_bar_arg)"
echo " 升级 pip ..."
"${pip_bin}" install -U pip setuptools wheel \
--disable-pip-version-check \
--progress-bar "${bar}"
}
pip_install_requirements() {
local pip_bin="$1"
local req_file="$2"
local label="${3:-依赖}"
local bar
bar="$(pip_progress_bar_arg)"
echo " 安装${label} (下方为 pip 进度) ..."
[[ -f "${req_file}" ]] || die "缺少依赖文件: ${req_file}"
"${pip_bin}" install -r "${req_file}" \
--disable-pip-version-check \
--retries 5 \
--progress-bar "${bar}"
echo " ${label}安装完成"
}
install_backup_cron() {
step "安装每日备份 cron"
local inst="${REPO_ROOT}/scripts/install_backup_cron.sh"
if [[ -f "${inst}" ]]; then
chmod +x "${inst}"
bash "${inst}" || log "警告: cron 安装失败"
else
log "未找到 scripts/install_backup_cron.sh,跳过"
fi
}
remove_backup_cron() {
step "移除备份 cron"
local tmp removed=0
tmp="$(mktemp)"
if ! crontab -l 2>/dev/null >"${tmp}"; then
rm -f "${tmp}"
return 0
fi
local filtered
filtered="$(grep -vF "backup_data.sh" "${tmp}" || true)"
if [[ "${filtered}" != "$(cat "${tmp}")" ]]; then
printf '%s\n' "${filtered}" | awk '
BEGIN { tz = 0 }
/^CRON_TZ=Asia\/Shanghai$/ {
if (tz++) next
}
{ print }
' | crontab -
removed=1
fi
rm -f "${tmp}"
if [[ "${removed}" -eq 1 ]]; then
log "已移除 backup_data.sh 相关 cron"
fi
}
pm2_app_exists() {
local name="$1"
# pm2 pid 在部分版本对不存在的进程仍返回 0;改用 jlist 精确匹配 name
if ! command -v pm2 >/dev/null 2>&1; then
return 1
fi
if ! command -v python3 >/dev/null 2>&1; then
pm2 describe "${name}" 2>/dev/null | grep -q "status"
return $?
fi
pm2 jlist 2>/dev/null | python3 -c '
import json,sys
name=sys.argv[1]
try:
apps=json.load(sys.stdin)
except Exception:
sys.exit(1)
sys.exit(0 if any(isinstance(a,dict) and a.get("name")==name for a in (apps or [])) else 1)
' "${name}"
}
pm2_stop_project_apps() {
if ! command -v pm2 >/dev/null 2>&1; then
log "未安装 pm2,跳过"
return 0
fi
local name stopped=0
for name in "${PM2_APPS[@]}" "${PM2_APPS_LEGACY[@]}"; do
if pm2_app_exists "${name}"; then
log "pm2 stop ${name}"
pm2 stop "${name}" 2>/dev/null || true
stopped=$((stopped + 1))
fi
done
if [[ "${stopped}" -eq 0 ]]; then
log "未发现本项目 PM2 进程(其它 PM2 不受影响)"
fi
}
pm2_delete_project_apps() {
if ! command -v pm2 >/dev/null 2>&1; then
return 0
fi
local name deleted=0
for name in "${PM2_APPS[@]}" "${PM2_APPS_LEGACY[@]}"; do
if pm2_app_exists "${name}"; then
log "pm2 delete ${name}"
pm2 delete "${name}" 2>/dev/null || true
deleted=$((deleted + 1))
fi
done
if [[ "${deleted}" -gt 0 ]]; then
pm2 save 2>/dev/null || true
fi
}
pm2_start_app() {
step "启动 PM2: ${PM2_APP_NAME}"
cd "${REPO_ROOT}"
[[ -f "${REPO_ROOT}/ecosystem.config.cjs" ]] || die "缺少 ecosystem.config.cjs"
[[ -x "${REPO_ROOT}/.venv/bin/python" ]] || die "缺少 .venv,请先运行 setup_env.sh"
if pm2_app_exists "${PM2_APP_NAME}"; then
log "已存在,执行 restart --update-env"
if ! pm2 restart "${PM2_APP_NAME}" --update-env; then
log "restart 失败,改为 start ecosystem.config.cjs"
pm2 start ecosystem.config.cjs
fi
else
log "首次启动: pm2 start ecosystem.config.cjs"
pm2 start ecosystem.config.cjs
fi
}
pm2_save_startup() {
step "PM2 save & startup"
pm2 save 2>/dev/null || true
if pm2 startup systemd -u root --hp /root 2>/dev/null | grep -q "sudo"; then
pm2 startup systemd -u root --hp /root 2>/dev/null | grep "^sudo" | bash || true
else
pm2 startup 2>/dev/null || true
fi
}
is_deployed() {
local root="$1"
[[ -x "${root}/.venv/bin/python" ]]
}
read_app_port() {
local env_file="${REPO_ROOT}/.env"
local port="${APP_PORT}"
if [[ -f "${env_file}" ]]; then
local line
line="$(grep -E '^[[:space:]]*APP_PORT=' "${env_file}" | tail -n1 || true)"
if [[ -n "${line}" ]]; then
port="${line#*=}"
port="$(echo "${port}" | tr -d '"' | tr -d "'" | xargs)"
fi
fi
echo "${port:-5010}"
}
check_http() {
local url="$1"
local code
code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 8 "${url}" 2>/dev/null || echo "000")"
[[ "${code}" == "200" || "${code}" == "302" || "${code}" == "301" || "${code}" == "401" || "${code}" == "403" ]]
}
verify_deployment() {
local ip="${1:-$(detect_server_ip)}"
local port
local ok=1
local attempt max_attempts=20
port="$(read_app_port)"
step "部署验收"
if command -v pm2 >/dev/null 2>&1 && pm2_app_exists "${PM2_APP_NAME}"; then
echo " [✓] PM2 ${PM2_APP_NAME} 已注册"
else
echo " [✗] PM2 缺少进程 ${PM2_APP_NAME}"
ok=0
pm2 list 2>/dev/null || true
fi
# Flask 冷启动常需数秒;立刻 curl 易误报
echo " ... 等待页面就绪 (最多约 ${max_attempts}s,端口 ${port})"
local http_ok=0
for attempt in $(seq 1 "${max_attempts}"); do
if check_http "http://127.0.0.1:${port}/"; then
http_ok=1
break
fi
# 进程已挂掉则提前失败
if command -v pm2 >/dev/null 2>&1 && ! pm2_app_exists "${PM2_APP_NAME}"; then
echo " [✗] 等待期间 PM2 进程消失"
ok=0
break
fi
local status=""
status="$(pm2 jlist 2>/dev/null | python3 -c '
import json,sys
name="'"${PM2_APP_NAME}"'"
try:
apps=json.load(sys.stdin)
except Exception:
print(""); sys.exit(0)
for a in apps or []:
if isinstance(a,dict) and a.get("name")==name:
print((a.get("pm2_env") or {}).get("status") or "")
break
' 2>/dev/null || true)"
if [[ "${status}" == "errored" || "${status}" == "stopped" ]]; then
echo " [✗] PM2 状态为 ${status}"
ok=0
break
fi
sleep 1
done
if [[ "${http_ok}" -eq 1 ]]; then
echo " [✓] 页面可访问 (http://127.0.0.1:${port}/)"
else
echo " [✗] 页面不可访问 (http://127.0.0.1:${port}/)"
ok=0
echo " 排查: pm2 logs ${PM2_APP_NAME} --lines 50"
echo " 确认端口: grep APP_PORT ${REPO_ROOT}/.env"
fi
if [[ "${ok}" -eq 1 ]]; then
echo ""
log "验收通过: 进程在线 + 页面可打开"
return 0
fi
echo ""
log "验收未完全通过,可执行: pm2 logs ${PM2_APP_NAME} --lines 30"
return 1
}
print_post_install_guide() {
local ip="${1:-$(detect_server_ip)}"
local port
port="$(read_app_port)"
cat <<EOF
═══════════════════════════════════════════
✅ CoinOX 部署完成(默认模拟盘 sim)
访问: http://${ip}:${port}
入口: /hedge-plan(对冲) /options(期权)
登录账号: admin
登录密码: 见 .env 中 APP_PASSWORD(首次多为 admin123)
(请尽快在系统设置 / env 配置中修改)
模式说明:
- SIM_DEFAULT_MODE=sim(本地模拟资金,不打真实私有下单)
- 实盘: 在页面/env 切 live,并配置 OKX_API_* 与 LIVE_TRADING_ENABLED
下一步:
1. 浏览器打开上方地址
2. 模拟盘直接测期权 / 对冲计划
3. 需要实盘时再填 API / 代理并切 live
管理:
bash ${INSTALL_ROOT}/deploy/manage.sh
仓库:
${GIT_URL}
═══════════════════════════════════════════
EOF
}
doctor_report() {
step "环境识别"
if [[ -f /etc/os-release ]]; then
# shellcheck source=/dev/null
source /etc/os-release
echo " OS: ${PRETTY_NAME:-${ID:-unknown} ${VERSION_ID:-}}"
else
echo " OS: unknown"
fi
echo " 用户: $(id -un) (uid=$(id -u))"
echo " 主机: $(hostname 2>/dev/null || echo '?')"
echo " IP: $(detect_server_ip)"
echo " 安装根: ${INSTALL_ROOT}"
echo " 仓库: ${GIT_URL} (${GIT_BRANCH})"
step "依赖检测"
local item cmd
for item in "python3:python3" "git:git" "curl:curl" "node:node" "npm:npm" "pm2:pm2"; do
cmd="${item#*:}"
if command -v "${cmd}" >/dev/null 2>&1; then
case "${cmd}" in
python3) echo " [✓] python3: $(python3 --version 2>&1)" ;;
node) echo " [✓] node: $(node -v 2>&1)" ;;
npm) echo " [✓] npm: $(npm -v 2>&1)" ;;
pm2) echo " [✓] pm2: $(pm2 -v 2>&1)" ;;
*) echo " [✓] ${cmd}: $(command -v "${cmd}")" ;;
esac
else
echo " [✗] ${cmd}: 未安装"
fi
done
if command -v python3 >/dev/null 2>&1; then
local ver major minor
ver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
major="${ver%%.*}"
minor="${ver#*.}"
if [[ "${major}" -gt 3 ]] || [[ "${major}" -eq 3 && "${minor}" -ge 10 ]]; then
echo " [✓] Python 版本满足 3.10+ (${ver})"
else
echo " [✗] Python 需要 3.10+,当前 ${ver}"
fi
fi
step "项目状态"
local root=""
if root="$(resolve_repo_root 2>/dev/null)"; then
echo " [✓] 仓库就绪: ${root}"
[[ -f "${root}/requirements.txt" ]] && echo " [✓] requirements.txt" || echo " [✗] requirements.txt"
[[ -f "${root}/ecosystem.config.cjs" ]] && echo " [✓] ecosystem.config.cjs" || echo " [✗] ecosystem.config.cjs"
[[ -f "${root}/.env" ]] && echo " [✓] .env 已存在" || echo " [!] .env 尚未生成"
if [[ -x "${root}/.venv/bin/python" ]]; then
echo " [✓] .venv: $("${root}/.venv/bin/python" --version 2>&1)"
else
echo " [!] .venv 未创建"
fi
else
echo " [!] 尚未安装到 ${INSTALL_ROOT}"
fi
if command -v pm2 >/dev/null 2>&1; then
step "PM2"
pm2 list 2>/dev/null | head -n 40 || true
fi
}