From ac6687181e2e43a5e3359da7bbc4c8b7355bbd5c Mon Sep 17 00:00:00 2001 From: yilsem Date: Sat, 12 Sep 2026 11:52:12 +0300 Subject: [PATCH] fix(windows): probe compute host liveness without sending signals --- tests/tui_gateway/test_compute_host_phase1.py | 21 +++++++++++++++++++ tui_gateway/host_supervisor.py | 16 +++++--------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/tests/tui_gateway/test_compute_host_phase1.py b/tests/tui_gateway/test_compute_host_phase1.py index edf78cf..f713eb7 100644 --- a/tests/tui_gateway/test_compute_host_phase1.py +++ b/tests/tui_gateway/test_compute_host_phase1.py @@ -15,6 +15,7 @@ from tui_gateway.host_supervisor import ( MUTATOR_ROUTE_TABLE, HostSupervisor, append_log_record, + _pid_alive, ) @@ -164,6 +165,26 @@ def test_supervisor_startup_reconcile_pid_reuse_guard(tmp_path, monkeypatch): assert not registry.exists() +def test_pid_probe_does_not_terminate_a_live_process(): + # Use the base interpreter, not the Windows venv launcher/worker chain. + child = subprocess.Popen( + [getattr(sys, "_base_executable", sys.executable), "-c", + "import sys; sys.stdin.readline(); print('still-alive', flush=True)"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + try: + assert _pid_alive(child.pid) + output, errors = child.communicate(input=b"ping\n", timeout=10) + assert child.returncode == 0, errors.decode("utf-8", errors="replace") + assert output.strip() == b"still-alive", "A liveness query terminated the process" + finally: + if child.poll() is None: + child.terminate() + child.wait(timeout=10) + assert not _pid_alive(child.pid) + + def _make_compress_host_session(events: list) -> dict: class _Agent: model = "host-model" diff --git a/tui_gateway/host_supervisor.py b/tui_gateway/host_supervisor.py index ca50e3a..db644d3 100644 --- a/tui_gateway/host_supervisor.py +++ b/tui_gateway/host_supervisor.py @@ -22,6 +22,8 @@ from collections.abc import Callable from pathlib import Path from typing import Any +import psutil + from hermes_constants import get_hermes_home from tools.environments.local import hermes_subprocess_env @@ -112,17 +114,9 @@ def _default_registry_path() -> Path: def _pid_alive(pid: int) -> bool: - if pid <= 0: - return False - try: - os.kill(pid, 0) - return True - except ProcessLookupError: - return False - except PermissionError: - return True - except Exception: - return False + # Signal zero is not a portable liveness query (CTRL_C_EVENT on Windows). + # pid_exists also distinguishes exited Windows processes with open handles. + return pid > 0 and psutil.pid_exists(pid) def _pid_command(pid: int) -> str: