]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-134466: Don't run when termios is inaccessible (GH-138911) (GH-139030)
authorŁukasz Langa <lukasz@langa.pl>
Wed, 17 Sep 2025 16:25:40 +0000 (17:25 +0100)
committerGitHub <noreply@github.com>
Wed, 17 Sep 2025 16:25:40 +0000 (18:25 +0200)
Without the ability to set required capabilities, the REPL cannot
function properly (syntax highlighting and multiline editing can't
work).

We refuse to work in this degraded state.
(cherry picked from commit 2fc7004d5437e7bb0a1f5b962be441ef0ee7434b)

Lib/_pyrepl/fancy_termios.py
Lib/_pyrepl/unix_console.py
Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-14-04-56.gh-issue-134466.yR4fYW.rst [new file with mode: 0644]

index 0468b9a267026737b2fdf9597b351ea8d89b0c9e..8d5bd183f21339805f72054f820b5fcb595e1f5a 100644 (file)
 import termios
 
 
+TYPE_CHECKING = False
+
+if TYPE_CHECKING:
+    from typing import cast
+else:
+    cast = lambda typ, val: val
+
+
 class TermState:
-    def __init__(self, tuples):
-        (
-            self.iflag,
-            self.oflag,
-            self.cflag,
-            self.lflag,
-            self.ispeed,
-            self.ospeed,
-            self.cc,
-        ) = tuples
+    def __init__(self, attrs: list[int | list[bytes]]) -> None:
+        self.iflag = cast(int, attrs[0])
+        self.oflag = cast(int, attrs[1])
+        self.cflag = cast(int, attrs[2])
+        self.lflag = cast(int, attrs[3])
+        self.ispeed = cast(int, attrs[4])
+        self.ospeed = cast(int, attrs[5])
+        self.cc = cast(list[bytes], attrs[6])
 
-    def as_list(self):
+    def as_list(self) -> list[int | list[bytes]]:
         return [
             self.iflag,
             self.oflag,
@@ -45,32 +51,32 @@ class TermState:
             self.cc[:],
         ]
 
-    def copy(self):
+    def copy(self) -> "TermState":
         return self.__class__(self.as_list())
 
 
-def tcgetattr(fd):
+def tcgetattr(fd: int) -> TermState:
     return TermState(termios.tcgetattr(fd))
 
 
-def tcsetattr(fd, when, attrs):
+def tcsetattr(fd: int, when: int, attrs: TermState) -> None:
     termios.tcsetattr(fd, when, attrs.as_list())
 
 
 class Term(TermState):
     TS__init__ = TermState.__init__
 
-    def __init__(self, fd=0):
+    def __init__(self, fd: int = 0) -> None:
         self.TS__init__(termios.tcgetattr(fd))
         self.fd = fd
-        self.stack = []
+        self.stack: list[list[int | list[bytes]]] = []
 
-    def save(self):
+    def save(self) -> None:
         self.stack.append(self.as_list())
 
-    def set(self, when=termios.TCSANOW):
+    def set(self, when: int = termios.TCSANOW) -> None:
         termios.tcsetattr(self.fd, when, self.as_list())
 
-    def restore(self):
+    def restore(self) -> None:
         self.TS__init__(self.stack.pop())
         self.set()
index feb0fc3f5ce3c0f3b1aec15837a2554d66262505..f0cb950170a6badde8ce0f53b7b8026b164ad88b 100644 (file)
@@ -34,7 +34,7 @@ from fcntl import ioctl
 
 from . import curses
 from .console import Console, Event
-from .fancy_termios import tcgetattr, tcsetattr
+from .fancy_termios import tcgetattr, tcsetattr, TermState
 from .trace import trace
 from .unix_eventqueue import EventQueue
 from .utils import wlen
@@ -44,16 +44,19 @@ TYPE_CHECKING = False
 
 # types
 if TYPE_CHECKING:
-    from typing import IO, Literal, overload
+    from typing import AbstractSet, IO, Literal, overload, cast
 else:
     overload = lambda func: None
+    cast = lambda typ, val: val
 
 
 class InvalidTerminal(RuntimeError):
-    pass
+    def __init__(self, message: str) -> None:
+        super().__init__(errno.EIO, message)
 
 
 _error = (termios.error, curses.error, InvalidTerminal)
+_error_codes_to_ignore = frozenset([errno.EIO, errno.ENXIO, errno.EPERM])
 
 SIGWINCH_EVENT = "repaint"
 
@@ -118,12 +121,13 @@ except AttributeError:
 
         def register(self, fd, flag):
             self.fd = fd
+
         # note: The 'timeout' argument is received as *milliseconds*
         def poll(self, timeout: float | None = None) -> list[int]:
             if timeout is None:
                 r, w, e = select.select([self.fd], [], [])
             else:
-                r, w, e = select.select([self.fd], [], [], timeout/1000)
+                r, w, e = select.select([self.fd], [], [], timeout / 1000)
             return r
 
     poll = MinimalPoll  # type: ignore[assignment]
@@ -159,8 +163,15 @@ class UnixConsole(Console):
             and os.getenv("TERM_PROGRAM") == "Apple_Terminal"
         )
 
+        try:
+            self.__input_fd_set(tcgetattr(self.input_fd), ignore=frozenset())
+        except _error as e:
+            raise RuntimeError(f"termios failure ({e.args[1]})")
+
         @overload
-        def _my_getstr(cap: str, optional: Literal[False] = False) -> bytes: ...
+        def _my_getstr(
+            cap: str, optional: Literal[False] = False
+        ) -> bytes: ...
 
         @overload
         def _my_getstr(cap: str, optional: bool) -> bytes | None: ...
@@ -226,7 +237,6 @@ class UnixConsole(Console):
             self.input_buffer_pos = 0
         return ret
 
-
     def change_encoding(self, encoding: str) -> None:
         """
         Change the encoding used for I/O operations.
@@ -338,6 +348,8 @@ class UnixConsole(Console):
         """
         Prepare the console for input/output operations.
         """
+        self.__buffer = []
+
         self.__svtermstate = tcgetattr(self.input_fd)
         raw = self.__svtermstate.copy()
         raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
@@ -349,14 +361,7 @@ class UnixConsole(Console):
         raw.lflag |= termios.ISIG
         raw.cc[termios.VMIN] = 1
         raw.cc[termios.VTIME] = 0
-        try:
-            tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
-        except termios.error as e:
-            if e.args[0] != errno.EIO:
-                # gh-135329: when running under external programs (like strace),
-                # tcsetattr may fail with EIO. We can safely ignore this
-                # and continue with default terminal settings.
-                raise
+        self.__input_fd_set(raw)
 
         # In macOS terminal we need to deactivate line wrap via ANSI escape code
         if self.is_apple_terminal:
@@ -365,8 +370,6 @@ class UnixConsole(Console):
         self.screen = []
         self.height, self.width = self.getheightwidth()
 
-        self.__buffer = []
-
         self.posxy = 0, 0
         self.__gone_tall = 0
         self.__move = self.__move_short
@@ -388,11 +391,7 @@ class UnixConsole(Console):
         self.__disable_bracketed_paste()
         self.__maybe_write_code(self._rmkx)
         self.flushoutput()
-        try:
-            tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
-        except termios.error as e:
-            if e.args[0] != errno.EIO:
-                raise
+        self.__input_fd_set(self.__svtermstate)
 
         if self.is_apple_terminal:
             os.write(self.output_fd, b"\033[?7h")
@@ -831,3 +830,17 @@ class UnixConsole(Console):
                 os.write(self.output_fd, self._pad * nchars)
             else:
                 time.sleep(float(delay) / 1000.0)
+
+    def __input_fd_set(
+        self,
+        state: TermState,
+        ignore: AbstractSet[int] = _error_codes_to_ignore,
+    ) -> bool:
+        try:
+            tcsetattr(self.input_fd, termios.TCSADRAIN, state)
+        except termios.error as te:
+            if te.args[0] not in ignore:
+                raise
+            return False
+        else:
+            return True
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-14-04-56.gh-issue-134466.yR4fYW.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-14-04-56.gh-issue-134466.yR4fYW.rst
new file mode 100644 (file)
index 0000000..4fae7e0
--- /dev/null
@@ -0,0 +1,2 @@
+Don't run PyREPL in a degraded environment where setting termios attributes
+is not allowed.