如何在goloang ssh会话中捕获交错的stdout和stderr?

如何在goloang ssh会话中捕获交错的stdout和stderr?,go,ssh,Go,Ssh,如何在表单2>&1的转到模型shell重定向中捕获ssh.Session的交错stderr/stdout输出 我尝试将会话中的stdout和stderr管道的输出组合到一个多读取器中,然后使用扫描仪在go例程中异步捕获来自多读取器的数据 这有点奏效了。捕获了所有数据,但没有交错stderr数据。它出现在最后 我能够通过反转io.MultiReader()参数的顺序使stderr输出出现在开头,但它仍然没有交错 这是我期望的输出 $ ./gentestdata -i 5 -d -l -n 12 -

如何在表单
2>&1
的转到模型shell重定向中捕获ssh.Session的交错stderr/stdout输出

我尝试将会话中的stdout和stderr管道的输出组合到一个多读取器中,然后使用扫描仪在go例程中异步捕获来自多读取器的数据

这有点奏效了。捕获了所有数据,但没有交错stderr数据。它出现在最后

我能够通过反转io.MultiReader()参数的顺序使stderr输出出现在开头,但它仍然没有交错

这是我期望的输出

$ ./gentestdata -i 5 -d -l -n 12 -w 32 -a 'Lorem ipsum dolor sit amet'
     1 Lorem ipsum dolor sit am
     2 Lorem ipsum dolor sit am
     3 Lorem ipsum dolor sit am
     4 Lorem ipsum dolor sit am
     5 Lorem ipsum dolor sit am
     6 Lorem ipsum dolor sit am
     7 Lorem ipsum dolor sit am
     8 Lorem ipsum dolor sit am
     9 Lorem ipsum dolor sit am
    10 Lorem ipsum dolor sit am
    11 Lorem ipsum dolor sit am
    12 Lorem ipsum dolor sit am

$ # note that two of the lines were output to stderr
$ ./gentestdata -i 5 -d -l -n 12 -w 32 -a 'Lorem ipsum dolor sit amet' 1>/dev/null
     5 Lorem ipsum dolor sit am
    10 Lorem ipsum dolor sit am
gentestdata程序是我开发的,它允许我进行这种测试。可以在此处找到源:

以下是我看到的输出:

$ ./sshx $(pwd)/gentestdata -i 5 -d -l -n 12 -w 32 -a 'Lorem ipsum dolor sit amet'
     1 Lorem ipsum dolor sit am
     2 Lorem ipsum dolor sit am
     3 Lorem ipsum dolor sit am
     4 Lorem ipsum dolor sit am
     6 Lorem ipsum dolor sit am
     7 Lorem ipsum dolor sit am
     8 Lorem ipsum dolor sit am
     9 Lorem ipsum dolor sit am
    11 Lorem ipsum dolor sit am
    12 Lorem ipsum dolor sit am
     5 Lorem ipsum dolor sit am
    10 Lorem ipsum dolor sit am
请注意,stderr的最后两行出现故障

下面是完整的源代码。注意exec()函数

// Simple demonstration of how I thought that I could capture interleaved
// stdout and stderr output generated during go ssh.Session to model the
// bash 2>&1 redirection behavior.
package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "os"
    "os/signal"
    "runtime"
    "strings"
    "syscall"

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

func main() {
    user := strings.TrimSpace(os.Getenv("LOGNAME"))
    auth := getPassword(fmt.Sprintf("%v's password: ", user))
    addr := "localhost:22"
    if len(os.Args) > 1 {
        cmd := getCmd(os.Args[1:])
        config := &ssh.ClientConfig{
            User: user,
            Auth: []ssh.AuthMethod{
                ssh.Password(auth),
            },
        }
        exec(cmd, addr, config)
    }
}

// Execute the command.
func exec(cmd string, addr string, config *ssh.ClientConfig) {
    // Create the connection.
    conn, err := ssh.Dial("tcp", addr, config)
    check(err)
    session, err := conn.NewSession()
    check(err)
    defer session.Close()

    // Collect the output from stdout and stderr.
    // The idea is to duplicate the shell IO redirection
    // 2>&1 where both streams are interleaved.
    stdoutPipe, err := session.StdoutPipe()
    check(err)
    stderrPipe, err := session.StderrPipe()
    check(err)
    outputReader := io.MultiReader(stdoutPipe, stderrPipe)
    outputScanner := bufio.NewScanner(outputReader)

    // Start the session.
    err = session.Start(cmd)
    check(err)

    // Capture the output asynchronously.
    outputLine := make(chan string)
    outputDone := make(chan bool)
    go func(scan *bufio.Scanner, line chan string, done chan bool) {
        defer close(line)
        defer close(done)
        for scan.Scan() {
            line <- scan.Text()
        }
        done <- true
    }(outputScanner, outputLine, outputDone)

    // Use a custom wait.
    outputBuf := ""
    running := true
    for running {
        select {
        case <-outputDone:
            running = false
        case line := <-outputLine:
            outputBuf += line + "\n"
        }
    }
    session.Close()

    // Output the data.
    fmt.Print(outputBuf)
}

func check(e error) {
    if e != nil {
        _, _, lineno, _ := runtime.Caller(1)
        log.Fatalf("ERROR:%v %v", lineno, e)
    }
}

// Convert a slice of tokens to a command string.
// It inserts quotes where necessary.
func getCmd(args []string) (cmd string) {
    cmd = ""
    for i, token := range args {
        if i > 0 {
            cmd += " "
        }
        cmd += quote(token)
    }
    return
}

// Quote an individual token.
// Very simple, not suitable for production.
func quote(token string) string {
    q := false
    r := ""
    for _, c := range token {
        switch c {
        case '"':
            q = true
            r += "\""
        case ' ', '\t':
            q = true
        }
        r += string(c)
    }
    if q {
        r = "\"" + r + "\""
    }
    return r
}

func getPassword(prompt string) string {
    // Get the initial state of the terminal.
    initialTermState, e1 := terminal.GetState(syscall.Stdin)
    if e1 != nil {
        panic(e1)
    }

    // Restore it in the event of an interrupt.
    // CITATION: Konstantin Shaposhnikov - https://groups.google.com/forum/#!topic/golang-nuts/kTVAbtee9UA
    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt, os.Kill)
    go func() {
        <-c
        _ = terminal.Restore(syscall.Stdin, initialTermState)
        os.Exit(1)
    }()

    // Now get the password.
    fmt.Print(prompt)
    p, err := terminal.ReadPassword(syscall.Stdin)
    fmt.Println("")
    if err != nil {
        panic(err)
    }

    // Stop looking for ^C on the channel.
    signal.Stop(c)

    // Return the password as a string.
    return string(p)
}
它改变了一切,但输出仍然没有交错。这是输出 从测试运行开始

     5 9FqBZonjaaWDcXMm8biABker
    10 zMd1JTT3ZGR5mEuJOaJCo9AZ
     1 bPlNFGdSC2wd8f2QnFhk5A84
     2 H9H2FHFuvUs9Jz8UvBHv3Vc5
     3 wsp2nChCIwVQztA2n95rXrtz
     4 eDZ0tHBxFq6Pysq3N267L1vq
     6 DF2EsjYyTQWCfIuilZxV2FCn
     7 fGOILa0u1wXnEw1GDGuvdSew
     8 fj84Qyu6uRn8CTECWzT5s4ZJ
     9 KykqOn91fMwNqsk2Wrc5uhk2
    11 0p7opMMsnA87D6TSTAXY5NAC
    12 HYixe6pj0dHuKlxQyyNenUNQ
现在,stderr数据显示在开始处

更新#2:显示SSH也分隔了FD 在JimB最后一次评论之后,我决定在Mac和Linux主机上使用SSH运行实验,并使用gentest。请注意,SSH还分隔了输出,因此解决了此问题

终点站 SSH
请注意,最后两行(stderr)不是交错的。

根据@JimB的反馈和我在update#2中的实验,您必须在shell命令中指定
&code>或
2>&1来交错stderr和stdout。

在编写后尝试刷新stderr和stdout。它可能缓存在某个地方。谢谢,这听起来是个好主意,但是如何从ssh会话刷新stderr和stdout?我没有编写器,只有扫描仪中的多读取器处理的管道。你能提供一个代码片段吗?@JoeLinoff:来自“它们是按顺序读取的”。不做你想做的?谢谢你@JimB,我完全错过了。这当然解释了我所看到的,所以行为是正确的。Session.CombinedOutput可能会起作用。我会试试看。要真正获得与
2>&1
完全相同的行为,唯一的方法是让shell使用该重定向执行您的命令,或者让您的命令自己写入一个FD。即使没有ssh,stdout和stderr之间也永远不会有同步。输出将发送到2个单独的FD(其中stderr可能具有不同的缓冲或延迟),并通过2个单独的通道发送。
     5 9FqBZonjaaWDcXMm8biABker
    10 zMd1JTT3ZGR5mEuJOaJCo9AZ
     1 bPlNFGdSC2wd8f2QnFhk5A84
     2 H9H2FHFuvUs9Jz8UvBHv3Vc5
     3 wsp2nChCIwVQztA2n95rXrtz
     4 eDZ0tHBxFq6Pysq3N267L1vq
     6 DF2EsjYyTQWCfIuilZxV2FCn
     7 fGOILa0u1wXnEw1GDGuvdSew
     8 fj84Qyu6uRn8CTECWzT5s4ZJ
     9 KykqOn91fMwNqsk2Wrc5uhk2
    11 0p7opMMsnA87D6TSTAXY5NAC
    12 HYixe6pj0dHuKlxQyyNenUNQ
$ # Interleaved on the terminal.
$ /user/jlinoff/bin/gentestdata -l -i 5 -w 32 -n 12
     1 bPlNFGdSC2wd8f2QnFhk5A84
     2 H9H2FHFuvUs9Jz8UvBHv3Vc5
     3 wsp2nChCIwVQztA2n95rXrtz
     4 eDZ0tHBxFq6Pysq3N267L1vq
     5 9FqBZonjaaWDcXMm8biABker
     6 DF2EsjYyTQWCfIuilZxV2FCn
     7 fGOILa0u1wXnEw1GDGuvdSew
     8 fj84Qyu6uRn8CTECWzT5s4ZJ
     9 KykqOn91fMwNqsk2Wrc5uhk2
    10 zMd1JTT3ZGR5mEuJOaJCo9AZ
    11 0p7opMMsnA87D6TSTAXY5NAC
    12 HYixe6pj0dHuKlxQyyNenUNQ
$ ssh hqxsv-cmdev3-jlinoff /user/jlinoff/bin/gentestdata -l -i 5 -w 32 -n 12
     1 bPlNFGdSC2wd8f2QnFhk5A84
     2 H9H2FHFuvUs9Jz8UvBHv3Vc5
     3 wsp2nChCIwVQztA2n95rXrtz
     4 eDZ0tHBxFq6Pysq3N267L1vq
     6 DF2EsjYyTQWCfIuilZxV2FCn
     7 fGOILa0u1wXnEw1GDGuvdSew
     8 fj84Qyu6uRn8CTECWzT5s4ZJ
     9 KykqOn91fMwNqsk2Wrc5uhk2
    11 0p7opMMsnA87D6TSTAXY5NAC
    12 HYixe6pj0dHuKlxQyyNenUNQ
     5 9FqBZonjaaWDcXMm8biABker
    10 zMd1JTT3ZGR5mEuJOaJCo9AZ