fix(cron): resolve .sh script bash via Git for Windows probe before WSL stub - #61629
fix(cron): resolve .sh script bash via Git for Windows probe before WSL stub#61629LiteSoul wants to merge 2 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for identifying a real native-Windows cron path: current main still executes the raw shutil.which("bash") result in cron/scheduler.py:2083.
Problems
- The new resolver is a second, incomplete Windows discovery chain.
tools/environments/local.py:532-565already supportsHERMES_GIT_BASH_PATH, Hermes-managed PortableGit, and a LocalAppData Git install; the PR's resolver omits them, so cron can still miss the interpreter used by the native terminal path. - The unguarded Windows-path assertions in
tests/cron/test_cron_script.py:450cannot pass on POSIX: the proposed classifier splits using hostos.sep, which is/on Linux and does not splitC:\\Windows\\System32\\bash.exe. tests/cron/test_cron_script.py:649creates directories withtempfile.mkdtemp; assigningmonkeypatch._tmp_pathdoes not register cleanup.
Suggested changes
- Share/extend the established resolver instead of duplicating a subset, and add WSL-stub rejection there.
- Normalize both path separator styles in the classifier and use
tmp_pathfor test directories.
Automated hermes-sweeper review.
| emits a clear install-hint error in that case). | ||
| """ | ||
| # 1. Windows: probe well-known Git for Windows locations first. | ||
| if sys.platform == "win32": |
There was a problem hiding this comment.
Please share or extend the native resolver in tools/environments/local.py:521-574 rather than introduce this subset. That resolver honors HERMES_GIT_BASH_PATH, installer-managed PortableGit, and LocalAppData Git locations; this one skips all of them, so cron can still diverge from Hermes's supported Windows interpreter discovery.
There was a problem hiding this comment.
Resolved in ece1a9578c: the parallel _resolve_bash / _is_wsl_launcher / _GIT_WIN_BASH_PATHS block is deleted from cron/scheduler.py; _run_job_script now lazy-imports and calls tools.environments.local._find_bash() directly. Cron now uses the exact same chain as the native terminal path — HERMES_GIT_BASH_PATH, Hermes-managed PortableGit, LocalAppData Git, ProgramFiles\Git, the usr/bin MinGit fallback, and the new WSL-stub rejection — so it can no longer diverge from Hermes's supported Windows interpreter discovery.
The WSL-stub rejection was added to _find_bash itself (lines 521+ and the final shutil.which("bash") fallback) rather than left in a cron-local helper, so every existing caller of _find_bash (native terminal spawn_local, _find_shell delegation) gets the fix too. TestSharedFindBashUsedEverywhere::test_cron_scheduler_delegates_to_find_bash is a new source guard pinning this invariant.
| def test_is_wsl_launcher_system32(self): | ||
| from cron.scheduler import _is_wsl_launcher | ||
|
|
||
| assert _is_wsl_launcher(r"C:\Windows\System32\bash.exe") is True |
There was a problem hiding this comment.
This unguarded assertion fails on POSIX CI: the proposed classifier splits with host os.sep (/ on Linux), which does not split this backslash-separated Windows path. Normalize both separator styles, or otherwise make the classifier host-independent before keeping these cross-platform tests.
There was a problem hiding this comment.
Resolved in ece1a9578c: the classifier now uses pathlib.PureWindowsPath instead of os.sep splitting, so it splits Windows-style paths (C:\Windows\System32\bash.exe) correctly on Linux/macOS CI runners — not just on a real Windows host. test_forward_slash_separated_windows_path_still_classified in TestIsWSLLauncher pins this explicitly (a forward-slash-separated C:/Windows/System32/bash.exe is still recognized).
The os.sep-based classifier and all the host-dependent assertions that failed outside Windows are gone; TestIsWSLLauncher runs five cases (system32, WindowsApps alias, Git Bash locations NOT flagged, POSIX paths + empty NOT flagged, forward-slash Windows path) and all pass on this Windows host without any skip guard.
| def monkeypatch_tmp_root(monkeypatch): | ||
| """A throwaway directory uniquely scoped to this test invocation.""" | ||
| import tempfile | ||
| d = tempfile.mkdtemp(prefix="cron_resolve_bash_") |
There was a problem hiding this comment.
tempfile.mkdtemp() is not cleaned up by assigning a private attribute to monkeypatch. Accept pytest's tmp_path fixture in the tests/helper instead so these directories are managed and removed by pytest.
There was a problem hiding this comment.
Resolved in ece1a9578c: the tempfile.mkdtemp() + monkeypatch._tmp_path helper is deleted. All relocated tests in tests/tools/test_find_shell.py accept pytest's tmp_path fixture, so the temp directories are managed and removed by pytest. The monkeypatch_tmp_root helper and the whole TestResolveBashWindows block were removed from tests/cron/test_cron_script.py (220 lines) and the tests rewritten under TestFindBashSkipsWSLStub / TestIsWSLLauncher in test_find_shell.py, the natural home alongside the existing _find_bash tests.
|
@teknium1 — addressed all three points in Per-thread replies map each of the three line comments to the fix. Summary of what changed:
Bonus — sibling bug fixed. While auditing the Stale source guard updated. Tests verified locally on this Windows host: 63 passed / 2 skipped / 2 pre-existing failures across the three test files after the v1 patch (retested post-rebase — see follow-up comment). The 2 failures ( Out-of-scope findings (not touched, FYI): |
…SL stub
The cron scheduler's _run_job_script picked the bash interpreter for
.sh/.bash scripts with shutil.which('bash'). On a common Windows
configuration — Git for Windows installed with its *default* PATH option
(which adds Git\cmd for git.exe but NOT Git\bin / Git\usr\bin for bash.exe)
AND WSL enabled with no distributions installed — shutil.which('bash')
resolves to C:\Windows\System32\bash.exe, the WSL launcher stub. Every
.sh cron script then fails with WSL_E_DEFAULT_DISTRO_NOT_FOUND, and on a
host with WSL enabled but unpopulated this is the *only* outcome: the
'bash not found' fall-back error never fires because the WSL stub does
satisfy which('bash'). The failure is invisible to the interactive terminal
backend (which uses git-bash directly, with Git\usr\bin prepended to PATH),
so the bug only surfaces on scheduled ticks.
Fix: extract _resolve_bash() and _is_wsl_launcher(). On Windows, probe the
well-known Git for Windows install paths (ProgramFiles\Git\bin\bash.exe,
ProgramFiles\Git\usr\bin\bash.exe, plus x86 variants) BEFORE consulting
shutil.which('bash'); if which('bash') returns a path under system32 or
WindowsApps, treat it as the WSL stub and skip it, falling through to
/bin/bash (None on Windows). The 'bash not found, install Git for Windows'
error message is preserved for the genuine no-bash case.
Reproduced on Windows 11 + default Git for Windows install + WSL enabled
with no distros: shutil.which('bash') -> System32\bash.exe (WSL stub);
_run_job_script('test.sh') -> WSL_E_DEFAULT_DISTRO_NOT_FOUND. With the
patch, _resolve_bash() -> Program Files\Git\bin\bash.exe and test.sh runs.
Tests: new TestResolveBashWindows class in tests/cron/test_cron_script.py
exercises _is_wsl_launcher (all path-family cases), _resolve_bash
(probe-beats-stub, stub-skip, which-when-not-stub, posix-fallback), and an
end-to-end .sh script run via the resolved interpreter. Cross-platform
monkeypatched tests run on Linux CI; Windows-real-host tests are skipif
guarded inversely. All 44 existing tests in test_cron_script.py still pass.
Fixes the whole bug class (every .sh cron job on an affected Windows host),
not just one job.
…ron + webhook Supersedes the cron-only _resolve_bash from the previous commit per review feedback (teknium1, PR NousResearch#61629): "fix the whole bug class, don't duplicate the resolver." Moves the WSL-stub rejection + Git for Windows well-known-path probe into the shared tools.environments.local._find_bash (used already by the native terminal path) and makes the two callers that carried the inline shutil.which fallback anti-pattern delegate to it. Rebased onto upstream main (post NousResearch#63955) so this commit now integrates with, rather than duplicates, upstream's behavioural _bash_starts() probe: the _is_wsl_launcher path classifier filters the WSL stub at candidate-collection time (faster + clearer debug logs + defense-in-depth for the WSL-gets-a-distro edge case); _bash_starts() still verifies the chosen candidate actually starts at selection time. The two fixes are complementary — the path filter catches the WSL stub before spawning a ~15ms subprocess probe per candidate; the probe catches a broken HERMES_GIT_BASH_PATH pointing at a partially-uninstalled Git (the NousResearch#63955 case) that the path filter can't see. Source changes: - tools/environments/local._find_bash: add _is_wsl_launcher() classifier (PureWindowsPath-based so the classifier is host-independent: it splits Windows-style paths correctly on the Linux/macOS CI runners that audit Windows behaviour, not just on a real Windows host) and reject the WSL launcher stub from the final shutil.which("bash") candidate collection. When the only bash on PATH is the WSL stub, also re-probe the usr/bin layout of the standard Git installs (MinGit / recent PortableGit put bash under usr/bin rather than bin, which the earlier probe skipped) before raising the actionable "install Git for Windows" RuntimeError. - cron/scheduler._run_job_script: delete the parallel _resolve_bash / _is_wsl_launcher / _GIT_WIN_BASH_PATHS block and the now-dead import shutil; lazy-import and call _find_bash() in the .sh/.bash branch, keeping the same "bash not found, install Git for Windows" error message for the no-bash case. - gateway/platforms/webhook_filters.run_route_script: same sibling bug (verbatim copy of the shutil.which anti-pattern, used to run .sh webhook route scripts). Delegate to _find_bash() too. Tests: - tests/tools/test_find_shell.py: new TestIsWSLLauncher (host-independent PureWindowsPath classifier assertions), TestFindBashSkipsWSLStub (HERMES_GIT_BASH_PATH override / PortableGit / ProgramFiles / usr/bin re-probe / Runtime-when-no-real-bash, all using tmp_path and patch.dict(os.environ) + _IS_WINDOWS mockout + a hermetic monkeypatched _bash_starts so tests are deterministic on Linux/macOS CI and don't depend on the host having a real Git install), TestPOSIXFindBashUnchanged (non-Windows path unchanged), TestSharedFindBashUsedEverywhere (source guards: cron AND webhook_filters delegate to _find_bash and carry no inline shutil.which("bash") fallback). - tests/cron/test_cron_script.py: drop TestResolveBashWindows (relocated to test_find_shell.py — the tests assert on _find_bash, not the cron-local resolver, so they belong with the other _find_bash tests). - tests/tools/test_windows_native_support.py: update TestCronSchedulerBashResolution to assert delegation (cron/scheduler.py imports _find_bash and carries no inline shutil.which fallback) instead of the stale "shutil.which literally appears in source" assertion. Verified locally on this Windows host: all 16 bash-resolution tests pass (my 15 + upstream's TestFindBashSkipsBrokenCustomPath). The 2 outstanding failures (TestFindShellPrefersUserShell) are pre-existing POSIX-only-in-spirit tests lacking a Windows skip guard — confirmed to fail identically on the clean PR branch HEAD before any of my edits. Out of scope.
eda7938 to
ece1a95
Compare
|
Update — rebased onto upstream main (live SHA is now Triggered by the "this branch has conflicts that must be resolved" banner. The actual conflict was one hunk in I chose What the integration looks like in
Tests updated for the new design:
Verification (post-rebase): 53 passed / 1 skipped / 0 failed locally across the bash-resolution + cron suites (15 of my new tests + upstream's |
Problem
The cron scheduler's
_run_job_scriptpicks the bash interpreter for.sh/.bashscripts with:On a common Windows configuration this resolves to the WSL launcher stub and every
.shcron script silently fails:Git\cmd(shipsgit.exe) but NOTGit\bin/Git\usr\bin(which shipbash.exe). The third option that addsusr\binis explicitly flagged in the installer as risky because it overrides Windows builtins likefind.exe/sort.exe, so most users (and most CI base images) leave it on the safer default.C:\Windows\System32\bash.exe(the WSL launcher stub) whenever the WSL optional feature is on, prevalent across modern Windows 10/11 SKUs.In that configuration — not a one-in-a-thousand setup —
shutil.which("bash")resolves toC:\Windows\System32\bash.exe(the WSL stub), and every.shcron script fails:The
if _bash is Nonefall-back error message ("bash not found, install Git for Windows") never fires because the WSL stub does satisfywhich("bash"). The "bash found, but it's the wrong bash" case is unhandled.The failure is invisible to the interactive
terminaltool (which goes through the git-bash terminal backend whose PATH prependsGit\usr\bin), so the bug only surfaces on scheduled ticks — making it a particularly confusing trap to diagnose. (For full disclosure: I hit this on my own setup with a.sh-wrapped Brainiac data-collection cron job, and spent four rounds of misdiagnosis before reading the source. The repro below is the actual configuration. The bug is real, just narrow.)Repro
On Windows 11 with Git for Windows installed using the default PATH option and WSL enabled with no distributions:
Fix
Extract two helpers next to
_run_job_script:_is_wsl_launcher(path)— best-effort classifier: anybashresolved undersystem32/orWindowsApps/is the WSL launcher stub, not real Git Bash._resolve_bash()— pick a bash interpreter with this precedence:ProgramFiles\Git\bin\bash.exe,ProgramFiles\Git\usr\bin\bash.exe(plus(x86)variants). Beatswhich("bash")so the WSL stub undersystem32never wins when WSL has no distros.shutil.which("bash")— the historical resolution, correct on Linux/macOS and on Windows hosts that did addGit\usr\binto PATH — unless it resolves to the WSL stub (then skip and keep looking)./bin/bash— the POSIX fallback when nothing is on PATH.The 'bash not found, install Git for Windows' error message is preserved verbatim for the genuine no-bash case. The
elsebranch (sys.executablefor.pyscripts) is untouched. Total diff: +76 lines inscheduler.py.Tests
New
TestResolveBashWindowsclass intests/cron/test_cron_script.py(this was the first test class exercising the.shinterpreter branch — the existing suite only covered the.pypath, which is part of why this bug went unnoticed):_is_wsl_launcher:system32/WindowsApps→ true;Git\bin/Git\usr\bin//bin/bash/ empty → false._resolve_bashprecedence: probe-beats-WSL-stub (simulated), no-Git-returns-skip-stub (never returns the stub path), which-when-not-stub (uses a non-stubwhichhit), posix-prefers-which..shcron script runs via the resolved interpreter (probe-beats-stub proves itself with noWSLerrors in output).Cross-platform-runnable via monkeypatched
sys.platform+platform.system+platform.release(per the existingtests/agent/test_prompt_builder.py::TestEnvironmentHintspattern called out in AGENTS.md), so Linux CI exercises the Windows branch and a Windows runner exercises the real-host probe.@pytest.mark.skipifguards split the host-conditional ones inversely.Verification:
TestResolveBashWindows: 7 pass, 3 skip (the inversely-guarded host-conditional ones) on this Windows host.tests/cron/test_cron_script.py: 44 pass, 4 skip, 0 failures — no regressions to existing tests.Scope
This is a narrow, surgical fix to one function (
_run_job_script) and adds 220 lines of dedicated test coverage for code paths that previously had none. It does not touch:else→sys.executablebranch (.pyscripts unaffected);It clears the "real bugs, well… fix the whole bug class, sibling call paths included" bar from
CONTRIBUTING/AGENTS.md: every.shcron job on an affected Windows host is fixed, not just the one this PR was motivated by.Out of scope (worth flagging)
The deeper class — "Windows tools that silently resolve to wrong executables" — has analogs in other parts of the codebase that probe
shutil.whichfor shell-ish tools. I'm deliberately not touching those in this PR; that's a separate audit and would explode the diff.