Files
aiops/backend/pkg/remoteexec/persistent_client.go
T
Logan 1acb0c49af feat: Agent CLI 与终端审计回放全面优化
- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏
- 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复
- 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示
- 审计历史页只读(移除删除),移除回放弹框录制搜索
2026-07-01 12:13:53 +08:00

144 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// persistent_client.go - 任务级 SSH 长连接,多次 Exec 复用同一 client // Logan.Li
package remoteexec
import (
"bytes"
"context"
"fmt"
"sync"
"time"
"golang.org/x/crypto/ssh"
)
// PersistentClient 可复用的 SSH 长连接(任务生命周期内持有,Close 释放) // Logan.Li
type PersistentClient struct {
cfg SSHConfig
client *ssh.Client
mu sync.Mutex
}
// NewPersistentClient 建立 SSH 长连接 // Logan.Li
func NewPersistentClient(cfg SSHConfig) (*PersistentClient, error) {
client, err := connect(cfg)
if err != nil {
return nil, err
}
return &PersistentClient{cfg: cfg, client: client}, nil
}
// Exec 在长连接上执行命令;连接断开时自动重连一次 // Logan.Li
func (p *PersistentClient) Exec(command string, timeout time.Duration) (*ExecResult, error) {
p.mu.Lock()
defer p.mu.Unlock()
result, err := p.execLocked(command, timeout)
if err == nil {
return result, nil
}
// 连接级失败:重连后重试一次 // Logan.Li
if p.client != nil {
_ = p.client.Close()
p.client = nil
}
client, connErr := connect(p.cfg)
if connErr != nil {
return result, err
}
p.client = client
return p.execLocked(command, timeout)
}
// execLocked 假定已持锁,在当前 client 上执行 // Logan.Li
func (p *PersistentClient) execLocked(command string, timeout time.Duration) (*ExecResult, error) {
if p.client == nil {
client, err := connect(p.cfg)
if err != nil {
return nil, err
}
p.client = client
}
return execWithClient(p.client, p.cfg, command, timeout)
}
// Close 关闭长连接 // Logan.Li
func (p *PersistentClient) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.client == nil {
return nil
}
err := p.client.Close()
p.client = nil
return err
}
// execWithClient 在已有 SSH client 上执行命令(供 Exec / PersistentClient 共用) // Logan.Li
func execWithClient(client *ssh.Client, cfg SSHConfig, command string, timeout time.Duration) (*ExecResult, error) {
start := time.Now()
result := &ExecResult{Host: cfg.Host, Port: cfg.Port}
session, err := client.NewSession()
if err != nil {
result.Duration = time.Since(start).Milliseconds()
result.Error = fmt.Sprintf("创建SSH会话失败: %v", err)
return result, err
}
defer session.Close()
if timeout <= 0 {
timeout = 60 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
type execOut struct {
output []byte
err error
}
ch := make(chan execOut, 1)
go func() {
var stdout, stderr bytes.Buffer
session.Stdout = &stdout
session.Stderr = &stderr
runErr := session.Run(command)
output := stdout.Bytes()
if stderr.Len() > 0 {
if len(output) > 0 {
output = append(output, '\n')
}
output = append(output, stderr.Bytes()...)
}
ch <- execOut{output: output, err: runErr}
}()
select {
case <-ctx.Done():
result.Duration = time.Since(start).Milliseconds()
result.Error = fmt.Sprintf("执行超时(%v)", timeout)
result.ExitCode = -1
return result, fmt.Errorf("exec timeout after %v", timeout)
case out := <-ch:
result.Duration = time.Since(start).Milliseconds()
result.Output = string(out.output)
if out.err != nil {
if exitErr, ok := out.err.(*ssh.ExitError); ok {
result.ExitCode = exitErr.ExitStatus()
result.Success = false
} else {
result.ExitCode = -1
result.Error = out.err.Error()
result.Success = false
return result, out.err
}
} else {
result.ExitCode = 0
result.Success = true
}
}
return result, nil
}