'Python asyncio program does not exit waiting for stdin input
I have a complex Python 3.9 program that does not exit if interrupted with Ctrl-C (SIGINT). The following is a minimal repro of the problem. Run with python sample.py
You should see two prompts for input, then a normal exit. If you hit Ctrl-C on the first prompt, it exits as expected. If you hit Ctrl-C on the second prompt, nothing happens, until you hit Enter (and then an uncaught KeyboardInterrupt happens deep inside selectors.py).
import asyncio
async def ainput(prompt: str = "") -> str:
return await asyncio.get_event_loop().run_in_executor(None, input, prompt)
async def main():
try:
input("1>") # ctrl-C here works as expected, prints the message and exits
await ainput("2>") # ctrl-C here does nothing until you hit Enter
except KeyboardInterrupt:
print("Ctrl-C")
asyncio.run(main())
Question
Is this a bug in the code above? Or a bug in asyncio? How can I fix this?
Output
$ python sample.py
1>asks
2>lsls
$ python sample.py
1>^CCtrl-C
$ python sample.py
1>slsl
2>^C # **nothing happens here until you hit Enter**
Traceback (most recent call last):
File "/Users/me/work/experimental/sample.py", line 15, in <module>
asyncio.run(main())
File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/asyncio/base_events.py", line 634, in run_until_complete
self.run_forever()
File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/asyncio/base_events.py", line 601, in run_forever
self._run_once()
File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/asyncio/base_events.py", line 1869, in _run_once
event_list = self._selector.select(timeout)
File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/selectors.py", line 562, in select
kev_list = self._selector.control(None, max_ev, timeout)
KeyboardInterrupt
Solution 1:[1]
What happens is the KeyboardInterrupt
is caught by asyncio
since you are using threading
. What asyncio
then does is feed this into your coroutine by attempting to cancel it, which raises asyncio.CancelledError
. You can handle it like this:
async def main():
try:
input("1>")
await ainput("2>")
except KeyboardInterrupt:
print("Ctrl-C")
except asyncio.CancelledError:
print("Cancelled?")
raise
There isn't any way to stop asyncio.run(...)
from raising the KeyboardInterrupt
however, at least AFAIK.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Simply Beautiful Art |