Skip to content

Commit 35e4397

Browse files
gitttt-1234claude
andauthored
fix(gui): decode inference subprocess output as UTF-8 (#2744) (#2801)
The GUI inference runner read the subprocess output in text mode without specifying an encoding, so Python used the locale default — cp1252 on Windows. When sleap-nn emitted any non-cp1252 byte (e.g. the UTF-8 box-drawing glyphs in a `rich`-rendered traceback or progress bar), the reader crashed with `UnicodeDecodeError: 'charmap' codec can't decode byte 0x90`, masking the real underlying error from the user. Force `encoding="utf-8", errors="replace"` on the `Popen` reader (and on the sibling raw-bytes `.decode()` path) so the real subprocess output — including error tracebacks — surfaces in the GUI log on all platforms. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fc6d58e commit 35e4397

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎sleap/gui/learning/runners.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,15 @@ def _run_inference_item(
239239
stdout=subprocess.PIPE,
240240
stderr=subprocess.STDOUT, # Merge stderr into stdout
241241
text=True, # Text mode (required for line buffering)
242+
# Force UTF-8 decoding of the child's output. Without this, text mode
243+
# decodes using the locale default, which is cp1252 on Windows, so any
244+
# non-cp1252 byte in the subprocess output (e.g. the UTF-8 box-drawing
245+
# glyphs in a `rich`-rendered traceback or progress bar) crashes the
246+
# reader with `UnicodeDecodeError: 'charmap' codec can't decode byte ...`,
247+
# masking the real error. `errors="replace"` keeps reading even if the
248+
# child emits a stray non-UTF-8 byte. (gh discussion #2744)
249+
encoding="utf-8",
250+
errors="replace",
242251
bufsize=1, # Line buffered
243252
env=env,
244253
) as proc:
@@ -700,7 +709,9 @@ def predict_subprocess(
700709
while proc.poll() is None:
701710
# Read line.
702711
line = proc.stdout.readline()
703-
line = line.decode().rstrip()
712+
# Decode as UTF-8 with replacement so a stray non-UTF-8 byte in
713+
# the subprocess output can't crash the reader (see #2744).
714+
line = line.decode("utf-8", errors="replace").rstrip()
704715

705716
is_json = False
706717
if line.startswith("{"):

‎tests/gui/learning/test_runners.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Tests for inference/training subprocess runners."""
2+
3+
import sys
4+
5+
from unittest.mock import MagicMock
6+
7+
from sleap.gui.learning.runners import InferenceWorker
8+
9+
10+
def test_inference_worker_reads_non_utf8_subprocess_output(qtbot, tmp_path):
11+
"""Reading inference subprocess output must not crash on a stray non-UTF-8 byte.
12+
13+
Regression test for gh discussion #2744 / sleap-nn #655: on Windows the GUI
14+
decoded the inference subprocess output using the cp1252 locale default, so a
15+
byte like ``0x90`` (e.g. from a ``rich``-rendered traceback or progress bar)
16+
raised ``UnicodeDecodeError: 'charmap' codec can't decode byte 0x90`` and
17+
masked the real error. A lone ``0x90`` is also invalid UTF-8, so it reproduces
18+
the crash on any platform when the reader does not pass ``errors="replace"``.
19+
"""
20+
# Child process emits a lone 0x90 byte (invalid UTF-8 / cp1252-undefined),
21+
# stays alive briefly so the parent loop reads + decodes it, then exits
22+
# non-zero so the success/load-slp branch is skipped.
23+
script = (
24+
"import sys, time; "
25+
"sys.stdout.buffer.write(b'progress \\x90 line\\n'); "
26+
"sys.stdout.flush(); "
27+
"time.sleep(0.5); "
28+
"sys.exit(1)"
29+
)
30+
output_path = str(tmp_path / "out.slp")
31+
32+
task = MagicMock()
33+
task.make_predict_cli_call.return_value = (
34+
[sys.executable, "-c", script],
35+
output_path,
36+
)
37+
38+
items = MagicMock()
39+
items.total_frame_count = 0
40+
41+
worker = InferenceWorker(task, items)
42+
43+
logged = []
44+
worker.logOutput.connect(logged.append)
45+
46+
# Must not raise UnicodeDecodeError while reading the subprocess output.
47+
out_path, ret = worker._run_inference_item(MagicMock(), 0, 1)
48+
49+
assert out_path == output_path
50+
assert ret == 1 # non-zero exit propagated; no decode crash
51+
# The malformed line was still captured (with the bad byte replaced).
52+
assert any("progress" in line and "line" in line for line in logged)

0 commit comments

Comments
 (0)