Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions hermes_cli/gateway_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ def _build_gateway_cmd_script(
The script:
- cd's into the project directory
- exports HERMES_HOME, PYTHONIOENCODING, VIRTUAL_ENV
- on uv-managed venvs (per ``_resolve_detached_python``), also exports
PYTHONPATH so the base ``pythonw.exe`` can import the venv's
``site-packages``
- invokes ``pythonw -m hermes_cli.main [--profile X] gateway run``
directly so the wrapper cmd.exe exits without a visible gateway console

Expand All @@ -314,12 +317,21 @@ def _build_gateway_cmd_script(
lines.append(f'set "HERMES_HOME={hermes_home}"')
lines.append('set "PYTHONIOENCODING=utf-8"')
lines.append('set "HERMES_GATEWAY_DETACHED=1"')
# VIRTUAL_ENV lets the gateway's own python detection find the venv
# if someone imports hermes_constants-based logic during startup.
venv_dir = str(Path(python_path).resolve().parent.parent)
lines.append(f'set "VIRTUAL_ENV={venv_dir}"')
# Mirror _build_gateway_argv: uv-managed venvs ship a shim pythonw.exe
# that respawns the base console python.exe, which surfaces as a visible
# cmd.exe window when launched from the Scheduled Task. Resolve to the
# base interpreter and inject site-packages on PYTHONPATH the same way
# the direct-spawn path does.
pythonw_path, venv_dir, extra_pythonpath = _resolve_detached_python(python_path)
lines.append(f'set "VIRTUAL_ENV={venv_dir.resolve()}"')
if extra_pythonpath:
# The emitted ``.cmd`` is consumed by cmd.exe on Windows, so use the
# Windows path separator literally rather than ``os.pathsep`` — the
# latter would yield ``:`` if this script were ever generated off
# Windows (e.g. cross-platform build tooling or unit tests).
pythonpath_value = ";".join([working_dir, *extra_pythonpath])
lines.append(f'set "PYTHONPATH={pythonpath_value}"')
Comment on lines +325 to +333

pythonw_path = _derive_venv_pythonw(python_path)
prog_args = [pythonw_path, "-m", "hermes_cli.main"]
if profile_arg:
prog_args.extend(profile_arg.split())
Expand Down
50 changes: 50 additions & 0 deletions tests/hermes_cli/test_gateway_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,56 @@ def test_build_gateway_argv_uses_base_pythonw_for_uv_venv_launcher(monkeypatch,
assert str(site_packages) in env_overlay["PYTHONPATH"].split(gateway_windows.os.pathsep)


def test_build_gateway_cmd_script_uses_base_pythonw_for_uv_venv_launcher(monkeypatch, tmp_path):
"""Scheduled-Task .cmd wrapper must mirror _build_gateway_argv on uv venvs.

The venv/Scripts/pythonw.exe shipped by uv is a launcher shim that
respawns the base console python.exe, which renders as a visible cmd.exe
window when started by the Scheduled Task. The .cmd generator must
resolve to the base pythonw.exe and inject the venv site-packages on
PYTHONPATH the same way the direct-spawn path does.
"""
project = tmp_path / "project"
scripts = project / "venv" / "Scripts"
site_packages = project / "venv" / "Lib" / "site-packages"
base = tmp_path / "uv" / "python" / "cpython-3.11-windows-x86_64-none"
scripts.mkdir(parents=True)
site_packages.mkdir(parents=True)
base.mkdir(parents=True)

venv_python = scripts / "python.exe"
venv_pythonw = scripts / "pythonw.exe"
base_pythonw = base / "pythonw.exe"
for exe in (venv_python, venv_pythonw, base_pythonw):
exe.write_text("", encoding="utf-8")
(project / "venv" / "pyvenv.cfg").write_text(
f"home = {base}\nimplementation = CPython\nuv = 0.11.14\nversion_info = 3.11.15\n",
encoding="utf-8",
)

content = gateway_windows._build_gateway_cmd_script(
str(venv_python),
str(project),
str(tmp_path / "hermes-home"),
"--profile alice",
)

assert str(base_pythonw) in content
assert str(venv_pythonw) not in content
assert f'set "VIRTUAL_ENV={(project / "venv").resolve()}"' in content
pythonpath_line = next(
line for line in content.splitlines() if line.startswith('set "PYTHONPATH=')
)
pythonpath_value = pythonpath_line.split("=", 1)[1].rstrip('"')
# The generated script is Windows-targeted, so its PYTHONPATH always
# uses ``;`` as the separator regardless of the host OS running this
# test. Use the literal Windows separator instead of os.pathsep.
pythonpath_entries = pythonpath_value.split(";")
assert str(project) in pythonpath_entries
assert str(site_packages) in pythonpath_entries
assert "gateway run" in content


def _arrange_startup_fallback(monkeypatch, tmp_path, running_pids):
script_path = tmp_path / "Hermes_Gateway_alice.cmd"
startup_entry = tmp_path / "Startup" / "Hermes_Gateway_alice.cmd"
Expand Down
Loading