You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang中同一SSH Session执行多命令并解析输出的问题

问题描述

我需要在同一个SSH会话中执行多条命令,每条命令执行后解析其输出。目前我只能打印出STDOUT的第一行,但尝试打印全部输出时程序会挂起,似乎在等待STDOUT。我希望实现执行一条命令、解析输出后,在同一会话中执行第二条命令。以下是我的示例代码:

package main

import (
    "bufio"
    "fmt"
    "log"

    "golang.org/x/crypto/ssh"
)

func main() {
    // SSH credentials
    sshConfig := &ssh.ClientConfig{
        User: "rob",
        Auth: []ssh.AuthMethod{
            ssh.Password("OMGAPASSWORD....EEEEEEE"),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    // Connect to SSH server
    client, err := ssh.Dial("tcp", "*************:22", sshConfig)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Open a new session
    session, err := client.NewSession()
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()

    // Set up input and output streams
    sshIn, _ := session.StdinPipe()
    sshOut, _ := session.StdoutPipe()
    //sshErr, _ := session.StderrPipe()

    // Start the SSH session
    if err := session.Start("/bin/bash"); err != nil {
        log.Fatalf("Failed to start SSH session: %v", err)
    }

    // Send the first command
    cmd1 := "ls -al"
    fmt.Fprintf(sshIn, "%s\n", cmd1)

    lines := make(chan string)

    go func() {
        scanner := bufio.NewScanner(sshOut)
        for scanner.Scan() {
            lines <- scanner.Text()
        }
        close(lines)
    }()

    for line := range lines {
        fmt.Println(line)
    }
}

问题原因

原代码挂起的核心问题是:启动/bin/bash后,执行完ls -al命令,bash不会主动关闭标准输出,而是会一直等待新的命令输入。你的scanner会持续监听stdout,永远不会触发scanner.Scan()返回false的情况,导致程序阻塞在读取循环中。

同时,代码没有明确的标记来判断一条命令是否执行完毕,无法准确切换到下一条命令的执行。


解决方案

通过设置自定义提示符来标记命令执行的边界,这样就能准确识别每条命令的输出结束点,避免无限等待。同时分别处理标准输出和错误输出,完善会话的生命周期管理。

修改后的代码如下:

package main

import (
    "bufio"
    "fmt"
    "log"
    "strings"

    "golang.org/x/crypto/ssh"
)

func main() {
    // SSH credentials
    sshConfig := &ssh.ClientConfig{
        User: "rob",
        Auth: []ssh.AuthMethod{
            ssh.Password("OMGAPASSWORD....EEEEEEE"),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    // Connect to SSH server
    client, err := ssh.Dial("tcp", "*************:22", sshConfig)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Open a new session
    session, err := client.NewSession()
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()

    // Set up input and output streams
    sshIn, err := session.StdinPipe()
    if err != nil {
        log.Fatalf("Failed to get stdin pipe: %v", err)
    }
    sshOut, err := session.StdoutPipe()
    if err != nil {
        log.Fatalf("Failed to get stdout pipe: %v", err)
    }
    sshErr, err := session.StderrPipe()
    if err != nil {
        log.Fatalf("Failed to get stderr pipe: %v", err)
    }

    // 设置独特的自定义提示符,用于识别命令执行结束
    customPrompt := "CMD_DONE>>>"
    setupPromptCmd := fmt.Sprintf("PS1='%s'\n", customPrompt)

    // Start the SSH session
    if err := session.Start("/bin/bash"); err != nil {
        log.Fatalf("Failed to start SSH session: %v", err)
    }

    // 后台监听错误输出
    go func() {
        scanner := bufio.NewScanner(sshErr)
        for scanner.Scan() {
            fmt.Printf("[STDERR] %s\n", scanner.Text())
        }
    }()

    scanner := bufio.NewScanner(sshOut)
    // 读取初始bash输出,直到出现默认提示符
    for scanner.Scan() {
        if strings.HasSuffix(scanner.Text(), "$") || strings.HasSuffix(scanner.Text(), "#") {
            break
        }
    }

    // 发送自定义提示符设置命令,读取到新提示符出现
    fmt.Fprint(sshIn, setupPromptCmd)
    for scanner.Scan() {
        if strings.HasSuffix(scanner.Text(), customPrompt) {
            break
        }
    }

    // 执行第一条命令并读取输出
    cmd1 := "ls -al"
    fmt.Printf("\n=== Executing: %s ===\n", cmd1)
    fmt.Fprintf(sshIn, "%s\n", cmd1)

    var cmd1Output []string
    for scanner.Scan() {
        line := scanner.Text()
        if strings.HasSuffix(line, customPrompt) {
            break
        }
        cmd1Output = append(cmd1Output, line)
    }

    // 解析第一条命令输出
    fmt.Println("Command 1 Output:")
    for _, line := range cmd1Output {
        fmt.Println(line)
    }

    // 执行第二条命令并读取输出
    cmd2 := "pwd"
    fmt.Printf("\n=== Executing: %s ===\n", cmd2)
    fmt.Fprintf(sshIn, "%s\n", cmd2)

    var cmd2Output []string
    for scanner.Scan() {
        line := scanner.Text()
        if strings.HasSuffix(line, customPrompt) {
            break
        }
        cmd2Output = append(cmd2Output, line)
    }

    // 解析第二条命令输出
    fmt.Println("Command 2 Output:")
    for _, line := range cmd2Output {
        fmt.Println(line)
    }

    // 退出bash并等待会话结束
    fmt.Fprint(sshIn, "exit\n")
    if err := session.Wait(); err != nil {
        log.Printf("Session exit error: %v", err)
    }
}

关键改动说明

  1. 自定义提示符:通过PS1设置独特的提示符CMD_DONE>>>,作为命令执行完成的标记,解决了无法判断输出边界的问题。
  2. 错误输出处理:新增goroutine监听stderr,避免错误信息被忽略。
  3. 会话生命周期管理:最后发送exit命令并调用session.Wait(),确保会话正常结束。
  4. 分阶段读取输出:每条命令执行后,读取输出直到自定义提示符出现,确保获取完整的命令输出后再执行下一条命令。

内容的提问来源于stack exchange,提问作者Robert YT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 16:35:33