On Windows, subprocess text=True without an explicit encoding decodes child output with the ANSI code page (e.g. 'gbk'); non-ASCII bytes then raise UnicodeDecodeError inside subprocess._readerthread, killing the Hermes backend before it becomes ready and surfacing as the desktop boot timeout. Sweep every hermes_cli text=True subprocess call to encoding='utf-8', errors='replace', and add an AST-based regression test that fails when a future text-mode call omits the encoding. Fixes #55658
42 lines
1.5 KiB
Python
42 lines
1.5 KiB
Python
"""Guard: every text-mode subprocess call in hermes_cli pins utf-8 decoding (#55658).
|
|
|
|
On Windows, ``text=True`` without ``encoding`` decodes child output with the ANSI
|
|
code page (e.g. 'gbk'), and non-ASCII bytes raise UnicodeDecodeError inside
|
|
``subprocess._readerthread``, killing the backend before it becomes ready.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
HERMES_CLI = Path(__file__).resolve().parents[2] / "hermes_cli"
|
|
|
|
|
|
def _iter_call_text_nodes(tree: ast.AST) -> list[ast.Call]:
|
|
return [n for n in ast.walk(tree) if isinstance(n, ast.Call)]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
sorted(p for p in HERMES_CLI.rglob("*.py") if "__pycache__" not in p.parts and "test" not in p.name),
|
|
ids=lambda p: str(p.relative_to(HERMES_CLI)),
|
|
)
|
|
def test_text_mode_subprocess_calls_pin_utf8(path: Path) -> None:
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
for call in _iter_call_text_nodes(tree):
|
|
names = {kw.arg for kw in call.keywords}
|
|
for kw in call.keywords:
|
|
if (
|
|
kw.arg == "text"
|
|
and isinstance(kw.value, ast.Constant)
|
|
and kw.value.value is True
|
|
):
|
|
assert "encoding" in names, (
|
|
f"{path.relative_to(HERMES_CLI)}:{kw.lineno}: subprocess text=True "
|
|
"without encoding= — on Windows this decodes with the ANSI code page "
|
|
"and can raise UnicodeDecodeError on non-ASCII child output (#55658)"
|
|
)
|