mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
fix(windows): suppress console window flashes in env probes, lazy installs, and platform.win32_ver()
From windowless processes (the pythonw gateway and the kanban workers it
spawns), three spawn paths flash visible console windows on Windows:
1. tools/env_probe.py::_run() ran its interpreter/pip probes
(python3 / python / pip / 'python3 -m pip' / PEP-668 check, ~5 per
worker start) without creationflags — one console flash per probe.
2. tools/lazy_deps.py had four spawn sites with the same defect:
'uv pip install', the 'pip --version' probe, ensurepip, and the
pip install fallback.
Both now pass creationflags=windows_hide_flags() (CREATE_NO_WINDOW on
Windows, 0 on POSIX) — stdio capture still works because the child is
hidden, not detached.
3. CPython 3.11's platform.win32_ver() unconditionally calls
_syscmd_ver(), which runs 'cmd /c ver' via
subprocess.check_output(shell=True) with no window suppression. Any
dependency touching platform.uname()/version()/platform() at import
time flashes one 'cmd' window per windowless process. New helper
_subprocess_compat.suppress_platform_ver_console() (Windows-only,
never raises) stubs platform._syscmd_ver so win32_ver() falls back to
sys.getwindowsversion().platform_version — verified byte-identical
platform.platform() output on CPython 3.11
('Windows-10-10.0.26100-SP0' either way). Called at the top of
hermes_cli/main.py, right after the hermes_bootstrap guard, before
heavyweight imports.
Verified on Windows 11 by polling EnumWindows at ~15 ms and attributing
new visible HWNDs to the suspect process tree (conhost child presence is
NOT evidence of a visible window — it appears even with
CREATE_NO_WINDOW). Tests: tests/tools/test_windows_native_support.py,
test_env_probe.py, test_lazy_deps.py, test_lazy_deps_durable_target.py —
153 passed; the 3 failures are pre-existing on upstream/main in a
Windows environment (POSIX-only assertions and NTFS chmod semantics).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
385a0655e5
commit
5c5960d9f9
3 changed files with 53 additions and 0 deletions
|
|
@ -36,6 +36,7 @@ from typing import Sequence
|
|||
__all__ = [
|
||||
"IS_WINDOWS",
|
||||
"resolve_node_command",
|
||||
"suppress_platform_ver_console",
|
||||
"windows_detach_flags",
|
||||
"windows_detach_flags_without_breakaway",
|
||||
"windows_hide_flags",
|
||||
|
|
@ -226,6 +227,43 @@ def windows_hide_flags() -> int:
|
|||
return _CREATE_NO_WINDOW
|
||||
|
||||
|
||||
def suppress_platform_ver_console() -> None:
|
||||
"""Stub out ``platform._syscmd_ver`` on Windows so it can never flash a
|
||||
console window. No-op on non-Windows.
|
||||
|
||||
CPython's ``platform.win32_ver()`` — reached by ``platform.uname()``,
|
||||
``platform.version()``, and ``platform.platform()`` — unconditionally
|
||||
shells out ``cmd /c ver`` via ``subprocess.check_output(..., shell=True)``
|
||||
with no ``CREATE_NO_WINDOW``. From a windowless parent (the pythonw
|
||||
gateway and every kanban worker it spawns) that allocates a fresh
|
||||
*visible* console: one flashing ``cmd`` window per process, triggered by
|
||||
any dependency that merely touches ``platform.uname()`` at import time.
|
||||
|
||||
With ``_syscmd_ver`` stubbed to return its inputs, ``win32_ver()`` hits
|
||||
the documented ``ValueError`` fallback and reads the version from
|
||||
``sys.getwindowsversion().platform_version`` — same information, queried
|
||||
in-process, no subprocess, no window. Verified equivalent on
|
||||
CPython 3.11 (``platform()`` → ``Windows-10-10.0.xxxxx-SP0`` either way).
|
||||
|
||||
Call early, before heavyweight imports — the flash typically happens
|
||||
during a dependency's import, not from Hermes' own code.
|
||||
"""
|
||||
if not IS_WINDOWS:
|
||||
return
|
||||
try:
|
||||
import platform
|
||||
|
||||
if hasattr(platform, "_syscmd_ver"):
|
||||
def _quiet_syscmd_ver(system="", release="", version="",
|
||||
supported_platforms=("win32", "win16", "dos")):
|
||||
return system, release, version
|
||||
|
||||
platform._syscmd_ver = _quiet_syscmd_ver
|
||||
except Exception:
|
||||
# Purely cosmetic hardening — never let it break startup.
|
||||
pass
|
||||
|
||||
|
||||
def windows_detach_popen_kwargs() -> dict:
|
||||
"""Return a dict of Popen kwargs that detach a child on Windows and
|
||||
fall back to the POSIX equivalent (``start_new_session=True``) on
|
||||
|
|
|
|||
|
|
@ -61,6 +61,15 @@ try:
|
|||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
# Windows: neutralize CPython's ``platform._syscmd_ver`` before anything else
|
||||
# imports — it shells out ``cmd /c ver`` (shell=True, no CREATE_NO_WINDOW), so
|
||||
# any dependency touching ``platform.uname()`` at import time flashes a
|
||||
# visible console when this process is windowless (pythonw gateway + every
|
||||
# kanban worker). No-op on POSIX; never raises.
|
||||
from hermes_cli._subprocess_compat import suppress_platform_ver_console
|
||||
|
||||
suppress_platform_ver_console()
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ from dataclasses import dataclass
|
|||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -668,6 +670,7 @@ def _venv_pip_install(specs: tuple[str, ...], *, timeout: int = 300) -> _Install
|
|||
[uv_bin, "pip", "install", *target_args, *constraint_args, *specs],
|
||||
capture_output=True, text=True, timeout=timeout, env=uv_env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
if r.returncode == 0:
|
||||
if target is not None:
|
||||
|
|
@ -684,6 +687,7 @@ def _venv_pip_install(specs: tuple[str, ...], *, timeout: int = 300) -> _Install
|
|||
pip_cmd + ["--version"],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
stdin=subprocess.DEVNULL,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
if probe.returncode != 0:
|
||||
raise FileNotFoundError("pip not in venv")
|
||||
|
|
@ -693,6 +697,7 @@ def _venv_pip_install(specs: tuple[str, ...], *, timeout: int = 300) -> _Install
|
|||
[sys.executable, "-m", "ensurepip", "--upgrade", "--default-pip"],
|
||||
capture_output=True, text=True, timeout=120, check=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
return _InstallResult(False, "",
|
||||
|
|
@ -703,6 +708,7 @@ def _venv_pip_install(specs: tuple[str, ...], *, timeout: int = 300) -> _Install
|
|||
pip_cmd + ["install", *target_args, *constraint_args, *specs],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
stdin=subprocess.DEVNULL,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
if r.returncode == 0 and target is not None:
|
||||
_activate_target_on_syspath(target)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue