TIL that /bin/sh -c has subtle portability issues
I tried to use a Python auto-reloader program and found that it ran correctly under macOS but was failing to correctly restart the monitored program under Linux (Debian, to be specific).
After some investigation, I found that it used subprocess.Popen
with shell=True
; this basically runs /bin/sh -c <specified command>
, which itself runs the command. We initially get the following process hierarchy:
[PID X] python
\_ [PID Y] /bin/sh -c <specified command>
But then things start to differ depending on shell used. If /bin/sh
is zsh
(default under macOS) or bash
, the shell directly replaces itself with the command being run (using exec
), and we get:
[PID X] python
\_ [PID Y] <specified command>
But if /bin/sh is dash
(default under Debian), it runs the command in a subprocess and waits for it to exit. This gives:
[PID X] python
\_ [PID Y] /bin/sh -c <specified command>
\_ [PID Z] <specified command>
And this is were things break if if the Python program calls kill()
to kill the started process. In both cases, it will send a KILL
signal to PID Y; under macOS, this will kill the process, but under Debian it will kill the intermediate shell and leave the program running!
Moral of the story:
- Portability is hard
- This is yet another reason to not use shell=True in Python programs!