Skip to content

Commit d27487e

Browse files
Drain the pty masters in ScreenTests and release the GIL in endwin()
ScreenTests drives curses over pseudo-terminals whose master ends are never read. On macOS (unlike Linux) the tcdrain() that curses performs inside endwin(), and even a plain write(), blocks once the unread output fills the pty buffer, so the test hung until the timeout. Drain the masters synchronously before endwin(), leaving room for its output. Also release the GIL around the endwin() call, so that it no longer blocks other threads while it talks to the terminal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0028058 commit d27487e

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

Lib/test/test_curses.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,11 +1773,26 @@ def setUp(self):
17731773
self.save_signals = SaveSignals()
17741774
self.save_signals.save()
17751775
self.addCleanup(self.save_signals.restore)
1776+
self.pty_masters = []
1777+
1778+
def drain_ptys(self):
1779+
# Discard whatever curses has written to the screens. Nothing reads
1780+
# the master end, so on platforms such as macOS (but not Linux) the
1781+
# tcdrain() that curses performs inside endwin() -- and even a plain
1782+
# write() -- blocks once the unread output fills the pty buffer.
1783+
# Draining here, before endwin(), leaves room for its output to drain.
1784+
for master in self.pty_masters:
1785+
try:
1786+
while os.read(master, 65536):
1787+
pass
1788+
except BlockingIOError:
1789+
pass
17761790

17771791
def tearDown(self):
17781792
# Leave visual mode and reclaim the screens the test created, while
17791793
# their pseudo-terminals are still open (closing them happens later,
17801794
# via the make_pty() cleanups).
1795+
self.drain_ptys()
17811796
try:
17821797
curses.endwin()
17831798
except curses.error:
@@ -1786,6 +1801,8 @@ def tearDown(self):
17861801

17871802
def make_pty(self):
17881803
master, slave = os.openpty()
1804+
os.set_blocking(master, False)
1805+
self.pty_masters.append(master)
17891806
self.addCleanup(os.close, master)
17901807
self.addCleanup(os.close, slave)
17911808
return slave

Modules/_cursesmodule.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3723,7 +3723,18 @@ De-initialize the library, and return terminal to normal status.
37233723
static PyObject *
37243724
_curses_endwin_impl(PyObject *module)
37253725
/*[clinic end generated code: output=c0150cd96d2f4128 input=e172cfa43062f3fa]*/
3726-
NoArgNoReturnFunctionBody(endwin)
3726+
{
3727+
PyCursesStatefulInitialised(module);
3728+
3729+
/* endwin() writes to the terminal and may call tcdrain(), which can block
3730+
(e.g. on a pty whose output is not being read); release the GIL so other
3731+
threads -- including one draining that terminal -- can run meanwhile. */
3732+
int code;
3733+
Py_BEGIN_ALLOW_THREADS
3734+
code = endwin();
3735+
Py_END_ALLOW_THREADS
3736+
return curses_check_err(module, code, "endwin", NULL);
3737+
}
37273738

37283739
/*[clinic input]
37293740
_curses.erasechar

0 commit comments

Comments
 (0)