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 }