Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
56094b3cb6
|
|||
0da098e9f9
|
|||
f0881c9fd6
|
|||
029b408749
|
@@ -14,6 +14,7 @@ type CommandRunner struct {
|
|||||||
listener []CommandListener
|
listener []CommandListener
|
||||||
enforceExitCodes *[]int
|
enforceExitCodes *[]int
|
||||||
enforceNoTimeout bool
|
enforceNoTimeout bool
|
||||||
|
enforceNoStderr bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func Runner(program string) *CommandRunner {
|
func Runner(program string) *CommandRunner {
|
||||||
@@ -25,6 +26,7 @@ func Runner(program string) *CommandRunner {
|
|||||||
listener: make([]CommandListener, 0),
|
listener: make([]CommandListener, 0),
|
||||||
enforceExitCodes: nil,
|
enforceExitCodes: nil,
|
||||||
enforceNoTimeout: false,
|
enforceNoTimeout: false,
|
||||||
|
enforceNoStderr: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +75,11 @@ func (r *CommandRunner) FailOnTimeout() *CommandRunner {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *CommandRunner) FailOnStderr() *CommandRunner {
|
||||||
|
r.enforceNoStderr = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func (r *CommandRunner) Listen(lstr CommandListener) *CommandRunner {
|
func (r *CommandRunner) Listen(lstr CommandListener) *CommandRunner {
|
||||||
r.listener = append(r.listener, lstr)
|
r.listener = append(r.listener, lstr)
|
||||||
return r
|
return r
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
var ErrExitCode = errors.New("process exited with an unexpected exitcode")
|
var ErrExitCode = errors.New("process exited with an unexpected exitcode")
|
||||||
var ErrTimeout = errors.New("process did not exit after the specified timeout")
|
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 {
|
type CommandResult struct {
|
||||||
StdOut string
|
StdOut string
|
||||||
@@ -53,12 +54,27 @@ func run(opt CommandRunner) (CommandResult, error) {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stderrFailChan := make(chan bool)
|
||||||
|
|
||||||
outputChan := make(chan resultObj)
|
outputChan := make(chan resultObj)
|
||||||
go func() {
|
go func() {
|
||||||
// we need to first fully read the pipes and then call Wait
|
// we need to first fully read the pipes and then call Wait
|
||||||
// see https://pkg.go.dev/os/exec#Cmd.StdoutPipe
|
// 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 {
|
if err != nil {
|
||||||
outputChan <- resultObj{stdout, stderr, stdcombined, err}
|
outputChan <- resultObj{stdout, stderr, stdcombined, err}
|
||||||
_ = cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
@@ -115,6 +131,34 @@ func run(opt CommandRunner) (CommandResult, error) {
|
|||||||
return res, nil
|
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:
|
case outobj := <-outputChan:
|
||||||
if exiterr, ok := outobj.err.(*exec.ExitError); ok {
|
if exiterr, ok := outobj.err.(*exec.ExitError); ok {
|
||||||
excode := exiterr.ExitCode()
|
excode := exiterr.ExitCode()
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package cmdext
|
package cmdext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -289,16 +290,40 @@ func TestLongStdout(t *testing.T) {
|
|||||||
func TestFailOnTimeout(t *testing.T) {
|
func TestFailOnTimeout(t *testing.T) {
|
||||||
|
|
||||||
_, err := Runner("sleep").Arg("2").Timeout(200 * time.Millisecond).FailOnTimeout().Run()
|
_, 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)
|
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) {
|
func TestFailOnExitcode(t *testing.T) {
|
||||||
|
|
||||||
_, err := Runner("false").Timeout(200 * time.Millisecond).FailOnExitCode().Run()
|
_, err := Runner("false").Timeout(200 * time.Millisecond).FailOnExitCode().Run()
|
||||||
if err != ErrExitCode {
|
if !errors.Is(err, ErrExitCode) {
|
||||||
t.Errorf("wrong err := %v", err)
|
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.method", req.Method)
|
||||||
b.Str("gin.path", g.FullPath())
|
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 {
|
if req.URL != nil {
|
||||||
b.Str("gin.url", req.URL.String())
|
b.Str("gin.url", req.URL.String())
|
||||||
}
|
}
|
||||||
@@ -367,6 +367,20 @@ func formatHeader(header map[string][]string) string {
|
|||||||
return r
|
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
|
// 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/exerr"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PreContext struct {
|
type PreContext struct {
|
||||||
@@ -18,6 +19,7 @@ type PreContext struct {
|
|||||||
body any
|
body any
|
||||||
form any
|
form any
|
||||||
header any
|
header any
|
||||||
|
timeout *time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pctx *PreContext) URI(uri any) *PreContext {
|
func (pctx *PreContext) URI(uri any) *PreContext {
|
||||||
@@ -45,6 +47,11 @@ func (pctx *PreContext) Header(header any) *PreContext {
|
|||||||
return pctx
|
return pctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pctx *PreContext) WithTimeout(to time.Duration) *PreContext {
|
||||||
|
pctx.timeout = &to
|
||||||
|
return pctx
|
||||||
|
}
|
||||||
|
|
||||||
func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
|
func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
|
||||||
if pctx.uri != nil {
|
if pctx.uri != nil {
|
||||||
if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil {
|
if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil {
|
||||||
@@ -92,6 +99,14 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
|
|||||||
Build()
|
Build()
|
||||||
return nil, nil, langext.Ptr(Error(err))
|
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 {
|
} else {
|
||||||
err := exerr.New(exerr.TypeBindFailFormData, "missing form body").
|
err := exerr.New(exerr.TypeBindFailFormData, "missing form body").
|
||||||
Str("struct_type", fmt.Sprintf("%T", pctx.form)).
|
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)
|
actx := CreateAppContext(pctx.ginCtx, ictx, cancel)
|
||||||
|
|
||||||
return actx, pctx.ginCtx, nil
|
return actx, pctx.ginCtx, nil
|
||||||
|
2
go.mod
2
go.mod
@@ -14,7 +14,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // 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-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 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-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-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-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
package goext
|
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