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.outdirectly 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 itssetpgidis 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 thesetpgidwill 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 -itsomething, 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:
signalorsigactionto make the process ignore theSIGTTINsignalsigprocmaskto block the process from receiving theSIGTTINsignaltcsetpgrp(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
SIGTTINdoesn'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
TOSTOPset (which is not the default), then you'd have to repeat the fix forSIGTTOUor for standard output and standard error (except for option 4, which wouldn't need to be repeated at all).