Skip to content

Commit 2976259

Browse files
committed
gh-146194: Fix nested KeyboardInterrupt handling in asyncio
- Modify _on_sigint to cancel main task on every SIGINT - Allow nested cancellations to propagate correctly through multiple levels - Add test_nested_keyboardinterrupt_handling to test_runners.py - Add NEWS entry Fixes issue where third Ctrl+C would crash with: 'Task was destroyed but it is pending!'
1 parent d357a7d commit 2976259

File tree

3 files changed

+62
-4
lines changed

3 files changed

+62
-4
lines changed

Lib/asyncio/runners.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,14 @@ def _lazy_init(self):
158158

159159
def _on_sigint(self, signum, frame, main_task):
160160
self._interrupt_count += 1
161-
if self._interrupt_count == 1 and not main_task.done():
161+
162+
if not main_task.done():
162163
main_task.cancel()
163-
# wakeup loop if it is blocked by select() with long timeout
164164
self._loop.call_soon_threadsafe(lambda: None)
165-
return
166-
raise KeyboardInterrupt()
165+
166+
167+
if self._interrupt_count > 10:
168+
raise KeyboardInterrupt()
167169

168170

169171
def run(main, *, debug=None, loop_factory=None):

Lib/test/test_asyncio/test_runners.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,51 @@ async def coro():
522522

523523
self.assertEqual(0, result.repr_count)
524524

525+
def test_nested_keyboardinterrupt_handling(self):
526+
"""Test that multiple KeyboardInterrupts are handled correctly."""
527+
results = []
528+
529+
async def nested_coro():
530+
try:
531+
while True:
532+
await asyncio.sleep(0.1)
533+
results.append('*')
534+
except asyncio.CancelledError:
535+
results.append('first_cancelled')
536+
try:
537+
while True:
538+
await asyncio.sleep(0.1)
539+
results.append('#')
540+
except asyncio.CancelledError:
541+
results.append('second_cancelled')
542+
try:
543+
while True:
544+
await asyncio.sleep(0.1)
545+
results.append('!')
546+
except asyncio.CancelledError:
547+
results.append('third_cancelled')
548+
549+
550+
def run_with_cancels():
551+
async def main():
552+
task = asyncio.create_task(nested_coro())
553+
await asyncio.sleep(0.2)
554+
task.cancel()
555+
await asyncio.sleep(0.2)
556+
task.cancel()
557+
await asyncio.sleep(0.2)
558+
task.cancel()
559+
await asyncio.sleep(0.1)
560+
561+
asyncio.run(main())
562+
563+
run_with_cancels()
564+
565+
566+
self.assertIn('first_cancelled', results)
567+
self.assertIn('second_cancelled', results)
568+
self.assertIn('third_cancelled', results)
569+
525570

526571
if __name__ == '__main__':
527572
unittest.main()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.. gh-issue: 146194
2+
3+
.. section: Library
4+
5+
Fix nested :exc:`KeyboardInterrupt` handling in :mod:`asyncio`.
6+
Previously,
7+
multiple Ctrl+C presses would cause a crash with ``Task was
8+
destroyed but it
9+
is pending!``. Now nested cancellations propagate correctly through
10+
multiple
11+
levels.

0 commit comments

Comments
 (0)