From e003828e26186538eec7ebe2757b3eb20612dc69 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:30:47 +0200 Subject: [PATCH] fix: ask nothing when the output goes to a file install.py > log.txt at a terminal wrote the menu into the file and waited for an answer nobody saw. A question now needs stdin and stdout on a terminal, otherwise the run goes on as with --no-input and says so. --- install.py | 15 +++++++++++++++ tests/test_install_experience.py | 24 ++++++++++++++++++++++++ wiki/installer.md | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/install.py b/install.py index e857d7ac..cc258bf0 100644 --- a/install.py +++ b/install.py @@ -8933,6 +8933,14 @@ def _unknown_target(target: str, plat: str, targets: dict) -> None: print(f" {plat} publishes no target list", file=sys.stderr) +def _is_terminal(stream: TextIO) -> bool: + isatty = getattr(stream, "isatty", None) + try: + return bool(isatty and isatty()) + except (OSError, ValueError): + return False + + def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: """One run of the installer, once the options are read.""" KEPT.reset() @@ -8940,6 +8948,13 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: interactive = sys.stdin.isatty() and not args.no_input and not args.quiet listing = (args.list_targets or args.list_systems or args.list_cores or args.list_emulators) + if interactive and not _is_terminal(real_stdout): + # A question goes to stdout: sent to a file, it waited on an answer + # to a prompt nobody could see. + interactive = False + if not listing and not args.check: + print("Output goes to a file, so nothing is asked: the run goes on as " + "with --no-input.", file=sys.stderr) print(f"{term.bold('RetroBIOS')} {term.dim(installer_id())}\n") os_type = detect_os() diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 76dc68f4..2a74b539 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -566,6 +566,30 @@ class AFolderThisUserCannotEnter(unittest.TestCase): self.assertEqual(state, "wrong_hash") +class OutputToAFileAsksNothing(unittest.TestCase): + """`install.py > log.txt` at a terminal: the menu went into the file + and the run waited on an answer to a prompt nobody could see.""" + + @unittest.skipIf(os.name == "nt", "a pseudo-terminal for stdin") + def test_the_run_goes_on_as_with_no_input(self): + import pty + + master, slave = pty.openpty() + try: + with FakeRepository() as repo: + proc = subprocess.run( + repo.command(), env=repo.env(), stdin=slave, capture_output=True, + text=True, timeout=60, check=False) + installed = repo.installed() + finally: + os.close(master) + os.close(slave) + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.assertIn("nothing is asked", proc.stderr) + self.assertNotIn("Enter) install everything", proc.stdout) + self.assertEqual(len(installed), len(FakeRepository.PAYLOADS)) + + class _Tty(io.StringIO): def isatty(self) -> bool: return True diff --git a/wiki/installer.md b/wiki/installer.md index cb1dcedf..a3efbc96 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -701,7 +701,7 @@ what the options name, everything by default. | `--list-cores` | Print the cores whose extra files a platform installs | | `--list-emulators` | Print every name `--emulator` accepts, each buildbot name beside the emulator it stands for | | `--romm-firmware TEMPLATE` | RomM: where each platform's firmware goes in the library, as `filesystem.structure.firmware` in RomM's `config.yml` says (`{platform}/bios` for a library laid out as `{platform}/roms`). Read from `config.yml` beside the library when it is there. See [RomM firmware layout](#romm-firmware-layout) | -| `--no-input` | Never ask a question, even at a terminal | +| `--no-input` | Never ask a question, even at a terminal. A run whose output goes to a file or a pipe asks nothing either, and says so on stderr | | `--check` | Report and exit without writing | | `--dry-run` | Show what would be downloaded, the totals first, then by system and with sizes, without writing anything. A plan taller than the screen names the ten largest systems, and `--verbose` lists every system and file | | `--yes`, `-y` | Do not ask before a download of 1 GB or more |