github.com/sijibomii/docker@v0.0.0-20231230191044-5cf6ca554647/pkg/integration/utils.go (about)

     1  package integration
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"reflect"
    14  	"strings"
    15  	"syscall"
    16  	"time"
    17  
    18  	"github.com/docker/docker/pkg/stringutils"
    19  )
    20  
    21  // GetExitCode returns the ExitStatus of the specified error if its type is
    22  // exec.ExitError, returns 0 and an error otherwise.
    23  func GetExitCode(err error) (int, error) {
    24  	exitCode := 0
    25  	if exiterr, ok := err.(*exec.ExitError); ok {
    26  		if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok {
    27  			return procExit.ExitStatus(), nil
    28  		}
    29  	}
    30  	return exitCode, fmt.Errorf("failed to get exit code")
    31  }
    32  
    33  // ProcessExitCode process the specified error and returns the exit status code
    34  // if the error was of type exec.ExitError, returns nothing otherwise.
    35  func ProcessExitCode(err error) (exitCode int) {
    36  	if err != nil {
    37  		var exiterr error
    38  		if exitCode, exiterr = GetExitCode(err); exiterr != nil {
    39  			// TODO: Fix this so we check the error's text.
    40  			// we've failed to retrieve exit code, so we set it to 127
    41  			exitCode = 127
    42  		}
    43  	}
    44  	return
    45  }
    46  
    47  // IsKilled process the specified error and returns whether the process was killed or not.
    48  func IsKilled(err error) bool {
    49  	if exitErr, ok := err.(*exec.ExitError); ok {
    50  		status, ok := exitErr.Sys().(syscall.WaitStatus)
    51  		if !ok {
    52  			return false
    53  		}
    54  		// status.ExitStatus() is required on Windows because it does not
    55  		// implement Signal() nor Signaled(). Just check it had a bad exit
    56  		// status could mean it was killed (and in tests we do kill)
    57  		return (status.Signaled() && status.Signal() == os.Kill) || status.ExitStatus() != 0
    58  	}
    59  	return false
    60  }
    61  
    62  // RunCommandWithOutput runs the specified command and returns the combined output (stdout/stderr)
    63  // with the exitCode different from 0 and the error if something bad happened
    64  func RunCommandWithOutput(cmd *exec.Cmd) (output string, exitCode int, err error) {
    65  	exitCode = 0
    66  	out, err := cmd.CombinedOutput()
    67  	exitCode = ProcessExitCode(err)
    68  	output = string(out)
    69  	return
    70  }
    71  
    72  // RunCommandWithStdoutStderr runs the specified command and returns stdout and stderr separately
    73  // with the exitCode different from 0 and the error if something bad happened
    74  func RunCommandWithStdoutStderr(cmd *exec.Cmd) (stdout string, stderr string, exitCode int, err error) {
    75  	var (
    76  		stderrBuffer, stdoutBuffer bytes.Buffer
    77  	)
    78  	exitCode = 0
    79  	cmd.Stderr = &stderrBuffer
    80  	cmd.Stdout = &stdoutBuffer
    81  	err = cmd.Run()
    82  	exitCode = ProcessExitCode(err)
    83  
    84  	stdout = stdoutBuffer.String()
    85  	stderr = stderrBuffer.String()
    86  	return
    87  }
    88  
    89  // RunCommandWithOutputForDuration runs the specified command "timeboxed" by the specified duration.
    90  // If the process is still running when the timebox is finished, the process will be killed and .
    91  // It will returns the output with the exitCode different from 0 and the error if something bad happened
    92  // and a boolean whether it has been killed or not.
    93  func RunCommandWithOutputForDuration(cmd *exec.Cmd, duration time.Duration) (output string, exitCode int, timedOut bool, err error) {
    94  	var outputBuffer bytes.Buffer
    95  	if cmd.Stdout != nil {
    96  		err = errors.New("cmd.Stdout already set")
    97  		return
    98  	}
    99  	cmd.Stdout = &outputBuffer
   100  
   101  	if cmd.Stderr != nil {
   102  		err = errors.New("cmd.Stderr already set")
   103  		return
   104  	}
   105  	cmd.Stderr = &outputBuffer
   106  
   107  	// Start the command in the main thread..
   108  	err = cmd.Start()
   109  	if err != nil {
   110  		err = fmt.Errorf("Fail to start command %v : %v", cmd, err)
   111  	}
   112  
   113  	type exitInfo struct {
   114  		exitErr  error
   115  		exitCode int
   116  	}
   117  
   118  	done := make(chan exitInfo, 1)
   119  
   120  	go func() {
   121  		// And wait for it to exit in the goroutine :)
   122  		info := exitInfo{}
   123  		info.exitErr = cmd.Wait()
   124  		info.exitCode = ProcessExitCode(info.exitErr)
   125  		done <- info
   126  	}()
   127  
   128  	select {
   129  	case <-time.After(duration):
   130  		killErr := cmd.Process.Kill()
   131  		if killErr != nil {
   132  			fmt.Printf("failed to kill (pid=%d): %v\n", cmd.Process.Pid, killErr)
   133  		}
   134  		timedOut = true
   135  	case info := <-done:
   136  		err = info.exitErr
   137  		exitCode = info.exitCode
   138  	}
   139  	output = outputBuffer.String()
   140  	return
   141  }
   142  
   143  var errCmdTimeout = fmt.Errorf("command timed out")
   144  
   145  // RunCommandWithOutputAndTimeout runs the specified command "timeboxed" by the specified duration.
   146  // It returns the output with the exitCode different from 0 and the error if something bad happened or
   147  // if the process timed out (and has been killed).
   148  func RunCommandWithOutputAndTimeout(cmd *exec.Cmd, timeout time.Duration) (output string, exitCode int, err error) {
   149  	var timedOut bool
   150  	output, exitCode, timedOut, err = RunCommandWithOutputForDuration(cmd, timeout)
   151  	if timedOut {
   152  		err = errCmdTimeout
   153  	}
   154  	return
   155  }
   156  
   157  // RunCommand runs the specified command and returns the exitCode different from 0
   158  // and the error if something bad happened.
   159  func RunCommand(cmd *exec.Cmd) (exitCode int, err error) {
   160  	exitCode = 0
   161  	err = cmd.Run()
   162  	exitCode = ProcessExitCode(err)
   163  	return
   164  }
   165  
   166  // RunCommandPipelineWithOutput runs the array of commands with the output
   167  // of each pipelined with the following (like cmd1 | cmd2 | cmd3 would do).
   168  // It returns the final output, the exitCode different from 0 and the error
   169  // if something bad happened.
   170  func RunCommandPipelineWithOutput(cmds ...*exec.Cmd) (output string, exitCode int, err error) {
   171  	if len(cmds) < 2 {
   172  		return "", 0, errors.New("pipeline does not have multiple cmds")
   173  	}
   174  
   175  	// connect stdin of each cmd to stdout pipe of previous cmd
   176  	for i, cmd := range cmds {
   177  		if i > 0 {
   178  			prevCmd := cmds[i-1]
   179  			cmd.Stdin, err = prevCmd.StdoutPipe()
   180  
   181  			if err != nil {
   182  				return "", 0, fmt.Errorf("cannot set stdout pipe for %s: %v", cmd.Path, err)
   183  			}
   184  		}
   185  	}
   186  
   187  	// start all cmds except the last
   188  	for _, cmd := range cmds[:len(cmds)-1] {
   189  		if err = cmd.Start(); err != nil {
   190  			return "", 0, fmt.Errorf("starting %s failed with error: %v", cmd.Path, err)
   191  		}
   192  	}
   193  
   194  	var pipelineError error
   195  	defer func() {
   196  		// wait all cmds except the last to release their resources
   197  		for _, cmd := range cmds[:len(cmds)-1] {
   198  			if err := cmd.Wait(); err != nil {
   199  				pipelineError = fmt.Errorf("command %s failed with error: %v", cmd.Path, err)
   200  				break
   201  			}
   202  		}
   203  	}()
   204  	if pipelineError != nil {
   205  		return "", 0, pipelineError
   206  	}
   207  
   208  	// wait on last cmd
   209  	return RunCommandWithOutput(cmds[len(cmds)-1])
   210  }
   211  
   212  // UnmarshalJSON deserialize a JSON in the given interface.
   213  func UnmarshalJSON(data []byte, result interface{}) error {
   214  	if err := json.Unmarshal(data, result); err != nil {
   215  		return err
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  // ConvertSliceOfStringsToMap converts a slices of string in a map
   222  // with the strings as key and an empty string as values.
   223  func ConvertSliceOfStringsToMap(input []string) map[string]struct{} {
   224  	output := make(map[string]struct{})
   225  	for _, v := range input {
   226  		output[v] = struct{}{}
   227  	}
   228  	return output
   229  }
   230  
   231  // CompareDirectoryEntries compares two sets of FileInfo (usually taken from a directory)
   232  // and returns an error if different.
   233  func CompareDirectoryEntries(e1 []os.FileInfo, e2 []os.FileInfo) error {
   234  	var (
   235  		e1Entries = make(map[string]struct{})
   236  		e2Entries = make(map[string]struct{})
   237  	)
   238  	for _, e := range e1 {
   239  		e1Entries[e.Name()] = struct{}{}
   240  	}
   241  	for _, e := range e2 {
   242  		e2Entries[e.Name()] = struct{}{}
   243  	}
   244  	if !reflect.DeepEqual(e1Entries, e2Entries) {
   245  		return fmt.Errorf("entries differ")
   246  	}
   247  	return nil
   248  }
   249  
   250  // ListTar lists the entries of a tar.
   251  func ListTar(f io.Reader) ([]string, error) {
   252  	tr := tar.NewReader(f)
   253  	var entries []string
   254  
   255  	for {
   256  		th, err := tr.Next()
   257  		if err == io.EOF {
   258  			// end of tar archive
   259  			return entries, nil
   260  		}
   261  		if err != nil {
   262  			return entries, err
   263  		}
   264  		entries = append(entries, th.Name)
   265  	}
   266  }
   267  
   268  // RandomTmpDirPath provides a temporary path with rand string appended.
   269  // does not create or checks if it exists.
   270  func RandomTmpDirPath(s string, platform string) string {
   271  	tmp := "/tmp"
   272  	if platform == "windows" {
   273  		tmp = os.Getenv("TEMP")
   274  	}
   275  	path := filepath.Join(tmp, fmt.Sprintf("%s.%s", s, stringutils.GenerateRandomAlphaOnlyString(10)))
   276  	if platform == "windows" {
   277  		return filepath.FromSlash(path) // Using \
   278  	}
   279  	return filepath.ToSlash(path) // Using /
   280  }
   281  
   282  // ConsumeWithSpeed reads chunkSize bytes from reader before sleeping
   283  // for interval duration. Returns total read bytes. Send true to the
   284  // stop channel to return before reading to EOF on the reader.
   285  func ConsumeWithSpeed(reader io.Reader, chunkSize int, interval time.Duration, stop chan bool) (n int, err error) {
   286  	buffer := make([]byte, chunkSize)
   287  	for {
   288  		var readBytes int
   289  		readBytes, err = reader.Read(buffer)
   290  		n += readBytes
   291  		if err != nil {
   292  			if err == io.EOF {
   293  				err = nil
   294  			}
   295  			return
   296  		}
   297  		select {
   298  		case <-stop:
   299  			return
   300  		case <-time.After(interval):
   301  		}
   302  	}
   303  }
   304  
   305  // ParseCgroupPaths parses 'procCgroupData', which is output of '/proc/<pid>/cgroup', and returns
   306  // a map which cgroup name as key and path as value.
   307  func ParseCgroupPaths(procCgroupData string) map[string]string {
   308  	cgroupPaths := map[string]string{}
   309  	for _, line := range strings.Split(procCgroupData, "\n") {
   310  		parts := strings.Split(line, ":")
   311  		if len(parts) != 3 {
   312  			continue
   313  		}
   314  		cgroupPaths[parts[1]] = parts[2]
   315  	}
   316  	return cgroupPaths
   317  }
   318  
   319  // ChannelBuffer holds a chan of byte array that can be populate in a goroutine.
   320  type ChannelBuffer struct {
   321  	C chan []byte
   322  }
   323  
   324  // Write implements Writer.
   325  func (c *ChannelBuffer) Write(b []byte) (int, error) {
   326  	c.C <- b
   327  	return len(b), nil
   328  }
   329  
   330  // Close closes the go channel.
   331  func (c *ChannelBuffer) Close() error {
   332  	close(c.C)
   333  	return nil
   334  }
   335  
   336  // ReadTimeout reads the content of the channel in the specified byte array with
   337  // the specified duration as timeout.
   338  func (c *ChannelBuffer) ReadTimeout(p []byte, n time.Duration) (int, error) {
   339  	select {
   340  	case b := <-c.C:
   341  		return copy(p[0:], b), nil
   342  	case <-time.After(n):
   343  		return -1, fmt.Errorf("timeout reading from channel")
   344  	}
   345  }
   346  
   347  // RunAtDifferentDate runs the specified function with the given time.
   348  // It changes the date of the system, which can led to weird behaviors.
   349  func RunAtDifferentDate(date time.Time, block func()) {
   350  	// Layout for date. MMDDhhmmYYYY
   351  	const timeLayout = "010203042006"
   352  	// Ensure we bring time back to now
   353  	now := time.Now().Format(timeLayout)
   354  	dateReset := exec.Command("date", now)
   355  	defer RunCommand(dateReset)
   356  
   357  	dateChange := exec.Command("date", date.Format(timeLayout))
   358  	RunCommand(dateChange)
   359  	block()
   360  	return
   361  }