Catching KeyboardInterrupt in Python during program shutdown
Asked Answered
H

3

148

I'm writing a command line utility in Python which, since it is production code, ought to be able to shut down cleanly without dumping a bunch of stuff (error codes, stack traces, etc.) to the screen. This means I need to catch keyboard interrupts.

I've tried using both a try catch block like:

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print 'Interrupted'
        sys.exit(0)

and catching the signal itself (as in this post):

import signal
import sys

def sigint_handler(signal, frame):
    print 'Interrupted'
    sys.exit(0)
signal.signal(signal.SIGINT, sigint_handler)

Both methods seem to work quite well during normal operation. However, if the interrupt comes during cleanup code at the end of the application, Python seems to always print something to the screen. Catching the interrupt gives

^CInterrupted
Exception KeyboardInterrupt in <bound method MyClass.__del__ of <path.to.MyClass object at 0x802852b90>> ignored

whereas handling the signal gives either

^CInterrupted
Exception SystemExit: 0 in <Finalize object, dead> ignored

or

^CInterrupted
Exception SystemExit: 0 in <bound method MyClass.__del__ of <path.to.MyClass object at 0x802854a90>> ignored

Not only are these errors ugly, they're not very helpful (especially to an end user with no source code)!

The cleanup code for this application is fairly big, so there's a decent chance that this issue will be hit by real users. Is there any way to catch or block this output, or is it just something I'll have to deal with?

Horsa answered 14/1, 2014 at 18:17 Comment(6)
Why don't you replace sys.stdout/sys.stderr? Like sys.stderr = open(os.devnull, 'w')?. If you really don't care about the final output then this seems like the obvious solution.Tagmemic
There is an os._exit but it looks like nasal demons to me. Where is your cleanup code, are you using atexit module for that?Erhard
@Bakuriu: While redirecting stderr will quiet the output, it also squashes legitimate errors that the user can do something about, like file-not-found or host-unreachable.Horsa
@Horsa It doesn't have to be /dev/null. You can write a custom file-like object that hides only messages with a given format.Tagmemic
@Bakuriu: Still seems pretty hacky to me. I'll do that if I have to, but I feel like this is something that ought to be built into the language.Horsa
Doesn't the answer by the third Dan give the right answer?Noh
N
184

Checkout this thread, it has some useful information about exiting and tracebacks.

If you are more interested in just killing the program, try something like this (this will take the legs out from under the cleanup code as well):

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print('Interrupted')
        try:
            sys.exit(130)
        except SystemExit:
            os._exit(130)

[Edited to change the exit code as suggested in comments. 130 is the code typically returned on Linux for a script terminated by Ctrl-C. We may not be on Linux, but the important thing is to return a non-zero value, and 130 is as good as any.]

Neuron answered 15/1, 2014 at 17:48 Comment(12)
I'd recommend excepting as, then grabbing and reusing the exit code.Homoiousian
Yes. One should definitly not exit with 0 on KeyboardInterrupt. If anyone is using your script in a script, pipe or what ever they will think your code executed as normal.Grilse
Linux typically exits with 130: 128 + 2 tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF , Can not find any cross platform exit-code function on python. But 1 is definitively better then 0Grilse
I don't disagree with having a better exit code. That said, the code provided was a copy paste from the OP, and typically when you ctrl-c something you don't really care about the exit code as something else has already gone horribly wrong.Neuron
If the user calls the python program from a bash script, and they use set -e in the script (as they should), you'd want to interrupt the entire bash script after the user presses CTRL+C. This would require returning a non-zero exit code.Maegan
@Grilse than*Docilu
@Bersan: Thank you! I read up on grammarly.com/blog/than-then , now I only have to remember it :DGrilse
@Homoiousian How do I grab the exit code from the exception?Diedra
@Diedra try: ... except SystemExit as e: os._exit(e.code) or something.Homoiousian
why try sys.exit() and then fail to os._exit() instead of just going straight to os._exit()?Alkalify
@Alkalify - This is a super old thread (~8 years). That said there are some specific use cases I had in mind when I wrote this up in 2014, I think it had to do with letting the python logger finish flushing / close any open files etc. Reference this as to the difference between the two calls https://mcmap.net/q/101688/-what-is-difference-between-sys-exit-0-and-os-_exit-0Neuron
Per this comment #48571712 I would not recommend using os._exit(...). Just use sys.exit(130) in the except block unless you know what you're doing. I think the answer should be edited to have the most commonly wanted behavior as the standard advice, and separately it can mention that one can use os._exit as a hard(er) way to kill the script if you have a specific reason to want to bypass attempts at detecting a script exit.Magpie
C
14

You could ignore SIGINTs after shutdown starts by calling signal.signal(signal.SIGINT, signal.SIG_IGN) before you start your cleanup code.

Careerist answered 15/1, 2014 at 15:38 Comment(1)
If the code is expected to be used in other scripts, you should return signal.SIGINT to signal.SIG_DFL after the cleanup code to return it to its default state.Theophylline
T
0
import inspect
import signal
import sys


def sigint_handler(signal, frame):
    sys.stderr.write('\nInterrupted')
    cleanup(0)


def _cleanup(attr, method, args):
    code = 0
    if hasattr(attr, method):
        try:
            getattr(attr, method)(*args)
        except Exception as exc:
            sys.stderr.write(f"Error cleaning up attribute {repr(attr)}: {exc}")
            code = 1
    return code


def cleanup(code=0):
    for attr in globals().values():
        if not(inspect.isclass(attr) or inspect.isfunction(attr)):
            if not code:
                code |= _cleanup(attr, "__del__", ())
                code |= _cleanup(attr, "__exit__", (None, None, None))
    
    exit(code)


signal.signal(signal.SIGINT, sigint_handler)


# -- Maincode down here --

def main():
    pass

# -- Maincode Ends


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        sys.stderr.write('\nInterrupted')
        cleanup(0)

This should help. It adds some object cleanup functionalities.

Terbecki answered 20/2 at 11:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.