107 lines
2.9 KiB
Go
107 lines
2.9 KiB
Go
// SPDX-License-Identifier: MIT
|
|||
|
|
|
||
|
|
// Package execx provides a thin, testable abstraction over external command
|
||
|
|
// execution.
|
||
|
|
//
|
||
|
|
// All privileged work in prx goes through a Runner so that the orchestration
|
||
|
|
// logic can be unit tested without touching the real system.
|
||
|
|
package execx
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"os/exec"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Result is the outcome of running an external command.
|
||
|
|
type Result struct {
|
||
|
|
Stdout []byte
|
||
|
|
Stderr []byte
|
||
|
|
Code int
|
||
|
|
}
|
||
|
|
|
||
|
|
// Runner abstracts external command execution.
|
||
|
|
type Runner interface {
|
||
|
|
// Run executes name with args. If stdin is non-nil it is fed to the
|
||
|
|
// process standard input. A non-nil error is returned when the command
|
||
|
|
// could not be started or exited with a non-zero status (*ExitError).
|
||
|
|
Run(ctx context.Context, stdin []byte, name string, args ...string) (Result, error)
|
||
|
|
// LookPath resolves the absolute path of an executable.
|
||
|
|
LookPath(name string) (string, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ExitError indicates that a command started but exited with a non-zero code.
|
||
|
|
type ExitError struct {
|
||
|
|
Name string
|
||
|
|
Args []string
|
||
|
|
Code int
|
||
|
|
Stderr []byte
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *ExitError) Error() string {
|
||
|
|
msg := strings.TrimSpace(string(e.Stderr))
|
||
|
|
if msg == "" {
|
||
|
|
msg = "no stderr"
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("%s %s: exit status %d: %s", e.Name, strings.Join(e.Args, " "), e.Code, msg)
|
||
|
|
}
|
||
|
|
|
||
|
|
// OSRunner runs commands on the host.
|
||
|
|
type OSRunner struct{}
|
||
|
|
|
||
|
|
// Run implements Runner.
|
||
|
|
func (OSRunner) Run(ctx context.Context, stdin []byte, name string, args ...string) (Result, error) {
|
||
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
||
|
|
if stdin != nil {
|
||
|
|
cmd.Stdin = bytes.NewReader(stdin)
|
||
|
|
}
|
||
|
|
var out, errb bytes.Buffer
|
||
|
|
cmd.Stdout = &out
|
||
|
|
cmd.Stderr = &errb
|
||
|
|
err := cmd.Run()
|
||
|
|
res := Result{Stdout: out.Bytes(), Stderr: errb.Bytes()}
|
||
|
|
if err != nil {
|
||
|
|
var ee *exec.ExitError
|
||
|
|
if errors.As(err, &ee) {
|
||
|
|
res.Code = ee.ExitCode()
|
||
|
|
return res, &ExitError{Name: name, Args: args, Code: res.Code, Stderr: res.Stderr}
|
||
|
|
}
|
||
|
|
return res, err
|
||
|
|
}
|
||
|
|
return res, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// LookPath implements Runner.
|
||
|
|
func (OSRunner) LookPath(name string) (string, error) { return exec.LookPath(name) }
|
||
|
|
|
||
|
|
// Call records a single fake invocation.
|
||
|
|
type Call struct {
|
||
|
|
Name string
|
||
|
|
Args []string
|
||
|
|
Stdin []byte
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fake is an in-memory Runner for tests.
|
||
|
|
type Fake struct {
|
||
|
|
// Handler produces the result for a call. When nil an empty successful
|
||
|
|
// result is returned.
|
||
|
|
Handler func(name string, args []string, stdin []byte) (Result, error)
|
||
|
|
// Calls records every invocation in order.
|
||
|
|
Calls []Call
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run implements Runner.
|
||
|
|
func (f *Fake) Run(_ context.Context, stdin []byte, name string, args ...string) (Result, error) {
|
||
|
|
f.Calls = append(f.Calls, Call{Name: name, Args: append([]string(nil), args...), Stdin: append([]byte(nil), stdin...)})
|
||
|
|
if f.Handler == nil {
|
||
|
|
return Result{}, nil
|
||
|
|
}
|
||
|
|
return f.Handler(name, args, stdin)
|
||
|
|
}
|
||
|
|
|
||
|
|
// LookPath implements Runner.
|
||
|
|
func (f *Fake) LookPath(name string) (string, error) { return "/usr/bin/" + name, nil }
|