Skip to content

os/exec: Inconsistent behaviour in exec.Cmd.Wait #20730

Closed
@eZanmoto

Description

@eZanmoto

What version of Go are you using (go version)?

go version go1.8.3 linux/amd64

Note that I'm running Go using Docker for reproducibility (see below).

What operating system and processor architecture are you using (go env)?

GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/go"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build270338682=/tmp/go-build -gno-record-gcc-switches"
CXX="g++"
CGO_ENABLED="1"
PKG_CONFIG="pkg-config"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"

What did you do?

I created main.go, as follows:

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "os/exec"
    "time"
)

func main() {
    cmd := exec.Command("git", "daemon")
    cmd.Stdout = ioutil.Discard
    cmd.Stderr = ioutil.Discard
    // cmd.Stdout = os.Stdout
    // cmd.Stderr = os.Stderr

    if err := cmd.Start(); err != nil {
        die("%v", err)
    }

    fmt.Println("Waiting for process to start...")
    time.Sleep(time.Second*2)

    wait := make(chan struct{})
    go func() {
        fmt.Println("Calling `cmd.Wait`...")
        err := cmd.Wait()
        fmt.Println("Process exited:", err)
        close(wait)
        fmt.Println("Broadcasted wait signal")
    }()

    fmt.Println("Waiting for `cmd.Wait` to be called...")
    time.Sleep(time.Second*2)

    fmt.Println("Sending kill signal to process...")
    proc := cmd.Process
    if err := proc.Kill(); err != nil {
        die("%v", err)
    }

    fmt.Println("Waiting for wait signal or timeout...")
    time.Sleep(time.Second*2)
    select {
    case <-wait:
        fmt.Println("Got wait signal")
    case <-time.After(time.Second*5):
        fmt.Println("Timeout.")
        fmt.Println("Checking process state (", cmd.ProcessState, ")")
        if cmd.ProcessState != nil {
            fmt.Println("Confirmed process exited.")
            fmt.Println("Retrying for wait signal or timeout...")
            select {
            case <-wait:
                fmt.Println("Got wait signal")
            case <-time.After(time.Second*5):
                fmt.Println("Timeout.")
            }
        }
    }

    out, err := exec.Command("bash", "-c", "ps aux | grep git").CombinedOutput()
    if err != nil {
        die("%v", err)
    }
    fmt.Println(string(out))
}

func die(msg string, args ...interface{}) {
    fmt.Fprintf(os.Stderr, msg+"\n", args...)
    os.Exit(1)
}

I ran it with the following Docker command:

docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:1.8 \
    go run main.go

I then uncommented the commented lines and ran the same command again.

What did you expect to see?

I expected the output of both invocations to match.

What did you see instead?

Running the program with output redirected to ioutil.Discard produced the
following output:

Waiting for process to start...
Waiting for `cmd.Wait` to be called...
Calling `cmd.Wait`...
Sending kill signal to process...
Waiting for wait signal or timeout...
Timeout.
Checking process state ( signal: killed )
Confirmed process exited.
Retrying for wait signal or timeout...
Timeout.
root        27  0.0  0.1  16268  1992 ?        S    17:21   0:00 git-daemon
root        29  0.0  0.2  20040  2748 ?        S    17:22   0:00 bash -c ps aux | grep git
root        31  0.0  0.0  11128   992 ?        S    17:22   0:00 grep git

Running the program with output redirected to io.Stdout and io.Stderr
produced the following output:

Waiting for process to start...
Waiting for `cmd.Wait` to be called...
Calling `cmd.Wait`...
Sending kill signal to process...
Waiting for wait signal or timeout...
Process exited: signal: killed
Broadcasted wait signal
Got wait signal
root        26  0.0  0.1  16268  1960 ?        S    17:21   0:00 git-daemon
root        28  0.0  0.2  20040  2752 ?        S    17:21   0:00 bash -c ps aux | grep git
root        31  0.0  0.0  11128   980 ?        S    17:21   0:00 grep git

The core issue in the above is that cmd.Wait() doesn't return when the command
exits in the case where ioutil.Discard is being used.

Additional notes

I consider the second invocation to have the correct and expected output. This
opinion is backed up as it conforms with the operation of os.Process; a quick
test of this is to replace cmd.Wait() in the above code with
cmd.Process.Wait() in both invocations and see that the output matches that of
the second invocation above.

The problem seems related to the child processes spawned by git daemon. This
can be further demonstrated by instead spawning go run hello.go with different
implementations of hello.go, such as the following:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world!")
}

main.go runs without producing a timeout with this implementation of
hello.go. Adding a time.Sleep(time.Second*5) before the Println in this
implementation also causes no timeout. However, if the Println is instead put
in an infinite for loop then a timeout occurs.

It appears that exec.Cmd.Wait currently requires all processes in the process
tree of the spawned process to exit before it can return. I suggest either
updating the behaviour to match that of os.Process, or else updating the
documentation to reflect this behaviour.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions