Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Capturing SIGINT using KeyboardInterrupt exception works in terminal, not in script

Tags:

I'm trying to catch SIGINT (or keyboard interrupt) in Python 2.7 program. This is how my Python test script test looks:

#!/usr/bin/python  import time  try:     time.sleep(100) except KeyboardInterrupt:     pass except:     print "error" 

Next I have a shell script test.sh:

./test & pid=$! sleep 1 kill -s 2 $pid 

When I run the script with bash, or sh, or something bash test.sh, the Python process test stays running and is not killable with SIGINT. Whereas when I copy test.sh command and paste it into (bash) terminal, the Python process test shuts down.

I cannot get what's going on, which I'd like to understand. So, where is difference, and why?

This is not about how to catch SIGINT in Python! According to docs – this is the way, which should work:

Python installs a small number of signal handlers by default: SIGPIPE ... and SIGINT is translated into a KeyboardInterrupt exception

It is indeed catching KeyboardInterrupt when SIGINT is sent by kill if the program is started directly from shell, but when the program is started from bash script run on background, it seems that KeyboardInterrupt is never raised.

like image 750
Velda Avatar asked Nov 23 '16 22:11

Velda


People also ask

Does exception catch KeyboardInterrupt?

The KeyboardInterrupt exception inherits the BaseException and, like other Python exceptions, is handled via a try-except statement to prevent the interpreter from abruptly quitting the program.

What is KeyboardInterrupt signal?

In computing, keyboard interrupt may refer to: A special case of signal (computing), a condition (often implemented as an exception) usually generated by the keyboard in the text user interface. A hardware interrupt generated when a key is pressed or released, see keyboard controller (computing)


1 Answers

There is one case in which the default sigint handler is not installed at startup, and that is when the signal mask contains SIG_IGN for SIGINT at program startup. The code responsible for this can be found here.

The signal mask for ignored signals is inherited from the parent process, while handled signals are reset to SIG_DFL. So in case SIGINT was ignored the condition if (Handlers[SIGINT].func == DefaultHandler) in the source won't trigger and the default handler is not installed, python doesn't override the settings made by the parent process in this case.

So let's try to show the used signal handler in different situations:

# invocation from interactive shell $ python -c "import signal; print(signal.getsignal(signal.SIGINT))" <built-in function default_int_handler>  # background job in interactive shell $ python -c "import signal; print(signal.getsignal(signal.SIGINT))" & <built-in function default_int_handler>  # invocation in non interactive shell $ sh -c 'python -c "import signal; print(signal.getsignal(signal.SIGINT))"' <built-in function default_int_handler>  # background job in non-interactive shell $ sh -c 'python -c "import signal; print(signal.getsignal(signal.SIGINT))" &' 1 

So in the last example, SIGINT is set to 1 (SIG_IGN). This is the same as when you start a background job in a shell script, as those are non interactive by default (unless you use the -i option in the shebang).

So this is caused by the shell ignoring the signal when launching a background job in a non interactive shell session, not by python directly. At least bash and dash behave this way, I've not tried other shells.

There are two options to deal with this situation:

  • manually install the default signal handler:

    import signal signal.signal(signal.SIGINT, signal.default_int_handler) 
  • add the -i option to the shebang of the shell script, e.g:

    #!/bin/sh -i 

edit: this behaviour is documented in the bash manual:

SIGNALS
...
When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.

which applies to non-interactive shells as they have job control disabled by default, and is actually specified in POSIX: Shell Command Language

like image 68
mata Avatar answered Sep 28 '22 02:09

mata