Skip to content

Commit 5b30f61

Browse files
committed
gh-134466: Don't run when termios is inaccessible
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.
1 parent 481588a commit 5b30f61

File tree

2 files changed

+62
-30
lines changed

2 files changed

+62
-30
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 & 11 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[int] = frozenset([errno.EIO, 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]
@@ -160,8 +164,15 @@ def __init__(
160164
self.terminfo = terminfo.TermInfo(term or None)
161165
self.term = term
162166

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

166177
@overload
167178
def _my_getstr(cap: str, optional: bool) -> bytes | None: ...
@@ -201,7 +212,9 @@ def _my_getstr(cap: str, optional: bool = False) -> bytes | None:
201212

202213
self.__setup_movement()
203214

204-
self.event_queue = EventQueue(self.input_fd, self.encoding, self.terminfo)
215+
self.event_queue = EventQueue(
216+
self.input_fd, self.encoding, self.terminfo
217+
)
205218
self.cursor_visible = 1
206219

207220
signal.signal(signal.SIGCONT, self._sigcont_handler)
@@ -213,7 +226,6 @@ def _sigcont_handler(self, signum, frame):
213226
def __read(self, n: int) -> bytes:
214227
return os.read(self.input_fd, n)
215228

216-
217229
def change_encoding(self, encoding: str) -> None:
218230
"""
219231
Change the encoding used for I/O operations.
@@ -325,6 +337,8 @@ def prepare(self):
325337
"""
326338
Prepare the console for input/output operations.
327339
"""
340+
self.__buffer = []
341+
328342
self.__svtermstate = tcgetattr(self.input_fd)
329343
raw = self.__svtermstate.copy()
330344
raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
@@ -336,7 +350,7 @@ def prepare(self):
336350
raw.lflag |= termios.ISIG
337351
raw.cc[termios.VMIN] = 1
338352
raw.cc[termios.VTIME] = 0
339-
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
353+
self.__input_fd_set(raw)
340354

341355
# In macOS terminal we need to deactivate line wrap via ANSI escape code
342356
if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
@@ -345,8 +359,6 @@ def prepare(self):
345359
self.screen = []
346360
self.height, self.width = self.getheightwidth()
347361

348-
self.__buffer = []
349-
350362
self.posxy = 0, 0
351363
self.__gone_tall = 0
352364
self.__move = self.__move_short
@@ -368,7 +380,7 @@ def restore(self):
368380
self.__disable_bracketed_paste()
369381
self.__maybe_write_code(self._rmkx)
370382
self.flushoutput()
371-
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
383+
self.__input_fd_set(self.__svtermstate)
372384

373385
if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
374386
os.write(self.output_fd, b"\033[?7h")
@@ -803,3 +815,17 @@ def __tputs(self, fmt, prog=delayprog):
803815
os.write(self.output_fd, self._pad * nchars)
804816
else:
805817
time.sleep(float(delay) / 1000.0)
818+
819+
def __input_fd_set(
820+
self,
821+
state: TermState,
822+
ignore: AbstractSet[int] = _error_codes_to_ignore,
823+
) -> bool:
824+
try:
825+
tcsetattr(self.input_fd, termios.TCSADRAIN, state)
826+
except termios.error as te:
827+
if te.args[0] not in ignore:
828+
raise
829+
return False
830+
else:
831+
return True

0 commit comments

Comments
 (0)