Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot catch KeyboardInterrupt in command prompt twice?

Today, I had to check how my script runs on the Windows command prompt[1], when I noticed something weird. I was working on something similar to this, but this is enough to demonstrate the problem. Here's the code.

def bing():
    try:
        raw_input()
    except KeyboardInterrupt:
        print 'This is what actually happened here!'

try:                     # pardon me for those weird strings
    bing()               # as it's consistent with everything in the chat room (see below)
    print 'Yoo hoo...'
except KeyboardInterrupt:
    print 'Nothing happens here too!'

Here's the situation. When the script runs, it waits for the input and the user is supposed to press Ctrl+C to raise a KeyboardInterrupt which would (should) be caught by the except block within bing(). So, this should be the actual output. And, this is what happens when I run it in my Ubuntu terminal and IDLE (on both Windows & Ubuntu).

This is what actually happened here!
Yoo hoo...

But, this doesn't go as expected on the Windows command prompt. I rather get a strange output.

This is what actually happened here! Nothing happens here too!

It looks like a single KeyboardInterrupt propagates throughout the program and finally terminates it.

I tried everything I could do. First, I used a signal.signal to handle the SIGINT (which didn't work), and then I used handling function to raise an Exception which I'd later catch (which didn't work either), and then things got more complicated than it used to be. So, I landed back to my good old try... catch. Then, I went to the room for Pythonists.

@poke suggested that an EOFError is raised when we press Ctrl+C. Then, @ZeroPiraeus said that EOFError is raised when one presses Ctrl+Z and Enter.

That was helpful, which drove the discussion after a few minutes of fiddling around. Soon, everything became chaos! Some results were good, some were unexpected, and a few went haywire!

Weirdo!

The conclusion was to stop using Windows and ask my friends to use the Terminal (I agree). I could however do a workaround by catching the EOFError along with the KeyboardInterrupt. While it feels lazy to press Ctrl+Z and Enter each time, that's not a big problem for me. But, this is an obsession for me.

On further research, I also noticed that there's no KeyboardInterrupt raised on the CMD when I press Ctrl+C.

Whaa???

There's nothing at the bottom. So, what happens here anyway? Why does the KeyboardInterrupt propagate? Is there any way (at all) to make the output consistent with the terminal?


[1]: I've always worked on the terminal, but today I needed to ensure that my script works on all platforms (especially because most of my friends are non-coders and just stick to Windows).

like image 936
Waffle's Crazy Peanut Avatar asked Jun 30 '15 00:06

Waffle's Crazy Peanut


People also ask

How do you handle KeyboardInterrupt?

In Python, there is no special syntax for the KeyboardInterrupt exception; it is handled in the usual try and except block. The code that potentially causes the problem is written inside the try block, and the 'raise' keyword is used to raise the exception, or the python interpreter raises it automatically.

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

The question user2357112 linked, explains this in some way: Why can't I handle a KeyboardInterrupt in python?.

The keyboard interrupt is raised asynchronously, so it does not immediately terminate the application. Instead, the Ctrl+C is handled in some kind of event loop that takes a while to get there. This unfortunately means that you cannot reliably catch the KeyboardInterrupt in this case. But we can do some things to get there.

As I explained yesterday, the exception that stops the raw_input call is not the KeyboardInterrupt but an EOFError. You can easily verify this by changing your bing function like this:

def bing():
    try:
        raw_input()
    except Exception as e:
        print(type(e))

You will see that the exception type that’s printed is EOFError and not KeyboardInterrupt. You will also see that the print did not even go through completely: There is no new line. That’s apparently because the output got interrupted by the interrupt which arrived just after the print statement wrote the exception type to stdout. You can see this also when you add a bit more stuff to the print:

def bing():
    try:
        raw_input()
    except EOFError as e:
        print 'Exception raised:', 'EOF Error'

Note that I’m using two separate arguments here for the print statement. When we execute this, we can see the “Exception raised” text, but the “EOF Error” won’t appear. Instead, the except from the outer call will trigger and the keyboard interrupt is caught.

Things get a bit more out of control in Python 3 though. Take this code:

def bing():
    try:
        input()
    except Exception as e:
        print('Exception raised:', type(e))

try:
    bing()
    print('After bing')
except KeyboardInterrupt:
    print('Final KeyboardInterrupt')

This is pretty much exactly what we did before, just amended for Python 3 syntax. If I run this, I get the following output:

Exception raised: <class 'EOFError'>
After bing
Final KeyboardInterrupt

So we can again see, that the EOFError is correctly caught, but for some reason Python 3 continues the execution a lot longer than Python 2 here, as the print after bing() is executed as well. What’s worse, in some executions with cmd.exe, I get the result that no keyboard interrupt is caught at all (so apparently, the interrupt got processed after the program already completed).

So what can we do about this if we want to make sure that we get a keyboard interrupt? One thing we know for sure is that interrupting an input() (or raw_input()) prompt always raises an EOFError: That’s the one consistent thing that we have seen all the time. So what we can do is just catch that, and then make sure that we get the keyboard interrupt.

One way to do this would be to just raise a KeyboardInterrupt from the exception handler for EOFError. But this not only feels a bit dirty, it also doesn’t guarantee that an interrupt is actually what terminated the input prompt in the first place (who knows what else can possibly raise an EOFError?). So we should have the already existing interrupt signal generate the exception.

The way we do this is quite simple: We wait. So far, our problem was, that the execution continued because the exception didn’t arrive fast enough. So what if we wait a bit to let the exception eventually arrive before we continue with other things?

import time
def bing():
    try:
        input() # or raw_input() for Python 2
    except EOFError:
        time.sleep(1)

try:
    bing()
    print('After bing')
except KeyboardInterrupt:
    print('Final KeyboardInterrupt')

Now, we just catch the EOFError and wait a bit to let the asynchronous processes in the back settle and decide on whether to break the execution or not. This consistently allows me to catch the KeyboardInterrupt in the outer try/catch and will not print anything else except what I do in the exception handler.

You might worry that one second is a long time to wait, but in our cases, where we interrupt the execution, that second never really lasts long. Just a few milliseconds after the time.sleep, the interrupt is caught and we’re in our exception handler. So the one second is just a fail-safe that will wait long enough that the exception definitely arrives in time. And in the worst case, when there isn’t actually an interrupt but just a “normal” EOFError? Then the program that was previously blocking infinitely for user input will take a second longer to continue; that shouldn’t really be a problem ever (not to mention that the EOFError is probably super rare).

So there we have our solution: Just catch the EOFError, and wait a bit. At least I hope that this is a solution that works on other machines than my own ^_^" After last night, I’m not too sure about this—but at least I got a consistent experience over all my terminals and different Python versions.

like image 132
poke Avatar answered Oct 07 '22 20:10

poke