From 40d3e86e553d75a8fdee164a431b5f92de4475a8 Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 18 Apr 2026 23:51:41 -0400 Subject: [PATCH] fix(updater): bootstrap fresh venv with deps; rebuild self-update argv from env - _run_pip: on first venv use, install decnet with its full dep tree so the bootstrapped environment actually has typer/fastapi/uvicorn. Subsequent updates keep --no-deps for a near-no-op refresh. - run_update_self: do not reuse sys.argv to re-exec the updater. Inside the live process, sys.argv is the uvicorn subprocess invocation (--ssl-keyfile etc.), which 'decnet updater' CLI rejects. Reconstruct the operator-visible command from env vars set by updater.server.run. --- decnet/updater/executor.py | 27 ++++++++++++++++++++++----- decnet/updater/server.py | 4 ++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/decnet/updater/executor.py b/decnet/updater/executor.py index ee4a99e..7eddca5 100644 --- a/decnet/updater/executor.py +++ b/decnet/updater/executor.py @@ -191,16 +191,20 @@ def _run_pip( """ idir = install_dir or release.parent.parent # releases/ -> install_dir venv_dir = _shared_venv(idir) - if not venv_dir.exists(): + fresh = not venv_dir.exists() + if fresh: subprocess.run( # nosec B603 [sys.executable, "-m", "venv", str(venv_dir)], check=True, capture_output=True, text=True, ) py = venv_dir / "bin" / "python" + # First install into a fresh venv: pull full dep tree. Subsequent updates + # use --no-deps so pip only replaces the decnet package. + args = [str(py), "-m", "pip", "install", "--force-reinstall", str(release)] + if not fresh: + args.insert(-1, "--no-deps") return subprocess.run( # nosec B603 - [str(py), "-m", "pip", "install", "--force-reinstall", "--no-deps", - str(release)], - check=False, capture_output=True, text=True, + args, check=False, capture_output=True, text=True, ) @@ -484,7 +488,20 @@ def run_update_self( _rotate(updater_install_dir) _point_current_at(updater_install_dir, _active_dir(updater_install_dir)) - argv = [str(_shared_venv(updater_install_dir) / "bin" / "decnet"), "updater"] + sys.argv[1:] + # Reconstruct the updater's original launch command from env vars set by + # `decnet.updater.server.run`. We can't reuse sys.argv: inside the app + # process this is the uvicorn subprocess invocation (--ssl-keyfile, etc.), + # not the operator-visible `decnet updater ...` command. + decnet_bin = str(_shared_venv(updater_install_dir) / "bin" / "decnet") + argv = [decnet_bin, "updater", + "--host", os.environ.get("DECNET_UPDATER_HOST", "0.0.0.0"), # nosec B104 + "--port", os.environ.get("DECNET_UPDATER_PORT", "8766"), + "--updater-dir", os.environ.get("DECNET_UPDATER_BUNDLE_DIR", + str(pki.DEFAULT_AGENT_DIR.parent / "updater")), + "--install-dir", os.environ.get("DECNET_UPDATER_INSTALL_DIR", + str(updater_install_dir.parent)), + "--agent-dir", os.environ.get("DECNET_UPDATER_AGENT_DIR", + str(pki.DEFAULT_AGENT_DIR))] if exec_cb is not None: exec_cb(argv) # tests stub this — we don't actually re-exec return {"status": "self_update_queued", "argv": argv} diff --git a/decnet/updater/server.py b/decnet/updater/server.py index 4a972a0..ed4b93d 100644 --- a/decnet/updater/server.py +++ b/decnet/updater/server.py @@ -47,6 +47,10 @@ def run( os.environ["DECNET_UPDATER_INSTALL_DIR"] = str(install_dir) os.environ["DECNET_UPDATER_UPDATER_DIR"] = str(install_dir / "updater") os.environ["DECNET_UPDATER_AGENT_DIR"] = str(agent_dir) + # Needed by run_update_self to rebuild the updater's launch argv. + os.environ["DECNET_UPDATER_BUNDLE_DIR"] = str(updater_dir) + os.environ["DECNET_UPDATER_HOST"] = str(host) + os.environ["DECNET_UPDATER_PORT"] = str(port) keyfile = updater_dir / "updater.key" certfile = updater_dir / "updater.crt"