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:
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user