github.com/cilium/cilium@v1.16.2/pkg/command/exec/exec.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package exec
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"os/exec"
    13  
    14  	"github.com/sirupsen/logrus"
    15  
    16  	"github.com/cilium/cilium/pkg/time"
    17  )
    18  
    19  func warnToLog(cmd *exec.Cmd, out []byte, scopedLog *logrus.Entry, err error) {
    20  	scopedLog.WithError(err).WithField("cmd", cmd.Args).Error("Command execution failed")
    21  	scanner := bufio.NewScanner(bytes.NewReader(out))
    22  	for scanner.Scan() {
    23  		scopedLog.Warn(scanner.Text())
    24  	}
    25  }
    26  
    27  // combinedOutput is the core implementation of catching deadline exceeded
    28  // options and logging errors.
    29  func combinedOutput(ctx context.Context, cmd *exec.Cmd, scopedLog *logrus.Entry, verbose bool) ([]byte, error) {
    30  	out, err := cmd.CombinedOutput()
    31  	if ctx.Err() != nil {
    32  		if !errors.Is(ctx.Err(), context.Canceled) {
    33  			scopedLog.WithError(err).WithField("cmd", cmd.Args).Error("Command execution failed")
    34  		}
    35  		return nil, fmt.Errorf("Command execution failed for %s: %w", cmd.Args, ctx.Err())
    36  	}
    37  	if err != nil && verbose {
    38  		warnToLog(cmd, out, scopedLog, err)
    39  	}
    40  	return out, err
    41  }
    42  
    43  // output is the equivalent to combinedOutput with only capturing stdout
    44  func output(ctx context.Context, cmd *exec.Cmd, scopedLog *logrus.Entry, verbose bool) ([]byte, error) {
    45  	out, err := cmd.Output()
    46  	if ctx.Err() != nil {
    47  		if !errors.Is(ctx.Err(), context.Canceled) {
    48  			scopedLog.WithError(err).WithField("cmd", cmd.Args).Error("Command execution failed")
    49  		}
    50  		return nil, fmt.Errorf("Command execution failed for %s: %w", cmd.Args, ctx.Err())
    51  	}
    52  	if err != nil {
    53  		var exitErr *exec.ExitError
    54  		if errors.As(err, &exitErr) {
    55  			err = fmt.Errorf("%w stderr=%q", exitErr, exitErr.Stderr)
    56  		}
    57  		if verbose {
    58  			warnToLog(cmd, out, scopedLog, err)
    59  		}
    60  	}
    61  	return out, err
    62  }
    63  
    64  // Cmd wraps exec.Cmd with a context to provide convenient execution of a
    65  // command with nice checking of the context timeout in the form:
    66  //
    67  // err := exec.Prog().WithTimeout(5*time.Second, myprog, myargs...).CombinedOutput(log, verbose)
    68  type Cmd struct {
    69  	*exec.Cmd
    70  	ctx      context.Context
    71  	cancelFn func()
    72  }
    73  
    74  // CommandContext wraps exec.CommandContext to allow this package to be used as
    75  // a drop-in replacement for the standard exec library.
    76  func CommandContext(ctx context.Context, prog string, args ...string) *Cmd {
    77  	return &Cmd{
    78  		Cmd: exec.CommandContext(ctx, prog, args...),
    79  		ctx: ctx,
    80  	}
    81  }
    82  
    83  // WithTimeout creates a Cmd with a context that times out after the specified
    84  // duration.
    85  func WithTimeout(timeout time.Duration, prog string, args ...string) *Cmd {
    86  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
    87  	cmd := CommandContext(ctx, prog, args...)
    88  	cmd.cancelFn = cancel
    89  	return cmd
    90  }
    91  
    92  // WithCancel creates a Cmd with a context that can be cancelled by calling the
    93  // resulting Cancel() function.
    94  func WithCancel(ctx context.Context, prog string, args ...string) (*Cmd, context.CancelFunc) {
    95  	newCtx, cancel := context.WithCancel(ctx)
    96  	cmd := CommandContext(newCtx, prog, args...)
    97  	return cmd, cancel
    98  }
    99  
   100  // CombinedOutput runs the command and returns its combined standard output and
   101  // standard error. Unlike the standard library, if the context is exceeded, it
   102  // will return an error indicating so.
   103  //
   104  // Logs any errors that occur to the specified logger.
   105  func (c *Cmd) CombinedOutput(scopedLog *logrus.Entry, verbose bool) ([]byte, error) {
   106  	out, err := combinedOutput(c.ctx, c.Cmd, scopedLog, verbose)
   107  	if c.cancelFn != nil {
   108  		c.cancelFn()
   109  	}
   110  	return out, err
   111  }
   112  
   113  // Output runs the command and returns only standard output, but not the
   114  // standard error. Unlike the standard library, if the context is exceeded,
   115  // it will return an error indicating so.
   116  //
   117  // Logs any errors that occur to the specified logger.
   118  func (c *Cmd) Output(scopedLog *logrus.Entry, verbose bool) ([]byte, error) {
   119  	out, err := output(c.ctx, c.Cmd, scopedLog, verbose)
   120  	if c.cancelFn != nil {
   121  		c.cancelFn()
   122  	}
   123  	return out, err
   124  }