From 55631a2fbce38a7f7369d524db84defd1d8dd23b Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:50:22 +0200 Subject: [PATCH] fix: end listings quietly when the reader leaves --- install.ps1 | 2 +- install.py | 9 ++++++++- install.sh | 2 +- scripts/auto_fetch.py | 4 +++- scripts/check_buildbot_system.py | 4 +++- scripts/check_freshness.py | 4 +++- scripts/check_release_assets.py | 4 +++- scripts/common.py | 17 +++++++++++++++++ scripts/cross_reference.py | 4 +++- scripts/dedup.py | 4 +++- scripts/diff_truth.py | 4 +++- scripts/download.py | 4 +++- scripts/export_native.py | 4 +++- scripts/fileless_audit.py | 4 +++- scripts/find_pinned_revision.py | 4 +++- scripts/generate_pack.py | 4 +++- scripts/generate_readme.py | 4 +++- scripts/generate_site.py | 4 +++- scripts/generate_truth.py | 4 +++- scripts/identity.py | 4 +++- scripts/list_platforms.py | 4 +++- scripts/mame_ref_audit.py | 4 +++- scripts/migrate.py | 4 +++- scripts/pipeline.py | 4 +++- scripts/profile_sync.py | 4 +++- scripts/provenance_report.py | 4 +++- scripts/refresh_data_dirs.py | 4 +++- scripts/refresh_stale.py | 4 +++- scripts/region_audit.py | 4 +++- scripts/release_record.py | 4 +++- scripts/restore_large_files.py | 4 +++- scripts/romset_recipes.py | 4 +++- scripts/slots.py | 4 +++- scripts/split_pack.py | 4 +++- scripts/torrentzip.py | 4 +++- scripts/validate_pr.py | 4 +++- scripts/validate_schemas.py | 4 +++- scripts/validate_site.py | 4 +++- scripts/verify.py | 4 +++- tests/test_audit_regressions.py | 23 +++++++++++++++++++++++ 40 files changed, 155 insertions(+), 38 deletions(-) diff --git a/install.ps1 b/install.ps1 index ce0c9662..657f4a0a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,7 @@ param( $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -$defaultInstallSha256 = "d336ca66e88def775842bca2ed4fae67dabf1d3effcb18e84d057da78470459c" +$defaultInstallSha256 = "61ffed8fddf816c02c1b45268dfd9bfdff9daeb1da2adedf2e13d14472f8654d" $maximumInstallerBytes = 2MB $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } $temporary = $null diff --git a/install.py b/install.py index 74b3f410..b7d2ce9a 100644 --- a/install.py +++ b/install.py @@ -3210,4 +3210,11 @@ def _install(args: argparse.Namespace, real_stdout) -> None: if __name__ == "__main__": - main() + # A reader that stops early (`install.py --list | head`) ends the run + # quietly instead of on a BrokenPipeError traceback. + try: + main() + sys.stdout.flush() + except BrokenPipeError: + os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) + sys.exit(141) diff --git a/install.sh b/install.sh index 4bd6d255..1d98fd16 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="d336ca66e88def775842bca2ed4fae67dabf1d3effcb18e84d057da78470459c" +DEFAULT_INSTALL_SHA256="61ffed8fddf816c02c1b45268dfd9bfdff9daeb1da2adedf2e13d14472f8654d" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/scripts/auto_fetch.py b/scripts/auto_fetch.py index f4fb3909..c311161a 100644 --- a/scripts/auto_fetch.py +++ b/scripts/auto_fetch.py @@ -463,4 +463,6 @@ def main(): if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/check_buildbot_system.py b/scripts/check_buildbot_system.py index 4958def2..ba5cb2c9 100644 --- a/scripts/check_buildbot_system.py +++ b/scripts/check_buildbot_system.py @@ -218,4 +218,6 @@ def pending(report: dict) -> list[dict]: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/check_freshness.py b/scripts/check_freshness.py index 801a0c69..2545b21e 100644 --- a/scripts/check_freshness.py +++ b/scripts/check_freshness.py @@ -1204,4 +1204,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/check_release_assets.py b/scripts/check_release_assets.py index a121d26c..517f9250 100644 --- a/scripts/check_release_assets.py +++ b/scripts/check_release_assets.py @@ -380,4 +380,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/common.py b/scripts/common.py index 78a148d9..02d306f6 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -2378,3 +2378,20 @@ from dumpcatalog import ( # noqa: E402,F401 annotate_provenance, write_provenance_snapshot, ) + + +def run_cli(main) -> None: + """Run a command-line entry point to its exit status. + + A reader that stops early (`verify.py --all | head`) closed the pipe + and the next write died on a BrokenPipeError traceback. Output nobody + reads any more ends the run quietly, with the status a shell gives a + writer whose reader left. + """ + try: + code = main() + sys.stdout.flush() + except BrokenPipeError: + os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) + sys.exit(141) + sys.exit(code) diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 3e1ca3f7..ac576d4f 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -776,4 +776,6 @@ def main(): if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/dedup.py b/scripts/dedup.py index d59f0e59..89dd4d3f 100644 --- a/scripts/dedup.py +++ b/scripts/dedup.py @@ -318,4 +318,6 @@ def main() -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/diff_truth.py b/scripts/diff_truth.py index a9b2b387..7e1b9114 100644 --- a/scripts/diff_truth.py +++ b/scripts/diff_truth.py @@ -222,4 +222,6 @@ def main() -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/download.py b/scripts/download.py index aa4d02d7..781cdcc8 100644 --- a/scripts/download.py +++ b/scripts/download.py @@ -383,4 +383,6 @@ To check files already in place, use: python install.py --check if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/export_native.py b/scripts/export_native.py index 03766171..c10c3dab 100644 --- a/scripts/export_native.py +++ b/scripts/export_native.py @@ -519,4 +519,6 @@ def main() -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/fileless_audit.py b/scripts/fileless_audit.py index 7e6e3d2a..7c635e31 100644 --- a/scripts/fileless_audit.py +++ b/scripts/fileless_audit.py @@ -107,4 +107,6 @@ def main() -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/find_pinned_revision.py b/scripts/find_pinned_revision.py index 669a5f76..d211fa9c 100644 --- a/scripts/find_pinned_revision.py +++ b/scripts/find_pinned_revision.py @@ -99,4 +99,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index ef0952e9..f8fc665d 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -4363,4 +4363,6 @@ from packresolve import ( # noqa: E402,F401 ) if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 1c6bb779..ccd2277c 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -768,4 +768,6 @@ def main(): if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 7a33f638..824b848a 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -4028,4 +4028,6 @@ from siterender import ( # noqa: E402,F401 ) if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/generate_truth.py b/scripts/generate_truth.py index 38cc0d66..5ce44b76 100644 --- a/scripts/generate_truth.py +++ b/scripts/generate_truth.py @@ -153,4 +153,6 @@ def main(argv: list[str] | None = None) -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/identity.py b/scripts/identity.py index e86d88e8..5a81f520 100644 --- a/scripts/identity.py +++ b/scripts/identity.py @@ -155,4 +155,6 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/list_platforms.py b/scripts/list_platforms.py index 00985da4..62d95e96 100644 --- a/scripts/list_platforms.py +++ b/scripts/list_platforms.py @@ -49,4 +49,6 @@ def main(): if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/mame_ref_audit.py b/scripts/mame_ref_audit.py index 5f1b6111..3790956b 100644 --- a/scripts/mame_ref_audit.py +++ b/scripts/mame_ref_audit.py @@ -141,4 +141,6 @@ def main() -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/migrate.py b/scripts/migrate.py index 38d84d9c..341c47ff 100644 --- a/scripts/migrate.py +++ b/scripts/migrate.py @@ -564,4 +564,6 @@ def main(): if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/pipeline.py b/scripts/pipeline.py index dae8c37d..3224e59e 100644 --- a/scripts/pipeline.py +++ b/scripts/pipeline.py @@ -688,4 +688,6 @@ def main(): if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 4683835a..0802689c 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -3660,4 +3660,6 @@ def _print_elided( if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/provenance_report.py b/scripts/provenance_report.py index 57d31d54..8b128769 100644 --- a/scripts/provenance_report.py +++ b/scripts/provenance_report.py @@ -202,4 +202,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/refresh_data_dirs.py b/scripts/refresh_data_dirs.py index f034a4bf..50ae3092 100644 --- a/scripts/refresh_data_dirs.py +++ b/scripts/refresh_data_dirs.py @@ -559,4 +559,6 @@ def main() -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/refresh_stale.py b/scripts/refresh_stale.py index f5f438fa..003f7870 100755 --- a/scripts/refresh_stale.py +++ b/scripts/refresh_stale.py @@ -470,4 +470,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/region_audit.py b/scripts/region_audit.py index 2eac6da9..82697c16 100644 --- a/scripts/region_audit.py +++ b/scripts/region_audit.py @@ -210,4 +210,6 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/release_record.py b/scripts/release_record.py index 2e775cbf..ee1a6694 100644 --- a/scripts/release_record.py +++ b/scripts/release_record.py @@ -156,4 +156,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/restore_large_files.py b/scripts/restore_large_files.py index cccfeb5f..0f95af08 100644 --- a/scripts/restore_large_files.py +++ b/scripts/restore_large_files.py @@ -103,4 +103,6 @@ def main() -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/romset_recipes.py b/scripts/romset_recipes.py index d1b58d64..dd7dec33 100644 --- a/scripts/romset_recipes.py +++ b/scripts/romset_recipes.py @@ -471,4 +471,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/slots.py b/scripts/slots.py index eed245a2..7ed776ba 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -900,4 +900,6 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/split_pack.py b/scripts/split_pack.py index 1c131809..78c4f4af 100644 --- a/scripts/split_pack.py +++ b/scripts/split_pack.py @@ -243,4 +243,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/torrentzip.py b/scripts/torrentzip.py index 914513f5..580421c0 100644 --- a/scripts/torrentzip.py +++ b/scripts/torrentzip.py @@ -143,4 +143,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/validate_pr.py b/scripts/validate_pr.py index 0b5debb1..fcba4e55 100644 --- a/scripts/validate_pr.py +++ b/scripts/validate_pr.py @@ -408,4 +408,6 @@ def main(): if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index 9ace88f2..1c2f1644 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -391,4 +391,6 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + from common import run_cli + + run_cli(main) diff --git a/scripts/validate_site.py b/scripts/validate_site.py index 982eda74..685eb959 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -292,4 +292,6 @@ def main() -> None: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/scripts/verify.py b/scripts/verify.py index fcf25cea..3e24bc85 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -2309,4 +2309,6 @@ def _overall_summary(group_results: list[tuple[dict, list[str]]]) -> str: if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 5660f650..0eab84b9 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -3442,5 +3442,28 @@ class MessagesSeparateWithAColon(unittest.TestCase): self.assertEqual(found, []) + +class AReaderThatLeavesEarlyIsNoCrash(unittest.TestCase): + """`verify.py --list-emulators | head -1` ended on a BrokenPipeError + traceback; every listing and report did.""" + + def test_a_closed_pipe_ends_quietly(self): + proc = subprocess.Popen( + [sys.executable, "scripts/verify.py", "--list-emulators"], cwd=ROOT, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + proc.stdout.readline() + proc.stdout.close() + _, err = proc.communicate(timeout=120) + self.assertNotIn(b"Traceback", err) + self.assertEqual(proc.returncode, 141) + + def test_every_entry_point_goes_through_run_cli(self): + plain = re.compile(r'if __name__ == "__main__":\n (main\(\)|sys\.exit\(main\(\)\))') + found = [p.name for p in sorted((ROOT / "scripts").glob("*.py")) + if plain.search(p.read_text())] + self.assertEqual(found, []) + + if __name__ == "__main__": unittest.main()