【问题标题】:Reliably capture the output of an external command可靠地捕获外部命令的输出
【发布时间】:2020-02-19 12:37:58
【问题描述】:

我需要快速连续调用许多短期(有时是一些长期)外部进程,并实时处理stdoutstderr。我使用StdoutPipeStderrPipe 找到了许多解决方案,每个都有bufio.Scanner,打包到goroutines 中。这在大多数情况下都有效,但偶尔会吞下外部命令的输出,我不知道为什么。

这是一个在 MacOS X (Mojave) 和 Linux 上显示该行为的最小示例:

package main

import (
    "bufio"
    "log"
    "os/exec"
    "sync"
)

func main() {
    for i := 0; i < 50000; i++ {
        log.Println("Loop")

        var wg sync.WaitGroup

        cmd := exec.Command("echo", "1")
        stdout, err := cmd.StdoutPipe()
        if err != nil {
            panic(err)
        }

        cmd.Start()

        stdoutScanner := bufio.NewScanner(stdout)
        stdoutScanner.Split(bufio.ScanLines)

        wg.Add(1)
        go func() {
            for stdoutScanner.Scan() {
                line := stdoutScanner.Text()
                log.Printf("[stdout] %s\n", line)
            }
            wg.Done()
        }()

        cmd.Wait()
        wg.Wait()
    }
}

我已经省略了 stderr 对此的处理。运行它时,我只得到大约 49,900 [stdout] 1 行(实际数量随每次运行而变化),但应该有 50,000。我看到了 50,000 条 loop 行,所以它似乎不会过早死亡。这闻起来像是某处的比赛条件,但我不知道在哪里。

如果我不将扫描循环放在 goroutine 中,它工作得很好,但是我失去了同时读取 stderr 的能力,这是我需要的。

我已尝试使用 -race 运行此程序,Go 报告没有数据竞争。

我没有想法,我做错了什么?

【问题讨论】:

    标签: go


    【解决方案1】:

    您没有在多个地方检查错误。

    在某些情况下,这实际上并没有引起问题,但检查一下仍然是个好主意:

    cmd.Start()
    

    可能会返回错误,在这种情况下该命令从未运行。 (这不是实际问题。)

    stdoutScanner.Scan() 返回 false 时,stdoutScanner.Err() 可能会显示错误。如果你开始检查这个,你会发现一些错误:

    2020/02/19 15:38:17 [stdout err] read |0: file already closed
    

    这不是真正的问题,但是——啊哈——这符合您看到的症状:并非所有输出都被看到。现在,为什么阅读stdout 会声称文件已关闭?好吧,stdout 是从哪里来的?它来自这里:

    stdout, err := cmd.StdoutPipe()
    

    看看source code for this function,它以这些行结尾:

    c.closeAfterStart = append(c.closeAfterStart, pw)
    c.closeAfterWait = append(c.closeAfterWait, pr)
    return pr, nil
    

    (而pr 是管道读取的返回值)。嗯:closeAfterWait 是什么意思?

    现在,这是循环中的最后两行:

    cmd.Wait()
    wg.Wait()
    

    也就是说,首先我们等待cmd 完成。 (当cmd 完成时,什么会关闭?)然后我们等待读取cmd 的标准输出的goroutine 完成。 (嗯,pr 管道还能读取什么?)

    修复现在很明显:将等待 stdout 管道的使用者完成读取的 wg.Wait() 与等待 echo ... 退出然后关闭读取结束的 cmd.Wait() 交换管道。如果您在读者仍在阅读时关闭,他们可能永远不会阅读您所期望的内容。

    【讨论】:

    • 非常感谢!这很有见地。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-01
    • 2016-12-30
    • 2017-02-26
    • 2013-01-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多