fix(cron): degrade to the caller's interpreter when selected_venv raises

_posix_cron_python_invocation called selected_venv(repo) unguarded; the
four RuntimeError cases it is documented to raise would escape through
_script_argv (which runs before _run_job_script's try), crash the tick,
and leave the execution row in running forever — the exact half-migrated
install the test docstring already claimed was supported. Mirror the
Windows bootstrap's degrade-don't-crash contract: warn and run on the
caller's interpreter. Adds the raising-selection case to the POSIX
invocation test (red on the previous head), a sealed-payload caveat on
the e2e test's hand-written .pth, and a pointer from the Windows
invocation docstring to its POSIX counterpart.

(cherry picked from commit 8d5b3b5e9727d612d91b2a7485e0f4bb245358f3)
This commit is contained in:
liuhao1024
2026-09-26 23:04:53 +08:00
committed by kshitij
parent cb11c994f7
commit 03156afc7b
2 changed files with 42 additions and 6 deletions

View File

@@ -126,7 +126,10 @@ def _posix_cron_python_invocation(python_exe: str) -> tuple[str, dict[str, str]]
and crash (#123440). Lazy installs are disabled for script children so a script importing
``hermes_bootstrap`` off the store-record venv cannot republish launchers (#123440).
Installs without a committed store (source checkouts, pre-PM venvs) keep the caller's
interpreter."""
interpreter. A broken committed selection degrades to the caller's interpreter too:
``selected_venv`` is documented as a raising function, and this runs before
``_run_job_script``'s ``try``, so an escaping error would crash the tick and leave the
execution row in ``running`` forever."""
from hermes_cli._launchers import resolve_store_python
repo = Path(__file__).resolve().parents[1]
@@ -135,7 +138,18 @@ def _posix_cron_python_invocation(python_exe: str) -> tuple[str, dict[str, str]]
from pm.environments import selected_venv, venv_bin_dir
venv_python = venv_bin_dir(selected_venv(repo)) / "python"
try:
venv = selected_venv(repo)
except (RuntimeError, OSError) as exc:
# Degrade, don't crash — same contract as the Windows bootstrap's unresolvable-venv
# fallback below (a silent fallback would make the misconfiguration undiagnosable).
logger.warning(
"POSIX cron script: cannot select the dependency venv (%s); running on the "
"caller's interpreter",
exc,
)
return python_exe, {}
venv_python = venv_bin_dir(venv) / "python"
if not venv_python.is_file():
return python_exe, {}
return str(venv_python), {"HERMES_DISABLE_LAZY_INSTALLS": "1"}
@@ -144,7 +158,9 @@ def _posix_cron_python_invocation(python_exe: str) -> tuple[str, dict[str, str]]
def _windows_cron_python_invocation(python_exe: str) -> tuple[str, dict[str, str]]:
"""Hidden, output-capable Python invocation for Windows cron scripts. ``pythonw.exe`` loses
captured output; uv venv launchers can re-exec the base console python and flash a window
even with CREATE_NO_WINDOW, so run the base python directly with venv paths overlaid in env."""
even with CREATE_NO_WINDOW, so run the base python directly with venv paths overlaid in env.
Off-Windows callers are routed to ``_posix_cron_python_invocation`` (venv interpreter
selection, no env overlay)."""
if sys.platform != "win32":
return _posix_cron_python_invocation(python_exe)
@@ -362,7 +378,10 @@ def _script_argv(path: Path) -> tuple[Optional[list[str]], dict[str, str], Optio
python_exe, env_overlay = _windows_cron_python_invocation(sys.executable)
if env_overlay.get("PYTHONPATH"):
# The bootstrap exists to give PYTHONPATH overlays .pth processing (editable installs);
# non-PYTHONPATH overlays (the POSIX venv-interpreter path) pass through plain.
# non-PYTHONPATH overlays (the POSIX venv-interpreter path) pass through plain. Both
# overlay producers that exist today always set PYTHONPATH (the managed-store branch
# sets only it, the uv re-exec sets it alongside VIRTUAL_ENV); a future overlay-only
# producer must revisit this gate or it silently loses .pth processing.
return _windows_cron_bootstrap_argv(python_exe, env_overlay, str(path)), env_overlay, None
return [python_exe, str(path)], env_overlay, None

View File

@@ -313,7 +313,9 @@ class TestRunJobScript:
inherited overlay makes foreign-interpreter children import the store's 3.14 extension
modules first (#123440). No committed store → the caller's interpreter passes through
untouched (pre-PM venvs, source checkouts), as does a store whose venv python
vanished (half-migrated install must not crash the scheduler)."""
vanished (half-migrated install must not crash the scheduler) or whose committed
selection record is broken (``selected_venv`` raises → degrade, not propagate — the
call site runs before ``_run_job_script``'s ``try``)."""
from cron import scheduler_script
venv = tmp_path / "selected-venv" / "bin"
@@ -337,6 +339,17 @@ class TestRunJobScript:
{},
)
def _broken_selection(repo):
raise RuntimeError(
"dependency environment is missing or outside this install"
)
monkeypatch.setattr("pm.environments.selected_venv", _broken_selection)
assert scheduler_script._posix_cron_python_invocation(sys.executable) == (
sys.executable,
{},
)
monkeypatch.setattr(
"hermes_cli._launchers.resolve_store_python", lambda repo: None
)
@@ -398,7 +411,11 @@ class TestRunJobScript:
deps = dependency_site(fake_venv)
deps.mkdir(parents=True)
(deps / "probe_pkg.py").write_text("VALUE = 42\n", encoding="utf-8")
# Editable-style repo exposure, as a real PM venv carries for the checkout.
# Editable-style repo exposure, as a real PM venv carries for the checkout. Caveat:
# a real generation venv gets its repo pointer from uv's editable install of the
# generated workspace (not a hand-written .pth), and sealed-payload installs prune
# editable .pth files outright because the payload wires the repo snapshot itself
# (pm/environment.py prune_site_pth) — do not generalize this .pth shape to payloads.
repo = Path(__file__).resolve().parents[2]
(deps / "zz_repo.pth").write_text(f"{repo}\n", encoding="utf-8")