fix(windows): widen utf-8 subprocess decode guard to sibling desktop-backend sites

The salvaged #61978 covers tui_gateway/server.py. The crash reported on
Jul 24 came from a sibling site it doesn't touch: the desktop update
panel's _recent_upstream_commits() in hermes_cli/web_server.py runs
git log with text=True and no encoding. Commit 84db32484f put a bug
emoji (UTF-8 f0 9f 90 9b) in a subject on main; byte 0x90 is undefined
in cp1252, so every Windows desktop install behind that commit crashed
in subprocess._readerthread during the update check (#52649).

Guard every text=True capture site in the desktop-backend process with
encoding='utf-8', errors='replace':
- hermes_cli/web_server.py: git log update panel, memory-provider setup
  runner, WhatsApp bridge npm install, docker probe
- hermes_cli/banner.py: all 5 git sites (update check runs at startup)
- tui_gateway/host_supervisor.py: build-sha probe, ps probe, compute-host
  Popen drain threads
- tui_gateway/compute_host.py: build-sha probe, ps rss probe
This commit is contained in:
teknium1 2026-07-24 09:28:27 -07:00 committed by Teknium
parent a214f3026d
commit ead23aadb9
4 changed files with 43 additions and 3 deletions

View file

@ -160,6 +160,11 @@ def _git_stdout(args: list[str], *, cwd: Path, timeout: int = 5) -> Optional[str
["git", *args],
capture_output=True,
text=True,
# git output is UTF-8; on Windows text=True defaults to the ANSI
# code page and bytes like 0x90 (3rd byte of 🐛 in a commit
# subject) crash the stdlib reader thread (#52649).
encoding="utf-8",
errors="replace",
timeout=timeout,
cwd=str(cwd),
)
@ -179,7 +184,8 @@ def _check_via_rev(local_rev: str) -> Optional[int]:
try:
result = subprocess.run(
["git", "ls-remote", _UPSTREAM_REPO_URL, "refs/heads/main"],
capture_output=True, text=True, timeout=10,
capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=10,
)
except Exception:
return None
@ -241,7 +247,8 @@ def _check_via_local_git(repo_dir: Path) -> Optional[int]:
try:
result = subprocess.run(
["git", "rev-list", "--count", "HEAD..origin/main"],
capture_output=True, text=True, timeout=5,
capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=5,
cwd=str(repo_dir),
)
if result.returncode == 0:
@ -343,6 +350,8 @@ def _git_short_hash(repo_dir: Path, rev: str) -> Optional[str]:
["git", "rev-parse", "--short=8", rev],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
cwd=str(repo_dir),
)
@ -399,6 +408,8 @@ def get_git_banner_state(repo_dir: Optional[Path] = None) -> Optional[dict]:
["git", "rev-list", "--count", "origin/main..HEAD"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
cwd=str(repo_dir),
)
@ -435,6 +446,8 @@ def get_latest_release_tag(repo_dir: Optional[Path] = None) -> Optional[tuple]:
["git", "describe", "--tags", "--abbrev=0"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=3,
cwd=str(repo_dir),
)

View file

@ -4158,6 +4158,12 @@ def _recent_upstream_commits(n: int = 20) -> List[Dict[str, Any]]:
],
capture_output=True,
text=True,
# git log emits UTF-8 (commit subjects can carry emoji/CJK). On
# Windows text=True defaults to the ANSI code page — a byte like
# 0x90 (3rd byte of 🐛) is undefined in cp1252 and crashed the
# stdlib _readerthread, killing the desktop backend (#52649).
encoding="utf-8",
errors="replace",
timeout=5,
)
if out.returncode != 0:
@ -5780,6 +5786,10 @@ def _run_setup_command(
env=_memory_provider_setup_env(),
capture_output=True,
text=True,
# Lossy UTF-8 decode — setup tools emit UTF-8; never let a
# locale-mismatched byte raise in the reader thread (#52649).
encoding="utf-8",
errors="replace",
timeout=timeout,
check=False,
)
@ -8711,6 +8721,10 @@ def _ensure_whatsapp_bridge_dependencies(bridge_dir: Path) -> None:
cwd=str(bridge_dir),
capture_output=True,
text=True,
# npm output is UTF-8; guard the Windows ANSI-code-page default
# against undefined bytes crashing the reader thread (#52649).
encoding="utf-8",
errors="replace",
timeout=timeout,
env=with_hermes_node_path(),
creationflags=windows_hide_flags(),
@ -16199,6 +16213,8 @@ def _probe_docker_backend() -> tuple:
["docker", "info", "--format", "{{.ServerVersion}}"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=2,
)
if proc.returncode == 0:

View file

@ -113,6 +113,8 @@ def _build_sha() -> str:
["git", "rev-parse", "HEAD"],
cwd=str(_repo_root()),
text=True,
encoding="utf-8",
errors="replace",
stderr=subprocess.DEVNULL,
timeout=2,
).strip()
@ -686,7 +688,7 @@ class ComputeHost:
def _rss_mb(pid: int) -> float:
try:
out = subprocess.check_output(["ps", "-o", "rss=", "-p", str(pid)], text=True, stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2).strip()
out = subprocess.check_output(["ps", "-o", "rss=", "-p", str(pid)], text=True, encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2).strip()
return int(out.splitlines()[-1].strip()) / 1024.0 if out else 0.0
except Exception:
return 0.0

View file

@ -72,6 +72,8 @@ def _build_sha() -> str:
["git", "rev-parse", "HEAD"],
cwd=str(_repo_root()),
text=True,
encoding="utf-8",
errors="replace",
stderr=subprocess.DEVNULL,
timeout=2,
).strip()
@ -112,6 +114,8 @@ def _pid_command(pid: int) -> str:
return subprocess.check_output(
["ps", "-p", str(pid), "-o", "command="],
text=True,
encoding="utf-8",
errors="replace",
stderr=subprocess.DEVNULL,
timeout=2,
).strip()
@ -327,6 +331,11 @@ class HostSupervisor:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
# Lossy UTF-8 decode — the compute host emits UTF-8; a
# locale-mismatched byte must not raise inside the drain
# threads and kill the supervisor (#52649).
encoding="utf-8",
errors="replace",
bufsize=1,
start_new_session=True,
)