繁体   English   中英

从 exec.Command 逐行捕获 stdout 并通过管道传输到 os.Stdout

[英]Capture stdout from exec.Command line by line and also pipe to os.Stdout

任何人都可以帮忙吗?

我有一个通过 exec.CommandContext 运行的应用程序(所以我可以通过 ctx 取消它)。 它通常不会停止,除非它出错。

我目前将其输出中继到 os.stdOut ,效果很好。 但我也想通过通道获取每一行 - 这背后的想法是我将在该行上查找正则表达式,如果它为真,那么我将设置一个“错误”的内部状态,例如。

虽然我无法让它工作,但我尝试了 NewSscanner。 这是我的代码。

正如我所说,它确实输出到 os.StdOut,这很棒,但我希望在我设置的频道中接收每一行。

有任何想法吗 ?

提前致谢。

func (d *Daemon) Start() {
    ctx, cancel := context.WithCancel(context.Background())
    d.cancel = cancel

    go func() {
        args := "-x f -a 1"
        cmd := exec.CommandContext(ctx, "mydaemon", strings.Split(args, " ")...)

        var stdoutBuf, stderrBuf bytes.Buffer

        cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
        cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)

        lines := make(chan string)

        go func() {
            scanner := bufio.NewScanner(os.Stdin)
            for scanner.Scan() {
                fmt.Println("I am reading a line!")
                lines <- scanner.Text()
            }
        }()

        err := cmd.Start()
        if err != nil {
            log.Fatal(err)
        }

        select {
        case outputx := <-lines:
            // I will do somethign with this!
            fmt.Println("Hello!!", outputx)

        case <-ctx.Done():
            log.Println("I am done!, probably cancelled!")
        }
    }()
}

也尝试使用这个

        go func() {
            scanner := bufio.NewScanner(&stdoutBuf)
            for scanner.Scan() {
                fmt.Println("I am reading a line!")
                lines <- scanner.Text()
            }
        }()

即便如此,“我正在阅读一行”永远不会出来,我也调试了它,它从来没有进入“扫描仪..”

还尝试扫描&stderrBuf ,同样,没有进入。

cmd.Start()不会等待命令完成。 此外,需要调用cmd.Wait()以获知进程结束。

reader, writer := io.Pipe()

cmdCtx, cmdDone := context.WithCancel(context.Background())

scannerStopped := make(chan struct{})
go func() {
    defer close(scannerStopped)

    scanner := bufio.NewScanner(reader)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}()

cmd := exec.Command("ls")
cmd.Stdout = writer
_ = cmd.Start()
go func() {
    _ = cmd.Wait()
    cmdDone()
    writer.Close()
}()
<-cmdCtx.Done()

<-scannerStopped

增加了scannerStopped来演示scanner goroutine 现在停止了。

reader, writer := io.Pipe()

scannerStopped := make(chan struct{})
go func() {
    defer close(scannerStopped)

    scanner := bufio.NewScanner(reader)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}()

cmd := exec.Command("ls")
cmd.Stdout = writer
_ = cmd.Run()

go func() {
    _ = cmd.Wait()
    writer.Close()
}()

<-scannerStopped

并在有帮助时处理线条。

注意:写这个有点匆忙。 如果有任何不清楚或不正确的地方,请告诉我。

你必须扫描stdoutBuf而不是os.Stdin

scanner := bufio.NewScanner(&stdoutBuf)

当上下文取消时,命令终止。 如果在命令终止之前可以读取命令的所有输出,请使用以下代码:

func (d *Daemon) Start() {
    ctx, cancel := context.WithCancel(context.Background())
    d.cancel = cancel

    args := "-x f -a 1"
    cmd := exec.CommandContext(ctx, "mydaemon", strings.Split(args, " ")...)
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }
    err = cmd.Start()
    if err != nil {
        log.Fatal(err)
    }

    go func() {
        defer cmd.Wait()
        scanner := bufio.NewScanner(stdout)
        for scanner.Scan() {
            s := scanner.Text()
            fmt.Println(s) // echo to stdout
            // Do something with s
        }
    }()
}

当上下文被取消时,命令终止。
当命令终止时,读取stdout返回 io.EOF。 stdout返回错误时,goroutine 退出扫描循环。

对于一个正确的使用并发和goroutines的程序,我们至少应该确保没有数据竞争,程序不能死锁,goroutines不会泄漏。

完整代码

游乐场: https : //play.golang.org/p/VDW5rj1DltG 我建议在本地复制和运行,因为操场不会流输出 afaik 并且它有超时。

请注意,我已将测试命令更改为% find /usr/local ,这是一个典型的长时间运行命令(> 3 秒),具有大量输出行,因为它更适合我们应该测试的场景。

演练

让我们看看Daemon.Start方法。 最值得注意的是,新代码没有在Daemon.Start代码的大部分周围使用 goroutine。 即使没有这个, Daemon.Start方法仍然是非阻塞的并且会立即返回。


第一个值得注意的修复是这些更新的行。

    outR, outW := io.Pipe()
    cmd.Stdout = io.MultiWriter(outW, os.Stdout)

我们没有构造 bytes.Buffer 变量,而是调用io.Pipe 如果我们没有进行此更改并坚持使用 bytes.Buffer,那么一旦没有更多数据可读取,scanner.Scan scanner.Scan()将返回 false。 如果命令只是偶尔写入 stdout(对于这个问题,即使相隔一毫秒),就会发生这种情况。 scanner.Scan()返回false 后,goroutine 退出并且我们错过了处理未来的输出。

通过使用io.Pipe的读取端, io.Pipe scanner.Scan()将等待来自管道读取端的输入,直到管道的写入端关闭。

这解决了扫描仪和命令输出之间的竞争问题。


接下来,我们构造两个密切相关的 goroutine:第一个从<-lines消费,第二个从生产到lines<-

    go func() {
        for line := range lines {
            fmt.Println("output line from channel:", line)
            ...
        }
    }()
    go func() {
        defer close(lines)
        scanner := bufio.NewScanner(outR)
        for scanner.Scan() {
            lines <- scanner.Text()
        }
        ...
    }()

lines channel关闭时consumer goroutine会退出,因为channel的关闭自然会导致range loop终止; 生产者 goroutine 在退出时关闭lines

scanner.Scan()返回false 时,生产者goroutine 将退出,这发生在io.Pipe的写端关闭时。 这种关闭发生在即将到来的代码中。

注意上面两段,两个goroutines保证退出(即不会泄漏)。


接下来,我们启动命令。 标准的东西,它是一个非阻塞调用,它立即返回。

// Start the command.
if err := cmd.Start(); err != nil {
    log.Fatal(err)
}

转到Daemon.Start的最后一段代码。 这个 goroutine 通过cmd.Wait()等待命令退出。 处理这一点很重要,因为该命令可能出于上下文取消以外的原因。

特别是,我们想要关闭io.Pipe的写端(反过来, io.Pipe ,它会关闭输出行生产者 goroutine)。

    go func() {
        err := cmd.Wait()
        fmt.Println("command exited; error is:", err)
        outW.Close()
        ...
    }()

这个 goroutine 也保证退出。 cmd.Wait()返回时它将退出。 这可能是因为命令成功正常退出; 由于命令错误而失败退出; 或由于上下文取消而失败退出。


就是这样! 我们应该没有数据竞争,没有死锁,也没有泄露的 goroutine。

上面代码片段中省略的行 (" ... ") 适用于 Daemon 类型的Done()CmdErr()Cancel()方法。 这些方法在代码中得到了很好的记录,因此这些省略的行希望是不言自明的。

除此之外,根据您的需要查找您可能想要做的错误处理的TODO注释!

测试一下

使用此驱动程序来测试代码。

func main() {
    var d Daemon
    d.Start()

    // Enable this code to test Context cancellation:
    // time.AfterFunc(100*time.Millisecond, d.Cancel)

    <-d.Done()
    fmt.Println("d.CmdErr():", d.CmdErr())
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM