From 87d233659c483d8c08f022e65a2e6802b55e64f3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Apr 2026 03:19:10 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E4=BD=BF=E7=94=A8Shell=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=89=A7=E8=A1=8C=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现ExecuteCommands使用Shell模式,在同一个会话中顺序执行 - 解决H3C设备session.Run后EOF断开问题 - 增加cleanCommandOutput清理命令回显和版权信息 - display interface等待5秒,其他命令等待2秒 --- internal/device/parser.go | 29 ++++---- internal/ssh/client.go | 135 +++++++++++++++++++++++++++++++++++--- 2 files changed, 138 insertions(+), 26 deletions(-) diff --git a/internal/device/parser.go b/internal/device/parser.go index 8234c33..6c9c558 100644 --- a/internal/device/parser.go +++ b/internal/device/parser.go @@ -77,24 +77,17 @@ func DiscoverDevice(ip string, deviceType models.DeviceType, username, password // 获取命令列表 commands := parser.GetCommands() - // 执行命令 - 允许部分命令失败,增加详细日志和延迟防止设备速率限制 - outputs := make([]string, 0, len(commands)) - for i, cmd := range commands { - // 每个命令之间等待2秒,防止H3C交换机速率限制导致返回空数据或执行失败 - if i > 0 { - time.Sleep(2 * time.Second) - } - - fmt.Printf("[PARSER] Executing command %d/%d: %s\n", i+1, len(commands), cmd) - output, err := client.ExecuteCommand(cmd) - if err != nil { - // 记录警告但继续执行其他命令 - fmt.Printf("Warning: command '%s' failed: %v\n", cmd, err) - outputs = append(outputs, "") - } else { - fmt.Printf("[PARSER] Command '%s' returned %d bytes\n", cmd, len(output)) - outputs = append(outputs, output) - } + // 执行所有命令(使用Shell模式,在同一个会话中顺序执行) + outputs, err := client.ExecuteCommands(commands) + if err != nil { + device.ScanStatus = "failed" + device.ErrorMessage = err.Error() + return device, err + } + + // 打印调试信息 + for i, output := range outputs { + fmt.Printf("[PARSER] Command %d/%d returned %d bytes\n", i+1, len(commands), len(output)) } // 解析输出 diff --git a/internal/ssh/client.go b/internal/ssh/client.go index b97ba4f..b37cafc 100644 --- a/internal/ssh/client.go +++ b/internal/ssh/client.go @@ -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]) + 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 {