Skip to content

fix(serve): Windows parent-death watchdog false-triggers due to ppid mismatch - #83604

Closed
LKrystalL wants to merge 1 commit into
NousResearch:mainfrom
LKrystalL:fix/windows-parent-death-watchdog-ppid
Closed

fix(serve): Windows parent-death watchdog false-triggers due to ppid mismatch#83604
LKrystalL wants to merge 1 commit into
NousResearch:mainfrom
LKrystalL:fix/windows-parent-death-watchdog-ppid

Conversation

@LKrystalL

Copy link
Copy Markdown

Problem

After updating to v0.20.0 (commit 33f8e96), Hermes Desktop on Windows fails to start with:

Hermes backend exited before it became ready (0).

The backend process (hermes serve) starts, prints HERMES_BACKEND_READY port=XXXXX, then exits with code 0 within ~2 seconds — before the desktop Electron shell can complete its health-check handshake.

Root Cause

The newly introduced _start_parent_death_watchdog() in hermes_cli/web_server.py compares os.getppid() against HERMES_PARENT_PID (the Electron process PID) to detect whether the spawning parent has died. On Windows, the child process's getppid() often differs from the Electron PID because of intermediate launcher/shell processes in the spawn chain. The watchdog therefore immediately misidentifies the parent as dead and calls os._exit(0).

The _is_serve_orphaned check:

def _is_serve_orphaned(original_ppid: int, getppid=os.getppid) -> bool:
    return getppid() != original_ppid

On POSIX this works reliably because fork() + exec() preserves the ppid. On Windows, spawn() often inserts intermediate processes.

Fix

On Windows, replace the ppid-comparison check with a direct process-aliveness check via OpenProcess + GetExitCodeProcess. This is the correct Windows idiom for determining whether a process is still running, regardless of the process-tree topology.

A 2× poll grace period is added after spawn to let the process tree stabilise before the first aliveness probe.

POSIX behaviour is unchanged.

Verification

  • _demo() self-check passes
  • _is_windows_process_alive(os.getpid())True
  • _is_windows_process_alive(99999999)False
  • HERMES_DESKTOP=1 HERMES_PARENT_PID=$$ hermes.exe serve now stays alive (previously exited in <2s)
  • Desktop starts successfully after the fix
  • 148/151 existing tests pass (the 1 failure is pre-existing: test_start_server_keeps_bare_asyncio_run_on_posix on Windows)

Diff

+def _is_windows_process_alive(pid: int) -> bool:
+    import ctypes
+    from ctypes import wintypes
+    PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
+    STILL_ACTIVE = 259
+    kernel32 = ctypes.windll.kernel32
+    handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
+    if not handle:
+        return False
+    exit_code = wintypes.DWORD()
+    result = kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code))
+    kernel32.CloseHandle(handle)
+    if not result:
+        return False
+    return exit_code.value == STILL_ACTIVE

-    def _loop() -> None:
-        while not _is_serve_orphaned(original_ppid):
-            time.sleep(poll)
-        os._exit(0)
+    if sys.platform == "win32":
+        def _loop() -> None:
+            time.sleep(poll * 2)
+            while _is_windows_process_alive(original_ppid):
+                time.sleep(poll)
+            os._exit(0)
+    else:
+        def _loop() -> None:
+            while not _is_serve_orphaned(original_ppid):
+                time.sleep(poll)
+            os._exit(0)

…k instead of ppid comparison

On Windows, os.getppid() can differ from HERMES_PARENT_PID even when the
parent (Electron desktop) is alive — intermediate launcher/shell processes
change the apparent parent PID. The original ppid-based check would false-
trigger immediately, causing the backend to exit before desktop could
connect (exit code 0, no crash).

Replaced with OpenProcess + GetExitCodeProcess for direct parent-process
aliveness check on win32, with a 2x poll grace period after spawn to let
the process tree stabilise. POSIX path unchanged.
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/cli CLI entry point, hermes_cli/, setup wizard platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Aug 11, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks @LKrystalL — correct diagnosis of the same regression (the #83406 watchdog's PPID comparison never matches through the Windows venv shim). This landed on main via #83615, salvaged from #83556 which was submitted ~1h earlier and probes via the existing psutil-backed gateway.status._pid_exists() — the hand-rolled OpenProcess here reads access-denied as dead, which would re-introduce a false kill under permission edges. Credit to both of you for the fast catch. Closing as duplicate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P1 High — major feature broken, no workaround platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants