Reputation: 307
(I'm using Python 3.4.2) I have a script test.py, which handles SIGTERM etc. However, when it's called by some other script, the sig-handling wasn't correct.
This is test.py:
#! /path/to/python3
import time
import signal
import sys
def handleSIG(signal, frame):
for i in range(10):
print(i)
sys.exit()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT, signal.SIGHUP]:
signal.signal(sig, handleSIG)
time.sleep(30)
If I just call "test.py" and do "Ctrl+C", then it prints 0,1,...,9 to the console. However, if I call test.py in another script using subprocess.call, only 0 will be printed. For example, here's another script that calls test.py:
import subprocess
cmd = '/path/to/test.py'
subprocess.call(cmd)
Strangely, using subproces.Popen() makes this error go away.
Upvotes: 3
Views: 5844
Reputation: 18141
UPDATE: This Python-3 regression will be fixed in Python 3.7, via PR #5026. For additional background and discussion, see bpo-25942 and (rejected) PR #4283.
I ran into this issue myself recently. The explanation given by @pilcrow is correct.
The OP's solution (in the comments) of merely using the Python 2 implementation (Popen(*popenargs, **kwargs).wait()
) doesn't suffice for me, because I'm not 100% sure that the child will respond to SIGINT
in all cases. I still want it to be killed -- after a timeout.
I settled on simply re-waiting for the child (with timeout).
def nice_call(*popenargs, timeout=None, **kwargs):
"""
Like subprocess.call(), but give the child process time to
clean up and communicate if a KeyboardInterrupt is raised.
"""
with Popen(*popenargs, **kwargs) as p:
try:
return p.wait(timeout=timeout)
except KeyboardInterrupt:
if not timeout:
timeout = 0.5
# Wait again, now that the child has received SIGINT, too.
p.wait(timeout=timeout)
raise
except:
p.kill()
p.wait()
raise
Technically, this means that I'm potentially extending the life of the child beyond the original timeout
, but that's better than incorrect cleanup behavior.
Upvotes: 2
Reputation: 58534
The python 3.3 subprocess.call
implementation sends a SIGKILL to its child if its wait
is interrupted, which it is by your Ctrl-C (SIGINT -> KeyboardInterrupt exception).
So, you see a race between the child process handling the terminal's SIGINT (sent to the whole process group) and the parent's SIGKILL.
From the python 3.3 sources, edited for brevity:
def call(*popenargs, timeout=None, **kwargs):
with Popen(*popenargs, **kwargs) as p:
try:
return p.wait(timeout=timeout)
except:
p.kill()
p.wait()
raise
Contrast this with the python 2 implementation:
def call(*popenargs, **kwargs):
return Popen(*popenargs, **kwargs).wait()
What an unpleasant surprise. It appears that this behavior was introduced in 3.3 when the wait
and call
interfaces were extended to accommodate a timeout. I don't find this correct, and I've filed a bug.
Upvotes: 8