ninjaaron/replacing-bash-scripting-with-python

Feature Request: Handling of ctrl-c while running child process

Closed this issue · 2 comments

e00E commented

I have found this repository in an internet search. It is helpful. Thank you for writing it.

Do you have suggestions on how to handle ctrl c when running a subprocess where the subprocess wants to react and do something before it exits?

Here is an example.

outer.sh

echo start outer
python inner.py
echo end outer

inner.py

import time

print("start inner")
try:
    time.sleep(5)
except KeyboardInterrupt:
    print("got ctrl c inner")
    time.sleep(1)
    print("finished handling ctrl c inner")
print("end inner")

Running this without interrupting outputs

start outer
start inner
end inner
end outer

and with ctrl c

start outer
start inner
^Cgot ctrl c inner
finished handling ctrl c inner
end inner
end outer

but if we use this version of outer, outer.py

import subprocess

print("start outer")
subprocess.run(["python", "inner.py"], check=True)
print("end outer")

and ctrl c we get

start outer
start inner
^Cgot ctrl c inner
Traceback (most recent call last):
  File "outer.py", line 4, in <module>
    subprocess.run(["python", "inner.py"], check=True)
  File "/usr/lib/python3.7/subprocess.py", line 474, in run
    stdout, stderr = process.communicate(input, timeout=timeout)
  File "/usr/lib/python3.7/subprocess.py", line 931, in communicate
    self.wait()
  File "/usr/lib/python3.7/subprocess.py", line 990, in wait
    return self._wait(timeout=timeout)
  File "/usr/lib/python3.7/subprocess.py", line 1624, in _wait
    (pid, sts) = self._try_wait(0)
  File "/usr/lib/python3.7/subprocess.py", line 1582, in _try_wait
    (pid, sts) = os.waitpid(self.pid, wait_flags)
KeyboardInterrupt

The inner process was not able to react to the ctrl c.

There are various ways to attempt to fix this like catching the KeyboardInterrupt or the SIGTERM also in the outer process and calling terminate on the inner but I am not sure what the best approach is.

I'm not sure what the best is either!

It looks like if an error is caught in the outer process while waiting for subprocess.run to complete, SIGTERM is sent internally. KeyboardInterrupt is raise for SIGINT, so terminate just ends the process immediately. You could use the signal.signal to install a handler for SIGTERM in inner.py, but this isn't a very generic solution because you need to be the author of the program in the inner process to get it to work.

This is what I came up with:

import subprocess

print("start outer")
proc = subprocess.Popen(["python", "inner.py"])

while True:
    try:
        ret_code = proc.wait()
    except KeyboardInterrupt:
        pass
    else:
        break

print("end outer")

Creating a process with Popen means an interrupt when calling .wait() will not terminate the inner program. I put it in a loop so the inner process would get as many chances as it needs to handle the keyboard interrupt.

Again, I'm not promising that this is the "best" approach, but it's what I came up with and I would be comfortable using it in my own code.

e00E commented

Thanks for your answer. Feel free to close whenever.