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?
Python allows us to set up signal -handlers so when a particular signal arrives to our program we can have a behavior different from the default. For example when you run a program on the terminal and press Ctrl-C the default behavior is to quit the program.
While in a command line such as MS-DOS, Linux, and Unix, Ctrl + C is used to send a SIGINT signal, which cancels or terminates the currently-running program. For example, if a script or program is frozen or stuck in an infinite loop, pressing Ctrl + C cancels that command and returns you to the command line.
When you type CTRL-C, you tell the shell to send the INT (for "interrupt") signal to the current job; [CTRL-Z] sends TSTP (on most systems, for "terminal stop"). You can also send the current job a QUIT signal by typing CTRL-\ (control-backslash); this is sort of like a "stronger" version of [CTRL-C].
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
>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With