fix(update): protect dashboard wmic scan against UnicodeDecodeError on Windows non-UTF-8 locales (#17049)#17123
Conversation
…n Windows non-UTF-8 locales (NousResearch#17049) `hermes update` calls `_warn_stale_dashboard_processes()` to warn about dashboard processes still running the pre-update Python backend. On Windows, that scan shells out to `wmic process get ProcessId,CommandLine /FORMAT:LIST` with `text=True` and no explicit encoding. `wmic` emits text in the system code page (e.g. cp936 on zh-CN locales), not UTF-8. Without an explicit `encoding=`, Python's default UTF-8 decoder crashes the subprocess reader thread with `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd0 ...`. In Python 3.11 that crash is silently absorbed: `subprocess.run()` returns a `CompletedProcess` with `result.stdout = None`, the next line calls `result.stdout.split("\n")`, and `hermes update` aborts with the exact `AttributeError: 'NoneType' object has no attribute 'split'` trace reported in NousResearch#17049. Fix: pass `encoding="utf-8", errors="ignore"` so undecodable bytes cannot take down the reader thread (the parsing only matches the ASCII prefixes `CommandLine=` and `ProcessId=`, so dropping non-UTF-8 bytes is safe), and short-circuit when `result.stdout is None` as a defensive guard for environments where the reader thread still fails for other reasons. This is the same root cause as NousResearch#17074 (which patches `hermes_cli/gateway._scan_gateway_pids` for the `hermes setup` path). That PR does not touch `_warn_stale_dashboard_processes`, so `hermes update` remains broken on the same locales until this lands. Regression test in `tests/hermes_cli/test_update_stale_dashboard.py`: - `test_wmic_invoked_with_utf8_ignore_errors` asserts the explicit encoding/errors kwargs reach `subprocess.run`. - `test_wmic_returns_none_stdout_does_not_crash` simulates the reader-thread-crashed `result.stdout=None` aftermath and asserts the function returns silently instead of raising AttributeError. Both new tests fail against clean origin/main (7d46484) reproducing the original AttributeError; both pass with this patch. The remaining 3 failures in `tests/hermes_cli/test_cmd_update.py` and `test_update_autostash.py` are pre-existing baselines on origin/main — they reproduce identically without this change and are unrelated to the wmic scan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes a Windows-specific crash in hermes update when scanning for stale dashboard processes via wmic, where non‑UTF‑8 output could trigger a UnicodeDecodeError and cascade into stdout=None → AttributeError.
Changes:
- Make the Windows
wmicsubprocess call decode deterministically withencoding="utf-8", errors="ignore"and defensively return whenstdout is None. - Add regression tests covering the
encoding/errorsinvocation and thestdout=Nonescenario.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
hermes_cli/main.py |
Hardens the Windows wmic branch in _warn_stale_dashboard_processes() to avoid decode-thread crashes and None stdout handling. |
tests/hermes_cli/test_update_stale_dashboard.py |
Adds Windows-locale regression tests ensuring wmic is invoked with the right decoding settings and that stdout=None does not crash. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # wmic emits text in the system code page (e.g. cp936 on zh-CN | ||
| # locales), not UTF-8. Without an explicit encoding, Python's | ||
| # default UTF-8 decoder crashes the subprocess reader thread | ||
| # with UnicodeDecodeError, leaving result.stdout=None and the | ||
| # subsequent .split() call crashing `hermes update` with an | ||
| # AttributeError on Windows non-UTF-8 locales (#17049). |
There was a problem hiding this comment.
The explanatory comment is a bit misleading: the failure in #17049 happens when the subprocess output is decoded as UTF-8 (e.g., Python UTF-8 mode), and the key fix here is errors="ignore" (not merely “explicit encoding”). Consider rewording to reflect that the default encoding may be locale-dependent and that errors is what prevents the reader-thread UnicodeDecodeError / stdout=None cascade.
| # wmic emits text in the system code page (e.g. cp936 on zh-CN | |
| # locales), not UTF-8. Without an explicit encoding, Python's | |
| # default UTF-8 decoder crashes the subprocess reader thread | |
| # with UnicodeDecodeError, leaving result.stdout=None and the | |
| # subsequent .split() call crashing `hermes update` with an | |
| # AttributeError on Windows non-UTF-8 locales (#17049). | |
| # wmic may emit text in the system code page (for example cp936 | |
| # on zh-CN systems), not UTF-8. In text mode, subprocess output | |
| # decoding depends on Python's configuration (locale-dependent by | |
| # default, or UTF-8 in UTF-8 mode). The important protection here | |
| # is errors="ignore": it prevents a reader-thread | |
| # UnicodeDecodeError from leaving result.stdout=None and turning | |
| # the later .split() into an AttributeError (#17049). |
…ys.platform Two fix-ups for NousResearch#17123: 1. Reword the inline comment in `_warn_stale_dashboard_processes` to accurately describe the failure mode (locale-dependent decoder, not a "default UTF-8 decoder") and identify `errors="ignore"` as the load-bearing protection. Per Copilot's review. 2. Switch `TestWindowsWmicEncoding` from `patch("hermes_cli.main.sys")` to `monkeypatch.setattr(sys, "platform", "win32")` — the codebase's canonical pattern (e.g. `tests/hermes_cli/test_auth_ssl_macos.py`). The MagicMock-replacement approach passed locally on Python 3.12 but the platform-equality check failed under CI's xdist+Python 3.11, leaving both new tests red despite the fix being present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@copilot Finding addressed in commit Finding (line 5239 — explanatory comment is misleading): Reworded the inline comment to accurately describe the failure mode. Your reading is correct — the failure isn't "Python's default UTF-8 decoder crashes" (the default is locale-dependent, not always UTF-8); it's that whatever encoding subprocess.PIPE selects at the reader-thread level cannot decode wmic's locale-emitted bytes, and Drive-by: Also fixed the new |
|
CI audit — all 44 The two failures that look dashboard-related are also baselines:
The Nix failures ( Happy to put up a separate one-line fix for the autostash fixture if useful — adding |
…ys.platform Two fix-ups for #17123: 1. Reword the inline comment in `_warn_stale_dashboard_processes` to accurately describe the failure mode (locale-dependent decoder, not a "default UTF-8 decoder") and identify `errors="ignore"` as the load-bearing protection. Per Copilot's review. 2. Switch `TestWindowsWmicEncoding` from `patch("hermes_cli.main.sys")` to `monkeypatch.setattr(sys, "platform", "win32")` — the codebase's canonical pattern (e.g. `tests/hermes_cli/test_auth_ssl_macos.py`). The MagicMock-replacement approach passed locally on Python 3.12 but the platform-equality check failed under CI's xdist+Python 3.11, leaving both new tests red despite the fix being present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Merged via #17435 (#17435) alongside @Kailigithub's companion fix for the sibling wmic call site in gateway.py. Both your commits were cherry-picked with your authorship preserved (rebase-merge), including the regression tests. Thanks! |
…ys.platform Two fix-ups for NousResearch#17123: 1. Reword the inline comment in `_warn_stale_dashboard_processes` to accurately describe the failure mode (locale-dependent decoder, not a "default UTF-8 decoder") and identify `errors="ignore"` as the load-bearing protection. Per Copilot's review. 2. Switch `TestWindowsWmicEncoding` from `patch("hermes_cli.main.sys")` to `monkeypatch.setattr(sys, "platform", "win32")` — the codebase's canonical pattern (e.g. `tests/hermes_cli/test_auth_ssl_macos.py`). The MagicMock-replacement approach passed locally on Python 3.12 but the platform-equality check failed under CI's xdist+Python 3.11, leaving both new tests red despite the fix being present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ys.platform Two fix-ups for NousResearch#17123: 1. Reword the inline comment in `_warn_stale_dashboard_processes` to accurately describe the failure mode (locale-dependent decoder, not a "default UTF-8 decoder") and identify `errors="ignore"` as the load-bearing protection. Per Copilot's review. 2. Switch `TestWindowsWmicEncoding` from `patch("hermes_cli.main.sys")` to `monkeypatch.setattr(sys, "platform", "win32")` — the codebase's canonical pattern (e.g. `tests/hermes_cli/test_auth_ssl_macos.py`). The MagicMock-replacement approach passed locally on Python 3.12 but the platform-equality check failed under CI's xdist+Python 3.11, leaving both new tests red despite the fix being present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ys.platform Two fix-ups for NousResearch#17123: 1. Reword the inline comment in `_warn_stale_dashboard_processes` to accurately describe the failure mode (locale-dependent decoder, not a "default UTF-8 decoder") and identify `errors="ignore"` as the load-bearing protection. Per Copilot's review. 2. Switch `TestWindowsWmicEncoding` from `patch("hermes_cli.main.sys")` to `monkeypatch.setattr(sys, "platform", "win32")` — the codebase's canonical pattern (e.g. `tests/hermes_cli/test_auth_ssl_macos.py`). The MagicMock-replacement approach passed locally on Python 3.12 but the platform-equality check failed under CI's xdist+Python 3.11, leaving both new tests red despite the fix being present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ys.platform Two fix-ups for NousResearch#17123: 1. Reword the inline comment in `_warn_stale_dashboard_processes` to accurately describe the failure mode (locale-dependent decoder, not a "default UTF-8 decoder") and identify `errors="ignore"` as the load-bearing protection. Per Copilot's review. 2. Switch `TestWindowsWmicEncoding` from `patch("hermes_cli.main.sys")` to `monkeypatch.setattr(sys, "platform", "win32")` — the codebase's canonical pattern (e.g. `tests/hermes_cli/test_auth_ssl_macos.py`). The MagicMock-replacement approach passed locally on Python 3.12 but the platform-equality check failed under CI's xdist+Python 3.11, leaving both new tests red despite the fix being present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ys.platform Two fix-ups for NousResearch#17123: 1. Reword the inline comment in `_warn_stale_dashboard_processes` to accurately describe the failure mode (locale-dependent decoder, not a "default UTF-8 decoder") and identify `errors="ignore"` as the load-bearing protection. Per Copilot's review. 2. Switch `TestWindowsWmicEncoding` from `patch("hermes_cli.main.sys")` to `monkeypatch.setattr(sys, "platform", "win32")` — the codebase's canonical pattern (e.g. `tests/hermes_cli/test_auth_ssl_macos.py`). The MagicMock-replacement approach passed locally on Python 3.12 but the platform-equality check failed under CI's xdist+Python 3.11, leaving both new tests red despite the fix being present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ys.platform Two fix-ups for NousResearch#17123: 1. Reword the inline comment in `_warn_stale_dashboard_processes` to accurately describe the failure mode (locale-dependent decoder, not a "default UTF-8 decoder") and identify `errors="ignore"` as the load-bearing protection. Per Copilot's review. 2. Switch `TestWindowsWmicEncoding` from `patch("hermes_cli.main.sys")` to `monkeypatch.setattr(sys, "platform", "win32")` — the codebase's canonical pattern (e.g. `tests/hermes_cli/test_auth_ssl_macos.py`). The MagicMock-replacement approach passed locally on Python 3.12 but the platform-equality check failed under CI's xdist+Python 3.11, leaving both new tests red despite the fix being present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
hermes update's_warn_stale_dashboard_processes()shells out towmicwithtext=Trueand noencoding=, so on Windows non-UTF-8 locales (e.g. cp936) the subprocess reader thread crashes withUnicodeDecodeError, leavesresult.stdout = None, andhermes updateaborts with the exactAttributeError: 'NoneType' object has no attribute 'split'reported in [Bug]: UnicodeDecodeError and AttributeError in Windows gateway process scanning #17049.encoding="utf-8", errors="ignore"(matching the ASCII-only key parsing the function does), and add aresult.stdout is Nonedefensive guard.The bug
The reported #17049 stack trace lists two errors stacked:
The same buggy pattern exists in two places:
hermes_cli/gateway.py:279_scan_gateway_pidshermes setuphermes_cli/main.py:5239_warn_stale_dashboard_processeshermes update#17074 patches the gateway one. This PR patches the second one. Until both land, Chinese-locale (and other non-UTF-8) Windows users remain unable to run
hermes update— they hit the AttributeError before the upgrade can warn-or-skip its way to completion.The fix
hermes_cli/main.py:errors="ignore"is safe here because the parser only matches the literal ASCII prefixesCommandLine=andProcessId=. Dropping undecodable bytes can never produce a false positive PID match — and it cannot regress the existing tests, which all use plain ASCII fixtures.is Noneguard is defensive cover for any future / older Python where the reader thread still fails for some other reason.Test plan
tests/hermes_cli/test_update_stale_dashboard.py::TestWindowsWmicEncoding):test_wmic_invoked_with_utf8_ignore_errors— assertssubprocess.runis called withencoding="utf-8", errors="ignore".test_wmic_returns_none_stdout_does_not_crash— simulates the post-decode-crashresult.stdout=Nonestate and asserts no AttributeError.origin/main(7d46484) withAttributeError: 'NoneType' object has no attribute 'split'(the exact error from [Bug]: UnicodeDecodeError and AttributeError in Windows gateway process scanning #17049), proving the regression guard._warn_stale_dashboard_processestests still pass.tests/hermes_cli/test_cmd_update.py,test_update_autostash.py,test_update_check.py,test_update_gateway_restart.py,test_update_hangup_protection.py,test_update_config_clears_custom_fields.py— 93 pass; the 3 unrelated failures (test_update_refreshes_repo_and_tui_node_dependencies,test_cmd_update_succeeds_with_extras,test_cmd_update_retries_optional_extras_individually_when_all_fails) reproduce identically on clean origin/main and do not touch the wmic scan.Related / Positioning
hermes_cli/gateway.py::_scan_gateway_pidshermes setupcallsite; gateway PID detection.hermes_cli/main.py::_warn_stale_dashboard_processeshermes updatecallsite; dashboard staleness warning. Adds a Windows-locale regression test (the existing test file only covered the macOS/Linuxpsbranch).Same root cause, two callsites — these are intentionally separate, complementary PRs. There is no overlap in touched lines.
Related