package cmdext import ( "gogs.mikescher.com/BlackForestBytes/goext/mathext" "gogs.mikescher.com/BlackForestBytes/goext/syncext" "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 } preader := pipeReader{ stdout: stdoutPipe, stderr: stderrPipe, } err = cmd.Start() if err != nil { return CommandResult{}, err } type resultObj struct { stdout string stderr string stdcombined string err error } outputChan := make(chan resultObj) go func() { // we need to first fully read the pipes and then call Wait // see https://pkg.go.dev/os/exec#Cmd.StdoutPipe stdout, stderr, stdcombined, err := preader.Read(opt.listener) if err != nil { outputChan <- resultObj{stdout, stderr, stdcombined, err} } err = cmd.Wait() if err != nil { outputChan <- resultObj{stdout, stderr, stdcombined, err} } outputChan <- resultObj{stdout, stderr, stdcombined, nil} }() 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() } if fallback, ok := syncext.ReadChannelWithTimeout(outputChan, mathext.Min(32*time.Millisecond, *opt.timeout)); ok { // most of the time the cmd.Process.Kill() should also ahve finished the pipereader // and we can at least return the already collected stdout, stderr, etc return CommandResult{ StdOut: fallback.stdout, StdErr: fallback.stderr, StdCombined: fallback.stdcombined, ExitCode: -1, CommandTimedOut: true, }, nil } else { return CommandResult{ StdOut: "", StdErr: "", StdCombined: "", ExitCode: -1, CommandTimedOut: true, }, nil } case outobj := <-outputChan: if exiterr, ok := outobj.err.(*exec.ExitError); ok { excode := exiterr.ExitCode() for _, lstr := range opt.listener { lstr.Finished(excode) } return CommandResult{ StdOut: outobj.stdout, StdErr: outobj.stderr, StdCombined: outobj.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: outobj.stdout, StdErr: outobj.stderr, StdCombined: outobj.stdcombined, ExitCode: 0, CommandTimedOut: false, }, nil } } }