package integration import ( "archive/tar" "bytes" "encoding/json" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "reflect" "strings" "syscall" "time" "github.com/docker/docker/pkg/stringutils" ) // GetExitCode returns the ExitStatus of the specified error if its type is // exec.ExitError, returns 0 and an error otherwise. func GetExitCode(err error) (int, error) { exitCode := 0 if exiterr, ok := err.(*exec.ExitError); ok { if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { return procExit.ExitStatus(), nil } } return exitCode, fmt.Errorf("failed to get exit code") } // ProcessExitCode process the specified error and returns the exit status code // if the error was of type exec.ExitError, returns nothing otherwise. func ProcessExitCode(err error) (exitCode int) { if err != nil { var exiterr error if exitCode, exiterr = GetExitCode(err); exiterr != nil { // TODO: Fix this so we check the error's text. // we've failed to retrieve exit code, so we set it to 127 exitCode = 127 } } return } // IsKilled process the specified error and returns whether the process was killed or not. func IsKilled(err error) bool { if exitErr, ok := err.(*exec.ExitError); ok { status, ok := exitErr.Sys().(syscall.WaitStatus) if !ok { return false } // status.ExitStatus() is required on Windows because it does not // implement Signal() nor Signaled(). Just check it had a bad exit // status could mean it was killed (and in tests we do kill) return (status.Signaled() && status.Signal() == os.Kill) || status.ExitStatus() != 0 } return false } // RunCommandWithOutput runs the specified command and returns the combined output (stdout/stderr) // with the exitCode different from 0 and the error if something bad happened func RunCommandWithOutput(cmd *exec.Cmd) (output string, exitCode int, err error) { exitCode = 0 out, err := cmd.CombinedOutput() exitCode = ProcessExitCode(err) output = string(out) return } // RunCommandWithStdoutStderr runs the specified command and returns stdout and stderr separately // with the exitCode different from 0 and the error if something bad happened func RunCommandWithStdoutStderr(cmd *exec.Cmd) (stdout string, stderr string, exitCode int, err error) { var ( stderrBuffer, stdoutBuffer bytes.Buffer ) exitCode = 0 cmd.Stderr = &stderrBuffer cmd.Stdout = &stdoutBuffer err = cmd.Run() exitCode = ProcessExitCode(err) stdout = stdoutBuffer.String() stderr = stderrBuffer.String() return } // RunCommandWithOutputForDuration runs the specified command "timeboxed" by the specified duration. // If the process is still running when the timebox is finished, the process will be killed and . // It will returns the output with the exitCode different from 0 and the error if something bad happened // and a boolean whether it has been killed or not. func RunCommandWithOutputForDuration(cmd *exec.Cmd, duration time.Duration) (output string, exitCode int, timedOut bool, err error) { var outputBuffer bytes.Buffer if cmd.Stdout != nil { err = errors.New("cmd.Stdout already set") return } cmd.Stdout = &outputBuffer if cmd.Stderr != nil { err = errors.New("cmd.Stderr already set") return } cmd.Stderr = &outputBuffer // Start the command in the main thread.. err = cmd.Start() if err != nil { err = fmt.Errorf("Fail to start command %v : %v", cmd, err) } type exitInfo struct { exitErr error exitCode int } done := make(chan exitInfo, 1) go func() { // And wait for it to exit in the goroutine :) info := exitInfo{} info.exitErr = cmd.Wait() info.exitCode = ProcessExitCode(info.exitErr) done <- info }() select { case <-time.After(duration): killErr := cmd.Process.Kill() if killErr != nil { fmt.Printf("failed to kill (pid=%d): %v\n", cmd.Process.Pid, killErr) } timedOut = true case info := <-done: err = info.exitErr exitCode = info.exitCode } output = outputBuffer.String() return } var errCmdTimeout = fmt.Errorf("command timed out") // RunCommandWithOutputAndTimeout runs the specified command "timeboxed" by the specified duration. // It returns the output with the exitCode different from 0 and the error if something bad happened or // if the process timed out (and has been killed). func RunCommandWithOutputAndTimeout(cmd *exec.Cmd, timeout time.Duration) (output string, exitCode int, err error) { var timedOut bool output, exitCode, timedOut, err = RunCommandWithOutputForDuration(cmd, timeout) if timedOut { err = errCmdTimeout } return } // RunCommand runs the specified command and returns the exitCode different from 0 // and the error if something bad happened. func RunCommand(cmd *exec.Cmd) (exitCode int, err error) { exitCode = 0 err = cmd.Run() exitCode = ProcessExitCode(err) return } // RunCommandPipelineWithOutput runs the array of commands with the output // of each pipelined with the following (like cmd1 | cmd2 | cmd3 would do). // It returns the final output, the exitCode different from 0 and the error // if something bad happened. func RunCommandPipelineWithOutput(cmds ...*exec.Cmd) (output string, exitCode int, err error) { if len(cmds) < 2 { return "", 0, errors.New("pipeline does not have multiple cmds") } // connect stdin of each cmd to stdout pipe of previous cmd for i, cmd := range cmds { if i > 0 { prevCmd := cmds[i-1] cmd.Stdin, err = prevCmd.StdoutPipe() if err != nil { return "", 0, fmt.Errorf("cannot set stdout pipe for %s: %v", cmd.Path, err) } } } // start all cmds except the last for _, cmd := range cmds[:len(cmds)-1] { if err = cmd.Start(); err != nil { return "", 0, fmt.Errorf("starting %s failed with error: %v", cmd.Path, err) } } var pipelineError error defer func() { // wait all cmds except the last to release their resources for _, cmd := range cmds[:len(cmds)-1] { if err := cmd.Wait(); err != nil { pipelineError = fmt.Errorf("command %s failed with error: %v", cmd.Path, err) break } } }() if pipelineError != nil { return "", 0, pipelineError } // wait on last cmd return RunCommandWithOutput(cmds[len(cmds)-1]) } // UnmarshalJSON deserialize a JSON in the given interface. func UnmarshalJSON(data []byte, result interface{}) error { if err := json.Unmarshal(data, result); err != nil { return err } return nil } // ConvertSliceOfStringsToMap converts a slices of string in a map // with the strings as key and an empty string as values. func ConvertSliceOfStringsToMap(input []string) map[string]struct{} { output := make(map[string]struct{}) for _, v := range input { output[v] = struct{}{} } return output } // CompareDirectoryEntries compares two sets of FileInfo (usually taken from a directory) // and returns an error if different. func CompareDirectoryEntries(e1 []os.FileInfo, e2 []os.FileInfo) error { var ( e1Entries = make(map[string]struct{}) e2Entries = make(map[string]struct{}) ) for _, e := range e1 { e1Entries[e.Name()] = struct{}{} } for _, e := range e2 { e2Entries[e.Name()] = struct{}{} } if !reflect.DeepEqual(e1Entries, e2Entries) { return fmt.Errorf("entries differ") } return nil } // ListTar lists the entries of a tar. func ListTar(f io.Reader) ([]string, error) { tr := tar.NewReader(f) var entries []string for { th, err := tr.Next() if err == io.EOF { // end of tar archive return entries, nil } if err != nil { return entries, err } entries = append(entries, th.Name) } } // RandomTmpDirPath provides a temporary path with rand string appended. // does not create or checks if it exists. func RandomTmpDirPath(s string, platform string) string { tmp := "/tmp" if platform == "windows" { tmp = os.Getenv("TEMP") } path := filepath.Join(tmp, fmt.Sprintf("%s.%s", s, stringutils.GenerateRandomAlphaOnlyString(10))) if platform == "windows" { return filepath.FromSlash(path) // Using \ } return filepath.ToSlash(path) // Using / } // ConsumeWithSpeed reads chunkSize bytes from reader before sleeping // for interval duration. Returns total read bytes. Send true to the // stop channel to return before reading to EOF on the reader. func ConsumeWithSpeed(reader io.Reader, chunkSize int, interval time.Duration, stop chan bool) (n int, err error) { buffer := make([]byte, chunkSize) for { var readBytes int readBytes, err = reader.Read(buffer) n += readBytes if err != nil { if err == io.EOF { err = nil } return } select { case <-stop: return case <-time.After(interval): } } } // ParseCgroupPaths parses 'procCgroupData', which is output of '/proc//cgroup', and returns // a map which cgroup name as key and path as value. func ParseCgroupPaths(procCgroupData string) map[string]string { cgroupPaths := map[string]string{} for _, line := range strings.Split(procCgroupData, "\n") { parts := strings.Split(line, ":") if len(parts) != 3 { continue } cgroupPaths[parts[1]] = parts[2] } return cgroupPaths } // ChannelBuffer holds a chan of byte array that can be populate in a goroutine. type ChannelBuffer struct { C chan []byte } // Write implements Writer. func (c *ChannelBuffer) Write(b []byte) (int, error) { c.C <- b return len(b), nil } // Close closes the go channel. func (c *ChannelBuffer) Close() error { close(c.C) return nil } // ReadTimeout reads the content of the channel in the specified byte array with // the specified duration as timeout. func (c *ChannelBuffer) ReadTimeout(p []byte, n time.Duration) (int, error) { select { case b := <-c.C: return copy(p[0:], b), nil case <-time.After(n): return -1, fmt.Errorf("timeout reading from channel") } } // RunAtDifferentDate runs the specified function with the given time. // It changes the date of the system, which can led to weird behaviors. func RunAtDifferentDate(date time.Time, block func()) { // Layout for date. MMDDhhmmYYYY const timeLayout = "010203042006" // Ensure we bring time back to now now := time.Now().Format(timeLayout) dateReset := exec.Command("date", now) defer RunCommand(dateReset) dateChange := exec.Command("date", date.Format(timeLayout)) RunCommand(dateChange) block() return }