From 84965ef965e659e82334b931c43ca74bea3fc0eb Mon Sep 17 00:00:00 2001 From: ashu180 <18011166553@163.com> Date: Sun, 7 Jun 2026 13:38:22 +0800 Subject: [PATCH] fix(gateway): avoid Windows uv pythonw launcher console --- hermes_cli/gateway_windows.py | 30 ++++++++++++++++---- tests/hermes_cli/test_gateway_windows.py | 35 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/hermes_cli/gateway_windows.py b/hermes_cli/gateway_windows.py index 08c7d8c019c9..d961346ca5ff 100644 --- a/hermes_cli/gateway_windows.py +++ b/hermes_cli/gateway_windows.py @@ -340,15 +340,21 @@ def _build_gateway_cmd_script( working_dir: str, hermes_home: str, profile_arg: str, + project_root: str | None = None, ) -> str: """Build the ``gateway.cmd`` wrapper content (CRLF-terminated). The script: - cd's into a stable working directory - exports HERMES_HOME, PYTHONIOENCODING, VIRTUAL_ENV - - invokes ``pythonw -m hermes_cli.main [--profile X] gateway run`` + - invokes a no-console ``pythonw -m hermes_cli.main [--profile X] gateway run`` directly so the wrapper cmd.exe exits without a visible gateway console + uv-created venv launchers need special care: ``venv\\Scripts\\pythonw.exe`` + can respawn the base interpreter as console ``python.exe``. Reuse the same + resolver as direct detached starts so Scheduled Task launches use the base + ``pythonw.exe`` plus PYTHONPATH entries for the repo and venv site-packages. + We intentionally do NOT inline PATH overrides here — cmd.exe inherits the per-user PATH the Scheduled Task was created with, and forcibly rewriting PATH tends to break Homebrew/nvm-style installations. @@ -358,12 +364,18 @@ 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) + + pythonw_path, venv_dir, extra_pythonpath = _resolve_detached_python(python_path) lines.append(f'set "VIRTUAL_ENV={venv_dir}"') - pythonw_path = _derive_venv_pythonw(python_path) + repo_root = project_root or str(Path(python_path).resolve().parent.parent.parent) + pythonpath_entries = [repo_root, *extra_pythonpath] + if pythonpath_entries: + prefix = ";".join(str(Path(entry)) for entry in pythonpath_entries if entry) + lines.append( + f'if defined PYTHONPATH (set "PYTHONPATH={prefix};%PYTHONPATH%") else (set "PYTHONPATH={prefix}")' + ) + prog_args = [pythonw_path, "-m", "hermes_cli.main"] if profile_arg: prog_args.extend(profile_arg.split()) @@ -420,7 +432,13 @@ def _write_task_script() -> Path: hermes_home = str(Path(get_hermes_home()).resolve()) profile_arg = _profile_arg(hermes_home) - content = _build_gateway_cmd_script(python_path, working_dir, hermes_home, profile_arg) + content = _build_gateway_cmd_script( + python_path, + working_dir, + hermes_home, + profile_arg, + project_root=str(PROJECT_ROOT), + ) script_path = get_task_script_path() tmp = script_path.with_suffix(".tmp") tmp.write_text(content, encoding="utf-8", newline="") diff --git a/tests/hermes_cli/test_gateway_windows.py b/tests/hermes_cli/test_gateway_windows.py index 43f2b01dbf95..834e2996b7ec 100644 --- a/tests/hermes_cli/test_gateway_windows.py +++ b/tests/hermes_cli/test_gateway_windows.py @@ -206,6 +206,41 @@ def test_gateway_cmd_script_uses_pythonw_without_replace_or_start_churn(monkeypa assert "exit /b 0" in content +def test_gateway_cmd_script_uses_base_pythonw_for_uv_venv_launcher(tmp_path): + """Scheduled Task wrapper must match direct detached starts for uv venvs.""" + project = tmp_path / "project" + scripts = project / "venv" / "Scripts" + site_packages = project / "venv" / "Lib" / "site-packages" + base = tmp_path / "uv-base" + for directory in (scripts, site_packages, base): + directory.mkdir(parents=True, exist_ok=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}\nuv = true\n", + encoding="utf-8", + ) + + content = gateway_windows._build_gateway_cmd_script( + str(venv_python), + str(tmp_path / "hermes-home"), + str(tmp_path / "hermes-home"), + "", + project_root=str(project), + ) + + assert str(base_pythonw) in content + assert str(venv_pythonw) not in content + assert f'set "VIRTUAL_ENV={project / "venv"}"' in content + assert str(project) in content + assert str(site_packages) in content + assert "PYTHONPATH=" in content + + def test_elevated_gateway_command_uses_pythonw_hidden_console(monkeypatch): """UAC handoff should not leave a second elevated cmd.exe window open.""" calls = []