package cmdext import ( "bufio" "io" "os/exec" "time" ) type CommandResult struct { StdOut string StdErr string StdCombined string ExitCode int CommandTimedOut bool } func run(opt CommandRunner) (CommandResult, error) { cmd := exec.Command(opt.program, opt.args...) cmd.Env = append(cmd.Env, opt.env...) stdoutPipe, err := cmd.StdoutPipe() if err != nil { return CommandResult{}, err } stderrPipe, err := cmd.StderrPipe() if err != nil { return CommandResult{}, err } err = cmd.Start() if err != nil { return CommandResult{}, err } errch := make(chan error, 3) go func() { errch <- cmd.Wait() }() // [1] read raw stdout stdoutBufferReader, stdoutBufferWriter := io.Pipe() stdout := "" go func() { buf := make([]byte, 128) for true { n, out := stdoutPipe.Read(buf) if n > 0 { txt := string(buf[:n]) stdout += txt _, _ = stdoutBufferWriter.Write(buf[:n]) for _, lstr := range opt.listener { lstr.ReadRawStdout(buf[:n]) } } if out == io.EOF { break } if out != nil { errch <- out _ = cmd.Process.Kill() break } } _ = stdoutBufferWriter.Close() }() // [2] read raw stderr stderrBufferReader, stderrBufferWriter := io.Pipe() stderr := "" go func() { buf := make([]byte, 128) for true { n, err := stderrPipe.Read(buf) if n > 0 { txt := string(buf[:n]) stderr += txt _, _ = stderrBufferWriter.Write(buf[:n]) for _, lstr := range opt.listener { lstr.ReadRawStderr(buf[:n]) } } if err == io.EOF { break } if err != nil { errch <- err _ = cmd.Process.Kill() break } } _ = stderrBufferWriter.Close() }() combch := make(chan string, 32) stopCombch := make(chan bool) // [3] collect stdout line-by-line go func() { scanner := bufio.NewScanner(stdoutBufferReader) for scanner.Scan() { txt := scanner.Text() for _, lstr := range opt.listener { lstr.ReadStdoutLine(txt) } combch <- txt } }() // [4] collect stderr line-by-line go func() { scanner := bufio.NewScanner(stderrBufferReader) for scanner.Scan() { txt := scanner.Text() for _, lstr := range opt.listener { lstr.ReadStderrLine(txt) } combch <- txt } }() defer func() { stopCombch <- true }() // [5] combine stdcombined stdcombined := "" go func() { for { select { case txt := <-combch: stdcombined += txt + "\n" // this comes from bufio.Scanner and has no newlines... case <-stopCombch: return } } }() // [6] run var timeoutChan <-chan time.Time = make(chan time.Time, 1) if opt.timeout != nil { timeoutChan = time.After(*opt.timeout) } select { case <-timeoutChan: _ = cmd.Process.Kill() for _, lstr := range opt.listener { lstr.Timeout() } return CommandResult{ StdOut: stdout, StdErr: stderr, StdCombined: stdcombined, ExitCode: -1, CommandTimedOut: true, }, nil case err := <-errch: if exiterr, ok := err.(*exec.ExitError); ok { excode := exiterr.ExitCode() for _, lstr := range opt.listener { lstr.Finished(excode) } return CommandResult{ StdOut: stdout, StdErr: stderr, StdCombined: stdcombined, ExitCode: excode, CommandTimedOut: false, }, nil } else if err != nil { return CommandResult{}, err } else { for _, lstr := range opt.listener { lstr.Finished(0) } return CommandResult{ StdOut: stdout, StdErr: stderr, StdCombined: stdcombined, ExitCode: 0, CommandTimedOut: false, }, nil } } }