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 }