1
0
mirror of https://github.com/lxc/incus.git synced 2026-02-05 18:45:46 +01:00
Files
incus/shared/subprocess/run.go
JUN JIE NAN 23bfc8a010 shared/subprocess: Simplify code by using modern constructs
Using more modern features of Go, such as:
- conditional assignment -> built-in min or max in go1.21,
- sort.Slice -> slices.Sort in go1.21,
- loop assign map -> maps.Copy in go1.21,
- []byte(fmt.Sprintf...) -> fmt.Appendf(nil,...) in go1.19,
- strings.HasPrefix / strings.TrimPrefix -> strings.CutPrefix in go1.20

Signed-off-by: JUN JIE NAN <nanjunjie@gmail.com>
2025-05-17 12:33:44 -04:00

163 lines
5.0 KiB
Go

package subprocess
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
)
// RunError is the error from the RunCommand family of functions.
type RunError struct {
cmd string
args []string
err error
stdout *bytes.Buffer
stderr *bytes.Buffer
}
func (e RunError) Error() string {
if e.stderr.Len() == 0 {
return fmt.Sprintf("Failed to run: %s %s: %v", e.cmd, strings.Join(e.args, " "), e.err)
}
return fmt.Sprintf("Failed to run: %s %s: %v (%s)", e.cmd, strings.Join(e.args, " "), e.err, strings.TrimSpace(e.stderr.String()))
}
func (e RunError) Unwrap() error {
return e.err
}
// StdOut returns the stdout buffer.
func (e RunError) StdOut() *bytes.Buffer {
return e.stdout
}
// StdErr returns the stdout buffer.
func (e RunError) StdErr() *bytes.Buffer {
return e.stderr
}
// NewRunError returns new RunError.
func NewRunError(cmd string, args []string, err error, stdout *bytes.Buffer, stderr *bytes.Buffer) error {
return RunError{
cmd: cmd,
args: args,
err: err,
stdout: stdout,
stderr: stderr,
}
}
// RunCommandSplit runs a command with a supplied environment and optional arguments and returns the
// resulting stdout and stderr output as separate variables. If the supplied environment is nil then
// the default environment is used. If the command fails to start or returns a non-zero exit code
// then an error is returned containing the output of stderr too.
func RunCommandSplit(ctx context.Context, env []string, filesInherit []*os.File, name string, arg ...string) (string, string, error) {
cmd := exec.CommandContext(ctx, name, arg...)
if env != nil {
cmd.Env = env
}
if filesInherit != nil {
cmd.ExtraFiles = filesInherit
}
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return stdout.String(), stderr.String(), NewRunError(name, arg, err, &stdout, &stderr)
}
return stdout.String(), stderr.String(), nil
}
// RunCommandContext runs a command with optional arguments and returns stdout. If the command fails to
// start or returns a non-zero exit code then an error is returned containing the output of stderr.
func RunCommandContext(ctx context.Context, name string, arg ...string) (string, error) {
stdout, _, err := RunCommandSplit(ctx, nil, nil, name, arg...)
return stdout, err
}
// RunCommand runs a command with optional arguments and returns stdout. If the command fails to
// start or returns a non-zero exit code then an error is returned containing the output of stderr.
// Deprecated: Use RunCommandContext.
func RunCommand(name string, arg ...string) (string, error) {
stdout, _, err := RunCommandSplit(context.TODO(), nil, nil, name, arg...)
return stdout, err
}
// RunCommandInheritFds runs a command with optional arguments and passes a set
// of file descriptors to the newly created process, returning stdout. If the
// command fails to start or returns a non-zero exit code then an error is
// returned containing the output of stderr.
func RunCommandInheritFds(ctx context.Context, filesInherit []*os.File, name string, arg ...string) (string, error) {
stdout, _, err := RunCommandSplit(ctx, nil, filesInherit, name, arg...)
return stdout, err
}
// RunCommandCLocale runs a command with a LC_ALL=C.UTF-8 and LANGUAGE=en environment set with optional arguments and
// returns stdout. If the command fails to start or returns a non-zero exit code then an error is
// returned containing the output of stderr.
func RunCommandCLocale(name string, arg ...string) (string, error) {
stdout, _, err := RunCommandSplit(context.TODO(), append(os.Environ(), "LC_ALL=C.UTF-8", "LANGUAGE=en"), nil, name, arg...)
return stdout, err
}
// RunCommandWithFds runs a command with supplied file descriptors.
func RunCommandWithFds(ctx context.Context, stdin io.Reader, stdout io.Writer, name string, arg ...string) error {
cmd := exec.CommandContext(ctx, name, arg...)
if stdin != nil {
cmd.Stdin = stdin
}
if stdout != nil {
cmd.Stdout = stdout
}
var buffer bytes.Buffer
cmd.Stderr = &buffer
err := cmd.Run()
if err != nil {
return NewRunError(name, arg, err, nil, &buffer)
}
return nil
}
// TryRunCommand runs the specified command up to 20 times with a 500ms delay between each call
// until it runs without an error. If after 20 times it is still failing then returns the error.
func TryRunCommand(name string, arg ...string) (string, error) {
return TryRunCommandAttemptsDuration(20, 500*time.Millisecond, name, arg...)
}
// TryRunCommandAttemptsDuration runs the specified command up to a specified number times with a
// specified delay between each call until it runs without an error. If after the number of times
// it is still failing then returns the error.
func TryRunCommandAttemptsDuration(attempts int, delay time.Duration, name string, arg ...string) (string, error) {
var err error
var output string
for range attempts {
output, err = RunCommand(name, arg...)
if err == nil {
break
}
time.Sleep(delay)
}
return output, err
}