fix(windows): чёрные окна консоли выскакивали каждую минуту

Хаб — оконное приложение без консоли, поэтому каждый запуск консольного
exe открывал отдельное окно. Пока проверка аккаунтов шла по нажатию, это
было незаметно. A50 сделал проверку автоматической раз в минуту, и окна
agy.exe стали появляться постоянно, мешая работе.

Добавлен hidden_process_kwargs(): CREATE_NO_WINDOW плюс STARTUPINFO с
SW_HIDE, на не-Windows пусто. Применён ко всем ФОНОВЫМ вызовам:
опрос моделей agy, выполнение запроса agy, чтение ключей, codex_oauth,
launcher_bootstrap, git rev-parse и проверка версии в обновлении.

Вызовы, где окно нужно видимым, не тронуты: вход по OAuth сознательно
использует CREATE_NEW_CONSOLE, запуск установщика тоже должен быть виден.

574 passed, ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-31 19:39:12 +07:00
parent d17360bc2f
commit f0d06e4994
5 changed files with 32 additions and 1 deletions

View file

@ -162,6 +162,7 @@ def discover_models(profile_id: str | None = None) -> dict[str, str]:
errors="replace",
env=env,
stdin=subprocess.DEVNULL,
**hidden_process_kwargs(),
)
raw = result.stdout.strip()
if not raw or result.returncode != 0:
@ -618,6 +619,26 @@ BLOCKED_SECRET_PATTERNS: tuple[str, ...] = (
)
def hidden_process_kwargs() -> dict:
"""Флаги запуска подпроцесса без видимого окна консоли (только Windows).
Хаб оконное приложение без консоли, поэтому каждый запуск консольного
exe (agy.exe и прочие) открывал отдельное чёрное окно. Пока проверка шла
по нажатию, это было незаметно. После A50 проверка аккаунтов запускается
сама раз в минуту, и окна стали появляться постоянно, мешая работе.
Применять ко всем ФОНОВЫМ вызовам. Для входа по OAuth окно нужно
видимым там сознательно используется CREATE_NEW_CONSOLE.
"""
if os.name != "nt":
return {}
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
return {"creationflags": flags, "startupinfo": startupinfo}
def build_safe_subprocess_env(
base_env: dict[str, str] | None = None,
allow_extra_keys: set[str] | list[str] | None = None,
@ -748,6 +769,7 @@ def agy_generate(
encoding="utf-8",
errors="replace",
env=custom_env if custom_env is not None else build_safe_subprocess_env(),
**hidden_process_kwargs(),
)
except subprocess.TimeoutExpired:
return _error_completion(model_raw, "agy subprocess timed out")

View file

@ -9,6 +9,7 @@ import tempfile
from datetime import datetime
from pathlib import Path
from typing import Any, Callable
from antigravity_provider.agy_subprocess import hidden_process_kwargs
def _hermes_home() -> Path:
@ -94,6 +95,7 @@ def load_agy_keychain_credentials(*, runner: Callable[[], str] | None = None) ->
["security", "find-generic-password", "-a", "antigravity", "-s", "gemini", "-w"],
stderr=subprocess.DEVNULL,
timeout=5,
**hidden_process_kwargs(),
).decode("utf-8")
try:

View file

@ -25,6 +25,7 @@ from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from antigravity_provider.router.profile_manager import ProfileAuthManager, mask_email
from antigravity_provider.agy_subprocess import hidden_process_kwargs
logger = logging.getLogger("hermes.router.codex_oauth")
@ -380,6 +381,7 @@ def stop_running_codex_processes() -> list[int]:
out = subprocess.check_output(
["tasklist", "/FI", f"IMAGENAME eq {proc_name}", "/FO", "CSV", "/NH"],
stderr=subprocess.DEVNULL,
**hidden_process_kwargs(),
text=True,
)
for line in out.strip().splitlines():
@ -389,7 +391,7 @@ def stop_running_codex_processes() -> list[int]:
pid_str = parts[1].strip('"')
if pid_str.isdigit():
pid = int(pid_str)
subprocess.run(["taskkill", "/F", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(["taskkill", "/F", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **hidden_process_kwargs())
stopped_pids.append(pid)
except Exception:
pass

View file

@ -21,6 +21,7 @@ import sys
import traceback
from pathlib import Path
from typing import Dict, List, Tuple
from antigravity_provider.agy_subprocess import hidden_process_kwargs
def get_startup_log_path() -> Path:
@ -96,6 +97,7 @@ def self_heal_dependencies(missing_packages: List[str]) -> Tuple[bool, str]:
capture_output=True,
text=True,
timeout=90,
**hidden_process_kwargs(),
)
if res.returncode == 0:
log_startup("Self-healing successful. Re-verifying package imports...")

View file

@ -28,6 +28,7 @@ from typing import Any, Callable, Dict, Optional, Tuple
from antigravity_provider import paths
from antigravity_provider.version import __version__, CHANNEL, MINIMUM_HERMES_VERSION
from antigravity_provider.agy_subprocess import hidden_process_kwargs
logger = logging.getLogger("hermes.hub.updater")
@ -119,6 +120,7 @@ def get_installed_commit() -> str:
capture_output=True,
text=True,
timeout=5,
**hidden_process_kwargs(),
)
if res.returncode == 0 and res.stdout.strip():
return res.stdout.strip()
@ -778,6 +780,7 @@ class UpdateManager:
capture_output=True,
text=True,
timeout=15,
**hidden_process_kwargs(),
)
if res.returncode != 0 or "OK" not in res.stdout:
raise RuntimeError(f"Post-update verification failed: {res.stderr or res.stdout}")