1
0
Fork 0
golang-forgejo-f3-gof3/util/exec.go

153 lines
3 KiB
Go
Raw Permalink Normal View History

// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// 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
}