fix(update): fallback to ancestor-PID check for Windows update lock handoff

On Windows the Tauri updater writes its PID to the update marker, then
spawns 'hermes update' via the venv shim. The child receives
HERMES_UPDATE_HANDOFF_PID to recognize the parent lock, but on some
Windows configs the env var is not inherited through the shim subprocess
chain, deadlocking every GUI update.

Fix: when the env-var handoff fails, fall back to walking the Windows
process tree via wmic /format:csv to collect ancestor PIDs. If the
marker PID is found in the ancestor chain, treat it as our own
orchestrating parent and allow the update.

Also fixes wmic output encoding on localized Windows (GBK/CP936) by
using /format:csv which produces clean UTF-8 output.
This commit is contained in:
xenodmc
2026-07-30 16:42:40 +08:00
committed by Teknium
parent 087b2230c4
commit 6e8691f432
2 changed files with 72 additions and 0 deletions
+71
View File
@@ -42,6 +42,7 @@ from __future__ import annotations
import logging
import os
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
@@ -199,6 +200,66 @@ class UpdateLock:
self.acquired = False
self.holder: UpdateHolder | None = None
def _ancestor_pids(self) -> set[int]:
"""Return PIDs of this process's ancestor chain (Windows, best-effort).
The Tauri updater spawns ``hermes.exe`` (venv shim) → ``python.exe``,
so the marker owner (updater's PID) can be two levels up. Walk the
chain via ``wmic`` and return every ancestor PID we can find.
"""
pids: set[int] = set()
if os.name != "nt":
return pids
try:
current = os.getpid()
for _ in range(4): # up to great-grandparent
result = subprocess.run(
[
"wmic",
"process",
"where",
f"processid={current}",
"get",
"parentprocessid",
"/format:csv",
],
capture_output=True,
timeout=5,
)
# wmic on Chinese Windows outputs GBK (CP936); decode raw bytes
raw = result.stdout
if not raw:
break
try:
text = raw.decode("utf-8")
except UnicodeDecodeError:
try:
text = raw.decode("gbk")
except UnicodeDecodeError:
text = raw.decode("utf-8", errors="replace")
lines = [l.strip() for l in text.splitlines() if l.strip()]
# CSV format: Node,ParentProcessId
# First line is header, second line is data
if len(lines) >= 2:
cols = lines[1].split(",")
if len(cols) >= 2:
ppid_str = cols[1].strip().strip('"')
if ppid_str.isdigit():
ppid = int(ppid_str)
if ppid <= 0 or ppid in pids:
break
pids.add(ppid)
current = ppid
else:
break
else:
break
else:
break
return pids
except Exception:
return pids
def acquire(self) -> bool:
"""Claim the lock. Returns False (and sets ``holder``) if it's taken.
@@ -206,11 +267,21 @@ class UpdateLock:
orchestrating parent (the Tauri updater spawning `hermes update` as a
stage): we run under ITS claim rather than refusing or re-writing the
marker, and ``release`` leaves the parent's marker untouched.
On Windows the env-var handoff sometimes fails (the venv shim's
subprocess may not inherit ``HERMES_UPDATE_HANDOFF_PID``). As a
fallback we also check whether the marker's PID is in our own
ancestor chain — if the Tauri updater holds the lock and we are its
descendant (venv shim → python), it is our handoff partner.
"""
existing = read_live_update(path=self.path)
if existing is not None:
if existing.pid == _handoff_pid():
return True
# Windows fallback: env-var handoff sometimes fails; check
# if the marker owner is our parent/grandparent (the updater).
if os.name == "nt" and existing.pid in self._ancestor_pids():
return True
self.holder = existing
return False
try: