github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/command/command.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io" 8 "os" 9 "os/exec" 10 "strings" 11 "syscall" 12 13 "github.com/SAP/jenkins-library/pkg/log" 14 "github.com/SAP/jenkins-library/pkg/piperutils" 15 "github.com/pkg/errors" 16 ) 17 18 // Command defines the information required for executing a call to any executable 19 type Command struct { 20 ErrorCategoryMapping map[string][]string 21 StepName string 22 dir string 23 stdin io.Reader 24 stdout io.Writer 25 stderr io.Writer 26 env []string 27 exitCode int 28 } 29 30 type runner interface { 31 SetDir(dir string) 32 SetEnv(env []string) 33 AppendEnv(env []string) 34 Stdin(in io.Reader) 35 Stdout(out io.Writer) 36 Stderr(err io.Writer) 37 GetStdout() io.Writer 38 GetStderr() io.Writer 39 } 40 41 // ExecRunner mock for intercepting calls to executables 42 type ExecRunner interface { 43 runner 44 RunExecutable(executable string, params ...string) error 45 RunExecutableInBackground(executable string, params ...string) (Execution, error) 46 } 47 48 // ShellRunner mock for intercepting shell calls 49 type ShellRunner interface { 50 runner 51 RunShell(shell string, command string) error 52 } 53 54 // SetDir sets the working directory for the execution 55 func (c *Command) SetDir(dir string) { 56 c.dir = dir 57 } 58 59 // SetEnv sets explicit environment variables to be used for execution 60 func (c *Command) SetEnv(env []string) { 61 c.env = env 62 } 63 64 // AppendEnv appends environment variables to be used for execution 65 func (c *Command) AppendEnv(env []string) { 66 c.env = append(c.env, env...) 67 } 68 69 func (c *Command) GetOsEnv() []string { 70 return os.Environ() 71 } 72 73 // Stdin .. 74 func (c *Command) Stdin(stdin io.Reader) { 75 c.stdin = stdin 76 } 77 78 // Stdout .. 79 func (c *Command) Stdout(stdout io.Writer) { 80 c.stdout = stdout 81 } 82 83 // Stderr .. 84 func (c *Command) Stderr(stderr io.Writer) { 85 c.stderr = stderr 86 } 87 88 // GetStdout Returns the writer for stdout 89 func (c *Command) GetStdout() io.Writer { 90 return c.stdout 91 } 92 93 // GetStderr Retursn the writer for stderr 94 func (c *Command) GetStderr() io.Writer { 95 return c.stderr 96 } 97 98 // ExecCommand defines how to execute os commands 99 var ExecCommand = exec.Command 100 101 // RunShell runs the specified command on the shell 102 func (c *Command) RunShell(shell, script string) error { 103 c.prepareOut() 104 105 cmd := ExecCommand(shell) 106 107 if len(c.dir) > 0 { 108 cmd.Dir = c.dir 109 } 110 111 appendEnvironment(cmd, c.env) 112 113 in := bytes.Buffer{} 114 in.Write([]byte(script)) 115 cmd.Stdin = &in 116 117 log.Entry().Infof("running shell script: %v %v", shell, script) 118 119 if err := c.runCmd(cmd); err != nil { 120 return errors.Wrapf(err, "running shell script failed with %v", shell) 121 } 122 return nil 123 } 124 125 // RunExecutable runs the specified executable with parameters 126 // !! While the cmd.Env is applied during command execution, it is NOT involved when the actual executable is resolved. 127 // 128 // Thus the executable needs to be on the PATH of the current process and it is not sufficient to alter the PATH on cmd.Env. 129 func (c *Command) RunExecutable(executable string, params ...string) error { 130 c.prepareOut() 131 132 cmd := ExecCommand(executable, params...) 133 134 if len(c.dir) > 0 { 135 cmd.Dir = c.dir 136 } 137 138 log.Entry().Infof("running command: %v %v", executable, strings.Join(params, (" "))) 139 140 appendEnvironment(cmd, c.env) 141 142 if c.stdin != nil { 143 cmd.Stdin = c.stdin 144 } 145 146 if err := c.runCmd(cmd); err != nil { 147 return errors.Wrapf(err, "running command '%v' failed", executable) 148 } 149 return nil 150 } 151 152 // RunExecutableInBackground runs the specified executable with parameters in the background non blocking 153 // !! While the cmd.Env is applied during command execution, it is NOT involved when the actual executable is resolved. 154 // 155 // Thus the executable needs to be on the PATH of the current process and it is not sufficient to alter the PATH on cmd.Env. 156 func (c *Command) RunExecutableInBackground(executable string, params ...string) (Execution, error) { 157 c.prepareOut() 158 159 cmd := ExecCommand(executable, params...) 160 161 if len(c.dir) > 0 { 162 cmd.Dir = c.dir 163 } 164 165 log.Entry().Infof("running command: %v %v", executable, strings.Join(params, (" "))) 166 167 appendEnvironment(cmd, c.env) 168 169 if c.stdin != nil { 170 cmd.Stdin = c.stdin 171 } 172 173 execution, err := c.startCmd(cmd) 174 if err != nil { 175 return nil, errors.Wrapf(err, "starting command '%v' failed", executable) 176 } 177 178 return execution, nil 179 } 180 181 // GetExitCode allows to retrieve the exit code of a command execution 182 func (c *Command) GetExitCode() int { 183 return c.exitCode 184 } 185 186 func appendEnvironment(cmd *exec.Cmd, env []string) { 187 if len(env) > 0 { 188 189 // When cmd.Env is nil the environment variables from the current 190 // process are also used by the forked process. Our environment variables 191 // should not replace the existing environment, but they should be appended. 192 // Hence we populate cmd.Env first with the current environment in case we 193 // find it empty. In case there is already something, we append to that environment. 194 // In that case we assume the current values of `cmd.Env` has either been setup based 195 // on `os.Environ()` or that was initialized in another way for a good reason. 196 // 197 // In case we have the same environment variable as in the current environment (`os.Environ()`) 198 // and in `env`, the environment variable from `env` is effectively used since this is the 199 // later one. There is no merging between both environment variables. 200 // 201 // cf. https://golang.org/pkg/os/exec/#Command 202 // If Env contains duplicate environment keys, only the last 203 // value in the slice for each duplicate key is used. 204 205 if len(cmd.Env) == 0 { 206 cmd.Env = os.Environ() 207 } 208 cmd.Env = append(cmd.Env, env...) 209 } 210 } 211 212 func (c *Command) startCmd(cmd *exec.Cmd) (*execution, error) { 213 stdout, stderr, err := cmdPipes(cmd) 214 if err != nil { 215 return nil, errors.Wrap(err, "getting command pipes failed") 216 } 217 218 err = cmd.Start() 219 if err != nil { 220 return nil, errors.Wrap(err, "starting command failed") 221 } 222 223 execution := execution{cmd: cmd, ul: log.NewURLLogger(c.StepName)} 224 execution.wg.Add(2) 225 226 srcOut := stdout 227 srcErr := stderr 228 229 if c.ErrorCategoryMapping != nil { 230 prOut, pwOut := io.Pipe() 231 trOut := io.TeeReader(stdout, pwOut) 232 srcOut = prOut 233 234 prErr, pwErr := io.Pipe() 235 trErr := io.TeeReader(stderr, pwErr) 236 srcErr = prErr 237 238 execution.wg.Add(2) 239 240 go func() { 241 defer execution.wg.Done() 242 defer pwOut.Close() 243 c.scanLog(trOut) 244 }() 245 246 go func() { 247 defer execution.wg.Done() 248 defer pwErr.Close() 249 c.scanLog(trErr) 250 }() 251 } 252 253 go func() { 254 if c.StepName != "" { 255 var buf bytes.Buffer 256 br := bufio.NewWriter(&buf) 257 _, execution.errCopyStdout = piperutils.CopyData(io.MultiWriter(c.stdout, br), srcOut) 258 br.Flush() 259 execution.ul.Parse(buf) 260 } else { 261 _, execution.errCopyStdout = piperutils.CopyData(c.stdout, srcOut) 262 } 263 execution.wg.Done() 264 }() 265 266 go func() { 267 if c.StepName != "" { 268 var buf bytes.Buffer 269 bw := bufio.NewWriter(&buf) 270 _, execution.errCopyStderr = piperutils.CopyData(io.MultiWriter(c.stderr, bw), srcErr) 271 bw.Flush() 272 execution.ul.Parse(buf) 273 } else { 274 _, execution.errCopyStderr = piperutils.CopyData(c.stderr, srcErr) 275 } 276 execution.wg.Done() 277 }() 278 279 return &execution, nil 280 } 281 282 func (c *Command) scanLog(in io.Reader) { 283 scanner := bufio.NewScanner(in) 284 scanner.Split(scanShortLines) 285 for scanner.Scan() { 286 line := scanner.Text() 287 c.parseConsoleErrors(line) 288 } 289 if err := scanner.Err(); err != nil { 290 log.Entry().WithError(err).Info("failed to scan log file") 291 } 292 } 293 294 func scanShortLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 295 lenData := len(data) 296 if atEOF && lenData == 0 { 297 return 0, nil, nil 298 } 299 if lenData > 32767 && !bytes.Contains(data[0:lenData], []byte("\n")) { 300 // we will neglect long output 301 // no use cases known where this would be relevant 302 // current accepted implication: error pattern would not be found 303 // -> resulting in wrong error categorization 304 return lenData, nil, nil 305 } 306 if i := bytes.IndexByte(data, '\n'); i >= 0 && i < 32767 { 307 // We have a full newline-terminated line with a size limit 308 // Size limit is required since otherwise scanner would stall 309 return i + 1, data[0:i], nil 310 } 311 // If we're at EOF, we have a final, non-terminated line. Return it. 312 if atEOF { 313 return len(data), data, nil 314 } 315 // Request more data. 316 return 0, nil, nil 317 } 318 319 func (c *Command) parseConsoleErrors(logLine string) { 320 for category, categoryErrors := range c.ErrorCategoryMapping { 321 for _, errorPart := range categoryErrors { 322 if matchPattern(logLine, errorPart) { 323 log.SetErrorCategory(log.ErrorCategoryByString(category)) 324 return 325 } 326 } 327 } 328 } 329 330 func matchPattern(text, pattern string) bool { 331 if len(pattern) == 0 && len(text) != 0 { 332 return false 333 } 334 parts := strings.Split(pattern, "*") 335 for _, part := range parts { 336 if !strings.Contains(text, part) { 337 return false 338 } 339 } 340 return true 341 } 342 343 func (c *Command) runCmd(cmd *exec.Cmd) error { 344 execution, err := c.startCmd(cmd) 345 if err != nil { 346 return err 347 } 348 349 err = execution.Wait() 350 351 if execution.errCopyStdout != nil || execution.errCopyStderr != nil { 352 return fmt.Errorf("failed to capture stdout/stderr: '%v'/'%v'", execution.errCopyStdout, execution.errCopyStderr) 353 } 354 355 if err != nil { 356 // provide fallback to ensure a non 0 exit code in case of an error 357 c.exitCode = 1 358 // try to identify the detailed error code 359 if exitErr, ok := err.(*exec.ExitError); ok { 360 if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { 361 c.exitCode = status.ExitStatus() 362 } 363 } 364 return errors.Wrap(err, "cmd.Run() failed") 365 } 366 c.exitCode = 0 367 return nil 368 } 369 370 func (c *Command) prepareOut() { 371 // ToDo: check use of multiwriter instead to always write into os.Stdout and os.Stdin? 372 // stdout := io.MultiWriter(os.Stdout, &stdoutBuf) 373 // stderr := io.MultiWriter(os.Stderr, &stderrBuf) 374 375 if c.stdout == nil { 376 c.stdout = os.Stdout 377 } 378 if c.stderr == nil { 379 c.stderr = os.Stderr 380 } 381 } 382 383 func cmdPipes(cmd *exec.Cmd) (io.ReadCloser, io.ReadCloser, error) { 384 stdout, err := cmd.StdoutPipe() 385 if err != nil { 386 return nil, nil, errors.Wrap(err, "getting Stdout pipe failed") 387 } 388 389 stderr, err := cmd.StderrPipe() 390 if err != nil { 391 return nil, nil, errors.Wrap(err, "getting Stderr pipe failed") 392 } 393 394 return stdout, stderr, nil 395 }