From 808d0d517263330f015d5639d66b1f02c55b9a8d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 23 Sep 2026 21:29:36 +0300 Subject: [PATCH] gh-74112: Make Ctrl-C in the IDLE Shell interrupt blocking calls (GH-157662) Send a real SIGINT to the main thread of the user process instead of calling _thread.interrupt_main(), which only sets a flag checked between bytecodes. The signal is sent while holding a new lock which protects sending a message, so that the main thread is not interrupted in the middle of a message. An interrupted wait for a response now releases its lock, so that the socket thread does not deadlock. (cherry picked from commit 9232c21a1c1eb6a12b96acebbef6c86a698228e0) Co-authored-by: Serhiy Storchaka --- Lib/idlelib/idle_test/test_rpc.py | 14 +++++++ Lib/idlelib/idle_test/test_run.py | 32 +++++++++++++++ Lib/idlelib/rpc.py | 39 +++++++++++-------- Lib/idlelib/run.py | 15 ++++++- ...6-09-17-12-00-00.gh-issue-74112.sigint.rst | 2 + 5 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-09-17-12-00-00.gh-issue-74112.sigint.rst diff --git a/Lib/idlelib/idle_test/test_rpc.py b/Lib/idlelib/idle_test/test_rpc.py index 15c3ed14b8f6f3..d929165e2e5ec9 100644 --- a/Lib/idlelib/idle_test/test_rpc.py +++ b/Lib/idlelib/idle_test/test_rpc.py @@ -3,7 +3,9 @@ from idlelib import rpc import socket import struct +import threading import unittest +from unittest import mock class SocketIOTest(unittest.TestCase): @@ -22,6 +24,18 @@ def test_reconnect_discards_partial_packet(self): new_peer.sendall(struct.pack(' 0: - try: - r, w, x = select.select([], [self.sock], []) - n = self.sock.send(s[:BUFSIZE]) - except (AttributeError, TypeError): - raise OSError("socket no longer exists") - s = s[n:] + with self.sendlock: + while len(s) > 0: + try: + r, w, x = select.select([], [self.sock], []) + n = self.sock.send(s[:BUFSIZE]) + except (AttributeError, TypeError): + raise OSError("socket no longer exists") + s = s[n:] def pollpacket(self, wait): self._stage0() diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index 2725043b4ed925..c69060620f5f9f 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -9,6 +9,7 @@ import io import linecache import queue +import signal import sys import textwrap import time @@ -678,7 +679,19 @@ def runcode(self, code): def interrupt_the_server(self): if interruptible: - thread.interrupt_main() + handler = signal.getsignal(signal.SIGINT) + if handler not in (signal.SIG_DFL, signal.SIG_IGN, None): + # A real signal interrupts blocking calls such as + # time.sleep() (gh-74112). The lock prevents interrupting + # the main thread in the middle of sending a message. + with self.rpchandler.sendlock: + if hasattr(signal, 'pthread_kill'): + signal.pthread_kill(threading.main_thread().ident, + signal.SIGINT) + else: + signal.raise_signal(signal.SIGINT) + else: + thread.interrupt_main() def start_the_debugger(self, gui_adap_oid): return debugger_r.start_debugger(self.rpchandler, gui_adap_oid) diff --git a/Misc/NEWS.d/next/IDLE/2026-09-17-12-00-00.gh-issue-74112.sigint.rst b/Misc/NEWS.d/next/IDLE/2026-09-17-12-00-00.gh-issue-74112.sigint.rst new file mode 100644 index 00000000000000..18a230e75913b5 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-17-12-00-00.gh-issue-74112.sigint.rst @@ -0,0 +1,2 @@ +Ctrl-C in the IDLE Shell now interrupts blocking calls such as +:func:`time.sleep` and :meth:`socket.recv `.