Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Lib/idlelib/idle_test/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from idlelib import rpc
import socket
import struct
import threading
import unittest
from unittest import mock


class SocketIOTest(unittest.TestCase):
Expand All @@ -22,6 +24,18 @@ def test_reconnect_discards_partial_packet(self):
new_peer.sendall(struct.pack('<i', 3) + b'abc')
self.assertEqual(sockio.pollpacket(1), b'abc')

def test_getresponse_interrupted(self):
# gh-74112: an interrupted wait must release the lock and forget
# the sequence number, so that a late response is discarded.
sockio = rpc.SocketIO(mock.Mock(), debugging=False)
sockio.sockthread = None # Not the current thread.
cvar = sockio.cvars[7] = threading.Condition()
with mock.patch.object(cvar, 'wait', side_effect=KeyboardInterrupt):
with self.assertRaises(KeyboardInterrupt):
sockio._getresponse(7, 0.05)
self.assertNotIn(7, sockio.cvars)
self.assertTrue(cvar.acquire(blocking=False))
cvar.release()


class CodePicklerTest(unittest.TestCase):
Expand Down
32 changes: 32 additions & 0 deletions Lib/idlelib/idle_test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from idlelib import run
import io
import signal
import sys
import threading
import time
from test import support
from test.support import captured_output, captured_stderr
import unittest
from unittest import mock
Expand Down Expand Up @@ -522,5 +526,33 @@ def test_exceptions(self):
self.assertTrue(isinstance(e.__context__, ZeroDivisionError))


class InterruptTest(unittest.TestCase):

def setUp(self):
self.ex = run.Executive(mock.Mock(sendlock=threading.Lock()))
self.addCleanup(setattr, run, 'interruptible', run.interruptible)
run.interruptible = True

@unittest.skipIf(signal.getsignal(signal.SIGINT)
in (signal.SIG_DFL, signal.SIG_IGN, None),
'SIGINT is not handled by Python')
def test_interrupt_blocking_call(self):
# gh-74112: interrupt the main thread blocked in time.sleep().
timer = threading.Timer(0.1, self.ex.interrupt_the_server)
self.addCleanup(timer.join)
timer.start()
start = time.monotonic()
with self.assertRaises(KeyboardInterrupt):
time.sleep(support.SHORT_TIMEOUT)
self.assertLess(time.monotonic() - start, support.SHORT_TIMEOUT / 2)

def test_interrupt_ignored(self):
old_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
self.addCleanup(signal.signal, signal.SIGINT, old_handler)
with mock.patch.object(run.thread, 'interrupt_main') as interrupt_main:
self.ex.interrupt_the_server()
interrupt_main.assert_called_once_with()


if __name__ == '__main__':
unittest.main(verbosity=2)
39 changes: 23 additions & 16 deletions Lib/idlelib/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def __init__(self, sock, objtable=None, debugging=None):
self.objtable = objtable
self.responses = {}
self.cvars = {}
self.sendlock = threading.Lock()
# Receive buffer state. A new connection must not inherit a
# partially received packet from the old one (gh-89544).
self.buff = b''
Expand Down Expand Up @@ -319,15 +320,20 @@ def _getresponse(self, myseq, wait):
else:
# wait for notification from socket handling thread
cvar = self.cvars[myseq]
cvar.acquire()
while myseq not in self.responses:
cvar.wait()
response = self.responses[myseq]
self.debug("_getresponse:%s: thread woke up: response: %s" %
(myseq, response))
del self.responses[myseq]
del self.cvars[myseq]
cvar.release()
with cvar:
try:
while myseq not in self.responses:
cvar.wait()
except BaseException:
# Interrupted; a late response will be discarded.
del self.cvars[myseq]
self.responses.pop(myseq, None)
raise
response = self.responses[myseq]
self.debug("_getresponse:%s: thread woke up: response: %s" %
(myseq, response))
del self.responses[myseq]
del self.cvars[myseq]
return response

def newseq(self):
Expand All @@ -342,13 +348,14 @@ def putmessage(self, message):
print("Cannot pickle:", repr(message), file=sys.__stderr__)
raise
s = struct.pack("<i", len(s)) + s
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:]
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()
Expand Down
15 changes: 14 additions & 1 deletion Lib/idlelib/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io
import linecache
import queue
import signal
import sys
import textwrap
import time
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Ctrl-C in the IDLE Shell now interrupts blocking calls such as
:func:`time.sleep` and :meth:`socket.recv <socket.socket.recv>`.
Loading