Skip to content

Commit 2165c49

Browse files
Stop the pty reader thread before closing the fds in ScreenTests
On macOS, closing either end of a pty while a thread is blocked in read() on the master hangs. Drain with a poll() loop that a stop Event can interrupt, and stop and join the reader before closing the descriptors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 228f977 commit 2165c49

1 file changed

Lines changed: 26 additions & 12 deletions

File tree

Lib/test/test_curses.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import functools
22
import inspect
33
import os
4+
import select
45
import string
56
import sys
67
import tempfile
@@ -1785,27 +1786,40 @@ def tearDown(self):
17851786
gc_collect()
17861787

17871788
@staticmethod
1788-
def _drain_pty(master):
1789-
# Read and discard whatever curses writes to the screen.
1790-
try:
1791-
while os.read(master, 1024):
1792-
pass
1793-
except OSError:
1794-
pass
1789+
def _drain_pty(master, stop):
1790+
# Read and discard whatever curses writes to the screen, until asked to
1791+
# stop and nothing more is pending. poll() rather than a blocking
1792+
# read() so we can stop without closing the fd (closing it while this
1793+
# thread is blocked in read() hangs on macOS).
1794+
poller = select.poll()
1795+
poller.register(master, select.POLLIN)
1796+
while True:
1797+
if poller.poll(100):
1798+
try:
1799+
if not os.read(master, 1024):
1800+
break # EOF
1801+
except OSError:
1802+
break
1803+
elif stop.is_set():
1804+
break
17951805

17961806
def make_pty(self):
17971807
master, slave = os.openpty()
17981808
# Nothing reads the master end, so writing to the slave and the
17991809
# tcdrain() in endwin() can block on macOS once the pty buffer fills;
18001810
# drain it from a background thread (endwin() releases the GIL).
1801-
reader = threading.Thread(target=self._drain_pty, args=(master,),
1811+
stop = threading.Event()
1812+
reader = threading.Thread(target=self._drain_pty, args=(master, stop),
18021813
daemon=True)
18031814
reader.start()
1804-
self.addCleanup(reader.join, SHORT_TIMEOUT)
1805-
# Close the master first (cleanups run in reverse): on macOS, closing
1806-
# the slave first blocks until its pending output drains.
1807-
self.addCleanup(os.close, slave)
1815+
# Stop and join the reader before closing the fds: on macOS, closing
1816+
# either end while the reader is blocked in read() hangs.
1817+
def stop_reader():
1818+
stop.set()
1819+
reader.join(SHORT_TIMEOUT)
18081820
self.addCleanup(os.close, master)
1821+
self.addCleanup(os.close, slave)
1822+
self.addCleanup(stop_reader)
18091823
return slave
18101824

18111825
def test_newterm(self):

0 commit comments

Comments
 (0)