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  }