goext/cmdext/cmdrunner.go

197 lines
4.8 KiB
Go
Raw Permalink Normal View History

2022-12-23 20:08:59 +01:00
package cmdext
import (
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
2023-01-31 22:41:12 +01:00
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/syncext"
2022-12-23 20:08:59 +01:00
"os/exec"
"time"
)
var ErrExitCode = errors.New("process exited with an unexpected exitcode")
var ErrTimeout = errors.New("process did not exit after the specified timeout")
2023-08-09 17:48:06 +02:00
var ErrStderrPrint = errors.New("process did print to stderr stream")
2022-12-23 20:08:59 +01:00
type CommandResult struct {
StdOut string
StdErr string
StdCombined string
ExitCode int
CommandTimedOut bool
}
2023-01-29 21:27:55 +01:00
func run(opt CommandRunner) (CommandResult, error) {
cmd := exec.Command(opt.program, opt.args...)
2023-01-29 22:07:28 +01:00
cmd.Env = append(cmd.Env, opt.env...)
2022-12-23 20:08:59 +01:00
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return CommandResult{}, err
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return CommandResult{}, err
}
2023-01-31 22:41:12 +01:00
preader := pipeReader{
lineBufferSize: langext.Ptr(128 * 1024 * 1024), // 128MB max size of a single line, is hopefully enough....
stdout: stdoutPipe,
stderr: stderrPipe,
2023-01-31 22:41:12 +01:00
}
2022-12-23 20:08:59 +01:00
err = cmd.Start()
if err != nil {
return CommandResult{}, err
}
2023-01-31 22:41:12 +01:00
type resultObj struct {
stdout string
stderr string
stdcombined string
err error
}
2023-01-30 19:55:55 +01:00
2023-08-09 17:48:06 +02:00
stderrFailChan := make(chan bool)
2023-01-31 22:41:12 +01:00
outputChan := make(chan resultObj)
2023-01-30 19:55:55 +01:00
go func() {
2023-01-31 22:41:12 +01:00
// we need to first fully read the pipes and then call Wait
// see https://pkg.go.dev/os/exec#Cmd.StdoutPipe
2023-01-30 19:55:55 +01:00
2023-08-09 17:48:06 +02:00
listener := make([]CommandListener, 0)
listener = append(listener, opt.listener...)
if opt.enforceNoStderr {
listener = append(listener, genericCommandListener{
_readRawStderr: langext.Ptr(func(v []byte) {
if len(v) > 0 {
stderrFailChan <- true
}
}),
})
}
stdout, stderr, stdcombined, err := preader.Read(listener)
2023-01-31 22:41:12 +01:00
if err != nil {
outputChan <- resultObj{stdout, stderr, stdcombined, err}
_ = cmd.Process.Kill()
return
2022-12-23 20:08:59 +01:00
}
2023-01-30 19:55:55 +01:00
2023-01-31 22:41:12 +01:00
err = cmd.Wait()
if err != nil {
outputChan <- resultObj{stdout, stderr, stdcombined, err}
} else {
outputChan <- resultObj{stdout, stderr, stdcombined, nil}
2022-12-23 20:08:59 +01:00
}
}()
2023-01-29 21:27:55 +01:00
var timeoutChan <-chan time.Time = make(chan time.Time, 1)
if opt.timeout != nil {
timeoutChan = time.After(*opt.timeout)
}
select {
2022-12-23 20:08:59 +01:00
2023-01-29 21:27:55 +01:00
case <-timeoutChan:
_ = cmd.Process.Kill()
2023-01-30 19:55:55 +01:00
for _, lstr := range opt.listener {
lstr.Timeout()
}
2023-01-31 22:41:12 +01:00
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
res := CommandResult{
2023-01-31 22:41:12 +01:00
StdOut: fallback.stdout,
StdErr: fallback.stderr,
StdCombined: fallback.stdcombined,
ExitCode: -1,
CommandTimedOut: true,
}
if opt.enforceNoTimeout {
return res, ErrTimeout
}
return res, nil
2023-01-31 22:41:12 +01:00
} else {
res := CommandResult{
2023-01-31 22:41:12 +01:00
StdOut: "",
StdErr: "",
StdCombined: "",
ExitCode: -1,
CommandTimedOut: true,
}
if opt.enforceNoTimeout {
return res, ErrTimeout
}
return res, nil
2023-01-31 22:41:12 +01:00
}
2023-08-09 17:48:06 +02:00
case <-stderrFailChan:
_ = cmd.Process.Kill()
if fallback, ok := syncext.ReadChannelWithTimeout(outputChan, 32*time.Millisecond); ok {
// most of the time the cmd.Process.Kill() should also have finished the pipereader
// and we can at least return the already collected stdout, stderr, etc
res := CommandResult{
StdOut: fallback.stdout,
StdErr: fallback.stderr,
StdCombined: fallback.stdcombined,
ExitCode: -1,
CommandTimedOut: false,
}
return res, ErrStderrPrint
} else {
res := CommandResult{
StdOut: "",
StdErr: "",
StdCombined: "",
ExitCode: -1,
CommandTimedOut: false,
}
return res, ErrStderrPrint
}
2023-01-31 22:41:12 +01:00
case outobj := <-outputChan:
var exiterr *exec.ExitError
if errors.As(outobj.err, &exiterr) {
2023-01-30 19:55:55 +01:00
excode := exiterr.ExitCode()
for _, lstr := range opt.listener {
lstr.Finished(excode)
}
res := CommandResult{
2023-01-31 22:41:12 +01:00
StdOut: outobj.stdout,
StdErr: outobj.stderr,
StdCombined: outobj.stdcombined,
2023-01-30 19:55:55 +01:00
ExitCode: excode,
2023-01-29 21:27:55 +01:00
CommandTimedOut: false,
}
if opt.enforceExitCodes != nil && !langext.InArray(excode, *opt.enforceExitCodes) {
return res, ErrExitCode
}
return res, nil
2023-01-29 21:27:55 +01:00
} else if err != nil {
return CommandResult{}, err
} else {
2023-01-30 19:55:55 +01:00
for _, lstr := range opt.listener {
lstr.Finished(0)
}
res := CommandResult{
2023-01-31 22:41:12 +01:00
StdOut: outobj.stdout,
StdErr: outobj.stderr,
StdCombined: outobj.stdcombined,
2023-01-29 21:27:55 +01:00
ExitCode: 0,
CommandTimedOut: false,
}
if opt.enforceExitCodes != nil && !langext.InArray(0, *opt.enforceExitCodes) {
return res, ErrExitCode
}
return res, nil
2022-12-23 20:08:59 +01:00
}
}
}