Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
56094b3cb6
|
|||
0da098e9f9
|
|||
f0881c9fd6
|
|||
029b408749
|
@@ -14,6 +14,7 @@ type CommandRunner struct {
|
||||
listener []CommandListener
|
||||
enforceExitCodes *[]int
|
||||
enforceNoTimeout bool
|
||||
enforceNoStderr bool
|
||||
}
|
||||
|
||||
func Runner(program string) *CommandRunner {
|
||||
@@ -25,6 +26,7 @@ func Runner(program string) *CommandRunner {
|
||||
listener: make([]CommandListener, 0),
|
||||
enforceExitCodes: nil,
|
||||
enforceNoTimeout: false,
|
||||
enforceNoStderr: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +75,11 @@ func (r *CommandRunner) FailOnTimeout() *CommandRunner {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CommandRunner) FailOnStderr() *CommandRunner {
|
||||
r.enforceNoStderr = true
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CommandRunner) Listen(lstr CommandListener) *CommandRunner {
|
||||
r.listener = append(r.listener, lstr)
|
||||
return r
|
||||
|
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
var ErrExitCode = errors.New("process exited with an unexpected exitcode")
|
||||
var ErrTimeout = errors.New("process did not exit after the specified timeout")
|
||||
var ErrStderrPrint = errors.New("process did print to stderr stream")
|
||||
|
||||
type CommandResult struct {
|
||||
StdOut string
|
||||
@@ -53,12 +54,27 @@ func run(opt CommandRunner) (CommandResult, error) {
|
||||
err error
|
||||
}
|
||||
|
||||
stderrFailChan := make(chan bool)
|
||||
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
outputChan <- resultObj{stdout, stderr, stdcombined, err}
|
||||
_ = cmd.Process.Kill()
|
||||
@@ -115,6 +131,34 @@ func run(opt CommandRunner) (CommandResult, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
case <-stderrFailChan:
|
||||
_ = cmd.Process.Kill()
|
||||
for _, lstr := range opt.listener {
|
||||
lstr.Timeout()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
case outobj := <-outputChan:
|
||||
if exiterr, ok := outobj.err.(*exec.ExitError); ok {
|
||||
excode := exiterr.ExitCode()
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package cmdext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -289,16 +290,40 @@ func TestLongStdout(t *testing.T) {
|
||||
func TestFailOnTimeout(t *testing.T) {
|
||||
|
||||
_, err := Runner("sleep").Arg("2").Timeout(200 * time.Millisecond).FailOnTimeout().Run()
|
||||
if err != ErrTimeout {
|
||||
if !errors.Is(err, ErrTimeout) {
|
||||
t.Errorf("wrong err := %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFailOnStderr(t *testing.T) {
|
||||
|
||||
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"error\", file=sys.stderr, end='')").FailOnStderr().Run()
|
||||
if err == nil {
|
||||
t.Errorf("no err")
|
||||
}
|
||||
if res1.CommandTimedOut {
|
||||
t.Errorf("Timeout")
|
||||
}
|
||||
if res1.ExitCode != -1 {
|
||||
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
|
||||
}
|
||||
if res1.StdErr != "error" {
|
||||
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
|
||||
}
|
||||
if res1.StdOut != "" {
|
||||
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
|
||||
}
|
||||
if res1.StdCombined != "error\n" {
|
||||
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFailOnExitcode(t *testing.T) {
|
||||
|
||||
_, err := Runner("false").Timeout(200 * time.Millisecond).FailOnExitCode().Run()
|
||||
if err != ErrExitCode {
|
||||
if !errors.Is(err, ErrExitCode) {
|
||||
t.Errorf("wrong err := %v", err)
|
||||
}
|
||||
|
||||
|
@@ -306,7 +306,7 @@ func (b *Builder) GinReq(ctx context.Context, g *gin.Context, req *http.Request)
|
||||
}
|
||||
b.Str("gin.method", req.Method)
|
||||
b.Str("gin.path", g.FullPath())
|
||||
b.Str("gin.header", formatHeader(g.Request.Header))
|
||||
b.Strs("gin.header", extractHeader(g.Request.Header))
|
||||
if req.URL != nil {
|
||||
b.Str("gin.url", req.URL.String())
|
||||
}
|
||||
@@ -367,6 +367,20 @@ func formatHeader(header map[string][]string) string {
|
||||
return r
|
||||
}
|
||||
|
||||
func extractHeader(header map[string][]string) []string {
|
||||
r := make([]string, 0, len(header))
|
||||
for k, v := range header {
|
||||
for _, hval := range v {
|
||||
value := hval
|
||||
value = strings.ReplaceAll(value, "\n", "\\n")
|
||||
value = strings.ReplaceAll(value, "\r", "\\r")
|
||||
value = strings.ReplaceAll(value, "\t", "\\t")
|
||||
r = append(r, k+": "+value)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Build creates a new error, ready to pass up the stack
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PreContext struct {
|
||||
@@ -18,6 +19,7 @@ type PreContext struct {
|
||||
body any
|
||||
form any
|
||||
header any
|
||||
timeout *time.Duration
|
||||
}
|
||||
|
||||
func (pctx *PreContext) URI(uri any) *PreContext {
|
||||
@@ -45,6 +47,11 @@ func (pctx *PreContext) Header(header any) *PreContext {
|
||||
return pctx
|
||||
}
|
||||
|
||||
func (pctx *PreContext) WithTimeout(to time.Duration) *PreContext {
|
||||
pctx.timeout = &to
|
||||
return pctx
|
||||
}
|
||||
|
||||
func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
|
||||
if pctx.uri != nil {
|
||||
if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil {
|
||||
@@ -92,6 +99,14 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
|
||||
Build()
|
||||
return nil, nil, langext.Ptr(Error(err))
|
||||
}
|
||||
} else if pctx.ginCtx.ContentType() == "application/x-www-form-urlencoded" {
|
||||
if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil {
|
||||
err = exerr.Wrap(err, "Failed to read urlencoded-form").
|
||||
WithType(exerr.TypeBindFailFormData).
|
||||
Str("struct_type", fmt.Sprintf("%T", pctx.form)).
|
||||
Build()
|
||||
return nil, nil, langext.Ptr(Error(err))
|
||||
}
|
||||
} else {
|
||||
err := exerr.New(exerr.TypeBindFailFormData, "missing form body").
|
||||
Str("struct_type", fmt.Sprintf("%T", pctx.form)).
|
||||
@@ -110,7 +125,7 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
ictx, cancel := context.WithTimeout(context.Background(), pctx.wrapper.requestTimeout)
|
||||
ictx, cancel := context.WithTimeout(context.Background(), langext.Coalesce(pctx.timeout, pctx.wrapper.requestTimeout))
|
||||
actx := CreateAppContext(pctx.ginCtx, ictx, cancel)
|
||||
|
||||
return actx, pctx.ginCtx, nil
|
||||
|
2
go.mod
2
go.mod
@@ -14,7 +14,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.10.0-rc3 // indirect
|
||||
github.com/bytedance/sonic v1.10.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
|
2
go.sum
2
go.sum
@@ -4,6 +4,8 @@ github.com/bytedance/sonic v1.10.0-rc2 h1:oDfRZ+4m6AYCOC0GFeOCeYqvBmucy1isvouS2K
|
||||
github.com/bytedance/sonic v1.10.0-rc2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=
|
||||
github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk=
|
||||
github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
|
@@ -1,5 +1,5 @@
|
||||
package goext
|
||||
|
||||
const GoextVersion = "0.0.235"
|
||||
const GoextVersion = "0.0.239"
|
||||
|
||||
const GoextVersionTimestamp = "2023-08-09T14:40:16+0200"
|
||||
const GoextVersionTimestamp = "2023-08-11T16:32:34+0200"
|
||||
|
Reference in New Issue
Block a user