// Copyright Earl Warren // Copyright Loïc Dachary // SPDX-License-Identifier: MIT package util import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "code.forgejo.org/f3/gof3/v3/logger" ) type CommandOptions struct { ExitCodes []int Log logger.MessageInterface } func (o *CommandOptions) setDefaults() { if o.ExitCodes == nil { o.ExitCodes = []int{0} } if o.Log == nil { o.Log = logger.NewLogger() } } func CommandWithErr(ctx context.Context, options CommandOptions, prog string, args ...string) (err error) { defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { panic(r) } } }() CommandWithOptions(ctx, options, prog, args...) return nil } func Command(ctx context.Context, log logger.MessageInterface, prog string, args ...string) string { options := CommandOptions{ Log: log, } return CommandWithOptions(ctx, options, prog, args...) } func CommandWithOptions(ctx context.Context, options CommandOptions, prog string, args ...string) string { if ctx == nil { panic("ctx context.Context is nil") } options.setDefaults() options.Log.Log(1, logger.Trace, "%s\n", args) cmd := exec.Command(prog, args...) SetSysProcAttribute(cmd) var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out cmd.Env = append( cmd.Env, "GIT_TERMINAL_PROMPT=0", ) err := cmd.Start() if err != nil { panic(err) } ctxErr := watchCtx(ctx, cmd.Process) err = cmd.Wait() interruptErr := <-ctxErr // If cmd.Wait returned an error, prefer that. // Otherwise, report any error from the interrupt goroutine. if interruptErr != nil && err == nil { err = interruptErr } var code int if err == nil { code = 0 } else if exiterr, ok := err.(*exec.ExitError); ok { code = exiterr.ExitCode() if code == -1 { panic("killed") } } else { panic(err) } found := false for _, valid := range options.ExitCodes { if valid == code { found = true break } } if !found { panic(fmt.Errorf("%w: exit code: %d, %v, %v, %v", err, code, out.String(), prog, args)) } options.Log.Log(1, logger.Trace, "%s\n", out.String()) return out.String() } // wrappedError wraps an error without relying on fmt.Errorf. type wrappedError struct { prefix string err error } func (w wrappedError) Error() string { return w.prefix + ": " + w.err.Error() } func (w wrappedError) Unwrap() error { return w.err } func watchCtx(ctx context.Context, p *os.Process) <-chan error { if ctx == nil { return nil } errc := make(chan error) go func() { select { case errc <- nil: return case <-ctx.Done(): } var err error if killErr := kill(p); killErr == nil { // We appear to have successfully delivered a kill signal, so any // program behavior from this point may be due to ctx. err = ctx.Err() } else if !errors.Is(killErr, os.ErrProcessDone) { err = wrappedError{ prefix: "util: exec: error sending signal", err: killErr, } } errc <- err }() return errc }