mirror of
https://github.com/usestrix/strix.git
synced 2026-10-05 02:41:38 +00:00
fix(cli): force UTF-8 stdout/stderr on Windows so Rich output never raises
Windows hands a redirected or legacy console stream the ANSI code page (cp1252), which cannot encode Rich's panels or the model's text: one check mark in a finding ended a headless run with UnicodeEncodeError, and the error handler raised again rendering its own panel. Reconfigure both streams to UTF-8 at startup on win32, and open the Go TUI's os.devnull sink as UTF-8 so logging into it stops producing --- Logging error --- reports under the same code page.
This commit is contained in:
parent
45b775dcd5
commit
5c3476795a
3 changed files with 97 additions and 4 deletions
|
|
@ -415,12 +415,30 @@ def _bootstrap_scan(args: argparse.Namespace) -> None:
|
|||
telemetry_start(args)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
configure_dependency_logging()
|
||||
def _force_utf8_streams() -> None:
|
||||
"""Give stdout/stderr a UTF-8 encoding, whatever the console code page.
|
||||
|
||||
Windows hands a redirected or legacy console stream the ANSI code page
|
||||
(cp1252 and friends), which cannot encode Rich's output or the model's
|
||||
text: one ``\u2713`` in a finding ends a headless run with
|
||||
``UnicodeEncodeError``. Streams without ``reconfigure`` (test captures,
|
||||
replaced file objects) are left alone.
|
||||
"""
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
reconfigure = getattr(stream, "reconfigure", None)
|
||||
if reconfigure is None:
|
||||
continue
|
||||
with contextlib.suppress(ValueError, OSError):
|
||||
reconfigure(encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if sys.platform == "win32":
|
||||
_force_utf8_streams()
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
configure_dependency_logging()
|
||||
|
||||
if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"):
|
||||
try:
|
||||
parse_arguments()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import shutil
|
|||
import sys
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, TextIO
|
||||
|
||||
from strix.config import load_settings, persist_current
|
||||
from strix.core.agents import AgentCoordinator
|
||||
|
|
@ -58,6 +58,16 @@ class GoTuiPreActivationError(RuntimeError):
|
|||
"""A sidecar failure raised before the Go TUI activates."""
|
||||
|
||||
|
||||
def _open_output_sink() -> TextIO:
|
||||
"""A UTF-8 ``os.devnull`` handle for Python's stdout/stderr while the Go TUI owns the terminal.
|
||||
|
||||
UTF-8 is explicit because the locale encoding (cp1252 on Windows) cannot take
|
||||
the model's text, and a logging handler that fails to write there floods the
|
||||
log with ``--- Logging error ---`` reports.
|
||||
"""
|
||||
return Path(os.devnull).open("a", buffering=1, encoding="utf-8")
|
||||
|
||||
|
||||
class GoTuiRuntime:
|
||||
def __init__(self, args: argparse.Namespace) -> None:
|
||||
self.args = args
|
||||
|
|
@ -433,7 +443,7 @@ class GoTuiRuntime:
|
|||
# only the Python-level bindings change.
|
||||
original_stdout = sys.stdout
|
||||
original_stderr = sys.stderr
|
||||
output_sink = Path(os.devnull).open("a", buffering=1) # noqa: SIM115
|
||||
output_sink = _open_output_sink()
|
||||
sys.stdout = output_sink
|
||||
sys.stderr = output_sink
|
||||
backend_socket: socket.socket | None = None
|
||||
|
|
|
|||
65
tests/test_utf8_streams.py
Normal file
65
tests/test_utf8_streams.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Windows gives redirected stdout/stderr the ANSI code page; Strix forces UTF-8 (STR-742)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from rich.console import Console
|
||||
|
||||
from strix.interface.main import _force_utf8_streams
|
||||
from strix.interface.tui.runtime import _open_output_sink
|
||||
|
||||
|
||||
UNICODE_SAMPLE = "Reflected XSS in / \u2713 \u2192 \U0001f50d \u00b7 done"
|
||||
|
||||
|
||||
def _cp1252_stream() -> tuple[io.TextIOWrapper, io.BytesIO]:
|
||||
raw = io.BytesIO()
|
||||
return io.TextIOWrapper(raw, encoding="cp1252", line_buffering=True), raw
|
||||
|
||||
|
||||
def test_rich_output_raises_on_a_cp1252_stream(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(sys, "stdout", _cp1252_stream()[0])
|
||||
monkeypatch.setattr(sys, "stderr", _cp1252_stream()[0])
|
||||
|
||||
with pytest.raises(UnicodeEncodeError):
|
||||
Console().print(UNICODE_SAMPLE)
|
||||
|
||||
|
||||
def test_force_utf8_streams_lets_rich_print_unicode(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
stdout, stdout_bytes = _cp1252_stream()
|
||||
stderr, stderr_bytes = _cp1252_stream()
|
||||
monkeypatch.setattr(sys, "stdout", stdout)
|
||||
monkeypatch.setattr(sys, "stderr", stderr)
|
||||
|
||||
_force_utf8_streams()
|
||||
|
||||
assert stdout.encoding == "utf-8"
|
||||
assert stderr.encoding == "utf-8"
|
||||
Console().print(UNICODE_SAMPLE)
|
||||
Console(stderr=True).print(UNICODE_SAMPLE)
|
||||
stdout.flush()
|
||||
stderr.flush()
|
||||
assert UNICODE_SAMPLE in stdout_bytes.getvalue().decode("utf-8")
|
||||
assert UNICODE_SAMPLE in stderr_bytes.getvalue().decode("utf-8")
|
||||
|
||||
|
||||
def test_force_utf8_streams_skips_streams_without_reconfigure(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
stdout = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stdout", stdout)
|
||||
monkeypatch.setattr(sys, "stderr", io.StringIO())
|
||||
|
||||
_force_utf8_streams()
|
||||
|
||||
sys.stdout.write(UNICODE_SAMPLE)
|
||||
assert stdout.getvalue() == UNICODE_SAMPLE
|
||||
|
||||
|
||||
def test_tui_output_sink_accepts_unicode() -> None:
|
||||
with _open_output_sink() as sink:
|
||||
assert sink.encoding == "utf-8"
|
||||
sink.write(UNICODE_SAMPLE + "\n")
|
||||
Loading…
Add table
Reference in a new issue