Ctrl+C sends EOFError once after cancelling process [duplicate]
Asked Answered
K

1

7

I'm making a very basic command line utility using python, and I'm trying to set it up such that the user has the ability to interrupt any running operation during execution with Ctrl+C, and exit the program by sending an EOF with Ctrl+Z. However, I've been fighting with this frustrating issue for the last day where after cancelling a running operation with a KeyboardInterrupt, pressing Ctrl+C a second time sends an EOFError instead of a KeyboardInterrupt, which causes the program to exit. Hitting Ctrl+C subsequent times sends KeyboardInterrupt's as usual, except until I input any command or an empty line, where an additional KeyboardInterrupt is sent instead of the input I give. After doing so, hitting Ctrl+C again will send an EOFError again, and continues from there. Here's a minimal example of code demonstrating my issue;

import time

def parse(inp):
    time.sleep(1)
    print(inp)
    if inp == 'e':
        return 'exit'

while True:
    try:
        user_in = input("> ").strip()
    except (KeyboardInterrupt, Exception) as e:
        print(e.__class__.__name__)
        continue

    if not user_in:
        continue

    try:
        if parse(user_in) == 'exit':
            break
    except KeyboardInterrupt:
        print("Cancelled")

And here's some sample output of my code;

>
>
>
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
>
>
> ^Z
EOFError
> ^Z
EOFError
> ^Z
EOFError
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
>
> ^Z
EOFError
> KeyboardInterrupt
>
>

As you can see, when hitting Ctrl+C, Ctrl+Z, or a blank line, the prompt responds as you would expect with each error appropriately. However, if I run a command and try cancelling it during execution by hitting Ctrl+C;

> test
Cancelled
>
>
>
> EOFError
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
KeyboardInterrupt
>
>
> EOFError
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
KeyboardInterrupt
>
>
>

I only hit Ctrl+C and Enter in the above example; I first hit enter to send several blank lines, then hit Ctrl+C, which sends an EOFError at first. Hitting Ctrl+C mutliple times afterwards then sends KeyboardInterrupt's correctly. Afterwards, sending a blank line instead then sends a KeyboardInterrupt, and subsequent blank lines entered are received normally. This functionality is repeated onward from the program's execution there.

Why is this happening, and how can I fix it?

Kayser answered 30/8, 2019 at 15:45 Comment(2)
Adding detail: the issue occurs when run via the Windows 10 command line and does not occur when using the Bash shell on Windows 10.Cribwork
This is a really interesting issue. I was able to reproduce it. I'll dig into it tomorrow, why it's happening!Elasticize
E
4

So. You've found a pretty old Python bug. It's to do with the async nature of keyboard interrupts, AND how if you send a KeyboardInterrupt to Python while hanging, and it doesn't respond to the interrupt, the second interrupt will raise the even stronger EOFError. However, it seems, that these two collide, if you have an async KeyboardInterrupt caught followed by an input with a second KeyboardInterrupt, there will some stuff left in some buffer, which triggered EOFError.

I know this isn't a great explanation, nor is it very clear. However, it allows for a pretty simple fix. Let the buffer to catch up with all the async interrupts, and than start waiting for an input:

import time

def parse(inp):
    time.sleep(1)
    print(inp)
    if inp == 'e':
        return 'exit'

while True:
    try:
        user_in = input("> ").strip()
    except (KeyboardInterrupt, Exception) as e:
        print(e.__class__.__name__)
        continue

    if not user_in:
        continue

    try:
        if parse(user_in) == 'exit':
            break
    except KeyboardInterrupt:
        print("Cancelled")
        time.sleep(0.1)    # This is the only line that's added

Now doing the same actions you did produces this:

> test
Cancelled
>
>
>
>
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
> KeyboardInterrupt
>
>
> KeyboardInterrupt
> KeyboardInterrupt
>
Elasticize answered 4/9, 2019 at 9:34 Comment(2)
Thanks, I can't believe the fix is that simple.Kayser
Got a small question about this solution. Is the print("Cancelled") important here? Or would it still work if the time.sleep call was the only thing in the except block?Hydrated

© 2022 - 2024 — McMap. All rights reserved.