可靠地捕获外部命令的输出

1fl*_*flx 2 go

我需要快速连续地调用许多短期(偶尔也有一些长期存在)的外部进程,stdoutstderr实时处理。我已经找到了许多解决方案,StdoutPipe并将StderrPipebufio.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()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我试过用 运行它-race,Go 报告没有数据竞争。

我没有想法,我出了什么问题?

tor*_*rek 5

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

在某些情况下,这实际上并不会导致问题,但检查以下内容仍然是一个好主意:

cmd.Start()
Run Code Online (Sandbox Code Playgroud)

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

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

2020/02/19 15:38:17 [stdout err] read |0: file already closed
Run Code Online (Sandbox Code Playgroud)

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

stdout, err := cmd.StdoutPipe()
Run Code Online (Sandbox Code Playgroud)

看看这个函数源代码,它以以下几行结尾:

c.closeAfterStart = append(c.closeAfterStart, pw)
c.closeAfterWait = append(c.closeAfterWait, pr)
return pr, nil
Run Code Online (Sandbox Code Playgroud)

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

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

cmd.Wait()
wg.Wait()
Run Code Online (Sandbox Code Playgroud)

也就是说,首先我们等待cmd完成。(cmd完成后,什么会关闭?)然后我们等待正在读取cmdstdout的 goroutine完成。(嗯,还有什么可以从pr管道中读取?)

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