13

I want to launch a background Python job from a bash script and then gracefully kill it with SIGINT. This works fine from the shell, but I can't seem to get it to work in a script.

loop.py:

#! /usr/bin/env python
if __name__ == "__main__":
    try:
        print 'starting loop'
        while True:
            pass
    except KeyboardInterrupt:
        print 'quitting loop'

From the shell I can interrupt it:

$ python loop.py &
[1] 15420
starting loop
$ kill -SIGINT 15420
quitting loop
[1]+  Done                    python loop.py

kill.sh:

#! /bin/bash
python loop.py &
PID=$!
echo "sending SIGINT to process $PID"
kill -SIGINT $PID

But from a script I can't:

$ ./kill.sh 
starting loop
sending SIGINT to process 15452
$ ps ax | grep loop.py | grep -v grep
15452 pts/3    R      0:08 python loop.py

And, if it's been launched from a script I can no longer kill it from the shell:

$ kill -SIGINT 15452
$ ps ax | grep loop.py | grep -v grep
15452 pts/3    R      0:34 python loop.py

I'm assuming I'm missing some fine point of bash job control.

Ryan
  • 575
  • 2
  • 5
  • 7

5 Answers5

16

You're not registering a signal handler. Try the below. It seems to work fairly reliably. I think the rare exception is when it catches the signal before Python registers the script's handler. Note that KeyboardInterrupt is only supposed to be raised, "when the user hits the interrupt key". I think the fact that it works for a explicit (e.g. via kill) SIGINT at all is an accident of implementation.

import signal

def quit_gracefully(*args):
    print 'quitting loop'
    exit(0);

if __name__ == "__main__":
    signal.signal(signal.SIGINT, quit_gracefully)

    try:
        print 'starting loop'
        while True:
            pass
    except KeyboardInterrupt:
        quit_gracefully()
Matthew Flaschen
  • 278,309
  • 50
  • 514
  • 539
  • See the title `18.8. signal — Set handlers for **asynchronous** events` The fact that it works for a synchronous command is no accident. – Wyrmwood May 11 '17 at 21:11
  • This solution does not work when you try to send -SIGINT signal to python script from shell script. (As mentioned in the question nu @Ryan) – user3156262 Jul 09 '20 at 21:46
6

In addition to @matthew-flaschen's answer, you can use exec in the bash script to effectively replace the scope to the process being opened:

#!/bin/bash
exec python loop.py &
PID=$!
sleep 5  # waiting for the python process to come up

echo "sending SIGINT to process $PID"
kill -SIGINT $PID
Steen
  • 6,573
  • 3
  • 39
  • 56
4

I agree with Matthew Flaschen; the problem is with python, which apparently doesn't register the KeyboardInterrupt exception with SIGINT when it's not called from an interactive shell.

Of course, nothing prevents you from registering your signal handler like this:

def signal_handler(signum, frame):
    raise KeyboardInterrupt, "Signal handler"
Michiel Buddingh
  • 5,783
  • 1
  • 21
  • 32
2

When you run command in background with &, SIGINT will be ignored. Here's the relevant section of man bash:

Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers. Commands run as a result of command substitution ignore the keyboard-generated job control signals SIGTTIN, SIGTTOU, and SIGTSTP.

I think you need to set signal handler explicitly as Matthew commented.

The script kill.sh also have a problem. Since loop.py is sent to background, there's no guarantee that kill runs after python loop.py.

#! /bin/bash
python loop.py &
PID=$!
#
# NEED TO WAIT ON EXISTENCE OF python loop.py PROCESS HERE.
#
echo "sending SIGINT to process $PID"
kill -SIGINT $PID
Denilson Sá Maia
  • 47,466
  • 33
  • 109
  • 111
tomoe
  • 2,966
  • 1
  • 16
  • 4
1

Tried @Steen's approach, but alas, it does not apparently hold on Mac.

Another solution, pretty much the same as the above but a little more general, is to just re-install the default handler if SIGINT is being ignored:

def _ensure_sigint_handler():
    # On Mac, even using `exec <cmd>` in `bash` still yields an ignored SIGINT.
    sig = signal.getsignal(signal.SIGINT)
    if signal.getsignal(signal.SIGINT) == signal.SIG_IGN:
        signal.signal(signal.SIGINT, signal.default_int_handler)
# ...
_ensure_sigint_handler()
Eric Cousineau
  • 1,944
  • 14
  • 23