Skip to content
44 changes: 36 additions & 8 deletions Lib/_pyrepl/unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import signal
import struct
import termios
import threading
import time
import types
import platform
Expand Down Expand Up @@ -157,6 +158,8 @@ def __init__(

self.pollob = poll()
self.pollob.register(self.input_fd, select.POLLIN)
self._poll_lock = threading.RLock()
self._polling_thread: threading.Thread | None = None
self.terminfo = terminfo.TermInfo(term or None)
self.term = term

Expand Down Expand Up @@ -325,8 +328,11 @@ def prepare(self):
"""
Prepare the console for input/output operations.
"""
self.__svtermstate = tcgetattr(self.input_fd)
raw = self.__svtermstate.copy()
# gh-130168: prevents signal handlers from overwriting the original state
if not hasattr(self, '_UnixConsole__svtermstate'):
self.__svtermstate = tcgetattr(self.input_fd)

raw = tcgetattr(self.input_fd).copy()
raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
raw.oflag &= ~(termios.OPOST)
raw.cflag &= ~(termios.CSIZE | termios.PARENB)
Expand Down Expand Up @@ -368,7 +374,11 @@ def restore(self):
self.__disable_bracketed_paste()
self.__maybe_write_code(self._rmkx)
self.flushoutput()
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)

if hasattr(self, '_UnixConsole__svtermstate'):
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
# Clear the saved state so prepare() can save a fresh one next time
del self.__svtermstate

if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
os.write(self.output_fd, b"\033[?7h")
Expand Down Expand Up @@ -417,10 +427,25 @@ def wait(self, timeout: float | None = None) -> bool:
"""
Wait for events on the console.
"""
return (
not self.event_queue.empty()
or bool(self.pollob.poll(timeout))
)
if not self.event_queue.empty():
return True

current_thread = threading.current_thread()

if self._polling_thread is current_thread:
# This is a re-entrant call from the same thread
# like old repl runtime error
raise RuntimeError("can't re-enter readline")

if not self._poll_lock.acquire(blocking=False):
return False

try:
self._polling_thread = current_thread
return bool(self.pollob.poll(timeout))
finally:
self._polling_thread = None
self._poll_lock.release()

def set_cursor_vis(self, visible):
"""
Expand Down Expand Up @@ -786,7 +811,10 @@ def __tputs(self, fmt, prog=delayprog):
# using .get() means that things will blow up
# only if the bps is actually needed (which I'm
# betting is pretty unlkely)
bps = ratedict.get(self.__svtermstate.ospeed)
if hasattr(self, '_UnixConsole__svtermstate'):
bps = ratedict.get(self.__svtermstate.ospeed)
else:
bps = None
while True:
m = prog.search(fmt)
if not m:
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_pyrepl/test_unix_console.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import itertools
import os
import sys
import threading
import unittest
from functools import partial
from test.support import os_helper, force_not_colorized_test_class
Expand Down Expand Up @@ -303,3 +304,17 @@ def test_getheightwidth_with_invalid_environ(self, _os_write):
self.assertIsInstance(console.getheightwidth(), tuple)
os.environ = []
self.assertIsInstance(console.getheightwidth(), tuple)

def test_wait_reentry_protection(self, _os_write):
# gh-130168: Test signal handler re-entry protection
console = UnixConsole(term="xterm")
console.prepare()

console._polling_thread = threading.current_thread()

with self.assertRaises(RuntimeError) as cm:
console.wait(timeout=0)
self.assertEqual(str(cm.exception), "can't re-enter readline")

console._polling_thread = None
console.restore()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix: pyrepl messed up terminal if a signal handler expects stdin.
Loading