Skip to content

Commit 0609120

Browse files
committed
fix: pyrepl can messup and poll is not thread safe
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
1 parent d7b9ea5 commit 0609120

File tree

3 files changed

+52
-8
lines changed

3 files changed

+52
-8
lines changed

Lib/_pyrepl/unix_console.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import signal
2929
import struct
3030
import termios
31+
import threading
3132
import time
3233
import types
3334
import platform
@@ -157,6 +158,8 @@ def __init__(
157158

158159
self.pollob = poll()
159160
self.pollob.register(self.input_fd, select.POLLIN)
161+
self._poll_lock = threading.RLock()
162+
self._polling_thread = None
160163
self.terminfo = terminfo.TermInfo(term or None)
161164
self.term = term
162165

@@ -325,8 +328,11 @@ def prepare(self):
325328
"""
326329
Prepare the console for input/output operations.
327330
"""
328-
self.__svtermstate = tcgetattr(self.input_fd)
329-
raw = self.__svtermstate.copy()
331+
# gh-130168: prevents signal handlers from overwriting the original state
332+
if not hasattr(self, '_UnixConsole__svtermstate'):
333+
self.__svtermstate = tcgetattr(self.input_fd)
334+
335+
raw = tcgetattr(self.input_fd).copy()
330336
raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
331337
raw.oflag &= ~(termios.OPOST)
332338
raw.cflag &= ~(termios.CSIZE | termios.PARENB)
@@ -368,7 +374,11 @@ def restore(self):
368374
self.__disable_bracketed_paste()
369375
self.__maybe_write_code(self._rmkx)
370376
self.flushoutput()
371-
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
377+
378+
if hasattr(self, '_UnixConsole__svtermstate'):
379+
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
380+
# Clear the saved state so prepare() can save a fresh one next time
381+
del self.__svtermstate
372382

373383
if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
374384
os.write(self.output_fd, b"\033[?7h")
@@ -417,10 +427,25 @@ def wait(self, timeout: float | None = None) -> bool:
417427
"""
418428
Wait for events on the console.
419429
"""
420-
return (
421-
not self.event_queue.empty()
422-
or bool(self.pollob.poll(timeout))
423-
)
430+
if not self.event_queue.empty():
431+
return True
432+
433+
current_thread = threading.current_thread()
434+
435+
if self._polling_thread is current_thread:
436+
# This is a re-entrant call from the same thread
437+
# like old repl runtime error
438+
raise RuntimeError("can't re-enter readline")
439+
440+
if not self._poll_lock.acquire(blocking=False):
441+
return False
442+
443+
try:
444+
self._polling_thread = current_thread
445+
return bool(self.pollob.poll(timeout))
446+
finally:
447+
self._polling_thread = None
448+
self._poll_lock.release()
424449

425450
def set_cursor_vis(self, visible):
426451
"""
@@ -786,7 +811,10 @@ def __tputs(self, fmt, prog=delayprog):
786811
# using .get() means that things will blow up
787812
# only if the bps is actually needed (which I'm
788813
# betting is pretty unlkely)
789-
bps = ratedict.get(self.__svtermstate.ospeed)
814+
if hasattr(self, '_UnixConsole__svtermstate'):
815+
bps = ratedict.get(self.__svtermstate.ospeed)
816+
else:
817+
bps = None
790818
while True:
791819
m = prog.search(fmt)
792820
if not m:

Lib/test/test_pyrepl/test_unix_console.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import itertools
22
import os
33
import sys
4+
import threading
45
import unittest
56
from functools import partial
67
from test.support import os_helper, force_not_colorized_test_class
@@ -303,3 +304,17 @@ def test_getheightwidth_with_invalid_environ(self, _os_write):
303304
self.assertIsInstance(console.getheightwidth(), tuple)
304305
os.environ = []
305306
self.assertIsInstance(console.getheightwidth(), tuple)
307+
308+
def test_wait_reentry_protection(self, _os_write):
309+
# gh-130168: Test signal handler re-entry protection
310+
console = UnixConsole(term="xterm")
311+
console.prepare()
312+
313+
console._polling_thread = threading.current_thread()
314+
315+
with self.assertRaises(RuntimeError) as cm:
316+
console.wait(timeout=0)
317+
self.assertEqual(str(cm.exception), "can't re-enter readline")
318+
319+
console._polling_thread = None
320+
console.restore()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix: pyrepl messed up terminal if a signal handler expects stdin.

0 commit comments

Comments
 (0)