// 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 }