From 2a29181a9045556d49a5142d63816a52a2f392a1 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 25 Jul 2026 08:47:35 +0800 Subject: [PATCH] Add curl|bash manage.sh with deploy/uninstall/update menu. Co-authored-by: Cursor --- README.md | 22 ++++-- deploy/bootstrap.sh | 15 +--- deploy/lib/common.sh | 155 ++++++++++++++++++++++++++++++++++++ deploy/lib/install.sh | 67 ++++++++++++++++ deploy/lib/uninstall.sh | 80 +++++++++++++++++++ deploy/lib/update.sh | 29 +++++++ deploy/manage.sh | 158 +++++++++++++++++++++++++++++++++++++ deploy/pull_and_restart.sh | 15 ++-- scripts/deploy_remote.py | 24 ++++-- 9 files changed, 533 insertions(+), 32 deletions(-) mode change 100644 => 100755 deploy/bootstrap.sh create mode 100755 deploy/lib/common.sh create mode 100755 deploy/lib/install.sh create mode 100755 deploy/lib/uninstall.sh create mode 100755 deploy/lib/update.sh create mode 100755 deploy/manage.sh mode change 100644 => 100755 deploy/pull_and_restart.sh diff --git a/README.md b/README.md index 173e8bf..f793b3f 100644 --- a/README.md +++ b/README.md @@ -21,17 +21,23 @@ 服务器目录:`/opt/eth_hedge_sim` 更新方式:**只允许 `git pull`**,然后构建并 `pm2 startOrReload` 本项目进程。 -### 服务器上 +### 推荐:curl 一键管理器(菜单) ```bash -# 首次 -export REPO_URL=https://git.bz121.com/dekun/eth_hedge_sim.git -bash /opt/eth_hedge_sim/deploy/bootstrap.sh -# 若尚未 clone: -# git clone "$REPO_URL" /opt/eth_hedge_sim && bash /opt/eth_hedge_sim/deploy/bootstrap.sh +curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash +``` -# 日常更新 -bash /opt/eth_hedge_sim/deploy/pull_and_restart.sh +菜单: + +1. 一键部署 +2. 一键卸载(仅停删 `eth-hedge-api`,移走目录并备份 `.env`) +3. 更新(`git pull` + 构建 + reload) +0. 退出 + +已安装后也可: + +```bash +bash /opt/eth_hedge_sim/deploy/manage.sh ``` ### 本机触发远程更新 diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh old mode 100644 new mode 100755 index dd32f7b..bb1f7b5 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -1,13 +1,6 @@ #!/usr/bin/env bash -# 首次安装到 /opt/eth_hedge_sim(仅 git clone,后续一律 git pull) +# 兼容入口:转发到 manage.sh(推荐 curl | bash) +# curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash set -euo pipefail - -REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/eth_hedge_sim.git}" -ROOT="${INSTALL_ROOT:-/opt/eth_hedge_sim}" - -if [[ ! -d "$ROOT/.git" ]]; then - mkdir -p "$(dirname "$ROOT")" - git clone "$REPO_URL" "$ROOT" -fi - -bash "$ROOT/deploy/pull_and_restart.sh" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +exec bash "${ROOT}/deploy/manage.sh" "$@" diff --git a/deploy/lib/common.sh b/deploy/lib/common.sh new file mode 100755 index 0000000..b1cc1a8 --- /dev/null +++ b/deploy/lib/common.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# deploy/lib/common.sh — eth_hedge_sim 部署公共函数 +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 +} + +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/null 2>&1; then + apt-get update -qq + apt-get install -y git ca-certificates curl python3 python3-venv python3-pip + fi + if ! command -v node >/dev/null 2>&1; then + step "安装 Node.js ${NODE_MAJOR}" + curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - + apt-get install -y nodejs + fi + if ! command -v pm2 >/dev/null 2>&1; then + step "安装 PM2" + npm install -g pm2 + fi +} + +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 " eth_hedge_sim 部署完成" + 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 "" +} diff --git a/deploy/lib/install.sh b/deploy/lib/install.sh new file mode 100755 index 0000000..437fc6b --- /dev/null +++ b/deploy/lib/install.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# deploy/lib/install.sh — 一键部署 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +run_pipeline() { + local root="$1" + REPO_ROOT="${root}" + step "构建并启动 (pull_and_restart.sh)" + bash "${REPO_ROOT}/deploy/pull_and_restart.sh" + pm2_save_startup + verify_health || true + print_post_install_guide +} + +install_fresh() { + require_root + ensure_system_deps + step "克隆仓库 → ${INSTALL_ROOT}" + if [[ -d "${INSTALL_ROOT}" ]]; then + die "目录已存在: ${INSTALL_ROOT},请先卸载或选修复" + fi + mkdir -p "$(dirname "${INSTALL_ROOT}")" + git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" + run_pipeline "${INSTALL_ROOT}" +} + +install_repair() { + require_root + ensure_system_deps + if ! REPO_ROOT="$(resolve_repo_root)"; then + die "未找到安装目录 ${INSTALL_ROOT}" + fi + step "修复环境(保留 .env 与数据库)" + run_pipeline "${REPO_ROOT}" +} + +handle_existing() { + echo "" + echo "检测到已部署: ${INSTALL_ROOT}" + echo " a) 取消" + echo " b) 修复/重装环境(保留 .env 与 SQLite)" + local choice="" + cm_read choice "请选择 [a/b]: " + case "${choice}" in + b|B) install_repair ;; + *) log "已取消" ;; + esac +} + +main_install() { + require_root + if repo_ready "${INSTALL_ROOT}"; then + handle_existing + else + install_fresh + fi +} + +main_install "$@" diff --git a/deploy/lib/uninstall.sh b/deploy/lib/uninstall.sh new file mode 100755 index 0000000..c5700a4 --- /dev/null +++ b/deploy/lib/uninstall.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# deploy/lib/uninstall.sh — 一键卸载(仅本项目 PM2,不动其它进程) +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +main_uninstall() { + require_root + + local root="" + if ! root="$(resolve_repo_root)"; then + if [[ -d "${INSTALL_ROOT}" ]]; then + root="${INSTALL_ROOT}" + else + die "未找到安装目录 ${INSTALL_ROOT}" + fi + fi + REPO_ROOT="${root}" + + echo "" + echo "将卸载 eth_hedge_sim:" + echo " - 停止并删除 PM2 进程: ${PM2_APP_NAME}" + echo " - 移走目录: ${INSTALL_ROOT}" + echo " - 备份 .env 到 ${BACKUP_ROOT}" + echo " - 不影响其它 PM2 应用" + echo "" + if ! confirm_yes "确认卸载?"; then + log "已取消卸载" + return 0 + fi + + local stamp backup_dir removed_dir + stamp="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)" + backup_dir="${BACKUP_ROOT}/pre-uninstall-${stamp}" + removed_dir="${INSTALL_ROOT}.removed.${stamp}" + mkdir -p "${backup_dir}" + + step "备份配置" + if [[ -f "${REPO_ROOT}/.env" ]]; then + cp -a "${REPO_ROOT}/.env" "${backup_dir}/.env" + fi + if [[ -d "${REPO_ROOT}/backend/data" ]]; then + cp -a "${REPO_ROOT}/backend/data" "${backup_dir}/data" 2>/dev/null || true + fi + { + echo "created_at=${stamp}" + echo "install_root=${INSTALL_ROOT}" + echo "removed_dir=${removed_dir}" + echo "pm2_app=${PM2_APP_NAME}" + } >"${backup_dir}/uninstall.manifest" + + step "停止并移除 PM2 进程 ${PM2_APP_NAME}" + pm2_stop_project + pm2_delete_project + + step "移走安装目录" + if [[ -d "${INSTALL_ROOT}" ]]; then + mv "${INSTALL_ROOT}" "${removed_dir}" + log "已移动: ${INSTALL_ROOT} -> ${removed_dir}" + else + log "安装目录不存在,跳过" + fi + + echo "" + echo "卸载完成." + echo " 配置备份: ${backup_dir}" + echo " 旧目录: ${removed_dir} (确认无误后可手动 rm -rf)" + echo "" + echo "回滚示例:" + echo " mv ${removed_dir} ${INSTALL_ROOT}" + echo " bash ${INSTALL_ROOT}/deploy/manage.sh # 选 1 修复环境" +} + +main_uninstall "$@" diff --git a/deploy/lib/update.sh b/deploy/lib/update.sh new file mode 100755 index 0000000..571e3d7 --- /dev/null +++ b/deploy/lib/update.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# deploy/lib/update.sh — 更新(仅 git pull,禁止 scp) +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +main_update() { + require_root + if ! REPO_ROOT="$(resolve_repo_root)"; then + die "未找到安装目录 ${INSTALL_ROOT},请先执行「1) 一键部署」" + fi + if ! repo_ready "${REPO_ROOT}"; then + die "安装不完整,请先执行「1) 一键部署」" + fi + + step "更新 eth_hedge_sim (git pull + 构建 + pm2 reload)" + bash "${REPO_ROOT}/deploy/pull_and_restart.sh" + verify_health || true + echo "" + log "更新完成" +} + +main_update "$@" diff --git a/deploy/manage.sh b/deploy/manage.sh new file mode 100755 index 0000000..66ee351 --- /dev/null +++ b/deploy/manage.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# eth_hedge_sim 部署管理器 — 一键部署 / 卸载 / 更新 +# +# 新服务器(免克隆): +# curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash +# +# 已安装: +# bash /opt/eth_hedge_sim/deploy/manage.sh +# +set -e +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}" + +# curl | bash 时脚本从 stdin 执行,BASH_SOURCE[0] 为空;须先安全探测再 set -u +_script_src="${BASH_SOURCE[0]:-}" +if [[ -n "${_script_src}" && -f "${_script_src}" ]]; then + DEPLOY_DIR="$(cd "$(dirname "${_script_src}")" && pwd)" + REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" + LIB_DIR="${DEPLOY_DIR}/lib" +else + DEPLOY_DIR="" + REPO_ROOT="" + LIB_DIR="" +fi +unset _script_src + +set -u + +repo_ready() { + [[ -f "${1}/deploy/manage.sh" && -f "${1}/deploy/pull_and_restart.sh" ]] +} + +sync_repo_if_present() { + local root="$1" + if [[ -d "${root}/.git" ]] && command -v git >/dev/null 2>&1; then + git -C "${root}" pull -q --ff-only 2>/dev/null || true + fi +} + +bootstrap_repo() { + if repo_ready "${INSTALL_ROOT}"; then + REPO_ROOT="${INSTALL_ROOT}" + DEPLOY_DIR="${REPO_ROOT}/deploy" + LIB_DIR="${DEPLOY_DIR}/lib" + sync_repo_if_present "${REPO_ROOT}" + return 0 + fi + if [[ -n "${REPO_ROOT}" ]] && repo_ready "${REPO_ROOT}"; then + DEPLOY_DIR="${REPO_ROOT}/deploy" + LIB_DIR="${DEPLOY_DIR}/lib" + return 0 + fi + + echo "eth_hedge_sim 部署管理器 — 首次自举" + echo "将克隆到: ${INSTALL_ROOT}" + if [[ "$(id -u)" -ne 0 ]]; then + echo "错误: 请使用 root 执行" >&2 + exit 1 + fi + if ! command -v git >/dev/null 2>&1; then + if command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y git ca-certificates curl + else + echo "错误: 未找到 git" >&2 + exit 1 + fi + fi + if [[ -d "${INSTALL_ROOT}" ]]; then + echo "错误: ${INSTALL_ROOT} 已存在但不是有效仓库" >&2 + echo "请手动处理后再运行,或设置 INSTALL_ROOT 指向其它路径" >&2 + exit 1 + fi + mkdir -p "$(dirname "${INSTALL_ROOT}")" + git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" + # clone 后用仓库内脚本 + 终端 stdin(管道已 EOF) + exec bash "${INSTALL_ROOT}/deploy/manage.sh" "$@" /dev/tty 2>/dev/null || printf '%s' "${__prompt}" + fi + if [[ -r /dev/tty ]]; then + IFS= read -r __line /dev/null 2>&1; then - echo "ERROR: npm not found. Install Node.js 18+ first." + echo "ERROR: npm not found. Install Node.js 18+ first (manage.sh 一键部署会自动装)." exit 1 fi @@ -47,4 +52,4 @@ fi pm2 startOrReload "$ROOT/deploy/ecosystem.config.cjs" --update-env pm2 save -echo "[eth_hedge_sim] done. health: http://127.0.0.1:5155/health public: https://dc.hyf2.cc/health" +echo "[eth_hedge_sim] done. health: http://127.0.0.1:5155/health" diff --git a/scripts/deploy_remote.py b/scripts/deploy_remote.py index bb49418..cfdd835 100644 --- a/scripts/deploy_remote.py +++ b/scripts/deploy_remote.py @@ -7,6 +7,8 @@ DEPLOY_PASS 服务器密码 DEPLOY_ROOT 默认 /opt/eth_hedge_sim REPO_URL 默认 https://git.bz121.com/dekun/eth_hedge_sim.git + DEPLOY_MODE update|bootstrap 默认 update(跑 pull_and_restart) + bootstrap 时执行 manage 非交互更新路径 """ from __future__ import annotations @@ -19,6 +21,7 @@ USER = os.environ.get("DEPLOY_USER", "root") PASSWORD = os.environ.get("DEPLOY_PASS", "") ROOT = os.environ.get("DEPLOY_ROOT", "/opt/eth_hedge_sim") REPO = os.environ.get("REPO_URL", "https://git.bz121.com/dekun/eth_hedge_sim.git") +MODE = os.environ.get("DEPLOY_MODE", "update").strip().lower() def main() -> int: @@ -31,28 +34,33 @@ def main() -> int: print("pip install paramiko", file=sys.stderr) return 2 - remote = f""" + if MODE == "bootstrap": + remote = f""" set -euo pipefail export DEBIAN_FRONTEND=noninteractive -if ! command -v git >/dev/null; then apt-get update -y && apt-get install -y git; fi -if ! command -v python3 >/dev/null; then apt-get update -y && apt-get install -y python3 python3-venv python3-pip; fi -if ! command -v node >/dev/null; then - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - apt-get install -y nodejs -fi if [[ ! -d '{ROOT}/.git' ]]; then mkdir -p /opt git clone '{REPO}' '{ROOT}' fi bash '{ROOT}/deploy/pull_and_restart.sh' curl -fsS http://127.0.0.1:5155/health || true +""" + else: + remote = f""" +set -euo pipefail +if [[ ! -d '{ROOT}/.git' ]]; then + mkdir -p /opt + git clone '{REPO}' '{ROOT}' +fi +bash '{ROOT}/deploy/lib/update.sh' +curl -fsS http://127.0.0.1:5155/health || true """ client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) print(f"connecting {USER}@{HOST} ...") client.connect(HOST, username=USER, password=PASSWORD, timeout=30) - print("running remote bootstrap/pull ...") + print("running remote pull/update ...") stdin, stdout, stderr = client.exec_command(remote, get_pty=True) while True: chunk = stdout.read(1024)