fix(gateway): preserve WSL interop PATH in systemd units

This commit is contained in:
fiver 2026-04-27 14:17:58 +08:00 committed by Teknium
parent d90f73bcec
commit 8ab9f61dcf
2 changed files with 79 additions and 0 deletions

View file

@ -1608,6 +1608,46 @@ def _build_user_local_paths(home: Path, path_entries: list[str]) -> list[str]:
return [p for p in candidates if p not in path_entries and Path(p).exists()]
def _build_wsl_interop_paths(path_entries: list[str]) -> list[str]:
"""Return WSL Windows interop PATH entries for generated systemd units.
WSL shells normally inherit Windows PATH entries such as
``/mnt/c/WINDOWS/System32``. systemd user services do not, so gateway tools
that call ``powershell.exe``/``cmd.exe`` work in a terminal but fail in the
background service unless we persist the relevant entries at install time.
"""
if not is_wsl():
return []
candidates: list[str] = []
for entry in os.environ.get("PATH", "").split(os.pathsep):
if entry.startswith("/mnt/"):
candidates.append(entry)
for executable in ("powershell.exe", "cmd.exe", "explorer.exe", "wsl.exe"):
resolved = shutil.which(executable)
if resolved:
candidates.append(str(Path(resolved).parent))
for entry in (
"/mnt/c/WINDOWS/system32",
"/mnt/c/WINDOWS",
"/mnt/c/WINDOWS/System32/Wbem",
"/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/",
"/mnt/c/WINDOWS/System32/OpenSSH/",
):
if Path(entry).exists():
candidates.append(entry)
result: list[str] = []
seen = set(path_entries)
for entry in candidates:
if entry and entry not in seen:
seen.add(entry)
result.append(entry)
return result
def _remap_path_for_user(path: str, target_home_dir: str) -> str:
"""Remap *path* from the current user's home to *target_home_dir*.
@ -1699,6 +1739,7 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)
node_bin = _remap_path_for_user(node_bin, home_dir)
path_entries = [_remap_path_for_user(p, home_dir) for p in path_entries]
path_entries.extend(_build_user_local_paths(Path(home_dir), path_entries))
path_entries.extend(_build_wsl_interop_paths(path_entries))
path_entries.extend(common_bin_paths)
sane_path = ":".join(path_entries)
return f"""[Unit]
@ -1738,6 +1779,7 @@ WantedBy=multi-user.target
hermes_home = str(get_hermes_home().resolve())
profile_arg = _profile_arg(hermes_home)
path_entries.extend(_build_user_local_paths(Path.home(), path_entries))
path_entries.extend(_build_wsl_interop_paths(path_entries))
path_entries.extend(common_bin_paths)
sane_path = ":".join(path_entries)
return f"""[Unit]

View file

@ -182,6 +182,43 @@ class TestGeneratedSystemdUnits:
assert "/home/test/.nvm/versions/node/v24.14.0/bin" in unit
def test_user_unit_includes_wsl_windows_interop_paths(self, monkeypatch):
monkeypatch.setattr(gateway_cli, "is_wsl", lambda: True)
monkeypatch.setenv(
"PATH",
"/usr/local/bin:/mnt/c/WINDOWS/system32:/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/",
)
monkeypatch.setattr(gateway_cli.shutil, "which", lambda cmd: None)
unit = gateway_cli.generate_systemd_unit(system=False)
assert "/mnt/c/WINDOWS/system32" in unit
assert "/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/" in unit
def test_user_unit_omits_windows_interop_paths_outside_wsl(self, monkeypatch):
monkeypatch.setattr(gateway_cli, "is_wsl", lambda: False)
monkeypatch.setenv("PATH", "/usr/local/bin:/mnt/c/WINDOWS/system32")
monkeypatch.setattr(gateway_cli.shutil, "which", lambda cmd: None)
unit = gateway_cli.generate_systemd_unit(system=False)
assert "/mnt/c/WINDOWS/system32" not in unit
def test_system_unit_includes_wsl_windows_interop_paths(self, monkeypatch):
monkeypatch.setattr(gateway_cli, "is_wsl", lambda: True)
monkeypatch.setattr(
gateway_cli,
"_system_service_identity",
lambda run_as_user=None: ("alice", "alice", "/home/alice"),
)
monkeypatch.setattr(gateway_cli, "_hermes_home_for_target_user", lambda home: "/home/alice/.hermes")
monkeypatch.setenv("PATH", "/usr/local/bin:/mnt/c/WINDOWS/system32")
monkeypatch.setattr(gateway_cli.shutil, "which", lambda cmd: None)
unit = gateway_cli.generate_systemd_unit(system=True, run_as_user="alice")
assert "/mnt/c/WINDOWS/system32" in unit
def test_system_unit_avoids_recursive_execstop_and_uses_extended_stop_timeout(self):
unit = gateway_cli.generate_systemd_unit(system=True)