从 Go 中的 StdoutPipe() 读取冻结

Reading from a StdoutPipe() in Go freezes

我正在尝试从命令的 Stdout 读取,但每(大约)50 次它就会冻结一次。

func runProcess(process *exec.Cmd) (string, string, error) {
    var stdout strings.Builder
    var stderr string
    process := exec.Command(programPath, params...)

    go func() {
        pipe, err := process.StderrPipe()
        if err != nil {
            return
        }

        buf, err := io.ReadAll(pipe)
        if err != nil {
            log.Warn("Error reading stderr: %v", err)
        }
        stderr = string(buf)
    }()

    pipe, err := process.StdoutPipe()
    if err = process.Start(); err != nil {
        return "", "", err
    }

    buf := make([]byte, 1024)
    read, err := pipe.Read(buf)            // it reads correctly from the pipe
    for err == nil && read > 0 {
        _, err = stdout.Write(buf[:read])
        read, err = pipe.Read(buf)         // this is where is stalls
    }
    if err = process.Wait(); err != nil {
        return stdout.String(), stderr, err
    }

    return stdout.String(), stderr, nil
}

我尝试使用 stdout, err := io.ReadAll(pipe) 一次读取所有内容而不是读取块,但我得到了相同的行为。 调用的程序似乎已成功执行。它的日志文件已创建并且已完成。另外,当我第一次从管道读取时(在循环之前),所有的输出都在那里。但是在循环内部,当第二次调用 .Read() 并且它应该 return 一个 EOF(输出小于 1024 字节)时,它会冻结。

这段代码中有很多竞争条件。一般来说,如果你创建一个 goroutine,应该有某种同步——比如 chansync.Mutexsync.WaitGroup 或 atomic.

修复竞争条件。

  1. 在调用 Start() 之前调用 StderrPipe()。代码不会这样做。

  2. 等待 goroutine 完成后再返回。

竞争条件可能会破坏 exec.Cmd 结构...这可能意味着它泄漏了管道,这可以解释为什么 Read() 挂起(因为管道的写入端不是已关闭)。

根据经验,始终修复竞争条件。将它们视为高优先级错误。

下面是如何在没有竞争条件的情况下编写它的草图:

func runProcess(process *exec.Cmd) (stdout, stderr string, err error) {
    outPipe, err := process.StdoutPipe()
    if err != nil {
        return "", "", err
    }

    // Call StderrPipe BEFORE Start().
    // Easy way to do it: outside the goroutine.
    errPipe, err := process.StderrPipe()
    if err != nil {
        return "", "", err
    }

    // Start process.
    if err := process.Start(); err != nil {
        return "", "", err
    }

    // Read stderr in goroutine.
    var wg sync.WaitGroup
    var stderrErr error
    wg.Add(1)
    go func() {
        defer wg.Done()
        data, err := ioutil.ReadAll(errPipe)
        if err != nil {
            stderrErr = err
        } else {
            stderr = string(data)
        }
    }()

    // Read stdout in main thread.
    data, stdoutErr := ioutil.ReadAll(outPipe)

    // Wait until we are done reading stderr.
    wg.Wait()

    // Wait for process to finish.
    if err := process.Wait(); err != nil {
        return "", "", err
    }

    // Handle error from reading stdout.
    if stdoutErr != nil {
        return "", "", stderrErr
    }
    // Handle error from reading stderr.
    if stderrErr != nil {
        return "", "", stderrErr
    }

    stdout = string(data)
    return stdout, stderr, nil
}

更简单的代码

所有这些都是由 os/exec 包自动完成的。 StdoutStderr可以使用任何io.Writer,不限于*os.File.

func runProcess(process *exec.Cmd) (stdout, stderr string, err error) {
    var stdoutbuf, stderrbuf bytes.Buffer
    process.Stdout = &stdoutbuf
    process.Stderr = &stderrbuf
    if err := process.Run(); err != nil {
        return "", "", err
    }
    return stdoutbuf.String(), stderrbuf.String(), nil
}