| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 030e913 commit 9232c21
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,7 +3,9 @@ | |||
| 3 | 3 | from idlelib import rpc | |
| 4 | 4 | import socket | |
| 5 | 5 | import struct | |
| 6 | + import threading | ||
| 6 | 7 | import unittest | |
| 8 | + from unittest import mock | ||
| 7 | 9 | ||
| 8 | 10 | ||
| 9 | 11 | class SocketIOTest(unittest.TestCase): | |
@@ -22,6 +24,18 @@ def test_reconnect_discards_partial_packet(self): | |||
| 22 | 24 | new_peer.sendall(struct.pack('<i', 3) + b'abc') | |
| 23 | 25 | self.assertEqual(sockio.pollpacket(1), b'abc') | |
| 24 | 26 | ||
| 27 | + def test_getresponse_interrupted(self): | ||
| 28 | + # gh-74112: an interrupted wait must release the lock and forget | ||
| 29 | + # the sequence number, so that a late response is discarded. | ||
| 30 | + sockio = rpc.SocketIO(mock.Mock(), debugging=False) | ||
| 31 | + sockio.sockthread = None # Not the current thread. | ||
| 32 | + cvar = sockio.cvars[7] = threading.Condition() | ||
| 33 | + with mock.patch.object(cvar, 'wait', side_effect=KeyboardInterrupt): | ||
| 34 | + with self.assertRaises(KeyboardInterrupt): | ||
| 35 | + sockio._getresponse(7, 0.05) | ||
| 36 | + self.assertNotIn(7, sockio.cvars) | ||
| 37 | + self.assertTrue(cvar.acquire(blocking=False)) | ||
| 38 | + cvar.release() | ||
| 25 | 39 | ||
| 26 | 40 | ||
| 27 | 41 | class CodePicklerTest(unittest.TestCase): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,7 +2,11 @@ | |||
| 2 | 2 | ||
| 3 | 3 | from idlelib import run | |
| 4 | 4 | import io | |
| 5 | + import signal | ||
| 5 | 6 | import sys | |
| 7 | + import threading | ||
| 8 | + import time | ||
| 9 | + from test import support | ||
| 6 | 10 | from test.support import captured_output, captured_stderr | |
| 7 | 11 | import unittest | |
| 8 | 12 | from unittest import mock | |
@@ -522,5 +526,33 @@ def test_exceptions(self): | |||
| 522 | 526 | self.assertTrue(isinstance(e.__context__, ZeroDivisionError)) | |
| 523 | 527 | ||
| 524 | 528 | ||
| 529 | + class InterruptTest(unittest.TestCase): | ||
| 530 | + | ||
| 531 | + def setUp(self): | ||
| 532 | + self.ex = run.Executive(mock.Mock(sendlock=threading.Lock())) | ||
| 533 | + self.addCleanup(setattr, run, 'interruptible', run.interruptible) | ||
| 534 | + run.interruptible = True | ||
| 535 | + | ||
| 536 | + @unittest.skipIf(signal.getsignal(signal.SIGINT) | ||
| 537 | + in (signal.SIG_DFL, signal.SIG_IGN, None), | ||
| 538 | + 'SIGINT is not handled by Python') | ||
| 539 | + def test_interrupt_blocking_call(self): | ||
| 540 | + # gh-74112: interrupt the main thread blocked in time.sleep(). | ||
| 541 | + timer = threading.Timer(0.1, self.ex.interrupt_the_server) | ||
| 542 | + self.addCleanup(timer.join) | ||
| 543 | + timer.start() | ||
| 544 | + start = time.monotonic() | ||
| 545 | + with self.assertRaises(KeyboardInterrupt): | ||
| 546 | + time.sleep(support.SHORT_TIMEOUT) | ||
| 547 | + self.assertLess(time.monotonic() - start, support.SHORT_TIMEOUT / 2) | ||
| 548 | + | ||
| 549 | + def test_interrupt_ignored(self): | ||
| 550 | + old_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) | ||
| 551 | + self.addCleanup(signal.signal, signal.SIGINT, old_handler) | ||
| 552 | + with mock.patch.object(run.thread, 'interrupt_main') as interrupt_main: | ||
| 553 | + self.ex.interrupt_the_server() | ||
| 554 | + interrupt_main.assert_called_once_with() | ||
| 555 | + | ||
| 556 | + | ||
| 525 | 557 | if __name__ == '__main__': | |
| 526 | 558 | unittest.main(verbosity=2) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -139,6 +139,7 @@ def __init__(self, sock, objtable=None, debugging=None): | |||
| 139 | 139 | self.objtable = objtable | |
| 140 | 140 | self.responses = {} | |
| 141 | 141 | self.cvars = {} | |
| 142 | + self.sendlock = threading.Lock() | ||
| 142 | 143 | # Receive buffer state. A new connection must not inherit a | |
| 143 | 144 | # partially received packet from the old one (gh-89544). | |
| 144 | 145 | self.buff = b'' | |
@@ -319,15 +320,20 @@ def _getresponse(self, myseq, wait): | |||
| 319 | 320 | else: | |
| 320 | 321 | # wait for notification from socket handling thread | |
| 321 | 322 | cvar = self.cvars[myseq] | |
| 322 | - cvar.acquire() | ||
| 323 | - while myseq not in self.responses: | ||
| 324 | - cvar.wait() | ||
| 325 | - response = self.responses[myseq] | ||
| 326 | - self.debug("_getresponse:%s: thread woke up: response: %s" % | ||
| 327 | - (myseq, response)) | ||
| 328 | - del self.responses[myseq] | ||
| 329 | - del self.cvars[myseq] | ||
| 330 | - cvar.release() | ||
| 323 | + with cvar: | ||
| 324 | + try: | ||
| 325 | + while myseq not in self.responses: | ||
| 326 | + cvar.wait() | ||
| 327 | + except BaseException: | ||
| 328 | + # Interrupted; a late response will be discarded. | ||
| 329 | + del self.cvars[myseq] | ||
| 330 | + self.responses.pop(myseq, None) | ||
| 331 | + raise | ||
| 332 | + response = self.responses[myseq] | ||
| 333 | + self.debug("_getresponse:%s: thread woke up: response: %s" % | ||
| 334 | + (myseq, response)) | ||
| 335 | + del self.responses[myseq] | ||
| 336 | + del self.cvars[myseq] | ||
| 331 | 337 | return response | |
| 332 | 338 | ||
| 333 | 339 | def newseq(self): | |
@@ -342,13 +348,14 @@ def putmessage(self, message): | |||
| 342 | 348 | print("Cannot pickle:", repr(message), file=sys.__stderr__) | |
| 343 | 349 | raise | |
| 344 | 350 | s = struct.pack("<i", len(s)) + s | |
| 345 | - while len(s) > 0: | ||
| 346 | - try: | ||
| 347 | - r, w, x = select.select([], [self.sock], []) | ||
| 348 | - n = self.sock.send(s[:BUFSIZE]) | ||
| 349 | - except (AttributeError, TypeError): | ||
| 350 | - raise OSError("socket no longer exists") | ||
| 351 | - s = s[n:] | ||
| 351 | + with self.sendlock: | ||
| 352 | + while len(s) > 0: | ||
| 353 | + try: | ||
| 354 | + r, w, x = select.select([], [self.sock], []) | ||
| 355 | + n = self.sock.send(s[:BUFSIZE]) | ||
| 356 | + except (AttributeError, TypeError): | ||
| 357 | + raise OSError("socket no longer exists") | ||
| 358 | + s = s[n:] | ||
| 352 | 359 | ||
| 353 | 360 | def pollpacket(self, wait): | |
| 354 | 361 | self._stage0() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,6 +9,7 @@ | |||
| 9 | 9 | import io | |
| 10 | 10 | import linecache | |
| 11 | 11 | import queue | |
| 12 | + import signal | ||
| 12 | 13 | import sys | |
| 13 | 14 | import textwrap | |
| 14 | 15 | import time | |
@@ -678,7 +679,19 @@ def runcode(self, code): | |||
| 678 | 679 | ||
| 679 | 680 | def interrupt_the_server(self): | |
| 680 | 681 | if interruptible: | |
| 681 | - thread.interrupt_main() | ||
| 682 | + handler = signal.getsignal(signal.SIGINT) | ||
| 683 | + if handler not in (signal.SIG_DFL, signal.SIG_IGN, None): | ||
| 684 | + # A real signal interrupts blocking calls such as | ||
| 685 | + # time.sleep() (gh-74112). The lock prevents interrupting | ||
| 686 | + # the main thread in the middle of sending a message. | ||
| 687 | + with self.rpchandler.sendlock: | ||
| 688 | + if hasattr(signal, 'pthread_kill'): | ||
| 689 | + signal.pthread_kill(threading.main_thread().ident, | ||
| 690 | + signal.SIGINT) | ||
| 691 | + else: | ||
| 692 | + signal.raise_signal(signal.SIGINT) | ||
| 693 | + else: | ||
| 694 | + thread.interrupt_main() | ||
| 682 | 695 | ||
| 683 | 696 | def start_the_debugger(self, gui_adap_oid): | |
| 684 | 697 | return debugger_r.start_debugger(self.rpchandler, gui_adap_oid) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,2 @@ | |||
| 1 | + Ctrl-C in the IDLE Shell now interrupts blocking calls such as | ||
| 2 | + :func:`time.sleep` and :meth:`socket.recv <socket.socket.recv>`. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments