From 323cce7e94295a673dafafbdbee79a1010406bcc Mon Sep 17 00:00:00 2001 From: Strontvod Date: Mon, 25 May 2026 01:04:49 +0200 Subject: [PATCH] fix: exclude parent process chain from concurrent instance detection on Windows On Windows, the setuptools-generated hermes.exe launcher is a separate native process that spawns python.exe (the interpreter running the update code). os.getpid() returns the Python PID, but the launcher (which holds the file lock) is the parent. Without walking the parent chain, every 'hermes update' reports its own launcher as a concurrent instance - a false positive. This patch builds an exclusion set containing the Python process and its entire ancestor chain, so the running invocation never reports itself. --- hermes_cli/main.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 68432c3aa00..e11154206e1 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -7665,8 +7665,11 @@ def _detect_concurrent_hermes_instances( This helper enumerates processes whose ``exe`` matches one of the venv's shims (``hermes.exe`` / ``hermes-gateway.exe``) and returns ``(pid, - process_name)`` pairs. The caller's own PID is excluded so the running - ``hermes update`` invocation never reports itself. + process_name)`` pairs. The caller's own PID and its entire ancestor + chain are excluded so the running ``hermes update`` invocation never + reports itself — this matters on Windows where the setuptools .exe + launcher (``hermes.exe``) is a separate process from the Python + interpreter it loads (``python.exe``). Returns an empty list off-Windows, on missing psutil, or when no other instances exist. Never raises — process enumeration is best-effort. @@ -7679,8 +7682,32 @@ def _detect_concurrent_hermes_instances( except Exception: return [] - if exclude_pid is None: - exclude_pid = os.getpid() + # Build a set of PIDs to exclude: the Python process itself plus its + # entire parent chain. On Windows the setuptools-generated hermes.exe + # launcher is a separate native process that spawns python.exe (the + # interpreter that runs our code). os.getpid() returns the Python PID, + # but the launcher (which holds the file lock) is the parent. Without + # walking the parent chain, every ``hermes update`` reports its own + # launcher as a concurrent instance — a false positive. + if exclude_pid is not None: + exclude_pids: set[int] = {exclude_pid} + else: + exclude_pids = {os.getpid()} + try: + current = psutil.Process(next(iter(exclude_pids))) + while True: + try: + parent = current.parent() + except (psutil.NoSuchProcess, psutil.AccessDenied): + break + if parent is None or parent.pid <= 0: + break + if parent.pid in exclude_pids: + break # loop detected + exclude_pids.add(parent.pid) + current = parent + except (psutil.NoSuchProcess, psutil.AccessDenied, ValueError): + pass # Resolve every shim path to its canonical form once for cheap comparison. shim_paths: set[str] = set() @@ -7705,7 +7732,7 @@ def _detect_concurrent_hermes_instances( continue pid = info.get("pid") exe = info.get("exe") - if not exe or pid is None or pid == exclude_pid: + if not exe or pid is None or pid in exclude_pids: continue try: exe_norm = str(Path(exe).resolve()).lower()