From ead23aadb9a8a56e082c7886977bbd37af132e3a Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:28:27 -0700 Subject: [PATCH] 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 --- hermes_cli/banner.py | 17 +++++++++++++++-- hermes_cli/web_server.py | 16 ++++++++++++++++ tui_gateway/compute_host.py | 4 +++- tui_gateway/host_supervisor.py | 9 +++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/hermes_cli/banner.py b/hermes_cli/banner.py index e487475f62b1..90364d9c8dbd 100644 --- a/hermes_cli/banner.py +++ b/hermes_cli/banner.py @@ -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), ) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index aa034835dda5..4f9ede3b8707 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -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: diff --git a/tui_gateway/compute_host.py b/tui_gateway/compute_host.py index 9ff3e100cb38..102d8e6d47b3 100644 --- a/tui_gateway/compute_host.py +++ b/tui_gateway/compute_host.py @@ -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 diff --git a/tui_gateway/host_supervisor.py b/tui_gateway/host_supervisor.py index 2541e27a7773..dc81656f8c36 100644 --- a/tui_gateway/host_supervisor.py +++ b/tui_gateway/host_supervisor.py @@ -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, )