fix: drop restored upstream managed_uv — runtime_repair owns it, port self-lock
The upstream/main merge dragged hermes_cli/managed_uv.py back in via rename detection, but our branch deliberately deleted it in the pm-store rewrite (audit script: "this branch deliberately deleted hermes_cli.managed_uv.*"). Upstream's managed_uv is the old name of our runtime_repair.py plus the uv-acquisition half pm replaced — zero production code in the merged tree imports it. - Port upstream's _windows_runtime_self_lock (#93032) into runtime_repair: the Windows case where the updater itself runs from the live venv it must replace (structurally un-renamable; defer before provisioning). - Wire the self-lock check into repair_vulnerable_runtime after the existing _windows_runtime_holders check. - Re-point the 4 auto-merged upstream tests (test_managed_uv.py content) from hermes_cli.managed_uv to hermes_cli.runtime_repair; adapt calls to the keyword-only repair_vulnerable_runtime signature. - Delete hermes_cli/managed_uv.py; fix the stale docstring reference in hermes_state.py.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -861,6 +861,76 @@ def _windows_runtime_holders() -> tuple[bool, str]:
|
||||
return False, ""
|
||||
|
||||
|
||||
def _windows_runtime_self_lock(live: Path) -> tuple[bool, str]:
|
||||
"""Detect the one holder the generic scan above is blind to: THIS process.
|
||||
|
||||
``_detect_venv_python_processes`` excludes the calling process and its
|
||||
ancestors on purpose — a CLI ``hermes update`` itself runs from the
|
||||
venv python — which is correct for the dependency-sync path, where only
|
||||
a *loaded* ``.pyd`` image blocks the rewrite and a fresh child process
|
||||
dodges it. For the whole-venv park rename that exemption is fatal:
|
||||
Windows keeps the image of any executable a running process was started
|
||||
from mapped until that process exits, so a directory containing the
|
||||
updater's own ``python.exe`` (or a waiting ``hermes.exe`` launcher
|
||||
ancestor) can never be renamed from inside the updater. The retry loop
|
||||
in ``_cut_over_candidate`` cannot help against that — the lock is
|
||||
structural, not transient (#93032).
|
||||
|
||||
No-op off Windows: POSIX renames work fine while this process maps
|
||||
files from the renamed tree (open FDs and mmaps keep inodes alive).
|
||||
"""
|
||||
if platform.system() != "Windows":
|
||||
return False, ""
|
||||
try:
|
||||
live_res = str(live.resolve()).lower().rstrip(os.sep) + os.sep
|
||||
except OSError:
|
||||
live_res = str(live).lower().rstrip(os.sep) + os.sep
|
||||
|
||||
def _under_live(path_value: str | None) -> bool:
|
||||
if not path_value:
|
||||
return False
|
||||
try:
|
||||
resolved = str(Path(path_value).resolve()).lower()
|
||||
except (OSError, ValueError):
|
||||
resolved = str(path_value).lower()
|
||||
return resolved.startswith(live_res)
|
||||
|
||||
try:
|
||||
exe = sys.executable
|
||||
except Exception:
|
||||
exe = None
|
||||
if _under_live(exe):
|
||||
return True, (
|
||||
f"the updater itself runs from the live venv it must replace "
|
||||
f"({exe}); Windows cannot rename a directory while a process "
|
||||
"executes from inside it"
|
||||
)
|
||||
# Belt-and-braces: the venv\Scripts\hermes.exe launcher stays mapped
|
||||
# while it waits for this child, so an ancestor started from the venv
|
||||
# blocks the rename too.
|
||||
try:
|
||||
import psutil
|
||||
|
||||
try:
|
||||
parents = psutil.Process().parents()
|
||||
except Exception:
|
||||
parents = []
|
||||
for anc in parents:
|
||||
try:
|
||||
anc_exe = anc.exe()
|
||||
except Exception:
|
||||
continue
|
||||
if _under_live(anc_exe):
|
||||
return True, (
|
||||
f"ancestor process PID {anc.pid} runs from the live venv "
|
||||
f"({anc_exe}); Windows cannot rename a directory while a "
|
||||
"process executes from inside it"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return False, ""
|
||||
|
||||
|
||||
def _default_live_venv(root: Path) -> Path:
|
||||
"""Return the venv that runtime repair should target for *root*.
|
||||
|
||||
@@ -987,6 +1057,30 @@ def repair_vulnerable_runtime(
|
||||
sqlite_before=current.sqlite_version_string,
|
||||
)
|
||||
|
||||
self_locked, self_detail = _windows_runtime_self_lock(live)
|
||||
if self_locked:
|
||||
# Structural, not transient: this process maps the live venv's own
|
||||
# executable, so the park rename fails the same way on every run and
|
||||
# no number of retries converges. Defer BEFORE provisioning — a
|
||||
# candidate staged for a cutover that can never run only leaks an
|
||||
# incomplete generation (#93032).
|
||||
print(f" ⚠ SQLite runtime repair deferred: {self_detail}.")
|
||||
print(
|
||||
" Retrying `hermes update` from inside this venv cannot help: "
|
||||
"the mapped executable is released only when this process exits."
|
||||
)
|
||||
print(
|
||||
" To complete the repair, run the updater from an interpreter "
|
||||
"that lives outside this venv, e.g.:"
|
||||
)
|
||||
print(f" cd {root}")
|
||||
print(" <system Python> -m hermes_cli.main update")
|
||||
return RuntimeRepairResult(
|
||||
"skipped",
|
||||
self_detail,
|
||||
sqlite_before=current.sqlite_version_string,
|
||||
)
|
||||
|
||||
runtime_root = _runtime_dir(root)
|
||||
lock = _acquire_repair_lock(runtime_root)
|
||||
if lock is None:
|
||||
|
||||
@@ -1788,7 +1788,7 @@ def _log_journal_mode_upgrade_once(db_label: str, previous_mode: str) -> None:
|
||||
WARNING, not ERROR, and deliberately so. The reverse move is logged at
|
||||
ERROR by ``_log_wal_fallback_once`` because dropping to DELETE is a real
|
||||
loss of concurrency; this direction is normally the desirable one (see
|
||||
``hermes_cli/managed_uv._default_live_venv``, which treats a database
|
||||
``hermes_cli/runtime_repair._default_live_venv``, which treats a database
|
||||
stuck on DELETE as a bug worth repairing on update). The problem is not
|
||||
the change, it is that the change was invisible: an operator who chose
|
||||
DELETE deliberately had no way to learn their choice had been overwritten,
|
||||
|
||||
@@ -947,26 +947,26 @@ class TestWindowsRuntimeSelfLock:
|
||||
doomed rename (provisioning + cutover) whenever the updater itself
|
||||
maps the live venv; the park then fails with WinError 5 and the user
|
||||
gets the misleading 'next update will retry' message forever."""
|
||||
from hermes_cli import managed_uv
|
||||
from hermes_cli.managed_uv import repair_vulnerable_runtime
|
||||
from hermes_cli import runtime_repair as repair_mod
|
||||
from hermes_cli.runtime_repair import repair_vulnerable_runtime
|
||||
|
||||
root, live, sentinel, scripts_python = self._checkout(tmp_path)
|
||||
current = _runtime_info(scripts_python, (3, 50, 4))
|
||||
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(repair_mod.platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(sys, "executable", str(scripts_python))
|
||||
|
||||
with patch(
|
||||
"hermes_cli.managed_uv._windows_runtime_holders",
|
||||
"hermes_cli.runtime_repair._windows_runtime_holders",
|
||||
return_value=(False, ""),
|
||||
), \
|
||||
patch(
|
||||
"hermes_cli.managed_uv.probe_sqlite_runtime",
|
||||
"hermes_cli.runtime_repair.probe_sqlite_runtime",
|
||||
return_value=current,
|
||||
), \
|
||||
patch(
|
||||
"hermes_cli.managed_uv._install_safe_python_generation"
|
||||
"hermes_cli.runtime_repair._install_safe_python_generation"
|
||||
) as mock_install:
|
||||
result = repair_vulnerable_runtime("uv", project_root=root)
|
||||
result = repair_vulnerable_runtime(project_root=root)
|
||||
|
||||
assert result.status == "skipped"
|
||||
assert "live venv" in result.detail
|
||||
@@ -988,29 +988,29 @@ class TestWindowsRuntimeSelfLock:
|
||||
"""The guard must fail OPEN when the updater runs from outside the
|
||||
venv — an always-firing deferral would recreate the never-converging
|
||||
loop this fix removes (#86735 class)."""
|
||||
from hermes_cli import managed_uv
|
||||
from hermes_cli.managed_uv import repair_vulnerable_runtime
|
||||
from hermes_cli import runtime_repair as repair_mod
|
||||
from hermes_cli.runtime_repair import repair_vulnerable_runtime
|
||||
|
||||
root, live, sentinel, scripts_python = self._checkout(tmp_path)
|
||||
current = _runtime_info(scripts_python, (3, 50, 4))
|
||||
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(repair_mod.platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(
|
||||
sys, "executable", str(tmp_path / "outside" / "python.exe")
|
||||
)
|
||||
|
||||
with patch(
|
||||
"hermes_cli.managed_uv._windows_runtime_holders",
|
||||
"hermes_cli.runtime_repair._windows_runtime_holders",
|
||||
return_value=(False, ""),
|
||||
), \
|
||||
patch(
|
||||
"hermes_cli.managed_uv.probe_sqlite_runtime",
|
||||
"hermes_cli.runtime_repair.probe_sqlite_runtime",
|
||||
return_value=current,
|
||||
), \
|
||||
patch(
|
||||
"hermes_cli.managed_uv._install_safe_python_generation",
|
||||
"hermes_cli.runtime_repair._install_safe_python_generation",
|
||||
return_value=None,
|
||||
) as mock_install:
|
||||
result = repair_vulnerable_runtime("uv", project_root=root)
|
||||
result = repair_vulnerable_runtime(project_root=root)
|
||||
|
||||
assert result.status == "failed"
|
||||
assert "provision" in result.detail
|
||||
@@ -1020,22 +1020,22 @@ class TestWindowsRuntimeSelfLock:
|
||||
def test_self_lock_is_a_noop_off_windows(self, tmp_path, monkeypatch):
|
||||
"""POSIX renames work while the updater maps the venv, so the guard
|
||||
must stay Windows-only."""
|
||||
from hermes_cli import managed_uv
|
||||
from hermes_cli import runtime_repair as repair_mod
|
||||
|
||||
root, live, sentinel, scripts_python = self._checkout(tmp_path)
|
||||
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Linux")
|
||||
monkeypatch.setattr(repair_mod.platform, "system", lambda: "Linux")
|
||||
monkeypatch.setattr(sys, "executable", str(scripts_python))
|
||||
|
||||
locked, detail = managed_uv._windows_runtime_self_lock(live)
|
||||
locked, detail = repair_mod._windows_runtime_self_lock(live)
|
||||
assert (locked, detail) == (False, "")
|
||||
|
||||
def test_venv_launcher_ancestor_is_a_self_lock(self, tmp_path, monkeypatch):
|
||||
r"""The venv\Scripts\hermes.exe shim stays mapped while it waits for
|
||||
this child — an ancestor running from the venv blocks the rename too."""
|
||||
from hermes_cli import managed_uv
|
||||
from hermes_cli import runtime_repair as repair_mod
|
||||
|
||||
root, live, sentinel, scripts_python = self._checkout(tmp_path)
|
||||
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(repair_mod.platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(
|
||||
sys, "executable", str(tmp_path / "outside" / "python.exe")
|
||||
)
|
||||
@@ -1054,7 +1054,7 @@ class TestWindowsRuntimeSelfLock:
|
||||
),
|
||||
)
|
||||
with patch.dict(sys.modules, {"psutil": fake_psutil}):
|
||||
locked, detail = managed_uv._windows_runtime_self_lock(live)
|
||||
locked, detail = repair_mod._windows_runtime_self_lock(live)
|
||||
|
||||
assert locked
|
||||
assert "999" in detail
|
||||
|
||||
Reference in New Issue
Block a user