Peach
Peach

Reputation: 97

exec.Command hangs on Bash script containing nohup

When I use Go's exec.Command{} to execute some bash script that contains nohup, it will hang forever.

I don't know what are the differences between ping and ifconfig. I tried to redirect the stdin (< /dev/null), the stdout(> /dev/null) and the stderr(2> /dev/null), and their combination, some of them work some don't.

When I use sh to execute the script, it just ends up immediately.

The Go code:

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    cmd := exec.Command("sh", "a.sh")
    out, err := cmd.Output() // Or cmd.CombinedOutput()
    fmt.Println(string(out), err)
}

The Bash script (a.sh):

#!/bin/bash

# hangs
#nohup ping localhost &

# dot not hang
nohup ifconfig &

Upvotes: 1

Views: 1834

Answers (1)

torek
torek

Reputation: 488163

(Converting comments, with glitches fixed, to answer)

The use of nohup here is mostly a red herring. The real problem is that ping never finishes. However, nohup has some extra weirdness, which you can see if you run, from an interactive terminal, these two sets of commands:

$ nohup echo foo
nohup: ignoring input and appending output to 'nohup.out'
$ cat nohup.out
foo
$ 

vs:

$ nohup echo foo </dev/null 2>&1 | cat 
foo
$ 

Note how the first one printed a weird message, and then the output foo went to a file; the second did not, and then the output foo showed up on the regular output stream. This is because POSIX says that nohup should do these redirections if appropriate.1 When run with exec.Cmd and cmd.Output, the redirections are not performed.

At the OS level, on a Linux- or other Unix-like system, the exec code creates an OS pipe object by which the invoked command can send output back to the Go runtime. (There may be a separate pipe for its stderr output, or the two may both be directed to a single pipe, depending on how you run the command; see https://golang.org/src/os/exec/exec.go#L280.) This pipe winds up being passed to ping, so that ping can keep writing output there as long as it likes.

The shell itself exits, because the command nohup ping localhost & is backgrounded. However, ping still has write access to the pipe object, so the Go runtime continues calling the OS read code until the pipe is closed—which is never. If the pipe were ever closed, the Go runtime would receive EOF and call the wait system call to collect the shell's exit status, but that never happens.

Redirecting ping's output, such that the shell itself has the only write access to the pipe, should result in the pipe being closed as soon as the shell itself exits.

(Some shells may have a builtin nohup that may behave weirdly, especially in the presence of redirection. This is true of some particularly ancient shells.)


1See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/nohup.html for complete details. The Linux variant redirects stdin as well as stdout and stderr, if the input is a terminal, and if the output and stderr are terminals. The FreeBSD variant redirects only stdout and/or stderr. The "is a terminal" test is based on the C language isatty function, which does the same thing as https://godoc.org/golang.org/x/crypto/ssh/terminal#IsTerminal.

Upvotes: 3

Related Questions