130 lines
2.9 KiB
Go
130 lines
2.9 KiB
Go
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
|
|
}
|
|
}
|
|
}
|