blob: f9ed46a6dfade2245fb4ebb0a91ae12851610377 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package execution
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
)
// Executor supports chaining subcommand execution and error handling.
type Executor struct {
stdout io.Writer
stderr io.Writer
dir string
}
// Command represents a command to be run by an executor.
type Command struct {
Args []string
// If UserCausedError is set and the command fails with a non-zero exit
// code, the Executor will wrap the error with a userError.
UserCausedError bool
}
type userError struct {
error
}
func (e userError) Unwrap() error {
return e.error
}
func (e userError) IsInfraFailure() bool {
return false
}
// NewExecutor returns a new Executor that writes to stdout and stderr.
func NewExecutor(stdout, stderr io.Writer, dir string) *Executor {
return &Executor{stdout: stdout, stderr: stderr, dir: dir}
}
// Exec runs the command at path with args.
func (e Executor) Exec(ctx context.Context, path string, args ...string) error {
cmd := exec.CommandContext(ctx, path, args...)
cmd.Env = os.Environ()
cmd.Stdout = e.stdout
cmd.Stderr = e.stderr
cmd.Dir = e.dir
if err := cmd.Run(); err != nil {
cmdString := strings.Join(append([]string{path}, args...), " ")
// Unfortunately, exec.CommandContext doesn't return a context error
// when the context is canceled – instead it returns the error resulting
// from the subprocess being killed (e.g. "signal: killed" on Unix). So
// we assume that if the command failed and the context happens to be
// canceled, that the subprocess was killed due to a context
// cancelation.
if ctx.Err() != nil {
return fmt.Errorf("command was canceled (%s): %w", cmdString, ctx.Err())
}
return fmt.Errorf("command failed (%s): %w", cmdString, err)
}
return nil
}
// ExecAll runs all given commands. Upon encountering an error after execution
// stops and the error is returned. Returns an error if an element in cmds is
// empty.
func (e Executor) ExecAll(ctx context.Context, cmds []Command) error {
for i, cmd := range cmds {
if len(cmd.Args) == 0 {
return fmt.Errorf("forbidden empty list in cmds at position %d", i)
}
if err := e.Exec(ctx, cmd.Args[0], cmd.Args[1:]...); err != nil {
var errExit *exec.ExitError
if cmd.UserCausedError && errors.As(err, &errExit) {
return userError{error: err}
}
return err
}
}
return nil
}