Thank you for posting your sample code. What you are seeing is normal behaviour, but it’s a little unusual. What happens is your program forks /bin/sh, which forks sleep. Now you are waiting on CombinedOutput, which means the stdout and stderr from the command you executed, /bin/sh, which has forked sleep, then returned.
lucky(~/src/t) % ./t &
[1] 29285
lucky(~/src/t) % ps -lf
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
0 S dfc 28919 28918 0 80 0 - 6125 wait 22:08 pts/26 00:00:00 -bash
0 S dfc 29285 28919 0 80 0 - 809 futex_ 22:16 pts/26 00:00:00 ./t
0 S dfc 29290 1 0 80 0 - 1798 hrtime 22:16 pts/26 00:00:00 sleep 1000
4 R dfc 29291 28919 0 80 0 - 4611 - 22:16 pts/26 00:00:00 ps -lf
Here t is your program (i just called it that), and modified it to run sleep longer so I could get the rest of these details. So, you see t, your program is running, and so is sleep. Let’s look at stdout and stderr are connected to.
You’ll also note that the PPID of sleep is 1, as it’s been “daemonised” (sorta).
lucky(~/src/t) % ls -al /proc/{29285,29290}/fd
/proc/29285/fd:
total 0
dr-x------ 2 dfc dfc 0 Mar 10 22:16 .
dr-xr-xr-x 9 dfc dfc 0 Mar 10 22:16 ..
lrwx------ 1 dfc dfc 64 Mar 10 22:17 0 -> /dev/pts/26
lrwx------ 1 dfc dfc 64 Mar 10 22:17 1 -> /dev/pts/26
lrwx------ 1 dfc dfc 64 Mar 10 22:16 2 -> /dev/pts/26
lr-x------ 1 dfc dfc 64 Mar 10 22:17 4 -> pipe:[4788359]
This is t, note file descriptor four is connected to a pipe, this is the connection to the command you executed. There is only one pipe because we gave the same file descriptor to the child for both stdout and stderr, this is what CombinedOutput does.
So, t is connected to the child process and is waiting for fd 4 to be closed. Let’s look at the other end
/proc/29290/fd:
total 0
dr-x------ 2 dfc dfc 0 Mar 10 22:16 .
dr-xr-xr-x 9 dfc dfc 0 Mar 10 22:16 ..
lr-x------ 1 dfc dfc 64 Mar 10 22:17 0 -> /dev/null
l-wx------ 1 dfc dfc 64 Mar 10 22:17 1 -> pipe:[4788359]
l-wx------ 1 dfc dfc 64 Mar 10 22:16 2 -> pipe:[4788359]
This is the child process, sleep, stdin is connected to /dev/null, which makes sense, you didn’t supply anything for that (check the docs on exec.Cmd for details), and both 1 and 2, stdout and stderr are connected to the same pipe.
Now, sleep doesn’t say anything, so nothing will be sent back to t on the pipe, when sleep does finally exit, the operating system will close all the file descriptors associated with this process. So, the write side of the pipe will be closed, which will send EOF to the read side, which will unblock CombinedOutput and t will exit.
So, why does this behave differently when you just call cmd.Run ? Because your program forks /bin/sh, which then forks sleep, and returns. Your program is not waiting to read the output of the program, it’s just looking for the exit status of /bin/sh, which returns immediately.
Here are some exercises:
- Modify your code sample to use cmd.StdoutPipe
- Observe the changes in behaviour between waiting on cmd.StdoutPipe().Read() to return io.EOF (ioutils.ReadAll will be useful here) and another goroutine waiting on cmd.Run(). Be sure not to let your main goroutine exit til the other one reading from the pipe has exited.