From c692a8b52ff002c5adc9d497c68f565c4e8f1b65 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 4 Jul 2026 14:52:28 +0000 Subject: [PATCH] fix(mesh): fix archy-rnodeconf exiting 1 on success (frozen exit() gotcha) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed on the just-packaged binary: `archy-rnodeconf --info` printed everything correctly, then crashed with NameError: name 'exit' is not defined and returned exit code 1. rnodeconf.py's own graceful_exit() calls the bare exit() builtin, which is only ever defined by site.py for interactive Python — a frozen PyInstaller app skips that init, so any bundled script relying on it hits this the moment it tries to quit cleanly, after the real work already succeeded. Classic, well-documented PyInstaller gotcha; the standard fix is a runtime hook pre-defining exit/quit as sys.exit before the bundled script's own code runs. Co-Authored-By: Claude Sonnet 5 --- reticulum-daemon/build.sh | 6 ++++++ reticulum-daemon/pyi_rthook_exit_builtins.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 reticulum-daemon/pyi_rthook_exit_builtins.py diff --git a/reticulum-daemon/build.sh b/reticulum-daemon/build.sh index 3d8a5a3b..f99642d3 100755 --- a/reticulum-daemon/build.sh +++ b/reticulum-daemon/build.sh @@ -43,9 +43,15 @@ echo "Built dist/archy-reticulum-daemon ($(du -h dist/archy-reticulum-daemon | c # motivated shipping it as a first-class OS tool rather than an ad hoc script. RNODECONF_SRC="$(find .venv/lib -maxdepth 5 -path '*/site-packages/RNS/Utilities/rnodeconf.py' -print -quit)" if [ -n "$RNODECONF_SRC" ] && [ -f "$RNODECONF_SRC" ]; then + # --runtime-hook: rnodeconf's own graceful_exit() calls the bare + # exit()/quit() builtins, which only exist in interactive Python (site.py + # injects them) — a frozen app hits NameError right as it tries to quit + # cleanly, after all the real work already succeeded. See + # pyi_rthook_exit_builtins.py. .venv/bin/pyinstaller --onefile --name archy-rnodeconf --clean --noconfirm \ --collect-submodules RNS \ --collect-data RNS \ + --runtime-hook pyi_rthook_exit_builtins.py \ -d noarchive \ "$RNODECONF_SRC" echo "Built dist/archy-rnodeconf ($(du -h dist/archy-rnodeconf | cut -f1))" diff --git a/reticulum-daemon/pyi_rthook_exit_builtins.py b/reticulum-daemon/pyi_rthook_exit_builtins.py new file mode 100644 index 00000000..0b999229 --- /dev/null +++ b/reticulum-daemon/pyi_rthook_exit_builtins.py @@ -0,0 +1,18 @@ +# PyInstaller runtime hook — see build.sh. +# +# `exit()`/`quit()` aren't part of the language; they're `site.Quitter` +# instances the interactive interpreter injects into builtins at startup +# (site.py). A frozen PyInstaller app never runs that interactive-mode +# init, so any bundled script that calls bare `exit()` (RNS's own +# rnodeconf.py does, in its graceful_exit() cleanup path) hits +# `NameError: name 'exit' is not defined` right as it tries to quit +# cleanly — the real work above it already completed, but the process +# still exits 1, which is a foot-gun for anything scripting off the exit +# code. Pre-define both as sys.exit so that path is a no-op crash-wise. +import builtins +import sys + +if not hasattr(builtins, "exit"): + builtins.exit = sys.exit +if not hasattr(builtins, "quit"): + builtins.quit = sys.exit