diff --git a/npm_install.py b/npm_install.py index 1930111..50d12a5 100644 --- a/npm_install.py +++ b/npm_install.py @@ -1053,10 +1053,51 @@ def _install_certbot_stack(pip_path: Path, certbot_path: Path, env_build: dict): _ensure_certbot_symlink(certbot_path) + +def _venv_entrypoint_usable(path: Path, args: list[str] | None = None) -> tuple[bool, str]: + """Return whether a venv script/binary can be executed. + + Debian/Ubuntu upgrades can leave /opt/certbot/bin/* wrappers with a stale + shebang, for example /opt/certbot/bin/python3.11 no longer exists. In that + case subprocess may raise FileNotFoundError even though the wrapper file + itself exists. Treat that as a broken venv and rebuild it. + """ + args = args or ["--version"] + + if not path.exists(): + return False, f"missing: {path}" + + try: + first_line = path.read_bytes().splitlines()[0].decode("utf-8", "ignore") + except Exception: + first_line = "" + + if first_line.startswith("#!"): + interpreter = first_line[2:].strip().split()[0] + if interpreter and interpreter.startswith("/") and not Path(interpreter).exists(): + return False, f"stale interpreter in {path}: {interpreter}" + + try: + result = subprocess.run( + [str(path)] + args, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=20, + check=False, + ) + if result.returncode == 0: + return True, "ok" + return False, f"{path} exited with code {result.returncode}" + except FileNotFoundError as e: + return False, f"cannot execute {path}: {e}" + except Exception as e: + return False, f"cannot execute {path}: {e}" + def ensure_certbot_venv_ready(venv_dir: Path = Path("/opt/certbot"), force_rebuild: bool = False): """Fresh install: build full venv. Update: keep a complete existing venv. - Rebuild only when forced or when certbot/plugin packages are missing. + Rebuild when forced, when packages are missing, or when the existing venv + has stale entrypoints after an OS/Python upgrade. """ certbot_path = venv_dir / "bin" / "certbot" pip_path = venv_dir / "bin" / "pip" @@ -1065,23 +1106,45 @@ def ensure_certbot_venv_ready(venv_dir: Path = Path("/opt/certbot"), force_rebui with step("Removing certbot venv for forced rebuild"): shutil.rmtree(venv_dir, ignore_errors=True) - needs_rebuild = force_rebuild or not certbot_path.exists() or not pip_path.exists() + needs_rebuild = force_rebuild + rebuild_reason = "forced rebuild" if force_rebuild else "" + + if not needs_rebuild: + pip_ok, pip_reason = _venv_entrypoint_usable(pip_path, ["--version"]) + certbot_ok, certbot_reason = _venv_entrypoint_usable(certbot_path, ["--version"]) + if not pip_ok or not certbot_ok: + needs_rebuild = True + rebuild_reason = pip_reason if not pip_ok else certbot_reason if not needs_rebuild: for pkg in CERTBOT_REQUIRED_PACKAGES: - result = subprocess.run( - [str(pip_path), "show", pkg], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=False, - ) + try: + result = subprocess.run( + [str(pip_path), "show", pkg], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + except FileNotFoundError as e: + needs_rebuild = True + rebuild_reason = f"broken pip wrapper: {e}" + break + except Exception as e: + needs_rebuild = True + rebuild_reason = f"cannot verify certbot venv: {e}" + break + if result.returncode != 0: needs_rebuild = True + rebuild_reason = f"missing package: {pkg}" if DEBUG: print(f" Certbot venv missing package: {pkg}") break if needs_rebuild: + if venv_dir.exists(): + with step(f"Removing broken certbot venv ({rebuild_reason})"): + shutil.rmtree(venv_dir, ignore_errors=True) setup_certbot_venv(venv_dir) else: _ensure_certbot_symlink(certbot_path) @@ -3851,8 +3914,12 @@ def update_only( print(" ✓ Updated /etc/angie/angie.conf") if shutil.which("angie"): - subprocess.run(["angie", "-t"], check=False) - print(" ✓ Config syntax OK") + try: + run(["angie", "-t"], check=True) + print(" ✓ Config syntax OK") + except subprocess.CalledProcessError: + print(" ✖ Config syntax failed - keeping backup for manual rollback") + raise else: if DEBUG: print(" ⚠ /etc/angie/angie.conf unchanged (install mode)")