pkg/integration/utils.go
Aaron Lehmann eb7916eb53 Fix race in RunCommandWithOutputForDuration
This function was starting a goroutine that modifies one of its return
values. The intent is for the goroutine to only influence the return
value when it's causing the function to return, but it's racy and can
also modify the return value when the function is returning due to the
timeout. Fix the goroutine to not modify return values directly.

Also, give the channel a buffer so that the goroutine doesn't block
forever after a timeout.

Fixes #18305

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
2015-12-03 16:34:01 -08:00

354 lines
9.7 KiB
Go

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)
}
}
defer func() {
// wait all cmds except the last to release their resources
for _, cmd := range cmds[:len(cmds)-1] {
cmd.Wait()
}
}()
// 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 after every interval.
// Returns total read bytes.
func ConsumeWithSpeed(reader io.Reader, chunkSize int, interval time.Duration, stop chan bool) (n int, err error) {
buffer := make([]byte, chunkSize)
for {
select {
case <-stop:
return
default:
var readBytes int
readBytes, err = reader.Read(buffer)
n += readBytes
if err != nil {
if err == io.EOF {
err = nil
}
return
}
time.Sleep(interval)
}
}
}
// ParseCgroupPaths arses 'procCgroupData', which is output of '/proc/<pid>/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 specifed 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
}