Skip to content

Commit 3db6d82

Browse files
authored
[3.13] gh-134466: Don't run when termios is inaccessible (GH-138911) (GH-139030)
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 2fc7004)
1 parent 216f655 commit 3db6d82

File tree

3 files changed

+61
-40
lines changed

3 files changed

+61
-40
lines changed

Lib/_pyrepl/fancy_termios.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,25 @@
2020
import termios
2121

2222

23+
TYPE_CHECKING = False
24+
25+
if TYPE_CHECKING:
26+
from typing import cast
27+
else:
28+
cast = lambda typ, val: val
29+
30+
2331
class TermState:
24-
def __init__(self, tuples):
25-
(
26-
self.iflag,
27-
self.oflag,
28-
self.cflag,
29-
self.lflag,
30-
self.ispeed,
31-
self.ospeed,
32-
self.cc,
33-
) = tuples
32+
def __init__(self, attrs: list[int | list[bytes]]) -> None:
33+
self.iflag = cast(int, attrs[0])
34+
self.oflag = cast(int, attrs[1])
35+
self.cflag = cast(int, attrs[2])
36+
self.lflag = cast(int, attrs[3])
37+
self.ispeed = cast(int, attrs[4])
38+
self.ospeed = cast(int, attrs[5])
39+
self.cc = cast(list[bytes], attrs[6])
3440

35-
def as_list(self):
41+
def as_list(self) -> list[int | list[bytes]]:
3642
return [
3743
self.iflag,
3844
self.oflag,
@@ -45,32 +51,32 @@ def as_list(self):
4551
self.cc[:],
4652
]
4753

48-
def copy(self):
54+
def copy(self) -> "TermState":
4955
return self.__class__(self.as_list())
5056

5157

52-
def tcgetattr(fd):
58+
def tcgetattr(fd: int) -> TermState:
5359
return TermState(termios.tcgetattr(fd))
5460

5561

56-
def tcsetattr(fd, when, attrs):
62+
def tcsetattr(fd: int, when: int, attrs: TermState) -> None:
5763
termios.tcsetattr(fd, when, attrs.as_list())
5864

5965

6066
class Term(TermState):
6167
TS__init__ = TermState.__init__
6268

63-
def __init__(self, fd=0):
69+
def __init__(self, fd: int = 0) -> None:
6470
self.TS__init__(termios.tcgetattr(fd))
6571
self.fd = fd
66-
self.stack = []
72+
self.stack: list[list[int | list[bytes]]] = []
6773

68-
def save(self):
74+
def save(self) -> None:
6975
self.stack.append(self.as_list())
7076

71-
def set(self, when=termios.TCSANOW):
77+
def set(self, when: int = termios.TCSANOW) -> None:
7278
termios.tcsetattr(self.fd, when, self.as_list())
7379

74-
def restore(self):
80+
def restore(self) -> None:
7581
self.TS__init__(self.stack.pop())
7682
self.set()

Lib/_pyrepl/unix_console.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
from . import curses
3636
from .console import Console, Event
37-
from .fancy_termios import tcgetattr, tcsetattr
37+
from .fancy_termios import tcgetattr, tcsetattr, TermState
3838
from .trace import trace
3939
from .unix_eventqueue import EventQueue
4040
from .utils import wlen
@@ -44,16 +44,19 @@
4444

4545
# types
4646
if TYPE_CHECKING:
47-
from typing import IO, Literal, overload
47+
from typing import AbstractSet, IO, Literal, overload, cast
4848
else:
4949
overload = lambda func: None
50+
cast = lambda typ, val: val
5051

5152

5253
class InvalidTerminal(RuntimeError):
53-
pass
54+
def __init__(self, message: str) -> None:
55+
super().__init__(errno.EIO, message)
5456

5557

5658
_error = (termios.error, curses.error, InvalidTerminal)
59+
_error_codes_to_ignore = frozenset([errno.EIO, errno.ENXIO, errno.EPERM])
5760

5861
SIGWINCH_EVENT = "repaint"
5962

@@ -118,12 +121,13 @@ def __init__(self):
118121

119122
def register(self, fd, flag):
120123
self.fd = fd
124+
121125
# note: The 'timeout' argument is received as *milliseconds*
122126
def poll(self, timeout: float | None = None) -> list[int]:
123127
if timeout is None:
124128
r, w, e = select.select([self.fd], [], [])
125129
else:
126-
r, w, e = select.select([self.fd], [], [], timeout/1000)
130+
r, w, e = select.select([self.fd], [], [], timeout / 1000)
127131
return r
128132

129133
poll = MinimalPoll # type: ignore[assignment]
@@ -159,8 +163,15 @@ def __init__(
159163
and os.getenv("TERM_PROGRAM") == "Apple_Terminal"
160164
)
161165

166+
try:
167+
self.__input_fd_set(tcgetattr(self.input_fd), ignore=frozenset())
168+
except _error as e:
169+
raise RuntimeError(f"termios failure ({e.args[1]})")
170+
162171
@overload
163-
def _my_getstr(cap: str, optional: Literal[False] = False) -> bytes: ...
172+
def _my_getstr(
173+
cap: str, optional: Literal[False] = False
174+
) -> bytes: ...
164175

165176
@overload
166177
def _my_getstr(cap: str, optional: bool) -> bytes | None: ...
@@ -226,7 +237,6 @@ def __read(self, n: int) -> bytes:
226237
self.input_buffer_pos = 0
227238
return ret
228239

229-
230240
def change_encoding(self, encoding: str) -> None:
231241
"""
232242
Change the encoding used for I/O operations.
@@ -338,6 +348,8 @@ def prepare(self):
338348
"""
339349
Prepare the console for input/output operations.
340350
"""
351+
self.__buffer = []
352+
341353
self.__svtermstate = tcgetattr(self.input_fd)
342354
raw = self.__svtermstate.copy()
343355
raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
@@ -349,14 +361,7 @@ def prepare(self):
349361
raw.lflag |= termios.ISIG
350362
raw.cc[termios.VMIN] = 1
351363
raw.cc[termios.VTIME] = 0
352-
try:
353-
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
354-
except termios.error as e:
355-
if e.args[0] != errno.EIO:
356-
# gh-135329: when running under external programs (like strace),
357-
# tcsetattr may fail with EIO. We can safely ignore this
358-
# and continue with default terminal settings.
359-
raise
364+
self.__input_fd_set(raw)
360365

361366
# In macOS terminal we need to deactivate line wrap via ANSI escape code
362367
if self.is_apple_terminal:
@@ -365,8 +370,6 @@ def prepare(self):
365370
self.screen = []
366371
self.height, self.width = self.getheightwidth()
367372

368-
self.__buffer = []
369-
370373
self.posxy = 0, 0
371374
self.__gone_tall = 0
372375
self.__move = self.__move_short
@@ -388,11 +391,7 @@ def restore(self):
388391
self.__disable_bracketed_paste()
389392
self.__maybe_write_code(self._rmkx)
390393
self.flushoutput()
391-
try:
392-
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
393-
except termios.error as e:
394-
if e.args[0] != errno.EIO:
395-
raise
394+
self.__input_fd_set(self.__svtermstate)
396395

397396
if self.is_apple_terminal:
398397
os.write(self.output_fd, b"\033[?7h")
@@ -831,3 +830,17 @@ def __tputs(self, fmt, prog=delayprog):
831830
os.write(self.output_fd, self._pad * nchars)
832831
else:
833832
time.sleep(float(delay) / 1000.0)
833+
834+
def __input_fd_set(
835+
self,
836+
state: TermState,
837+
ignore: AbstractSet[int] = _error_codes_to_ignore,
838+
) -> bool:
839+
try:
840+
tcsetattr(self.input_fd, termios.TCSADRAIN, state)
841+
except termios.error as te:
842+
if te.args[0] not in ignore:
843+
raise
844+
return False
845+
else:
846+
return True
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Don't run PyREPL in a degraded environment where setting termios attributes
2+
is not allowed.

0 commit comments

Comments
 (0)