@@ -148,192 +148,295 @@ func (s *ServerHandler) executeScript(scriptContent, scriptName string) (string,
return strings . TrimSpace ( output ) , nil
}
}
func ( s * ServerHandler ) executeFullDeployment ( ) error {
s . sendTerminalOutput ( s . session . WebSocket , "开始执行完整自动化部署流程..." )
// 获取 下载链接
// 1. 获取并校验 下载链接
fileURL := config . NewServerService ( ) . GetFile ( )
if strings . TrimSpace ( fileURL ) == "" {
fileURL = strings . TrimSpace ( fileURL )
if fileURL == "" {
return fmt . Errorf ( "下载链接为空" )
}
// 前置校验: 确保链接是合法的URL( 包含http/https)
if ! strings . HasPrefix ( fileURL , "http://" ) && ! strings . HasPrefix ( fileURL , "https://" ) {
return fmt . Errorf ( "下载链接格式非法, 缺少http/https协议: %s" , fileURL )
}
s . sendTerminalOutput ( s . session . WebSocket , fmt . Sprintf ( "【前置检查】有效下载链接:%s" , fileURL ) )
// 生成随机文件名
// 2. 生成目标文件路径
randomFileName := s . generateRandomFileName ( )
remoteExePath := fmt . Sprintf ( "%s/%s" , defaultWorkDir , randomFileName )
remoteWorkDir := defaultWorkDir
onlineID := fmt . Sprintf ( "%d" , s . ServerList . OnlineID )
fixedScreenSession := "logic"
// 固定Screen会话名称为logic
const fixedScreenSession = "logic"
// 3. 定义部署脚本(给每个%s加唯一标记, 方便核对)
deploymentScriptTpl := `
set -e
set -x
// 创建完整的部署脚本(不包含#!/bin/bash)
deploymentScript := fmt . Sprintf ( `
set -e # 遇到错误立即退出
# ===== 检查并安装 screen =====
# ===== 检查并安装screen =====
echo "检查Screen是否已安装..."
if command -v screen &> /dev/null; then
echo "=== Screen已安装, 跳过 ==="
else
echo "=== Screen未安装, 开始安装 ==="
if command -v apt &> /dev/null; then
echo "检测到apt, 正在更新软件包列表..."
apt update -y > /dev/null 2>&1
echo "正在安装screen..."
apt install -y screen > /dev/null 2>&1
apt update -y && apt install -y screen
elif command -v yum &> /dev/null; then
echo "检测到yum, 正在安装 screen..."
yum install -y screen > /dev/null 2>&1
yum install -y screen
elif command -v dnf &> /dev/null; then
echo "检测到dnf, 正在安装 screen..."
dnf install -y screen > /dev/null 2>&1
dnf install -y screen
elif command -v pacman &> /dev/null; then
echo "=== 检测到pacman, 正在安装 screen ==="
pacman -S --noconfirm screen > /dev/null 2>&1
pacman -S --noconfirm screen
else
echo "=== 不支持的系统 包管理器,无法自动 安装Screen === "
exit 1
fi
if command -v screen &> /dev/null; then
echo "=== screen 安装成功! ==="
else
echo "=== screen 安装失败 ==="
echo "❌ 不支持的包管理器, 无法安装Screen"
exit 1
fi
command -v screen || { echo "❌ Screen安装失败"; exit 1; }
fi
# ===== 核心逻辑:给 logic会话发exit命令 + 循环等待Screen主进程退出 =====
echo "开始处理固定名称[logic]的Screen会话... "
# ===== 优雅终止 logic会话(先等内部程序退出 → 再退screen) =====
echo "===== 优雅终止logic会话 ===== "
SCREEN_PID=""
# 1. 先查找logic会话是否存在并提取主PID
if screen -ls "%s" | grep -q "%s"; then
echo "找到[logic]会话, 提取主PID..."
SCREEN_PID=$(screen -ls "%s" | grep -oE '[0-9]+\.'"$1" | cut -d. -f1)
# 2. 给Screen会话发送exit命令( 核心操作)
echo "给[logic]会话发送exit命令, 触发会话退出..."
screen -x -S "%s" -p 0 -X stuff "exit\n" 2>/dev/null || true
else
echo "=== 未找到名称为[logic]的Screen会话, 跳过终止 ==="
fi
# 你实际使用的screen名称( 从日志看是logic)
SCREEN_NAME="%s { screen_name}"
# 3. 循环等待Screen主进程退出( 核心: 等Screen本身退出 )
if [ -n "$SCREEN_PID" ] && [ "$SCREEN_PID" != "" ]; then
echo "开始循环等待[logic]会话主PID[$SCREEN_PID]退出..."
WAIT_COUNT=0
MAX_WAIT_SECONDS=30 # 最大等待30秒, 避免无限循环
while ps -p "$SCREEN_PID" > /dev/null 2>&1; do
sleep 0.5 # 每0.5秒检测一次Screen主进程状态
WAIT_COUNT=$((WAIT_COUNT + 1))
# 每2秒输出一次等待状态( 可选, 方便排查)
if [ $((WAIT_COUNT % 4)) -eq 0 ]; then
echo "等待中...已等待$((WAIT_COUNT/2))秒(最大$MAX_WAIT_SECONDS秒) "
fi
# 调试开关( 如需详细日志, 取消set -x注释 )
set -o pipefail
export PS4='[DEBUG] $ { BASH_SOURCE}:$ { LINENO} - $ { FUNCNAME[0]:+$ { FUNCNAME[0]}(): }'
# set -x
# 超过最大等待时间则退出循环,避免卡死
if [ $WAIT_COUNT -ge $((MAX_WAIT_SECONDS * 2)) ]; then
echo "⚠️ 等待超时($MAX_WAIT_SECONDS秒) , [logic]会话主PID[$SCREEN_PID]仍未退出"
# 兜底: 超时后给Screen主进程发kill 15
kill -15 "$SCREEN_PID " 2>/dev/null || true
break
fi
done
# 4. 检查Screen主进程最终状态
if ps -p "$SCREEN_PID" > /dev/null 2>&1; then
echo "❌ [logic]会话主PID[$SCREEN_PID]未退出,建议手动处理"
# 定义: 检查PID是否存活的函数
pid_is_alive() {
local pid=$1
# 仅检查PID是否存在, 不发送信号( 最安全的方式)
if [ -n "$pid" ] && kill -0 "$pid " 2>/dev/null; then
return 0 # PID存活
else
echo "✅ [logic]会话主PID[$SCREEN_PID]已成功退出"
return 1 # PID不存在
fi
}
# 定义安全的进程检测函数(带超时防卡)
get_inner_procs() {
local screen_pid=$1
# 5秒超时, 避免pstree卡住
local procs=$(timeout 5 pstree -p "$screen_pid" 2>/dev/null | grep -oE '\([0-9]+\)' | tr -d '()' | grep -v "$screen_pid" | sort -u)
# 兜底: pstree失败时用pgrep按screen名称查找
if [ -z "$procs" ]; then
procs=$(pgrep -f "SCREEN -S $SCREEN_NAME" 2>/dev/null | grep -v "$screen_pid")
fi
echo "$procs"
}
# 1. 检查logic会话是否存在
if screen -ls "$SCREEN_NAME" 2>/dev/null | grep -q -E "[0-9]+\.$SCREEN_NAME"; then
echo "找到$SCREEN_NAME会话, 提取主PID..."
SCREEN_PID=$(screen -ls "$SCREEN_NAME" | grep -oE '[0-9]+\.'"$SCREEN_NAME" | head -1 | cut -d. -f1)
if [ -z "$SCREEN_PID" ]; then
echo "⚠️ 提取$SCREEN_NAME会话PID失败"
else
echo "✅ 提取到$SCREEN_NAME会话主PID: $SCREEN_PID"
# ========== 步骤1: 给screen内所有子进程发优雅退出信号 ==========
echo "给$SCREEN_NAME内所有程序发送优雅退出信号(SIGTERM)..."
INNER_ALL_PROCS=$(get_inner_procs "$SCREEN_PID")
if [ -n "$INNER_ALL_PROCS" ]; then
echo "📌 检测到screen内进程列表: $INNER_ALL_PROCS"
for pid in $INNER_ALL_PROCS; do
# 发送信号前检查子进程是否存活
if pid_is_alive "$pid"; then
if kill -15 "$pid" 2>/dev/null; then
echo "✅ 已给进程$pid发送SIGTERM信号"
else
echo "⚠️ 进程$pid发送信号失败"
fi
else
echo "ℹ ️ 进程$pid已不存在, 跳过发送信号"
fi
done
else
echo "ℹ ️ 未检测到$SCREEN_NAME内的子进程"
fi
# 兜底: 给screen内Shell发送exit指令
echo "给screen内Shell发送exit指令( 兜底) ..."
screen -S "$SCREEN_NAME" -p 0 -X stuff $'exit\n' 2>/dev/null
sleep 1
# ========== 步骤2: 循环等待内部程序退出( 防卡优化) ==========
echo "开始循环等待$SCREEN_NAME内部所有程序退出( 最大60秒) ..."
WAIT_COUNT=0
MAX_WAIT_SECONDS=60
INNER_PROC_EXIST=true
while [ "$INNER_PROC_EXIST" = true ] && [ $WAIT_COUNT -lt $MAX_WAIT_SECONDS ]; do
INNER_PROCS=$(get_inner_procs "$SCREEN_PID")
# 强制退出条件:即使进程检测失败也不卡住
if [ -z "$INNER_PROCS" ]; then
INNER_PROC_EXIST=false
echo "✅ $SCREEN_NAME内部所有程序已退出( 或进程检测完成) "
else
sleep 1
WAIT_COUNT=$((WAIT_COUNT + 1))
# 每5秒输出状态
if [ $((WAIT_COUNT % 5)) -eq 0 ]; then
ELAPSED=$WAIT_COUNT
echo "⏳ 等待中...残留进程:$INNER_PROCS( 已等$ELAPSED秒, 剩余$((MAX_WAIT_SECONDS - ELAPSED))秒)"
fi
fi
done
# 超时提示
if [ "$INNER_PROC_EXIST" = true ]; then
echo "⚠️ 等待超时( 60秒) , $SCREEN_NAME内部程序仍未退出"
echo "📌 残留进程PID: $INNER_PROCS"
fi
# ========== 步骤3: 退出screen会话( 核心修复: 避免kill卡住) ==========
if [ "$INNER_PROC_EXIST" = false ]; then
echo "内部程序已退出,开始退出$SCREEN_NAME会话..."
# 第一步: 尝试正常退出screen会话
if screen -S "$SCREEN_NAME" -X quit 2>/dev/null; then
echo "✅ $SCREEN_NAME会话已通过screen -X quit退出"
else
echo "ℹ ️ screen -X quit执行失败( 会话可能已消失) , 检查PID是否存活..."
# 第二步: 仅当PID存活时, 才执行kill, 且加超时
if pid_is_alive "$SCREEN_PID"; then
echo "📌 PID $SCREEN_PID 仍存活, 尝试kill终止( 5秒超时) ..."
timeout 5 kill -15 "$SCREEN_PID" 2>/dev/null
if [ $? -eq 0 ]; then
echo "✅ 已给screen主进程$SCREEN_PID发送SIGTERM信号"
else
echo "⚠️ kill $SCREEN_PID 失败(超时/进程不存在)"
fi
else
echo "ℹ ️ PID $SCREEN_PID 已不存在, 跳过kill操作"
fi
fi
sleep 2
else
echo "⚠️ 内部程序未完全退出, 跳过退出screen会话"
fi
# 最终验证
if screen -ls "$SCREEN_NAME" 2>/dev/null | grep -q -E "[0-9]+\.$SCREEN_NAME"; then
echo "❌ $SCREEN_NAME会话最终仍未退出"
else
echo "✅ $SCREEN_NAME会话已完全退出"
fi
fi
else
echo "=== 未找到$SCREEN_NAME会话, 跳过终止 ==="
fi
# 关闭调试
# set +x
# ===== 准备下载目录 =====
echo "创建工作目录:%s { work_dir}"
mkdir -p "%s { work_dir}" || { echo "❌ 创建目录失败"; exit 1; }
# ===== 下载程序 =====
echo "开始下载程序到: %s "
echo "===== 开始下载程序 ===== "
echo "下载链接:%s { file_url}"
echo "目标路径:%s { exe_path}"
# 删除旧文件
if [ -f "%s" ]; then
echo "删除旧文件... "
rm -f "%s"
if [ -f "%s{ exe_path} " ]; then
echo "删除旧文件: %s{ exe_path} "
rm -f "%s{ exe_path} "
fi
# ===== 准备下载目录 =====
echo "创建工作目录:%s { work_dir}"
mkdir -p "%s { work_dir}" || { echo "❌ 创建目录失败"; exit 1; }
# ===== 下载程序 =====
echo "===== 开始下载程序 ====="
echo "下载链接:%s { file_url}"
echo "目标路径:%s { exe_path}"
# 删除旧文件(关键修复:这里要判断目标路径,不是下载链接)
if [ -f "%s { exe_path}" ]; then
echo "删除旧文件:%s { exe_path}"
rm -f "%s { exe_path}"
fi
echo "开始下载..."
DOWNLOAD_SUCCESS=0
if command -v wget >/dev/null 2>&1; then
wget --no-check-certificate -O "%s" "%s" 2>&1
# 正确格式: wget -O 目标路径 下载链接
wget --no-check-certificate --timeout=10 --tries=2 -O "%s { exe_path}" "%s { file_url}"
DOWNLOAD_SUCCESS=$?
elif command -v curl >/dev/null 2>&1; then
curl -L -o "%s" "%s" 2>&1
# 正确格式: curl -o 目标路径 下载链接
curl -L --connect-timeout 10 --max-time 30 -o "%s { exe_path}" "%s { file_url}"
DOWNLOAD_SUCCESS=$?
else
echo "系统未安装 wget或 curl"
echo "❌ 无 wget/ curl,无法下载 "
exit 1
fi
if [ $DOWNLOAD_SUCCESS -ne 0 ]; then
echo "❌ 下载失败,退出码:$DOWNLOAD_SUCCESS"
exit 1
fi
# 验证文件
if [ -f "%s" ] && [ -s "%s" ]; then
if [ -f "%s{ exe_path} " ] && [ -s "%s{ exe_path} " ]; then
echo "=== 文件下载完成 ==="
ls -la "%s"
ls -la "%s{ exe_path} "
# 检查文件大小( 至少1KB)
FILE_SIZE=$(stat -c%s "%s { exe_path}" 2>/dev/null || stat -f%z "%s { exe_path}" 2>/dev/null)
[ "$FILE_SIZE" -lt 1024 ] && { echo "❌ 文件太小($FILE_SIZE字节) "; exit 1; }
else
echo "=== 文件下载失败! === "
echo "=== 请检查下载链接是否有效 ==="
echo "❌ 文件下载失败或为空 "
exit 1
fi
# ===== 启动新程序 =====
echo "开始启动新程序... "
echo "设置执行权限:%s { exe_path} "
chmod +x "%s { exe_path}" || { echo "❌ 设置权限失败"; exit 1; }
# 设置执行权限
chmod +x "%s"
if [ $? -ne 0 ]; then
echo "设置执行权限失败"
exit 1
fi
echo "=== 权限设置完成 ==="
echo "启动Screen会话[%s { screen_name}]..."
screen -dmS "%s { screen_name}" bash -c '"%s { exe_path}" -id=%s { online_id} | tee -a "$HOME/run.log"'
# 启动新程序到固定名称[logic]的Screen会话( 重建会话)
echo "正在启动Screen会话: logic"
screen -dmS "logic" bash -c '"%s" -id=%s | tee -a "$HOME/run.log"'
# 等待2秒确保会话启动
sleep 2
# 检查logic会话是否启动成功
if screen -ls | grep -q "logic"; then
echo "=== 程序启动成功: Screen会话[logic]已创建 ==="
echo "=== 会话名称: logic ==="
if screen -ls | grep -q "%s { screen_name}"; then
echo "✅ 程序启动成功!会话名称:%s { screen_name}"
screen -ls
echo "程序已在后台Screen会话[logic]中运行"
else
echo "=== 程序启动失败: Screen会话[logic]未创建 === "
echo "❌ 程序启动失败,未创建[%s { screen_name}]会话 "
screen -ls
exit 1
fi
echo "#SCRIPT_EXECUTION_COMPLETE#"
` ,
fixedScreenSession , fixedScreenSession , fixedScreenSession , fixedScreenSession , // logic会话参数
remoteExePath , // 下载路径
remoteExePath , remoteExePath , // 删除旧文件
remoteExePath , fileURL , // wget下载
remoteExePath , fileURL , // curl下载
remoteExePath , remoteExePath , remoteExePath , // 文件验证
remoteExePath , // 执行权限
remoteExePath , onlineID , // 启动logic会话,
)
`
// 执行完整的部署脚本
// 4. 定义参数映射(用占位符替换,彻底避免数错顺序)
deploymentScript := strings . ReplaceAll ( deploymentScriptTpl , "%s{screen_name}" , fixedScreenSession )
deploymentScript = strings . ReplaceAll ( deploymentScript , "%s{work_dir}" , remoteWorkDir )
deploymentScript = strings . ReplaceAll ( deploymentScript , "%s{file_url}" , fileURL )
deploymentScript = strings . ReplaceAll ( deploymentScript , "%s{exe_path}" , remoteExePath )
deploymentScript = strings . ReplaceAll ( deploymentScript , "%s{online_id}" , onlineID )
// 5. 执行脚本
_ , err := s . executeScript ( deploymentScript , "full_deployment_" + grand . S ( 10 ) )
if err != nil {
return err
return fmt . Errorf ( "执行部署脚本失败:%w" , err)
}
// 保存固定的logic 会话名称
// 6. 保存会话名称
config . NewServerService ( ) . SetServerScreen ( s . ServerList . OnlineID , fixedScreenSession )
s . sendTerminalOutput ( s . session . WebSocket , "自动化部署完成" )
return nil
}