fix(update): strip PyInstaller bootloader env vars when re-execing after self-update

This commit is contained in:
Ahmed Allam 2026-08-25 21:47:03 +00:00
parent 187f41f36f
commit b3859ec489
3 changed files with 59 additions and 2 deletions

View file

@ -6,7 +6,6 @@ Strix Agent Interface
import argparse
import asyncio
import contextlib
import os
import sys
from pathlib import Path
@ -36,6 +35,7 @@ from strix.interface.update_check import (
is_binary_install,
notify_update,
prompt_update_if_available,
restart_after_update,
start_background_check,
)
from strix.interface.utils import (
@ -440,7 +440,7 @@ def main() -> None:
start_background_check()
if not args.non_interactive and prompt_update_if_available(Console()):
if is_binary_install() and sys.platform != "win32":
os.execv(sys.executable, sys.argv) # noqa: S606 # nosec B606
restart_after_update()
sys.exit(0)
check_docker_installed()

View file

@ -264,6 +264,36 @@ def prompt_update_if_available(console: Console) -> bool:
return run_package_upgrade(console, method)
def restart_env() -> dict[str, str]:
"""Environment for re-exec'ing the binary after a self-update.
The PyInstaller bootloader marks its child process via environment
variables (``_MEIPASS2`` on older versions, ``_PYI_*`` on 6.x) that
point at the already-extracted archive of the *running* version. If
they leak into the re-exec'd process, the new binary skips extraction
and runs the old code, so the update never appears to take effect.
Library-path variables the bootloader overrode are restored from the
``*_ORIG`` copies it saved.
"""
env = {
key: value
for key, value in os.environ.items()
if key != "_MEIPASS2" and not key.startswith("_PYI_")
}
for var in ("LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH"):
orig = env.pop(f"{var}_ORIG", None)
if orig is not None:
env[var] = orig
elif var in os.environ:
env.pop(var, None)
return env
def restart_after_update() -> None:
"""Replace the current process with the freshly updated binary."""
os.execve(sys.executable, sys.argv, restart_env()) # noqa: S606 # nosec B606
def _release_target() -> str | None:
raw_os = platform.system().lower()
os_name = {"darwin": "macos", "linux": "linux", "windows": "windows"}.get(raw_os)

View file

@ -153,6 +153,33 @@ def test_self_update_already_latest(monkeypatch: pytest.MonkeyPatch) -> None:
assert update_check.self_update() is True
def test_restart_env_strips_pyinstaller_vars(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("_MEIPASS2", "/stale/_MEIold")
monkeypatch.setenv("_PYI_APPLICATION_HOME_DIR", "/stale/_MEIold")
monkeypatch.setenv("_PYI_ARCHIVE_FILE", "/old/strix")
monkeypatch.setenv("_PYI_PARENT_PROCESS_LEVEL", "1")
monkeypatch.setenv("SOME_OTHER_VAR", "kept")
env = update_check.restart_env()
assert "SOME_OTHER_VAR" in env
assert "_MEIPASS2" not in env
assert not any(key.startswith("_PYI_") for key in env)
def test_restart_env_restores_library_paths(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LD_LIBRARY_PATH", "/stale/_MEIold/lib")
monkeypatch.setenv("LD_LIBRARY_PATH_ORIG", "/usr/lib/custom")
monkeypatch.setenv("DYLD_LIBRARY_PATH", "/stale/_MEIold/lib")
monkeypatch.delenv("DYLD_LIBRARY_PATH_ORIG", raising=False)
env = update_check.restart_env()
assert env["LD_LIBRARY_PATH"] == "/usr/lib/custom"
assert "LD_LIBRARY_PATH_ORIG" not in env
assert "DYLD_LIBRARY_PATH" not in env
def test_sha256_file(tmp_path: Path) -> None:
path = tmp_path / "blob"
path.write_bytes(b"strix")