Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid suppressing KeyboardInterrupt during garbage collection in python?

Default handler for SIGINT raises KeyboardInterrupt. However, if a program is inside a __del__ method (because of an ongoing garbage collection), the exception is ignored with the following message printed to stderr:

Exception KeyboardInterrupt in <...> ignored

As a result, the program continues to work despite receiving SIGINT. Of course, I can define my own handler for SIGINT that sets a global variable sigint_received to True, and then often check the value of the variable in my program. But this looks ugly.

Is there an elegant and reliable way to make sure that the python program gets interrupted after receiving SIGINT?

like image 812
Marcin Avatar asked Oct 31 '22 16:10

Marcin


1 Answers

Before I dive into my solution, I want to highlight the scary red "Warning:" sidebar in the docs for object.__del__ (emphasis mine):

Due to the precarious circumstances under which __del__() methods are invoked, exceptions that occur during their execution are ignored, and a warning is printed to sys.stderr instead. [...] __del__() methods should do the absolute minimum needed to maintain external invariants.

This suggests to me that any __del__ method that's at serious risk of being interrupted by an interactive user's Ctrl-C might be doing too much. So my first suggestion would be to look for ways to minimize your __del__ method, whatever it is.

Or to put it another way: If your __del__ method really does do "the absolute minimum needed", then how can it be safe to kill the process half-way through?

Custom Signal Handler

The only solution I could find was indeed a custom signal handler for signal.SIGINT... but a lot of the obvious tricks didn't work:

Failed: sys.exit

Calling sys.exit from the signal handler just raised a SystemExit exception, which was ignored. Python's C API docs suggest that it is impossible for the Python interpreter to raise any exception during a __del__ method:

voidPyErr_WriteUnraisable(PyObject *obj)

[Called when...] it is impossible for the interpreter to actually raise the exception [...] for example, when an exception occurs in an __del__() method.

Partial Success: Flag Variable

Your idea of setting a global "drop dead" variable inside the signal handler worked only partially --- although it updated the variable, nothing got a chance to read that variable until after the __del__ method returned. So for several seconds, the Ctrl-C appeared to have done nothing.

This might be good enough if you just want to terminate the process "eventually", since it will exit whenever the __del__ method returns. But since you probably want to shut down the process without waiting (both SIGINT and KeyboardInterrupt typically come from an impatient user), this won't do.

Success: os.kill

Since I couldn't find a way to convince the Python interpreter to kill itself, my solution was to have the (much more persuasive) operating system do it for me. This signal handler uses os.kill to send a stronger SIGTERM to its own process ID, causing the Python interpreter itself to exit.

def _sigterm_this_process(signum, frame):
    pid = os.getpid()
    os.kill(pid, signal.SIGTERM)
    return

# Elsewhere...
signal.signal(signal.SIGINT, _sigterm_this_process)

Once the custom signal handler was set, Ctrl-C caused the __del__ method (and the entire program) to exit immediately.

like image 125
Kevin J. Chase Avatar answered Nov 02 '22 09:11

Kevin J. Chase