feat(all): 重炼 crates/common 核心组件、上线 Web 运维看板与 Docker 容器化部署

This commit is contained in:
fmq
2026-07-28 10:31:57 +08:00
commit 4b4238d702
71 changed files with 163402 additions and 0 deletions
+511
View File
@@ -0,0 +1,511 @@
#!/usr/bin/env bash
# ==============================================================================
# deploy.sh — DCTS 统一部署与管理控制台
# ==============================================================================
# 支持交互式向导及命令行参数化调用(适用于 CI/CD 自动化运维)。
#
# 常用示例:
# ./scripts/deploy.sh # 交互式向导
# ./scripts/deploy.sh -e local -b compose -r all # 本地 + Docker Compose + 部署全栈
# ./scripts/deploy.sh -e remote -b systemd -r server # 远程 + Systemd + 仅部署服务端
# ./scripts/deploy.sh remove -e local -b systemd -r all # 停止并卸载本地 Systemd 服务
# ./scripts/deploy.sh -e remote -b compose -r all --skip-assets # 跳过静态资源同步,仅更新程序代码
# ==============================================================================
set -e
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# 定位至项目根目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${WORK_DIR}"
# =============================================================================
# 默认配置与环境变量
# =============================================================================
REMOTE_USER="${REMOTE_USER:-fmq}"
REMOTE_IP="${REMOTE_IP:-100.66.1.2}"
REMOTE_PORT="${REMOTE_PORT:-22}"
REMOTE_DIR="${REMOTE_DIR:-/vol1/1000/program/dcts}"
ENV_MODE="${DEPLOY_ENV:-}"
BACKEND_MODE="${DEPLOY_BACKEND:-}"
ROLE_TARGET="${DEPLOY_ROLE:-}"
ACTION="install"
SYNC_ASSETS="auto" # auto / skip / force
show_help() {
echo -e "${BLUE}DCTS 部署与管理控制台${NC}"
echo ""
echo "用法: $0 [动作] [选项]"
echo ""
echo "动作 (Actions):"
echo " install 安装或更新服务 (默认)"
echo " remove | clean 停止并卸载指定服务"
echo ""
echo "选项 (Options):"
echo " -e, --env <local|remote> 部署环境: local (本地) 或 remote (远程)"
echo " -b, --backend <compose|systemd> 底层平台: compose (Docker Compose) 或 systemd (Systemd 服务)"
echo " -r, --role <all|server|node> 目标模块: all (全量), server (服务端), node (计算节点)"
echo " --skip-assets 跳过 assets/ 静态资源同步,仅更新程序"
echo " --force-assets 强制重新全量同步 assets/ 静态资源"
echo " -h, --help 显示帮助信息"
echo ""
echo "示例: $0 -e remote -b compose -r all --skip-assets"
exit 0
}
# =============================================================================
# 1. 命令行参数解析
# =============================================================================
while [[ $# -gt 0 ]]; do
arg_lower="${1,,}"
case "$arg_lower" in
install|remove|clean|uninstall)
ACTION="$arg_lower"
if [ "${ACTION}" = "uninstall" ] || [ "${ACTION}" = "clean" ]; then
ACTION="remove"
fi
shift
;;
-e|--env)
ENV_MODE="${2,,}"
shift 2
;;
-b|--backend)
BACKEND_MODE="${2,,}"
shift 2
;;
-r|--role)
ROLE_TARGET="${2,,}"
shift 2
;;
--skip-assets)
SYNC_ASSETS="skip"
shift
;;
--force-assets)
SYNC_ASSETS="force"
shift
;;
-h|--help)
show_help
;;
*)
if [ "$arg_lower" = "server" ] || [ "$arg_lower" = "node" ] || [ "$arg_lower" = "all" ]; then
ROLE_TARGET="$arg_lower"
elif [ "$arg_lower" = "local" ] || [ "$arg_lower" = "remote" ]; then
ENV_MODE="$arg_lower"
elif [ "$arg_lower" = "compose" ] || [ "$arg_lower" = "systemd" ]; then
BACKEND_MODE="$arg_lower"
else
echo -e "${RED}错误: 未知参数 '$1'。使用 '$0 --help' 查看用法。${NC}"
exit 1
fi
shift
;;
esac
done
# =============================================================================
# 2. 交互式向导 (参数未指定时调用)
# =============================================================================
if [ -z "${ENV_MODE}" ] || [ -z "${BACKEND_MODE}" ] || [ -z "${ROLE_TARGET}" ]; then
echo -e "${BLUE}==============================================================${NC}"
echo -e "${CYAN} 🚀 DCTS 部署与管理控制台 ${NC}"
echo -e "${BLUE}==============================================================${NC}"
# [第一步]:选择环境
if [ -z "${ENV_MODE}" ]; then
echo -e "\n${YELLOW}【第一步】请选择部署环境:${NC}"
echo " 1) 本地部署 [Local]"
echo " 2) 远程部署 [Remote] (目标: ${REMOTE_USER}@${REMOTE_IP})"
read -p "请选择 [1-2, 默认 1]: " ENV_CHOICE
ENV_CHOICE=${ENV_CHOICE:-1}
case "$ENV_CHOICE" in
1) ENV_MODE="local" ;;
2) ENV_MODE="remote" ;;
*) echo -e "${RED}输入无效,默认选择 local。${NC}"; ENV_MODE="local" ;;
esac
fi
# [第二步]:选择平台及动作
if [ -z "${BACKEND_MODE}" ]; then
echo -e "\n${YELLOW}【第二步】请选择运行平台及操作:${NC}"
echo " 1) Docker Compose 容器化部署 (推荐)"
echo " 2) Systemd 原生后台服务部署"
echo " 3) 停止并清理 Docker Compose 容器服务"
echo " 4) 停止并清理 Systemd 后台服务"
read -p "请选择 [1-4, 默认 1]: " BACKEND_CHOICE
BACKEND_CHOICE=${BACKEND_CHOICE:-1}
case "$BACKEND_CHOICE" in
1) BACKEND_MODE="compose"; [ "${ACTION}" != "remove" ] && ACTION="install" ;;
2) BACKEND_MODE="systemd"; [ "${ACTION}" != "remove" ] && ACTION="install" ;;
3) BACKEND_MODE="compose"; ACTION="remove" ;;
4) BACKEND_MODE="systemd"; ACTION="remove" ;;
*) echo -e "${RED}输入无效,默认选择 compose (安装)。${NC}"; BACKEND_MODE="compose"; [ "${ACTION}" != "remove" ] && ACTION="install" ;;
esac
fi
# [第三步]:选择目标模块
if [ -z "${ROLE_TARGET}" ]; then
echo -e "\n${YELLOW}【第三步】请选择目标模块:${NC}"
echo " 1) 全量服务 [All: server + node]"
echo " 2) 仅主控服务端 [Server]"
echo " 3) 仅计算节点 [Node]"
read -p "请选择 [1-3, 默认 1]: " ROLE_CHOICE
ROLE_CHOICE=${ROLE_CHOICE:-1}
case "$ROLE_CHOICE" in
1) ROLE_TARGET="all" ;;
2) ROLE_TARGET="server" ;;
3) ROLE_TARGET="node" ;;
*) echo -e "${RED}输入无效,默认选择 all。${NC}"; ROLE_TARGET="all" ;;
esac
fi
# [第四步]:远程部署安装时选择静态资源同步策略
if [ "${ENV_MODE}" = "remote" ] && [ "${ACTION}" != "remove" ]; then
echo -e "\n${YELLOW}【第四步】请选择静态资源 (assets/) 同步策略:${NC}"
echo " 1) 自动模式 [Auto] (推荐: rsync 增量同步;若退化至 scp,目标端已有资源时自动跳过)"
echo " 2) 跳过同步 [Skip] (等同 --skip-assets,仅更新程序代码,不传输静态资源)"
echo " 3) 强制同步 [Force] (等同 --force-assets,无视目标端状态,强制全量覆盖传输)"
read -p "请选择 [1-3, 默认 1]: " ASSET_CHOICE
ASSET_CHOICE=${ASSET_CHOICE:-1}
case "$ASSET_CHOICE" in
1) SYNC_ASSETS="auto" ;;
2) SYNC_ASSETS="skip" ;;
3) SYNC_ASSETS="force" ;;
*) echo -e "${RED}输入无效,默认选择 auto。${NC}"; SYNC_ASSETS="auto" ;;
esac
fi
echo -e "${BLUE}==============================================================${NC}"
fi
# 参数校验
case "$ENV_MODE" in
local|remote) ;;
*) echo -e "${RED}错误: 不支持的环境类型 '$ENV_MODE'${NC}"; exit 1 ;;
esac
case "$BACKEND_MODE" in
compose|systemd) ;;
*) echo -e "${RED}错误: 不支持的平台类型 '$BACKEND_MODE'${NC}"; exit 1 ;;
esac
case "$ROLE_TARGET" in
all|server|node) ;;
*) echo -e "${RED}错误: 不支持的模块类型 '$ROLE_TARGET'${NC}"; exit 1 ;;
esac
# 变量映射
if [ "$ROLE_TARGET" = "all" ]; then
SERVICES="server node"
ROLES="server node"
ROLE_DESC="全量模块 (server + node)"
TAR_NAME="dcts_images_all.tar.gz"
elif [ "$ROLE_TARGET" = "server" ]; then
SERVICES="server"
ROLES="server"
ROLE_DESC="服务端 (server)"
TAR_NAME="dcts_images_server.tar.gz"
else
SERVICES="node"
ROLES="node"
ROLE_DESC="计算节点 (node)"
TAR_NAME="dcts_images_node.tar.gz"
fi
echo -e "${CYAN}📢 配置确认: 环境=[${YELLOW}${ENV_MODE}${CYAN}] 平台=[${YELLOW}${BACKEND_MODE}${CYAN}] 模块=[${YELLOW}${ROLE_DESC}${CYAN}] 动作=[${YELLOW}${ACTION}${CYAN}] 资源同步=[${YELLOW}${SYNC_ASSETS}${CYAN}]${NC}\n"
# =============================================================================
# 静态资源同步函数
# =============================================================================
sync_remote_assets() {
if [ "${SYNC_ASSETS}" = "skip" ]; then
echo -e "${YELLOW}[i] 已启用 --skip-assets,跳过静态资源 (assets/) 同步。${NC}"
return 0
fi
if [ ! -d "assets" ]; then
return 0
fi
echo "=== 检查并同步静态资源 (assets/) ==="
local local_rsync=false
local remote_rsync=false
if command -v rsync >/dev/null 2>&1; then local_rsync=true; fi
if ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "command -v rsync" >/dev/null 2>&1; then remote_rsync=true; fi
if [ "$local_rsync" = true ] && [ "$remote_rsync" = true ]; then
echo " -> 双方已就绪 rsync,开始增量同步 assets/..."
rsync -avz -e "ssh -p ${REMOTE_PORT} ${SSH_OPTS}" assets/ "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DIR}/assets/"
else
echo -e "${YELLOW}[!] 未检测到完备的 rsync,回退至 scp 模式...${NC}"
local remote_has_assets
remote_has_assets=$(ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "[ -d '${REMOTE_DIR}/assets' ] && ls -A '${REMOTE_DIR}/assets' 2>/dev/null | head -n 1 || echo ''")
if [ -n "${remote_has_assets}" ] && [ "${SYNC_ASSETS}" != "force" ]; then
echo -e "${GREEN}[√] 远端已有 assets/ 目录。为避免 scp 全量传输 2GB+ 数据,本次自动跳过 assets/ 同步。${NC}"
echo -e " (如需强制覆盖更新静态资源,请附加 --force-assets 参数或在交互向导中选择 Force 模式)"
else
if [ -n "${remote_has_assets}" ]; then
echo -e "${YELLOW}[!] 已指定 --force-assets,开始通过 scp 强制传输全量 assets/ 数据...${NC}"
else
echo -e "${CYAN} -> 远端为新部署环境,开始传输初始 assets/ 数据...${NC}"
fi
scp -P "${REMOTE_PORT}" ${SSH_OPTS} -r assets "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DIR}/"
fi
fi
}
# =============================================================================
# 3. 清理/卸载流程 (ACTION="remove")
# =============================================================================
if [ "${ACTION}" = "remove" ]; then
if [ "${ENV_MODE}" = "local" ] && [ "${BACKEND_MODE}" = "compose" ]; then
echo -e "${YELLOW}停止并清理本地 Docker Compose 服务 [${SERVICES}]...${NC}"
if [ "${ROLE_TARGET}" = "all" ]; then
docker compose down 2>/dev/null || true
else
docker compose stop ${SERVICES} 2>/dev/null || true
docker compose rm -f -s -v ${SERVICES} 2>/dev/null || true
fi
echo -e "${GREEN}[✓] 本地 Docker Compose 服务已清理。${NC}"
elif [ "${ENV_MODE}" = "local" ] && [ "${BACKEND_MODE}" = "systemd" ]; then
if [ "$EUID" -ne 0 ]; then
echo -e "${YELLOW}[!] 卸载 Systemd 服务需要 root 权限,正在申请提权...${NC}"
exec sudo -E "$0" -e local -b systemd -r "${ROLE_TARGET}" remove
fi
for r in ${ROLES}; do
SVC="dcts-${r}.service"
echo -e "${YELLOW}卸载 Systemd 服务 [${SVC}]...${NC}"
if systemctl is-active --quiet "dcts-${r}" 2>/dev/null; then systemctl stop "dcts-${r}" || true; fi
if systemctl is-enabled --quiet "dcts-${r}" 2>/dev/null; then systemctl disable "dcts-${r}" || true; fi
rm -f "/etc/systemd/system/${SVC}"
echo -e "${GREEN}[✓] 服务 ${SVC} 已彻底卸载。${NC}"
done
systemctl daemon-reload || true
systemctl reset-failed 2>/dev/null || true
elif [ "${ENV_MODE}" = "remote" ] && [ "${BACKEND_MODE}" = "compose" ]; then
echo -e "${YELLOW}清理远端 Docker Compose 服务 [${REMOTE_USER}@${REMOTE_IP}]...${NC}"
if [ "${ROLE_TARGET}" = "all" ]; then
ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_IP}" "cd '${REMOTE_DIR}' 2>/dev/null && docker compose down 2>/dev/null || true"
else
ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_IP}" "cd '${REMOTE_DIR}' 2>/dev/null && docker compose stop ${SERVICES} 2>/dev/null && docker compose rm -f -s -v ${SERVICES} 2>/dev/null || true"
fi
echo -e "${GREEN}[✓] 远端 Docker Compose 服务已清理。${NC}"
elif [ "${ENV_MODE}" = "remote" ] && [ "${BACKEND_MODE}" = "systemd" ]; then
echo -e "${YELLOW}清理远端 Systemd 服务 [${REMOTE_USER}@${REMOTE_IP}]...${NC}"
ssh -t -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_IP}" "cd '${REMOTE_DIR}' 2>/dev/null && sudo ./scripts/deploy.sh -e local -b systemd -r ${ROLE_TARGET} remove"
echo -e "${GREEN}[✓] 远端 Systemd 服务已清理。${NC}"
fi
exit 0
fi
# =============================================================================
# 4. 安装/更新流程 (ACTION="install")
# =============================================================================
# ─── 4.1 本地 + Docker Compose ────────────────────────────────────────────────
if [ "${ENV_MODE}" = "local" ] && [ "${BACKEND_MODE}" = "compose" ]; then
echo -e "${CYAN}>>> 启动本地 Docker Compose 服务构建与运行...${NC}"
docker compose build ${SERVICES}
if [ "${ROLE_TARGET}" = "all" ]; then
docker compose up -d
else
docker compose up -d ${SERVICES}
fi
echo -e " -> 等待 3 秒进行运行状态检测..."
sleep 3 && docker compose ps
echo -e "${GREEN}🎉 本地 Docker Compose 服务已成功启动!${NC}"
if [[ "${ROLES}" == *"server"* ]]; then
PORT=${DCTS_PORT:-8090}
echo -e "👉 面板访问地址: ${CYAN}http://localhost:${PORT}${NC}"
fi
# ─── 4.2 远程 + Docker Compose ────────────────────────────────────────────────
elif [ "${ENV_MODE}" = "remote" ] && [ "${BACKEND_MODE}" = "compose" ]; then
SSH_CONTROL_DIR="$HOME/.ssh/cm"
SSH_CONTROL_PATH="${SSH_CONTROL_DIR}/${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PORT}-$$"
SSH_OPTS="-o ControlMaster=auto -o ControlPath=${SSH_CONTROL_PATH} -o ControlPersist=1800"
mkdir -p "${SSH_CONTROL_DIR}"
cleanup_cm() { ssh -p "${REMOTE_PORT}" -o ControlPath="${SSH_CONTROL_PATH}" "${REMOTE_USER}@${REMOTE_IP}" -O exit 2>/dev/null || true; rm -f "${SSH_CONTROL_PATH}" 2>/dev/null || true; }
trap cleanup_cm EXIT
echo "=== 1. 建立 SSH 主连接复用 (SSH ControlMaster) ==="
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} -fN "${REMOTE_USER}@${REMOTE_IP}" || true
echo "=== 2. 在本地构建 Docker 镜像 [${SERVICES}] ==="
docker compose build ${SERVICES}
echo "=== 3. 导出镜像至归档包: ${TAR_NAME} ==="
if [ "$ROLE_TARGET" = "all" ]; then
docker save dcts-server:latest dcts-node:latest | gzip > "${TAR_NAME}"
elif [ "$ROLE_TARGET" = "server" ]; then
docker save dcts-server:latest | gzip > "${TAR_NAME}"
else
docker save dcts-node:latest | gzip > "${TAR_NAME}"
fi
echo "=== 4. 同步静态资源与配置文件 ==="
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "mkdir -p '${REMOTE_DIR}'"
sync_remote_assets
ENV_FILE=".env"
[ ! -f ".env" ] && ENV_FILE=".env.example"
scp -P "${REMOTE_PORT}" ${SSH_OPTS} "${TAR_NAME}" docker-compose.yml "${ENV_FILE}" "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DIR}/"
[ "${ENV_FILE}" = ".env.example" ] && ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "cp -n '${REMOTE_DIR}/.env.example' '${REMOTE_DIR}/.env' 2>/dev/null || true"
echo "=== 5. 远端导入镜像并启动 Docker Compose 服务 ==="
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "cd '${REMOTE_DIR}' && docker load < '${TAR_NAME}' && rm -f '${TAR_NAME}' && docker compose up -d ${SERVICES} && sleep 3 && docker compose ps"
echo "=== 6. 清理本地临时归档包 ==="
rm -f "${TAR_NAME}"
echo -e "${GREEN}🎉 远程 Docker Compose [${ROLE_DESC}] 部署完成!${NC}"
if [[ "${ROLES}" == *"server"* ]]; then
echo -e "👉 远程面板访问地址: ${CYAN}http://${REMOTE_IP}:8090${NC}"
fi
# ─── 4.3 本地 + Systemd ───────────────────────────────────────────────────────
elif [ "${ENV_MODE}" = "local" ] && [ "${BACKEND_MODE}" = "systemd" ]; then
if [ "$EUID" -ne 0 ]; then
echo -e "${YELLOW}[!] 安装 Systemd 服务需要 root 权限,正在申请提权...${NC}"
exec sudo -E "$0" -e local -b systemd -r "${ROLE_TARGET}" install
fi
TARGET_USER="${DCTS_DAEMON_USER:-${SUDO_USER:-$USER}}"
[ "$TARGET_USER" = "root" ] && [ $(id -u fmq 2>/dev/null || echo 0) -ne 0 ] && TARGET_USER="fmq"
REAL_USER="${TARGET_USER}"
if [ ! -f "${WORK_DIR}/.env" ]; then
if [ -f "${WORK_DIR}/.env.example" ]; then
cp "${WORK_DIR}/.env.example" "${WORK_DIR}/.env"
chown "${REAL_USER}:${REAL_USER}" "${WORK_DIR}/.env" 2>/dev/null || true
fi
fi
find_binary() {
local n="$1"
if [ -f "${WORK_DIR}/target/release/${n}" ]; then echo "${WORK_DIR}/target/release/${n}"
elif [ -f "${WORK_DIR}/${n}" ]; then echo "${WORK_DIR}/${n}"
elif [ -f "${WORK_DIR}/target/debug/${n}" ]; then echo "${WORK_DIR}/target/debug/${n}"
else echo ""; fi
}
for r in ${ROLES}; do
BIN_PATH=$(find_binary "$r")
if [ -z "$BIN_PATH" ]; then
echo -e "${YELLOW}[!] 未找到预编译的 ${r} 二进制文件,开始自动编译...${NC}"
if command -v cargo >/dev/null 2>&1; then
sudo -u "${REAL_USER}" env "PATH=$PATH:$HOME/.cargo/bin" cargo build --release -p "$r"
BIN_PATH="${WORK_DIR}/target/release/${r}"
else
echo -e "${RED}错误: 未找到可执行文件且无 Cargo 编译环境,部署终止。${NC}"
exit 1
fi
fi
SVC_NAME="dcts-${r}"
SVC_FILE="/etc/systemd/system/${SVC_NAME}.service"
SVC_DESC="DCTS ${r^} Daemon Service"
echo -e "${CYAN}=== 配置 Systemd 服务 [${SVC_NAME}] ===${NC}"
cat <<EOF > "${SVC_FILE}"
[Unit]
Description=${SVC_DESC}
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${REAL_USER}
WorkingDirectory=${WORK_DIR}
ExecStart=${BIN_PATH}
Restart=always
RestartSec=5s
Environment=RUST_LOG=info
EnvironmentFile=-${WORK_DIR}/.env
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
chmod 644 "${SVC_FILE}"
echo -e "${GREEN}[✓] 服务配置文件已写入: ${SVC_FILE}${NC}"
systemctl daemon-reload
systemctl enable "${SVC_NAME}"
systemctl restart "${SVC_NAME}"
echo -e "${GREEN}[✓] 服务 ${SVC_NAME} 已启动并配置开机自启。${NC}"
done
LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
LOCAL_IP=${LOCAL_IP:-"127.0.0.1"}
echo -e "\n${GREEN}🎊 本地 Systemd [${ROLE_DESC}] 服务部署完成!${NC}"
if [[ "${ROLES}" == *"server"* ]]; then
PORT=${DCTS_PORT:-8090}
echo -e "📌 面板访问地址: ${CYAN}http://localhost:${PORT}${NC}${CYAN}http://${LOCAL_IP}:${PORT}${NC}"
fi
echo -e "🛠 运维指令: ${YELLOW}sudo systemctl [status|restart|stop] dcts-<server|node>${NC}"
# ─── 4.4 远程 + Systemd ───────────────────────────────────────────────────────
elif [ "${ENV_MODE}" = "remote" ] && [ "${BACKEND_MODE}" = "systemd" ]; then
SSH_CONTROL_DIR="$HOME/.ssh/cm"
SSH_CONTROL_PATH="${SSH_CONTROL_DIR}/${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PORT}-$$"
SSH_OPTS="-o ControlMaster=auto -o ControlPath=${SSH_CONTROL_PATH} -o ControlPersist=1800"
mkdir -p "${SSH_CONTROL_DIR}"
cleanup_cm() { ssh -p "${REMOTE_PORT}" -o ControlPath="${SSH_CONTROL_PATH}" "${REMOTE_USER}@${REMOTE_IP}" -O exit 2>/dev/null || true; rm -f "${SSH_CONTROL_PATH}" 2>/dev/null || true; }
trap cleanup_cm EXIT
echo "=== 1. 建立 SSH 主连接复用 (SSH ControlMaster) ==="
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} -fN "${REMOTE_USER}@${REMOTE_IP}" || true
echo "=== 2. 检查架构与准备二进制文件 [${ROLES}] ==="
REMOTE_ARCH=$(ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "uname -m 2>/dev/null || echo unknown")
LOCAL_ARCH=$(uname -m)
if [ "${REMOTE_ARCH}" != "unknown" ] && [ "${REMOTE_ARCH}" != "${LOCAL_ARCH}" ]; then
echo -e "${YELLOW}[!] 警告: 本地架构 (${LOCAL_ARCH}) 与远程架构 (${REMOTE_ARCH}) 不一致,请确认已针对目标架构完成交叉编译。${NC}"
fi
for r in ${ROLES}; do
if [ ! -f "target/release/${r}" ] && [ ! -f "${r}" ]; then
echo -e "${YELLOW}未找到 ${r} release 产物,开始本地编译...${NC}"
cargo build --release -p "${r}"
fi
done
echo "=== 3. 传输文件与静态资源 ==="
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "mkdir -p '${REMOTE_DIR}/scripts' '${REMOTE_DIR}/data' '${REMOTE_DIR}/logs'"
sync_remote_assets
for r in ${ROLES}; do
BIN_SRC="target/release/${r}"
[ ! -f "${BIN_SRC}" ] && [ -f "${r}" ] && BIN_SRC="${r}"
echo " -> 传输二进制可执行文件: ${BIN_SRC}"
scp -P "${REMOTE_PORT}" ${SSH_OPTS} "${BIN_SRC}" "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DIR}/${r}.tmp"
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "mv -f '${REMOTE_DIR}/${r}.tmp' '${REMOTE_DIR}/${r}' && chmod +x '${REMOTE_DIR}/${r}'"
done
ENV_FILE=".env"
[ ! -f ".env" ] && ENV_FILE=".env.example"
scp -P "${REMOTE_PORT}" ${SSH_OPTS} "${ENV_FILE}" scripts/deploy.sh "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DIR}/scripts/"
[ "${ENV_FILE}" = ".env.example" ] && ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "cp -n '${REMOTE_DIR}/scripts/.env.example' '${REMOTE_DIR}/.env' 2>/dev/null || true" || ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "cp -n '${REMOTE_DIR}/scripts/.env' '${REMOTE_DIR}/.env' 2>/dev/null || true"
echo "=== 4. 在远端配置并启动 Systemd 服务 ==="
ssh -t -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "cd '${REMOTE_DIR}' && chmod +x scripts/deploy.sh && sudo ./scripts/deploy.sh -e local -b systemd -r ${ROLE_TARGET} install"
echo -e "\n${GREEN}👑 远程 Systemd [${ROLE_DESC}] 服务部署完成!${NC}"
if [[ "${ROLES}" == *"server"* ]]; then
echo -e "👉 远程面板访问地址: ${CYAN}http://${REMOTE_IP}:8090${NC}"
fi
fi
echo ""
exit 0