How to stream command outputs with a channel?
19:13 28 Apr 2020

I am trying to capture command output as it occurs so I can flush it as buffered data to an HTTP client but I am not using channels correctly and don't know how to fix it.

This is essentially how I'm implementing it. The output channel is never received and I don't get the command output. It must be something fairly obvious but I'm not seeing it.

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "os/exec"
)

func main() {
    output := make(chan []byte, 1024)
    done := make(chan struct{})

    cmd := exec.Command("echo", "hello")

    go execute(cmd, output, done)

    for {
        select {
        case data := <-output:
            fmt.Println("Got data")
            fmt.Println(string(data))
            //Flush to http.ResponseWriter
        case <-done:
            fmt.Println("Done")
            return
        }
    }
}

func execute(cmd *exec.Cmd, output chan []byte, done chan struct{}) {
    defer func() {
        done <- struct{}{}
    }()

    stdout, err := cmd.StdoutPipe()
    if err != nil {
        fmt.Println(err)
        output <- []byte(fmt.Sprintf("Error getting stdout pipe: %v", err))
        return
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        fmt.Println(err)
        output <- []byte(fmt.Sprintf("Error getting stderr pipe: %v", err))
        return
    }

    scanner := bufio.NewScanner(io.MultiReader(stdout, stderr))

    err = cmd.Start()
    if err != nil {
        fmt.Println(err)
        output <- []byte(fmt.Sprintf("Error executing: %v", err))
        return
    }

    go func() {
        for scanner.Scan() {
            fmt.Println(string(scanner.Bytes()))
            output <- scanner.Bytes()
            fmt.Println("Sent data")
        }
    }()

    err = cmd.Wait()
    if err != nil {
        fmt.Println(err)
        output <- []byte(fmt.Sprintf("Error waiting for the script to complete: %v", err))
        return
    }
}

EDIT:

After some thought, here is updated code. This working as I expect but still not sure if I'm doing this correctly. I removed the done channel and range over the output channel. This receives everything from the output as sent via the scanner in execute and won't block since output is closed when the command finished. This is not printing "hello" in Go Playground but I am getting expected behavior in my project.

Would still appreciate feedback.

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
    "os/exec"
)

func main() {
    output := make(chan []byte)

    cmd := exec.Command("echo", "hello")

    go execute(cmd, output)

    for data := range output {
        fmt.Println(string(data))
    }
}

func execute(cmd *exec.Cmd, output chan []byte) {
    defer close(output)

    stdout, err := cmd.StdoutPipe()
    if err != nil {
        fmt.Println(err)
        output <- []byte(fmt.Sprintf("Error getting stdout pipe: %v", err))
        return
    }
    cmd.Stderr = cmd.Stdout

    scanner := bufio.NewScanner(stdout)

    done := make(chan struct{})

    err = cmd.Start()
    if err != nil {
        output <- []byte(fmt.Sprintf("Error executing: %v", err))
        return
    }

    go func() {
        for scanner.Scan() {
            output <- scanner.Bytes()
        }
        done <- struct{}{}
    }()

    <-done

    err = cmd.Wait()
    if err != nil {
        fmt.Println(err)
        output <- []byte(fmt.Sprintf("Error waiting for the script to complete: %v", err))
    }
}
go concurrency