github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/exec/exec.go (about) 1 package exec 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 "os/exec" 9 "os/signal" 10 "syscall" 11 "time" 12 13 fsterr "github.com/fastly/cli/pkg/errors" 14 "github.com/fastly/cli/pkg/text" 15 "github.com/fastly/cli/pkg/threadsafe" 16 ) 17 18 // divider is used as separator lines around shell output. 19 const divider = "--------------------------------------------------------------------------------" 20 21 // Streaming models a generic command execution that consumers can use to 22 // execute commands and stream their output to an io.Writer. For example 23 // compute commands can use this to standardize the flow control for each 24 // compiler toolchain. 25 type Streaming struct { 26 // Args are the command positional arguments. 27 Args []string 28 // Command is the command to be executed. 29 Command string 30 // Env is the environment variables to set. 31 Env []string 32 // ForceOutput ensures output is displayed (default: only display on error). 33 ForceOutput bool 34 // Output is where to write output (e.g. stdout) 35 Output io.Writer 36 // Process is the process to terminal if signal received. 37 Process *os.Process 38 // SignalCh is a channel handling signal events. 39 SignalCh chan os.Signal 40 // Spinner is a specific spinner instance. 41 Spinner text.Spinner 42 // SpinnerMessage is the messaging to use. 43 SpinnerMessage string 44 // Timeout is the command timeout. 45 Timeout time.Duration 46 // Verbose outputs additional information. 47 Verbose bool 48 } 49 50 // MonitorSignals spawns a goroutine that configures signal handling so that 51 // the long running subprocess can be killed using SIGINT/SIGTERM. 52 func (s *Streaming) MonitorSignals() { 53 go s.MonitorSignalsAsync() 54 } 55 56 // MonitorSignalsAsync configures the signal notifications. 57 func (s *Streaming) MonitorSignalsAsync() { 58 signals := []os.Signal{ 59 syscall.SIGINT, 60 syscall.SIGTERM, 61 } 62 63 signal.Notify(s.SignalCh, signals...) 64 65 <-s.SignalCh 66 signal.Stop(s.SignalCh) 67 68 // NOTE: We don't do error handling here because the user might be doing local 69 // development with the --watch flag and that workflow will have already 70 // killed the process. The reason this line still exists is for users running 71 // their application locally without the --watch flag and who then execute 72 // Ctrl-C to kill the process. 73 _ = s.Signal(os.Kill) 74 } 75 76 // Exec executes the compiler command and pipes the child process stdout and 77 // stderr output to the supplied io.Writer, it waits for the command to exit 78 // cleanly or returns an error. 79 func (s *Streaming) Exec() error { 80 // Construct the command with given arguments and environment. 81 var cmd *exec.Cmd 82 if s.Timeout > 0 { 83 ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) 84 defer cancel() 85 // gosec flagged this: 86 // G204 (CWE-78): Subprocess launched with variable 87 // Disabling as the variables come from trusted sources. 88 // #nosec 89 // nosemgrep 90 cmd = exec.CommandContext(ctx, s.Command, s.Args...) 91 } else { 92 // gosec flagged this: 93 // G204 (CWE-78): Subprocess launched with variable 94 // Disabling as the variables come from trusted sources. 95 // #nosec 96 // nosemgrep 97 cmd = exec.Command(s.Command, s.Args...) 98 } 99 cmd.Env = append(os.Environ(), s.Env...) 100 101 // We store all output in a buffer to hide it unless there was an error. 102 var buf threadsafe.Buffer 103 var output io.Writer 104 output = &buf 105 106 // We only display the stored output if there is an error. 107 // But some commands like `compute serve` expect the full output regardless. 108 // So for those scenarios they can force all output. 109 if s.ForceOutput { 110 output = s.Output 111 } 112 113 if !s.Verbose { 114 text.Break(output) 115 } 116 text.Info(output, "Command output:") 117 text.Output(output, divider) 118 119 cmd.Stdout = output 120 cmd.Stderr = output 121 122 if err := cmd.Start(); err != nil { 123 text.Output(output, divider) 124 return err 125 } 126 127 // Store off os.Process so it can be killed by signal listener. 128 // 129 // NOTE: argparser.Process is nil until exec.Start() returns successfully. 130 s.Process = cmd.Process 131 132 if err := cmd.Wait(); err != nil { 133 // IMPORTANT: We MUST wrap the original error. 134 // This is because the `compute serve` command requires it for --watch 135 // Specifically we need to check the error message for "killed". 136 // This enables the watching logic to restart the Viceroy binary. 137 err = fmt.Errorf("error during execution process (see 'command output' above): %w", err) 138 139 text.Output(output, divider) 140 141 // If we're in verbose mode, the build output is shown. 142 // So in that case we don't want to have a spinner as it'll interweave output. 143 // In non-verbose mode we have a spinner running while the build is happening. 144 if !s.Verbose && s.Spinner != nil { 145 s.Spinner.StopFailMessage(s.SpinnerMessage) 146 if spinErr := s.Spinner.StopFail(); spinErr != nil { 147 return fmt.Errorf(text.SpinnerErrWrapper, spinErr, err) 148 } 149 } 150 151 // Display the buffer stored output as we have an error. 152 fmt.Fprintf(s.Output, "%s", buf.String()) 153 154 return err 155 } 156 157 text.Output(output, divider) 158 return nil 159 } 160 161 // Signal enables spawned subprocess to accept given signal. 162 func (s *Streaming) Signal(sig os.Signal) error { 163 if s.Process != nil { 164 err := s.Process.Signal(sig) 165 if err != nil { 166 return err 167 } 168 } 169 return nil 170 } 171 172 // CommandOpts are arguments for executing a streaming command. 173 type CommandOpts struct { 174 // Args are the command positional arguments. 175 Args []string 176 // Command is the command to be executed. 177 Command string 178 // Env is the environment variables to set. 179 Env []string 180 // ErrLog provides an interface for recording errors to disk. 181 ErrLog fsterr.LogInterface 182 // Output is where to write output (e.g. stdout) 183 Output io.Writer 184 // Spinner is a specific spinner instance. 185 Spinner text.Spinner 186 // SpinnerMessage is the messaging to use. 187 SpinnerMessage string 188 // Timeout is the command timeout. 189 Timeout int 190 // Verbose outputs additional information. 191 Verbose bool 192 } 193 194 // Command is an abstraction over a Streaming type. It is used by both the 195 // `compute init` and `compute build` commands to run post init/build scripts. 196 func Command(opts CommandOpts) error { 197 s := Streaming{ 198 Command: opts.Command, 199 Args: opts.Args, 200 Env: opts.Env, 201 Output: opts.Output, 202 Spinner: opts.Spinner, 203 SpinnerMessage: opts.SpinnerMessage, 204 Verbose: opts.Verbose, 205 } 206 if opts.Verbose { 207 s.ForceOutput = true 208 } 209 if opts.Timeout > 0 { 210 s.Timeout = time.Duration(opts.Timeout) * time.Second 211 } 212 if err := s.Exec(); err != nil { 213 opts.ErrLog.Add(err) 214 return err 215 } 216 return nil 217 }