2022-12-23 20:08:59 +01:00
|
|
|
package cmdext
|
|
|
|
|
|
|
|
import (
|
2023-02-09 16:49:33 +01:00
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2023-02-09 16:49:33 +01:00
|
|
|
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")
|
2023-02-09 16:49:33 +01:00
|
|
|
|
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{
|
2023-02-13 01:41:33 +01:00
|
|
|
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}
|
2023-02-13 01:41:33 +01:00
|
|
|
_ = 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}
|
2023-02-13 01:41:33 +01:00
|
|
|
} 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
|
2023-02-09 16:49:33 +01:00
|
|
|
res := CommandResult{
|
2023-01-31 22:41:12 +01:00
|
|
|
StdOut: fallback.stdout,
|
|
|
|
StdErr: fallback.stderr,
|
|
|
|
StdCombined: fallback.stdcombined,
|
|
|
|
ExitCode: -1,
|
|
|
|
CommandTimedOut: true,
|
2023-02-09 16:49:33 +01:00
|
|
|
}
|
|
|
|
if opt.enforceNoTimeout {
|
|
|
|
return res, ErrTimeout
|
|
|
|
}
|
|
|
|
return res, nil
|
2023-01-31 22:41:12 +01:00
|
|
|
} else {
|
2023-02-09 16:49:33 +01:00
|
|
|
res := CommandResult{
|
2023-01-31 22:41:12 +01:00
|
|
|
StdOut: "",
|
|
|
|
StdErr: "",
|
|
|
|
StdCombined: "",
|
|
|
|
ExitCode: -1,
|
|
|
|
CommandTimedOut: true,
|
2023-02-09 16:49:33 +01:00
|
|
|
}
|
|
|
|
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:
|
2023-09-25 18:02:48 +02:00
|
|
|
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)
|
|
|
|
}
|
2023-02-09 16:49:33 +01:00
|
|
|
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,
|
2023-02-09 16:49:33 +01:00
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
2023-02-09 16:49:33 +01:00
|
|
|
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,
|
2023-02-09 16:49:33 +01:00
|
|
|
}
|
|
|
|
if opt.enforceExitCodes != nil && !langext.InArray(0, *opt.enforceExitCodes) {
|
|
|
|
return res, ErrExitCode
|
|
|
|
}
|
|
|
|
return res, nil
|
2022-12-23 20:08:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|