Feat: 使用Shell模式执行命令

- 实现ExecuteCommands使用Shell模式,在同一个会话中顺序执行
- 解决H3C设备session.Run后EOF断开问题
- 增加cleanCommandOutput清理命令回显和版权信息
- display interface等待5秒,其他命令等待2秒
This commit is contained in:
Your Name
2026-04-26 03:19:10 +08:00
parent 4edc1b67fc
commit 87d233659c
2 changed files with 138 additions and 26 deletions
+127 -8
View File
@@ -5,6 +5,8 @@ import (
"fmt"
"net"
"os"
"regexp"
"strings"
"time"
"golang.org/x/crypto/ssh"
@@ -140,19 +142,136 @@ func (c *Client) ExecuteCommand(command string) (string, error) {
return stdoutBuf.String(), nil
}
// ExecuteCommands 执行多个命令
// ExecuteCommands 执行多个命令(使用Shell模式,在同一个会话中顺序执行)
func (c *Client) ExecuteCommands(commands []string) ([]string, error) {
results := make([]string, 0, len(commands))
for _, cmd := range commands {
result, err := c.ExecuteCommand(cmd)
if err != nil {
return results, fmt.Errorf("failed to execute command '%s': %w", cmd, err)
}
results = append(results, result)
if c.client == nil {
return nil, fmt.Errorf("not connected")
}
// 创建一个shell会话
session, err := c.client.NewSession()
if err != nil {
return nil, fmt.Errorf("failed to create session: %w", err)
}
defer session.Close()
// 请求PTY
modes := ssh.TerminalModes{
ssh.ECHO: 0, // 禁用回显
ssh.TTY_OP_ISPEED: 14400, // 输入速度
ssh.TTY_OP_OSPEED: 14400, // 输出速度
}
if err := session.RequestPty("dumb", 200, 1000, modes); err != nil {
return nil, fmt.Errorf("failed to request pty: %w", err)
}
// 获取stdin管道
stdin, err := session.StdinPipe()
if err != nil {
return nil, fmt.Errorf("failed to get stdin pipe: %w", err)
}
// 捕获输出
var stdoutBuf bytes.Buffer
var stderrBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Stderr = &stderrBuf
// 启动shell
if err := session.Shell(); err != nil {
return nil, fmt.Errorf("failed to start shell: %w", err)
}
// 执行命令并收集输出
results := make([]string, 0, len(commands))
for i, cmd := range commands {
// 等待一段时间防止设备速率限制
if i > 0 {
time.Sleep(2 * time.Second)
}
// 发送命令(添加换行符)
if _, err := stdin.Write([]byte(cmd + "\n")); err != nil {
return results, fmt.Errorf("failed to send command '%s': %w", cmd, err)
}
// 等待命令执行完成(不同命令需要不同等待时间)
sleepTime := 1 * time.Second
if cmd == "display interface" {
sleepTime = 5 * time.Second // 大输出命令需要更多时间
}
time.Sleep(sleepTime)
// 获取当前输出并清理
rawOutput := stdoutBuf.String()
cleanOutput := cleanCommandOutput(rawOutput, cmd)
results = append(results, cleanOutput)
// 清空缓冲区,为下一个命令做准备(通过位置标记)
stdoutBuf.Reset()
stderrBuf.Reset()
}
// 退出shell
stdin.Write([]byte("exit\n"))
session.Wait()
return results, nil
}
// cleanCommandOutput 清理命令输出,移除命令回显、分页提示和提示符
func cleanCommandOutput(output, command string) string {
// 清理\r\n为\n
output = strings.ReplaceAll(output, "\r\n", "\n")
lines := strings.Split(output, "\n")
var cleanLines []string
skipCommandEcho := true // 跳过命令本身的回显
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
// 跳过空行(如果是开头)
if trimmedLine == "" && len(cleanLines) == 0 {
continue
}
// 跳过命令回显(第一次出现)
if skipCommandEcho && trimmedLine == strings.TrimSpace(command) {
skipCommandEcho = false
continue
}
// 跳过分页提示
if strings.Contains(trimmedLine, "---- More ----") {
continue
}
// 跳过提示符行(如 <hostname> 或 [hostname]
if regexp.MustCompile(`^[<\[]\S+[>\]]$`).MatchString(trimmedLine) {
continue
}
// 跳过版权信息(开头)
if strings.HasPrefix(trimmedLine, "*********") {
continue
}
if strings.HasPrefix(trimmedLine, "* Copyright") {
continue
}
if strings.HasPrefix(trimmedLine, "* Without") {
continue
}
if strings.HasPrefix(trimmedLine, "* no decompiling") {
continue
}
cleanLines = append(cleanLines, trimmedLine)
}
return strings.Join(cleanLines, "\n")
}
// CheckSSH 检查主机是否开启SSH
func CheckSSH(host string, port int, timeout time.Duration) bool {
if port == 0 {