153 lines
3 KiB
Go
153 lines
3 KiB
Go
|
// 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
|
||
|
}
|