```
fix(logic): 移除main.go中的多余空行 移除PprofWeb函数后的多余空行,保持代码整洁性 fix(fight): 修正effect_13.go中的效果应用对象 将效果应用从对方上下文改为正确的目标对象,修复技能效果逻辑 feat(middleware): 增强server.go中的自动化部署功能 - 添加下载链接格式校验,确保包含http/https协议 - 重构部署脚本,优化screen会话终止逻辑 - 改进下载过程,添加超时和重试机制 - 增强错误处理和日志输出 refactor(config): 更新server.go中的数据库查询方法 - 修改GetPort方法返回类型为gdb.List以提高兼容性 - 使用统一的DBM方法替代不同的数据库查询方式 ```
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user