2026-01-07 02:30:21 +08:00
|
|
|
|
package middleware
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-01-28 23:28:25 +08:00
|
|
|
|
"blazing/common/utils"
|
|
|
|
|
|
"blazing/cool"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
"blazing/modules/config/model"
|
|
|
|
|
|
config "blazing/modules/config/service"
|
|
|
|
|
|
"bufio"
|
2026-01-07 02:30:21 +08:00
|
|
|
|
"context"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
"crypto/rand"
|
|
|
|
|
|
"encoding/hex"
|
2026-01-07 02:30:21 +08:00
|
|
|
|
"encoding/json"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
2026-01-07 02:30:21 +08:00
|
|
|
|
"net"
|
2026-04-30 00:02:18 +08:00
|
|
|
|
"strconv"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"sync/atomic"
|
2026-01-07 02:30:21 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gogf/gf/v2/os/glog"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
"github.com/gogf/gf/v2/util/grand"
|
2026-01-07 02:30:21 +08:00
|
|
|
|
"github.com/lxzan/gws"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
"golang.org/x/crypto/ssh"
|
2026-01-07 02:30:21 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
const (
|
|
|
|
|
|
PingInterval = 10 * time.Second
|
|
|
|
|
|
defaultWorkDir = "$HOME" // 全环境兼容
|
|
|
|
|
|
randomStrLength = 4 // 缩短随机串长度
|
2026-04-30 00:02:18 +08:00
|
|
|
|
cmdTimeout = 600 * time.Second // 延长超时(适配Screen安装+下载)
|
2026-01-09 08:31:30 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// SSHConfig SSH连接配置
|
|
|
|
|
|
type SSHConfig struct {
|
|
|
|
|
|
ServerIP string `json:"server_ip"`
|
|
|
|
|
|
Port string `json:"port"`
|
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
|
Password string `json:"password"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WebSSHMessage 消息结构
|
|
|
|
|
|
type WebSSHMessage struct {
|
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
|
Payload string `json:"payload"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TerminalSession 会话结构体(优化通道)
|
|
|
|
|
|
type TerminalSession struct {
|
|
|
|
|
|
WebSocket *gws.Conn
|
|
|
|
|
|
SSHClient *ssh.Client
|
|
|
|
|
|
SSHSession *ssh.Session
|
|
|
|
|
|
Stdin io.WriteCloser
|
|
|
|
|
|
Stdout io.Reader
|
|
|
|
|
|
Ready bool
|
|
|
|
|
|
outputBuf chan string // 增大通道缓存
|
|
|
|
|
|
closed atomic.Bool
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-07 02:30:21 +08:00
|
|
|
|
type ServerHandler struct {
|
|
|
|
|
|
gws.BuiltinEventHandler
|
2026-01-09 08:31:30 +08:00
|
|
|
|
session *TerminalSession
|
|
|
|
|
|
model.ServerList
|
|
|
|
|
|
isinstall uint32
|
|
|
|
|
|
target net.Conn
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成随机文件名
|
|
|
|
|
|
func (s *ServerHandler) generateRandomFileName() string {
|
|
|
|
|
|
randBytes := make([]byte, randomStrLength)
|
|
|
|
|
|
_, err := rand.Read(randBytes)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Sprintf("logic_%d", time.Now().UnixNano())
|
|
|
|
|
|
}
|
|
|
|
|
|
randStr := hex.EncodeToString(randBytes)
|
|
|
|
|
|
timestamp := time.Now().Format("20060102150405")
|
|
|
|
|
|
return fmt.Sprintf("logic_%s_%s", timestamp, randStr)
|
2026-01-07 02:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
// 执行脚本命令
|
|
|
|
|
|
func (s *ServerHandler) executeScript(scriptContent, scriptName string) (string, error) {
|
|
|
|
|
|
if s.session == nil || s.session.closed.Load() || !s.session.Ready {
|
|
|
|
|
|
return "", fmt.Errorf("session not ready")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成临时脚本路径
|
|
|
|
|
|
scriptPath := fmt.Sprintf("/tmp/%s.sh", scriptName)
|
|
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
writeScriptCmd := fmt.Sprintf("stty -echo\ncat > '%s' << 'DEPLOYMENT_SCRIPT_END'\n%s\nDEPLOYMENT_SCRIPT_END\nchmod +x '%s'\nbash '%s' 2>&1\nSCRIPT_STATUS=$?\nrm -f '%s'\nstty echo\necho '#SCRIPT_EXIT_STATUS:'$SCRIPT_STATUS'#'\necho '#SCRIPT_EXECUTION_COMPLETE#'\n", scriptPath, scriptContent, scriptPath, scriptPath, scriptPath)
|
2026-01-09 08:31:30 +08:00
|
|
|
|
_, err := s.session.Stdin.Write([]byte(writeScriptCmd))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
output := ""
|
2026-04-30 00:02:18 +08:00
|
|
|
|
exitStatus := 0
|
|
|
|
|
|
hasExitStatus := false
|
|
|
|
|
|
timeout := time.After(cmdTimeout)
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case line := <-s.session.outputBuf:
|
|
|
|
|
|
if strings.Contains(line, "#SCRIPT_EXECUTION_COMPLETE#") {
|
|
|
|
|
|
if hasExitStatus && exitStatus != 0 {
|
|
|
|
|
|
return strings.TrimSpace(output), fmt.Errorf("script exited with status %d", exitStatus)
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.TrimSpace(output), nil
|
2026-01-09 08:31:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
if strings.Contains(line, "#SCRIPT_EXIT_STATUS:") {
|
|
|
|
|
|
statusText := strings.TrimPrefix(line, "#SCRIPT_EXIT_STATUS:")
|
|
|
|
|
|
statusText = strings.TrimSuffix(statusText, "#")
|
|
|
|
|
|
if status, convErr := strconv.Atoi(strings.TrimSpace(statusText)); convErr == nil {
|
|
|
|
|
|
exitStatus = status
|
|
|
|
|
|
hasExitStatus = true
|
|
|
|
|
|
}
|
|
|
|
|
|
continue
|
2026-01-09 08:31:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if strings.Contains(line, "logout") || strings.Contains(line, "exit") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
output += line + "\n"
|
2026-04-30 00:02:18 +08:00
|
|
|
|
case <-timeout:
|
|
|
|
|
|
return strings.TrimSpace(output), fmt.Errorf("script execution timeout")
|
2026-01-09 08:31:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-28 23:28:25 +08:00
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
func (s *ServerHandler) executeFullDeployment() error {
|
|
|
|
|
|
s.sendTerminalOutput(s.session.WebSocket, "开始执行完整自动化部署流程...")
|
|
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
// 1. 获取并校验下载链接
|
2026-01-28 23:28:25 +08:00
|
|
|
|
// filename := config.NewServerService().GetFile()
|
|
|
|
|
|
filename, _ := utils.GetLatestLogicFile("public")
|
|
|
|
|
|
// fileURL := "http://sun.72wo.cn/" + filename
|
|
|
|
|
|
fileURL := "http://" + cool.Config.File.Domain + cool.Config.Address + "/" + filename
|
|
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
fileURL = strings.TrimSpace(fileURL)
|
2026-01-28 23:28:25 +08:00
|
|
|
|
fmt.Println("更新地址", fileURL)
|
2026-01-23 15:38:23 +08:00
|
|
|
|
if fileURL == "" {
|
2026-01-09 08:31:30 +08:00
|
|
|
|
return fmt.Errorf("下载链接为空")
|
|
|
|
|
|
}
|
2026-01-23 15:38:23 +08:00
|
|
|
|
// 前置校验:确保链接是合法的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))
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
// 2. 生成目标文件路径
|
2026-03-03 13:16:35 +08:00
|
|
|
|
//randomFileName := s.generateRandomFileName()
|
|
|
|
|
|
remoteExePath := fmt.Sprintf("%s/%s", defaultWorkDir, filename)
|
2026-01-23 15:38:23 +08:00
|
|
|
|
remoteWorkDir := defaultWorkDir
|
2026-01-09 08:31:30 +08:00
|
|
|
|
onlineID := fmt.Sprintf("%d", s.ServerList.OnlineID)
|
2026-01-23 15:38:23 +08:00
|
|
|
|
fixedScreenSession := "logic"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
// 3. 定义部署脚本(给每个%s加唯一标记,方便核对)
|
|
|
|
|
|
deploymentScriptTpl := `
|
2026-04-30 00:02:18 +08:00
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
set -euo pipefail
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
# ===== 检查并安装screen =====
|
2026-01-09 08:31:30 +08:00
|
|
|
|
echo "检查Screen是否已安装..."
|
|
|
|
|
|
if command -v screen &> /dev/null; then
|
|
|
|
|
|
echo "=== Screen已安装,跳过 ==="
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "=== Screen未安装,开始安装 ==="
|
|
|
|
|
|
if command -v apt &> /dev/null; then
|
2026-01-23 15:38:23 +08:00
|
|
|
|
apt update -y && apt install -y screen
|
2026-01-09 08:31:30 +08:00
|
|
|
|
elif command -v yum &> /dev/null; then
|
2026-01-23 15:38:23 +08:00
|
|
|
|
yum install -y screen
|
2026-01-09 08:31:30 +08:00
|
|
|
|
elif command -v dnf &> /dev/null; then
|
2026-01-23 15:38:23 +08:00
|
|
|
|
dnf install -y screen
|
2026-01-09 08:31:30 +08:00
|
|
|
|
elif command -v pacman &> /dev/null; then
|
2026-01-23 15:38:23 +08:00
|
|
|
|
pacman -S --noconfirm screen
|
2026-01-09 08:31:30 +08:00
|
|
|
|
else
|
2026-01-23 15:38:23 +08:00
|
|
|
|
echo "❌ 不支持的包管理器,无法安装Screen"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
2026-01-23 15:38:23 +08:00
|
|
|
|
command -v screen || { echo "❌ Screen安装失败"; exit 1; }
|
|
|
|
|
|
fi
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
# ===== 准备下载目录 =====
|
|
|
|
|
|
echo "创建工作目录:%s{work_dir}"
|
|
|
|
|
|
mkdir -p "%s{work_dir}" || { echo "❌ 创建目录失败"; exit 1; }
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
# ===== 下载程序 =====
|
2026-01-23 15:38:23 +08:00
|
|
|
|
echo "===== 开始下载程序 ====="
|
|
|
|
|
|
echo "下载链接:%s{file_url}"
|
|
|
|
|
|
echo "目标路径:%s{exe_path}"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
target_dir="%s{work_dir}"
|
|
|
|
|
|
tmp_path="%s{exe_path}.downloading"
|
|
|
|
|
|
|
|
|
|
|
|
echo "清理部署目录 ${target_dir} 下的 logic_ 和 login_ 旧文件..."
|
|
|
|
|
|
for pattern in logic_ login_; do
|
|
|
|
|
|
for file in "${target_dir}/${pattern}"*; do
|
|
|
|
|
|
if [ -f "$file" ]; then
|
|
|
|
|
|
echo "删除旧文件:$file"
|
|
|
|
|
|
rm -f "$file"
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
2026-03-03 00:19:08 +08:00
|
|
|
|
done
|
2026-04-30 00:02:18 +08:00
|
|
|
|
rm -f "${tmp_path}"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
file_size() {
|
|
|
|
|
|
stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || echo 0
|
|
|
|
|
|
}
|
2026-01-23 15:38:23 +08:00
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
human_bytes() {
|
|
|
|
|
|
bytes="$1"
|
|
|
|
|
|
if [ "$bytes" -ge 1048576 ]; then
|
|
|
|
|
|
awk -v b="$bytes" 'BEGIN { printf "%.2fMB", b / 1048576 }'
|
|
|
|
|
|
elif [ "$bytes" -ge 1024 ]; then
|
|
|
|
|
|
awk -v b="$bytes" 'BEGIN { printf "%.2fKB", b / 1024 }'
|
|
|
|
|
|
else
|
|
|
|
|
|
printf "%sB" "$bytes"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
2026-01-23 15:38:23 +08:00
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
show_download_speed() {
|
|
|
|
|
|
pid="$1"
|
|
|
|
|
|
last_size=0
|
|
|
|
|
|
last_time=$(date +%s)
|
|
|
|
|
|
while kill -0 "$pid" 2>/dev/null; do
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
now_time=$(date +%s)
|
|
|
|
|
|
now_size=$(file_size "${tmp_path}")
|
|
|
|
|
|
elapsed=$((now_time - last_time))
|
|
|
|
|
|
[ "$elapsed" -le 0 ] && elapsed=1
|
|
|
|
|
|
speed=$(((now_size - last_size) / elapsed))
|
|
|
|
|
|
[ "$speed" -lt 0 ] && speed=0
|
|
|
|
|
|
echo "下载进度:已下载 $(human_bytes "$now_size"),实时速度 $(human_bytes "$speed")/s"
|
|
|
|
|
|
last_size="$now_size"
|
|
|
|
|
|
last_time="$now_time"
|
|
|
|
|
|
done
|
|
|
|
|
|
}
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
echo "开始下载..."
|
2026-04-30 00:02:18 +08:00
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
|
|
|
|
curl -fsSL --connect-timeout 10 --max-time 300 -o "${tmp_path}" "%s{file_url}" &
|
|
|
|
|
|
DOWNLOAD_PID=$!
|
|
|
|
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
|
|
|
|
wget -q --no-check-certificate --timeout=10 --tries=2 -O "${tmp_path}" "%s{file_url}" &
|
|
|
|
|
|
DOWNLOAD_PID=$!
|
2026-01-09 08:31:30 +08:00
|
|
|
|
else
|
2026-01-23 15:38:23 +08:00
|
|
|
|
echo "❌ 无wget/curl,无法下载"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
show_download_speed "$DOWNLOAD_PID" &
|
|
|
|
|
|
SPEED_PID=$!
|
|
|
|
|
|
|
|
|
|
|
|
set +e
|
|
|
|
|
|
wait "$DOWNLOAD_PID"
|
|
|
|
|
|
DOWNLOAD_SUCCESS=$?
|
|
|
|
|
|
set -e
|
|
|
|
|
|
wait "$SPEED_PID" 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$DOWNLOAD_SUCCESS" -ne 0 ]; then
|
|
|
|
|
|
rm -f "${tmp_path}"
|
|
|
|
|
|
echo "❌ 下载失败,退出码:${DOWNLOAD_SUCCESS}"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# 验证文件
|
2026-04-30 00:02:18 +08:00
|
|
|
|
if [ -f "${tmp_path}" ] && [ -s "${tmp_path}" ]; then
|
2026-01-09 08:31:30 +08:00
|
|
|
|
echo "=== 文件下载完成 ==="
|
2026-04-30 00:02:18 +08:00
|
|
|
|
ls -la "${tmp_path}"
|
2026-01-23 15:38:23 +08:00
|
|
|
|
# 检查文件大小(至少1KB)
|
2026-04-30 00:02:18 +08:00
|
|
|
|
FILE_SIZE=$(file_size "${tmp_path}")
|
2026-01-23 15:38:23 +08:00
|
|
|
|
[ "$FILE_SIZE" -lt 1024 ] && { echo "❌ 文件太小($FILE_SIZE字节)"; exit 1; }
|
2026-01-09 08:31:30 +08:00
|
|
|
|
else
|
2026-01-23 15:38:23 +08:00
|
|
|
|
echo "❌ 文件下载失败或为空"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
mv -f "${tmp_path}" "%s{exe_path}"
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
# ===== 启动新程序 =====
|
2026-01-23 15:38:23 +08:00
|
|
|
|
echo "设置执行权限:%s{exe_path}"
|
|
|
|
|
|
chmod +x "%s{exe_path}" || { echo "❌ 设置权限失败"; exit 1; }
|
2026-03-03 13:16:35 +08:00
|
|
|
|
|
2026-04-30 00:02:18 +08:00
|
|
|
|
if screen -ls 2>/dev/null | grep -q "%s{screen_name}"; then
|
|
|
|
|
|
echo "检测到已有Screen会话[%s{screen_name}],不主动停止,继续启动新程序"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
echo "启动Screen会话[%s{screen_name}]..."
|
2026-04-30 00:02:18 +08:00
|
|
|
|
screen -dmS "%s{screen_name}" bash -lc '"%s{exe_path}" -id=%s{online_id} >> "$HOME/run_%s{randomFileName}.log" 2>&1'
|
2026-01-09 08:31:30 +08:00
|
|
|
|
|
2026-01-22 16:01:52 +00:00
|
|
|
|
sleep 2
|
2026-01-23 15:38:23 +08:00
|
|
|
|
if screen -ls | grep -q "%s{screen_name}"; then
|
|
|
|
|
|
echo "✅ 程序启动成功!会话名称:%s{screen_name}"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
screen -ls
|
|
|
|
|
|
else
|
2026-01-23 15:38:23 +08:00
|
|
|
|
echo "❌ 程序启动失败,未创建[%s{screen_name}]会话"
|
2026-01-09 08:31:30 +08:00
|
|
|
|
screen -ls
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
2026-01-23 15:38:23 +08:00
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
|
|
// 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)
|
2026-03-03 13:16:35 +08:00
|
|
|
|
deploymentScript = strings.ReplaceAll(deploymentScript, "%s{randomFileName}", filename)
|
2026-01-23 15:38:23 +08:00
|
|
|
|
// 5. 执行脚本
|
2026-01-09 08:31:30 +08:00
|
|
|
|
_, err := s.executeScript(deploymentScript, "full_deployment_"+grand.S(10))
|
|
|
|
|
|
if err != nil {
|
2026-01-23 15:38:23 +08:00
|
|
|
|
return fmt.Errorf("执行部署脚本失败:%w", err)
|
2026-01-09 08:31:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 15:38:23 +08:00
|
|
|
|
// 6. 保存会话名称
|
2026-01-23 20:34:52 +00:00
|
|
|
|
config.NewServerService().SetServerScreen(s.ServerList.OnlineID, filename)
|
2026-01-09 19:58:12 +08:00
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
return nil
|
2026-01-07 02:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
// executeCommand 执行单个命令并返回输出
|
|
|
|
|
|
func (s *ServerHandler) executeCommand(command string) (string, error) {
|
|
|
|
|
|
return s.executeScript(command, "cmd_"+grand.S(8))
|
2026-01-07 02:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
// ---------------- 主流程:OnOpen(严格顺序执行) ----------------
|
|
|
|
|
|
func (s *ServerHandler) OnOpen(socket *gws.Conn) {
|
|
|
|
|
|
// 1. 建立SSH连接
|
|
|
|
|
|
sshConfig := &ssh.ClientConfig{
|
|
|
|
|
|
User: s.ServerList.Account,
|
|
|
|
|
|
Auth: []ssh.AuthMethod{ssh.Password(s.ServerList.Password)},
|
|
|
|
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
|
|
|
|
Timeout: 30 * time.Second,
|
|
|
|
|
|
}
|
2026-01-07 02:30:21 +08:00
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
// 连接服务器(重试)
|
|
|
|
|
|
var sshClient *ssh.Client
|
|
|
|
|
|
var err error
|
|
|
|
|
|
for retry := 0; retry < 2; retry++ {
|
|
|
|
|
|
sshClient, err = ssh.Dial("tcp", s.ServerList.LoginAddr, sshConfig)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.sendError(socket, "SSH连接失败:"+err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-01-07 02:30:21 +08:00
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
// 创建会话(重试)
|
|
|
|
|
|
var session *ssh.Session
|
|
|
|
|
|
for retry := 0; retry < 2; retry++ {
|
|
|
|
|
|
session, err = sshClient.NewSession()
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
session.Close()
|
|
|
|
|
|
sshClient.Close()
|
|
|
|
|
|
s.sendError(socket, "创建SSH会话失败:"+err.Error())
|
|
|
|
|
|
return
|
2026-01-07 02:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
// 配置PTY(通用配置)
|
|
|
|
|
|
modes := ssh.TerminalModes{
|
|
|
|
|
|
ssh.ECHO: 1,
|
|
|
|
|
|
ssh.TTY_OP_ISPEED: 115200,
|
|
|
|
|
|
ssh.TTY_OP_OSPEED: 115200,
|
|
|
|
|
|
}
|
|
|
|
|
|
err = session.RequestPty("vt100", 100, 80, modes)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
session.Close()
|
|
|
|
|
|
sshClient.Close()
|
|
|
|
|
|
s.sendError(socket, "PTY请求失败:"+err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取Stdin/Stdout
|
|
|
|
|
|
stdin, err := session.StdinPipe()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
session.Close()
|
|
|
|
|
|
sshClient.Close()
|
|
|
|
|
|
s.sendError(socket, "获取Stdin失败:"+err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
stdout, err := session.StdoutPipe()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
session.Close()
|
|
|
|
|
|
sshClient.Close()
|
|
|
|
|
|
s.sendError(socket, "获取Stdout失败:"+err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 启动Shell
|
|
|
|
|
|
err = session.Shell()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
session.Close()
|
|
|
|
|
|
sshClient.Close()
|
|
|
|
|
|
s.sendError(socket, "启动Shell失败:"+err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化会话
|
|
|
|
|
|
s.session = &TerminalSession{
|
|
|
|
|
|
WebSocket: socket,
|
|
|
|
|
|
SSHClient: sshClient,
|
|
|
|
|
|
SSHSession: session,
|
|
|
|
|
|
Stdin: stdin,
|
|
|
|
|
|
Stdout: stdout,
|
|
|
|
|
|
Ready: true,
|
|
|
|
|
|
outputBuf: make(chan string, 2048), // 增大缓存
|
|
|
|
|
|
closed: atomic.Bool{},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 启动输出转发协程
|
|
|
|
|
|
s.startOutputForwarding()
|
|
|
|
|
|
|
|
|
|
|
|
s.sendSuccess(socket, "SSH连接成功,会话已就绪")
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- 严格按顺序执行自动化部署 ----------------
|
|
|
|
|
|
if s.isinstall == 1 {
|
|
|
|
|
|
// 执行完整的自动化部署脚本
|
|
|
|
|
|
err := s.executeFullDeployment()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.sendError(socket, "自动化部署失败: "+err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.sendSuccess(socket, "自动化部署完成")
|
|
|
|
|
|
}
|
2026-01-07 02:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
// startOutputForwarding 启动输出转发协程
|
|
|
|
|
|
func (s *ServerHandler) startOutputForwarding() {
|
|
|
|
|
|
if s.session == nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
|
glog.Error(context.Background(), "Output forwarding goroutine panic:", r)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
2026-01-07 02:30:21 +08:00
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
scanner := bufio.NewScanner(s.session.Stdout)
|
|
|
|
|
|
for scanner.Scan() {
|
|
|
|
|
|
line := scanner.Text()
|
2026-04-30 00:02:18 +08:00
|
|
|
|
select {
|
|
|
|
|
|
case s.session.outputBuf <- line:
|
|
|
|
|
|
default:
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(line, "#SCRIPT_EXECUTION_COMPLETE#") || strings.Contains(line, "#SCRIPT_EXIT_STATUS:") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-01-09 08:31:30 +08:00
|
|
|
|
s.sendTerminalOutput(s.session.WebSocket, line+"\r\n")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := scanner.Err(); err != nil && s.session != nil {
|
|
|
|
|
|
glog.Error(context.Background(), "读取 SSH 输出失败:", err)
|
|
|
|
|
|
s.sendError(s.session.WebSocket, "读取 SSH 输出失败: "+err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
2026-01-07 02:30:21 +08:00
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
// ---------------- 基础函数 ----------------
|
|
|
|
|
|
func (s *ServerHandler) OnMessage(socket *gws.Conn, gwsmessage *gws.Message) {
|
|
|
|
|
|
if s.session == nil || s.session.Stdin == nil {
|
|
|
|
|
|
s.sendError(socket, "SSH 会话未建立")
|
|
|
|
|
|
return
|
2026-01-07 02:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 08:31:30 +08:00
|
|
|
|
_, err := s.session.Stdin.Write([]byte(gwsmessage.Data.Bytes()))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.sendError(socket, "写入消息失败:"+err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *ServerHandler) OnClose(socket *gws.Conn, err error) {
|
2026-02-02 11:00:37 +08:00
|
|
|
|
//glog.Debug(context.Background(), "连接断开:", err)
|
2026-01-09 08:31:30 +08:00
|
|
|
|
if s.session != nil {
|
|
|
|
|
|
s.session.closed.Store(true)
|
|
|
|
|
|
if s.session.SSHSession != nil {
|
|
|
|
|
|
s.session.SSHSession.Close()
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.session.SSHClient != nil {
|
|
|
|
|
|
s.session.SSHClient.Close()
|
|
|
|
|
|
}
|
|
|
|
|
|
s.session = nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送终端输出
|
|
|
|
|
|
func (s *ServerHandler) sendTerminalOutput(ws *gws.Conn, output string) {
|
|
|
|
|
|
msg := WebSSHMessage{Type: "output", Payload: output}
|
|
|
|
|
|
data, _ := json.Marshal(msg)
|
|
|
|
|
|
ws.WriteMessage(gws.OpcodeText, data)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送成功消息
|
|
|
|
|
|
func (s *ServerHandler) sendSuccess(ws *gws.Conn, msg string) {
|
|
|
|
|
|
res := WebSSHMessage{Type: "success", Payload: msg}
|
|
|
|
|
|
data, _ := json.Marshal(res)
|
|
|
|
|
|
ws.WriteMessage(gws.OpcodeText, data)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送错误消息
|
|
|
|
|
|
func (s *ServerHandler) sendError(ws *gws.Conn, msg string) {
|
|
|
|
|
|
res := WebSSHMessage{Type: "error", Payload: msg}
|
|
|
|
|
|
data, _ := json.Marshal(res)
|
|
|
|
|
|
ws.WriteMessage(gws.OpcodeText, data)
|
2026-01-07 02:30:21 +08:00
|
|
|
|
}
|