简体   繁体   English

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

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

Can anyone help ?任何人都可以帮忙吗?

I have an application I am running via exec.CommandContext (so I can cancel it via ctx).我有一个通过 exec.CommandContext 运行的应用程序(所以我可以通过 ctx 取消它)。 it would normally not stop unless it errors out.它通常不会停止,除非它出错。

I currently have it relaying its output to os.stdOut which is working great.我目前将其输出中继到 os.stdOut ,效果很好。 But I also want to get each line via a channel - the idea behind this is I will look for a regular expression on the line and if its true then I will set an internal state of "ERROR" for example.但我也想通过通道获取每一行 - 这背后的想法是我将在该行上查找正则表达式,如果它为真,那么我将设置一个“错误”的内部状态,例如。

Although I can't get it to work, I tried NewSscanner.虽然我无法让它工作,但我尝试了 NewSscanner。 Here is my code.这是我的代码。

As I say, it does output to os.StdOut which is great but I would like to receive each line as it happens in my channel I setup.正如我所说,它确实输出到 os.StdOut,这很棒,但我希望在我设置的频道中接收每一行。

Any ideas ?有任何想法吗 ?

Thanks in advance.提前致谢。

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!")
        }
    }()
}

Also tried using this也尝试使用这个

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

Even with that, the "I am reading a line" never gets out, I also debugged it and it neve enters the "for scanner.."即便如此,“我正在阅读一行”永远不会出来,我也调试了它,它从来没有进入“扫描仪..”

Also tried scanning on &stderrBuf , same, nothing enters.还尝试扫描&stderrBuf ,同样,没有进入。

cmd.Start() does not wait for the command to finish. cmd.Start()不会等待命令完成。 Also, cmd.Wait() needs to be called to be informed about the end of the process.此外,需要调用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 is added to demonstrate that the scanner goroutine stops now.增加了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

And handle the lines as it helps.并在有帮助时处理线条。

Note: wrote this in a bit of hurry.注意:写这个有点匆忙。 Let me know if anything is unclear or not correct.如果有任何不清楚或不正确的地方,请告诉我。

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

scanner := bufio.NewScanner(&stdoutBuf)

The command is terminated when the context canceled.当上下文取消时,命令终止。 If it's OK to read all output from the command until the command is terminated, then use this code:如果在命令终止之前可以读取命令的所有输出,请使用以下代码:

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
        }
    }()
}

The command is terminated when the context is canceled.当上下文被取消时,命令终止。
Read on stdout returns io.EOF when the command is terminated.当命令终止时,读取stdout返回 io.EOF。 The goroutine breaks out of the scan loop when stdout returns an error.stdout返回错误时,goroutine 退出扫描循环。

For a correct program using concurrency and goroutines, we should at least ensure there are no data races, the program can't deadlock, and goroutines don't leak.对于一个正确的使用并发和goroutines的程序,我们至少应该确保没有数据竞争,程序不能死锁,goroutines不会泄漏。

Full code完整代码

Playground: https://play.golang.org/p/VDW5rj1DltG .游乐场: https : //play.golang.org/p/VDW5rj1DltG I recommend copying and running locally, because the playground doesn't stream output afaik and it has timeouts.我建议在本地复制和运行,因为操场不会流输出 afaik 并且它有超时。

Note that I've changed the test command to % find /usr/local , a typically long-running command (>3 seconds) with plenty of output lines, since it is better suited for the scenarios we should test.请注意,我已将测试命令更改为% find /usr/local ,这是一个典型的长时间运行命令(> 3 秒),具有大量输出行,因为它更适合我们应该测试的场景。

Walkthrough演练

Let's look at the Daemon.Start method.让我们看看Daemon.Start方法。 Most noticeably, the new code don't use a goroutine around a large part of the Daemon.Start code.最值得注意的是,新代码没有在Daemon.Start代码的大部分周围使用 goroutine。 Even without this, the Daemon.Start method remains non-blocking and will return immediately.即使没有这个, Daemon.Start方法仍然是非阻塞的并且会立即返回。


The first noteworthy fix is these updated lines.第一个值得注意的修复是这些更新的行。

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

Instead of constructing a bytes.Buffer variable, we call io.Pipe .我们没有构造 bytes.Buffer 变量,而是调用io.Pipe If we didn't make this change and stuck with a bytes.Buffer, then scanner.Scan() will return false as soon as there is no more data to read.如果我们没有进行此更改并坚持使用 bytes.Buffer,那么一旦没有更多数据可读取,scanner.Scan scanner.Scan()将返回 false。 This can happen if the command writes to stdout only occasionally (even a millisecond apart, for this matter).如果命令只是偶尔写入 stdout(对于这个问题,即使相隔一毫秒),就会发生这种情况。 After scanner.Scan() returns false, the goroutine exits and we miss processing future output.scanner.Scan()返回false 后,goroutine 退出并且我们错过了处理未来的输出。

By using the read end of io.Pipe , scanner.Scan() will wait for input from the pipe's read end until the pipe's write end is closed.通过使用io.Pipe的读取端, io.Pipe scanner.Scan()将等待来自管道读取端的输入,直到管道的写入端关闭。

This fixes the race issue between the scanner and the command output.这解决了扫描仪和命令输出之间的竞争问题。


Next, we construct two closely-related goroutines: the first to consume from <-lines , and the second to produce into lines<- .接下来,我们构造两个密切相关的 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()
        }
        ...
    }()

The consumer goroutine will exit when the lines channel is closed, as the closing of the channel would naturally cause the range loop to terminate;lines channel关闭时consumer goroutine会退出,因为channel的关闭自然会导致range loop终止; the producer goroutine closes lines upon exit.生产者 goroutine 在退出时关闭lines

The producer goroutine will exit when scanner.Scan() returns false, which happens when the write end of the io.Pipe is closed.scanner.Scan()返回false 时,生产者goroutine 将退出,这发生在io.Pipe的写端关闭时。 This closing happens in upcoming code.这种关闭发生在即将到来的代码中。

Note from the two paragraphs above that the two goroutines are guaranteed to exit (ie will not leak).注意上面两段,两个goroutines保证退出(即不会泄漏)。


Next, we start the command.接下来,我们启动命令。 Standard stuff, it's a non-blocking call, and it returns immediately.标准的东西,它是一个非阻塞调用,它立即返回。

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

Moving on to the final piece of code in Daemon.Start .转到Daemon.Start的最后一段代码。 This goroutine waits for the command to exit via cmd.Wait() .这个 goroutine 通过cmd.Wait()等待命令退出。 Handling this is important because the command may for reasons other than Context cancellation.处理这一点很重要,因为该命令可能出于上下文取消以外的原因。

Particularly, we want to close the write end of the io.Pipe (which, in turn, closes the output lines producer goroutine as mentioned earlier).特别是,我们想要关闭io.Pipe的写端(反过来, io.Pipe ,它会关闭输出行生产者 goroutine)。

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

This goroutine, too, is guaranteed to exit.这个 goroutine 也保证退出。 It will exit when cmd.Wait() returns.cmd.Wait()返回时它将退出。 This can happen either because the command exited normally with success;这可能是因为命令成功正常退出; exited with failure due to a command error;由于命令错误而失败退出; or exited with failure due to Context cancelation.或由于上下文取消而失败退出。


That's it!就是这样! We should have no data races, no deadlocks, and no leaked goroutines.我们应该没有数据竞争,没有死锁,也没有泄露的 goroutine。

The lines elided (" ... ") in the snippets above are geared towards the Done() , CmdErr() , and Cancel() methods of the Daemon type.上面代码片段中省略的行 (" ... ") 适用于 Daemon 类型的Done()CmdErr()Cancel()方法。 These methods are fairly well-documented in the code, so these elided lines are hopefully self-explanatory.这些方法在代码中得到了很好的记录,因此这些省略的行希望是不言自明的。

Besides that, look for the TODO comments for error handling you may want to do based on your needs!除此之外,根据您的需要查找您可能想要做的错误处理的TODO注释!

Test it测试一下

Use this driver program to test the code.使用此驱动程序来测试代码。

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