Given a Linux system, in Haskell GHCi 8.8.3, I can run a Docker command with:
System.Process> withCreateProcess (shell "docker run -it alpine sh -c \"echo hello\""){create_group=False} $ \_ _ _ pid -> waitForProcess pid
hello
ExitSuccess
However, when I switch to create_group=True
the process hangs. The effect of create_group
is to call set_pgid
with 0
in the child, and pid
in the parent. Why does that change cause a hang? Is this a bug in Docker? A bug in System.Process? Or an unfortunate but necessary interaction?
This isn't a bug in Haskell or a bug in Docker, but rather just the way that process groups work. Consider this C program:
If you compile that and run
./a.out
directly from your interactive shell, it will print "hello" as you'd expect. This is unsurprising, since the shell will have already put it in its own process group, so itssetpgid
is a no-op. If you run it with an intermediary program that forks a child to run it (sh -c ./a.out
,\time ./a.out
- note the backslash,strace ./a.out
, etc.), then thesetpgid
will put it in a new process group, and it will hang like it does in Haskell.The reason for the hang is explained in "Job Control Signals" in the glibc manual:
When you
docker run -it
something, Docker will attempt to read from stdin even if the command inside the container doesn't. Since you just created a new process group, and you didn't set it to be in the foreground, it counts as a background job. As such, Docker is getting stopped withSIGTTIN
, which causes it to appear to hang.Here's a list of options to fix this:
signal
orsigaction
to make the process ignore theSIGTTIN
signalsigprocmask
to block the process from receiving theSIGTTIN
signaltcsetpgrp(0, getpid())
to make your new process group be the foreground process group (note: this is the most complicated, since it will itself causeSIGTTOU
, so you'd have to ignore that signal at least temporarily anyway)Options 2 and 3 will also only work if the program doesn't actually need stdin, which is the case with Docker. When
SIGTTIN
doesn't stop the process, reads from stdin will still fail withEIO
, so if there's actually data you want to read, then you need to go with option 4 (and remember to set it back once the child exits).If you have
TOSTOP
set (which is not the default), then you'd have to repeat the fix forSIGTTOU
or for standard output and standard error (except for option 4, which wouldn't need to be repeated at all).