From bc4bcadcd38c3cefe6371503ce5d2163e21299bb Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:13:42 +0200 Subject: [PATCH] fix: keep a crash log for a card read error Any EIO was read as the window closing, so a failing SD card ended the interface with 129 and no error.log. The error now counts as a closed window only once the terminal itself no longer answers tcgetattr. --- install.py | 28 ++++++++++++++++++++++------ tests/test_install_tui.py | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/install.py b/install.py index 26d639ca..e8aa1050 100644 --- a/install.py +++ b/install.py @@ -12966,7 +12966,7 @@ def run_tui(args: argparse.Namespace) -> int: # The result is printed in the terminal on the way out. state.exit_code = state.code if state.code is not None else 1 except Exception as exc: # noqa: BLE001 - if flags["term"] or _closed_terminal(exc): + if flags["term"] or _closed_terminal(exc, terminal.fd): # The window closed under a screen that was changing, or the # SIGCONT that comes with its SIGHUP put the screen back: the # write failed before the signal was read. That is its exit. @@ -13030,13 +13030,29 @@ def _tui_startup(args: argparse.Namespace) -> tuple[TuiState, list[tuple]] | Non _CLOSED_TERMINAL = frozenset({errno.EIO, errno.EPIPE, errno.ENXIO}) -def _closed_terminal(exc: BaseException) -> bool: +def _closed_terminal(exc: BaseException, fd: int | None = None) -> bool: """Whether exc is the terminal gone: an OSError, or a termios.error, - whose first argument is the errno, answering EIO, EPIPE or ENXIO.""" + whose first argument is the errno, answering EIO, EPIPE or ENXIO, and, + given the terminal's fd, that terminal no longer answering. A failing + SD card answers EIO too, and that read error is kept in a crash log.""" if isinstance(exc, OSError): - return exc.errno in _CLOSED_TERMINAL - if type(exc).__name__ == "error" and type(exc).__module__ == "termios": - return bool(exc.args) and exc.args[0] in _CLOSED_TERMINAL + closed = exc.errno in _CLOSED_TERMINAL + elif type(exc).__name__ == "error" and type(exc).__module__ == "termios": + closed = bool(exc.args) and exc.args[0] in _CLOSED_TERMINAL + else: + closed = False + return closed and (fd is None or _terminal_gone(fd)) + + +def _terminal_gone(fd: int) -> bool: + """Whether the terminal at fd hung up: a closed window answers EIO to + tcgetattr, a live one its settings.""" + import termios # noqa: PLC0415 + + try: + termios.tcgetattr(fd) + except (termios.error, OSError): + return True return False diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 121104eb..2b03d2d8 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -2557,6 +2557,29 @@ class IdleExit(unittest.TestCase): ["RetroBIOS: No button pressed, closing."]) +class AReadErrorIsNotAClosedWindow(unittest.TestCase): + """Any EIO read as the window closing: a failing SD card answers EIO + too, and that run ended 129, no error.log, no launcher message.""" + + def test_eio_counts_only_once_the_terminal_hung_up(self): + import pty + + master, slave = pty.openpty() + try: + card = OSError(errno.EIO, "Input/output error", "/media/card/bios/x.bin") + self.assertFalse(install._closed_terminal(card, slave)) + self.assertFalse(install._closed_terminal( + OSError(errno.ENOENT, "No such file"), slave)) + os.close(master) + master = -1 + self.assertTrue(install._closed_terminal(card, slave)) + self.assertTrue(install._closed_terminal(termios.error(errno.EIO, "gone"), slave)) + finally: + os.close(slave) + if master >= 0: + os.close(master) + + class AuditRoundFour(unittest.TestCase): """tui_audit_3.md, each finding held where it was found."""