Skip to content

Commit d5dcfc4

Browse files
ambvmiss-islington
authored andcommitted
pythongh-134466: Don't run when termios is inaccessible (pythonGH-138911)
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) Co-authored-by: Łukasz Langa <[email protected]>
1 parent 6038447 commit d5dcfc4

File tree

3 files changed

+64
-41
lines changed

3 files changed

+64
-41
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: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
from . import terminfo
3737
from .console import Console, Event
38-
from .fancy_termios import tcgetattr, tcsetattr
38+
from .fancy_termios import tcgetattr, tcsetattr, TermState
3939
from .trace import trace
4040
from .unix_eventqueue import EventQueue
4141
from .utils import wlen
@@ -51,16 +51,19 @@
5151

5252
# types
5353
if TYPE_CHECKING:
54-
from typing import IO, Literal, overload
54+
from typing import AbstractSet, IO, Literal, overload, cast
5555
else:
5656
overload = lambda func: None
57+
cast = lambda typ, val: val
5758

5859

5960
class InvalidTerminal(RuntimeError):
60-
pass
61+
def __init__(self, message: str) -> None:
62+
super().__init__(errno.EIO, message)
6163

6264

6365
_error = (termios.error, InvalidTerminal)
66+
_error_codes_to_ignore = frozenset([errno.EIO, errno.ENXIO, errno.EPERM])
6467

6568
SIGWINCH_EVENT = "repaint"
6669

@@ -125,12 +128,13 @@ def __init__(self):
125128

126129
def register(self, fd, flag):
127130
self.fd = fd
131+
128132
# note: The 'timeout' argument is received as *milliseconds*
129133
def poll(self, timeout: float | None = None) -> list[int]:
130134
if timeout is None:
131135
r, w, e = select.select([self.fd], [], [])
132136
else:
133-
r, w, e = select.select([self.fd], [], [], timeout/1000)
137+
r, w, e = select.select([self.fd], [], [], timeout / 1000)
134138
return r
135139

136140
poll = MinimalPoll # type: ignore[assignment]
@@ -164,8 +168,15 @@ def __init__(
164168
and os.getenv("TERM_PROGRAM") == "Apple_Terminal"
165169
)
166170

171+
try:
172+
self.__input_fd_set(tcgetattr(self.input_fd), ignore=frozenset())
173+
except _error as e:
174+
raise RuntimeError(f"termios failure ({e.args[1]})")
175+
167176
@overload
168-
def _my_getstr(cap: str, optional: Literal[False] = False) -> bytes: ...
177+
def _my_getstr(
178+
cap: str, optional: Literal[False] = False
179+
) -> bytes: ...
169180

170181
@overload
171182
def _my_getstr(cap: str, optional: bool) -> bytes | None: ...
@@ -205,7 +216,9 @@ def _my_getstr(cap: str, optional: bool = False) -> bytes | None:
205216

206217
self.__setup_movement()
207218

208-
self.event_queue = EventQueue(self.input_fd, self.encoding, self.terminfo)
219+
self.event_queue = EventQueue(
220+
self.input_fd, self.encoding, self.terminfo
221+
)
209222
self.cursor_visible = 1
210223

211224
signal.signal(signal.SIGCONT, self._sigcont_handler)
@@ -217,7 +230,6 @@ def _sigcont_handler(self, signum, frame):
217230
def __read(self, n: int) -> bytes:
218231
return os.read(self.input_fd, n)
219232

220-
221233
def change_encoding(self, encoding: str) -> None:
222234
"""
223235
Change the encoding used for I/O operations.
@@ -329,6 +341,8 @@ def prepare(self):
329341
"""
330342
Prepare the console for input/output operations.
331343
"""
344+
self.__buffer = []
345+
332346
self.__svtermstate = tcgetattr(self.input_fd)
333347
raw = self.__svtermstate.copy()
334348
raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
@@ -340,14 +354,7 @@ def prepare(self):
340354
raw.lflag |= termios.ISIG
341355
raw.cc[termios.VMIN] = 1
342356
raw.cc[termios.VTIME] = 0
343-
try:
344-
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
345-
except termios.error as e:
346-
if e.args[0] != errno.EIO:
347-
# gh-135329: when running under external programs (like strace),
348-
# tcsetattr may fail with EIO. We can safely ignore this
349-
# and continue with default terminal settings.
350-
raise
357+
self.__input_fd_set(raw)
351358

352359
# In macOS terminal we need to deactivate line wrap via ANSI escape code
353360
if self.is_apple_terminal:
@@ -356,8 +363,6 @@ def prepare(self):
356363
self.screen = []
357364
self.height, self.width = self.getheightwidth()
358365

359-
self.__buffer = []
360-
361366
self.posxy = 0, 0
362367
self.__gone_tall = 0
363368
self.__move = self.__move_short
@@ -379,11 +384,7 @@ def restore(self):
379384
self.__disable_bracketed_paste()
380385
self.__maybe_write_code(self._rmkx)
381386
self.flushoutput()
382-
try:
383-
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
384-
except termios.error as e:
385-
if e.args[0] != errno.EIO:
386-
raise
387+
self.__input_fd_set(self.__svtermstate)
387388

388389
if self.is_apple_terminal:
389390
os.write(self.output_fd, b"\033[?7h")
@@ -820,3 +821,17 @@ def __tputs(self, fmt, prog=delayprog):
820821
os.write(self.output_fd, self._pad * nchars)
821822
else:
822823
time.sleep(float(delay) / 1000.0)
824+
825+
def __input_fd_set(
826+
self,
827+
state: TermState,
828+
ignore: AbstractSet[int] = _error_codes_to_ignore,
829+
) -> bool:
830+
try:
831+
tcsetattr(self.input_fd, termios.TCSADRAIN, state)
832+
except termios.error as te:
833+
if te.args[0] not in ignore:
834+
raise
835+
return False
836+
else:
837+
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)