fix(updates): re-exec correctly in both source and frozen builds
You were right — the original analysis was inverted. The unprefixed form (sys.argv only) is the correct fix for frozen/packaged builds (PyInstaller, zipapp, etc.) where sys.argv[0] == sys.executable == <binary>; master's [sys.executable] + sys.argv form re-inserts the binary as argv[1] in that case, which the interpreter then tries to parse as the script to run — leading to a recursive-reexec that never reaches bind(). But for a source checkout launched as `python server.py` via bootstrap.py / ctl.sh / start.sh, sys.argv[0] is the SCRIPT path and sys.executable is the interpreter. CPython treats argv[1] as the script to run, so we must pass [sys.executable] + sys.argv — the canonical CPython re-exec idiom. The two cases are mutually exclusive on argv shape, so a flat swap can't be right for both. Distinguish with sys.frozen (set by PyInstaller / zipapp / similar) and pick the right form per case. Also reworded the last-resort except comment — the prior "(e.g. frozen binary)" parenthetical was misleading; this branch is the unconditional fallback for any execv failure, not specifically the frozen case.
This commit is contained in:
parent
f12608487f
commit
1402b56155
1 changed files with 30 additions and 3 deletions
|
|
@ -1049,10 +1049,37 @@ def _schedule_restart(delay: float = 2.0) -> None:
|
|||
with _apply_lock:
|
||||
_wait_until_restart_safe()
|
||||
try:
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
# Re-exec into the just-pulled image.
|
||||
#
|
||||
# sys.argv[0]'s meaning depends on how the server was launched:
|
||||
#
|
||||
# * Source checkout (`python server.py` via bootstrap.py /
|
||||
# ctl.sh / start.sh): sys.argv[0] is the SCRIPT path
|
||||
# (e.g. "/root/hermes-webui/server.py"), sys.executable is
|
||||
# the interpreter. CPython treats argv[1] as the script to
|
||||
# run, so we must pass [sys.executable] + sys.argv.
|
||||
#
|
||||
# * Frozen/packaged build (PyInstaller, embedded zipapp,
|
||||
# etc.): sys.argv[0] == sys.executable == <binary>. Passing
|
||||
# [sys.executable] + sys.argv would re-insert the binary as
|
||||
# argv[1] — the kernel launches it, the interpreter treats
|
||||
# the binary itself as the "script" to run, and execv
|
||||
# effectively becomes a recursive no-op that never reaches
|
||||
# bind(), leaving the WebUI stuck "offline" after every
|
||||
# self-update. Pass argv as-is instead.
|
||||
#
|
||||
# Distinguish the two cases with sys.frozen (set by
|
||||
# PyInstaller / zipapp / similar). For source checkouts the
|
||||
# `[sys.executable] + sys.argv` form is the canonical CPython
|
||||
# re-exec idiom (same shape Flask/Django reloaders use) and
|
||||
# is the correct path.
|
||||
if getattr(sys, "frozen", False):
|
||||
os.execv(sys.executable, sys.argv)
|
||||
else:
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
except Exception:
|
||||
# Last-resort: if execv fails (e.g. frozen binary), just exit
|
||||
# so the process supervisor (start.sh / Docker) restarts us.
|
||||
# Last-resort: if execv fails for any reason, just exit so the
|
||||
# process supervisor (start.sh / Docker) restarts us.
|
||||
os._exit(0)
|
||||
|
||||
threading.Thread(target=_do, daemon=True).start()
|
||||
|
|
|
|||
Loading…
Reference in a new issue