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()