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.
This commit is contained in:
Strontvod 2026-05-25 01:04:49 +02:00 committed by Teknium
parent a3abeb5954
commit 323cce7e94

View file

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