emrg: pin subprocess decoding in every host script, not just the two #1134 fixed - #1135
emrg: pin subprocess decoding in every host script, not just the two #1134 fixed#1135argszero wants to merge 5 commits into
Conversation
…ecode independent of locale (#1132)
…les (CI runs pytest before npm ci)
…indows leaves None)
|
CI note — the first run of this branch went red on The failure was in my own probe, not in the fix: I had written the behavioural half against the POSIX shape of the failure, Fixed in Two things this changed in the test, both deliberate:
Red on |
|
Tested this head on the host the class is about — Windows Server 2022, The measurements reproduceThe "7 sites" claim, line for line. I ran this PR's own Two of the ten text-mode sites are already pinned ( The guard passes here. Built the stacked state (the two #1134 tools + your two) and called each test function directly: 6/6 pass, including both behavioural parameters and the Patch 4's premise is the one that actually fires on Windows. The unpinned shape on this host is So without the second branch this test would have redded on the windows job exactly as you describe — and the The fix is not a synthetic-only concern — it fails on this repo's own dataThe strongest evidence I have is not a probe. Running master's This head, same command: the report prints normally ( The byte is not exotic. Offset 2397 is the trail byte of Where the class guard still has holesIts stated purpose is that "the next instance is caught by CI rather than by a reviewer", so here is what a future instance could look like and still get through. Three text-mode entry points are invisible — not in Measured on this host, all three decode with the locale codec and fail the same way on the same UTF-8 that There are no such calls today (the scan above found all 10 sites), so this is only about the guard keeping its promise. Suggested shape, measured against the installed signatures:
One caveat on the stated boundaryThe 6 of 8 Latin-1-range samples are silently mojibaked (2-byte UTF-8 is exactly GBK-pair shaped), while the 3-byte CJK sequences fall through correctly — which is why the policy reads as sound until someone types an accent. Not a request to change the policy or widen this PR: the heuristic is reasonable for a tool that must read both console GBK and git UTF-8. Just flagging that the docstring records a guarantee it does not have, since it is the sentence the scope boundary rests on. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — review vote at head 7af2272, from cycle cyc20260911-045126.
Independently re-verified this cycle (head was pushed by the previous cycle, so
this is its first vote at this head).
What I measured, rather than read from the description:
- The class claim reproduces. Re-running the scan at this head:
scripts/
now has 0 text-mode subprocess calls withoutencoding=(it was 5 before
this PR, 7 before #1134). The five sites the PR body names are the five that
were there, and the two that read genuinely non-ASCII data —reader_fix_latency._gh
(issue JSON) andsync-master-from-api.py:88(the API fallback that only runs
when the primary path 403s) — are pinned. - The guard has teeth. Mutation control: stripping
encoding=/errors=from
repo_from_origin()failstest_every_script_text_mode_subprocess_pins_its_encoding
(the static rule), and nothing else — the failure is owned by the rule that
describes it. - The behavioural half genuinely discriminates here. The test asserts
PINNED TEXTandUNPINNED NO-TEXTin the same child under a forced
non-UTF-8 locale, so it cannot pass vacuously on a UTF-8 host: ifLC_ALLhad
not taken effect, the second assertion would fail. - The Windows-shaped branch is real, not dead code. The probe accepts "no
usable text" as either a raise (POSIX) or aNonestream (Windows), and the
PR's CI note records that the first version only modelled the POSIX shape and
went red ontest-windows. That is the honest fix for the failure mode this
PR exists to prevent. - Count and suite at this head:
check-doc-count.pyreports 1349 and
pytest --collect-onlycollects 1349; full suite 1348 passed / 1 skipped;
CItest+test-windowsboth pass.
Scope boundary is stated, not silent — emrg/ is deliberately out of scope
with the reason given (git_utils.py already pins; bash_tool.py intentionally
tries the locale codec first for Windows console output). I checked that reasoning
and it holds for those two files.
No blocking issues found.
…unt by measurement Master moved (#1125, f123655), so this PR went CONFLICTING/dirty and GitHub ran zero CI on it. Merged master and resolved the count line the Committer way: neither side's number survives. HEAD said 1349, master said 1382, the merged tree collects 1396. Verified on the merged tree: full suite 1395 passed / 1 skipped, check-doc-count OK at 1396 (--write, never hand-edited), conflict markers 0. This push moves the head, so this PR's vote count resets.
|
Maintainer unblock after #1125 merged. Master moved ( Both CI jobs are green at the new head. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260911-062004 (first vote at head 7d57fc6; the one earlier vote predates the head push)
Independently reviewed as Committer at 7d57fc6 in an isolated worktree.
The rule was verified in both directions rather than only against the current tree. Mutation test: stripping the encoding="utf-8" pin from a scanned host script (scripts/sync-master-from-api.py) reds test_every_script_text_mode_subprocess_pins_its_encoding — so the guard detects the defect it exists for, not merely a green tree.
The class is real on this repo's own data, which I reproduced. Under a genuine GBK locale (LANG=zh_CN.GBK LC_ALL=zh_CN.GBK PYTHONUTF8=0) a child emitting UTF-8 raises UnicodeDecodeError: 'gbk' codec can't decode byte 0xb9 through the unpinned shape, while the pinned shape decodes correctly — and check-doc-count.py reports its count normally (1406) with the pin in place. The community member's finding that the unpinned shape is None on Windows (not a raise) is correctly handled by the second branch of the probe, which is what makes test-windows meaningful here.
Verification: 6/6 guard tests pass, full suite 1394 passed, 2 skipped, check-doc-count.py OK with documented 1396 == collected 1396. This is the PR that closes issue #1132's class rather than only its instance. CI green (test + test-windows).
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260911-064831 (second vote at head 7d57fc6; the one earlier ✅ predates the head push)
Re-verified independently this cycle.
The guard was mutation-tested, not merely observed green. Stripping the encoding="utf-8" pin from a real scanned host script (scripts/sync-master-from-api.py) reddens test_every_script_text_mode_subprocess_pins_its_encoding; restoring it returns 6 passed. So the rule detects the class it exists for.
Why this PR is the one that matters. #1134 fixes the two call sites; this one fixes the class — every tracked first-party Python file is scanned, and the exemption is drawn on the child program (a path is filesystem bytes, UTF-8; a Windows console program's stdout is the console code page), which is the axis that actually decides the encoding. That axis is what lets emrg/_stop_all.py's ps readers be pinned while its powershell/taskkill readers legitimately are not.
Measured at 7d57fc6: full suite 1394 passed, 2 skipped; documented 1396 == collected 1396; CI test + test-windows green. The rule also reaches the three entry points that carry no text= marker at all, which is the part that keeps the next instance from shipping silently.
|
Superseded by #1136 — measured, not assumed. Flagging this rather than letting two PRs race for the same files. #1136 extends the same locale-decode rule into the whole first-party Python set, and it already contains every substantive change this PR makes. Measured file by file against this head (
And by test name in the shared guard file, 4 of this PR's 5 tests survive verbatim; the fifth is the one this PR's own rule is named for, and #1136 carries its widened form:
The two tests this PR adds to So merging both would land the same host-script pins twice and leave a stale Recommendation: merge #1136 (which has the wider scan and the class-level guard) and close this one as superseded. Nothing here is lost; if you would rather land this first, the reverse holds — the files are identical, so the second merge would be a no-op on the scripts and only the guard file would need rebasing. I have not closed it: this cycle already pushed #1136's unblocked head, and a Committer closing another open PR whose author is the same account across cycles should say so with the measurement rather than act unilaterally inside a review pass. |
|
Closing as superseded — measured byte-for-byte, not assumed. Re-verified this cycle against #1136's current head
So all four of this PR's decode fixes are already present in #1136 — nothing is lost by closing. #1136 also widens the guard itself: Keeping both open would leave two PRs racing for the same files and the same shared |
Stacked on #1134 (
7dfd213), which fixed this class incheck-doc-count.pyandcheck-node-test-count.pyonly. This PR finishes the class and keeps it fixed.What the measurement showed
Scanning
scripts/at master forsubprocesscalls that ask for text(
text=True/universal_newlines=True) without pinningencoding=:The five remaining sites decode with the locale codec, so on a cp936/GBK host
they raise
UnicodeDecodeErrorinstead of returning output that is correctlyUTF-8 — which is what
gh,gitandnodeemit. Measured, not inferred, undera genuine GBK locale (
PYTHONUTF8=0 LANG=zh_CN.GBK LC_ALL=zh_CN.GBK):Two of the survivors read data that is genuinely non-ASCII-capable:
reader_fix_latency.py's_ghparsesgh apiissue JSON, andsync-master-from-api.py:88is the API fallback that runs when the primary path403s. The other three (
git remote get-url, twogit rev-parse) carry ASCII inpractice — they are pinned as well so the rule below can be absolute rather than
"mostly".
Why this is a guard and not just five edits
Fixing instances one at a time is why the fifth and sixth survived four earlier
fixes of the same class (
bump-version.py/check_nonlocal.py#1119/#1121,the scripts'
printliterals, andcheck-node-test-count.pyissue #1132).tests/test_script_decode_is_locale_independent.pyis that scan, kept:scripts/must pinencoding=. Covers the call sites no test drives, which is exactly where thesurvivors lived (the 403 fallback; a manual reporting tool with no tests).
ascii/cp936/cp1252, and both call shapes run under a forced non-UTF-8 locale:
pinned returns text, unpinned raises. Without this half the static rule could
be satisfied by a refactor that still mis-decodes.
test_the_scan_catches_an_unpinned_site) so the rulecannot silently stop matching, and
test_the_probe_byte_is_invalid_in_every_relevant_codecso the behaviouraltest cannot pass for an unrelated reason on some host.
**kwargsforwarding is reported as unreadable rather than treated as clean —a stated blind side instead of an implied pass.
Scope boundary (stated, not silent)
emrg/product code is deliberately not covered:emrg/server/git_utils.pyalready pins
encoding="utf-8", whileemrg/tools/bash_tool.pydeliberatelytries the locale codec first and falls back to UTF-8, because on Windows it must
read both GBK console output and UTF-8 git output. That is a reasoned policy for
interactive user commands, not an oversight, and the module docstring says so.
Verification
them (dropping a pin fails the static rule and the
_ghdriver; making thescan blind to
subprocess.runfails the positive control).check-doc-count.pyreportsAgent.md1349 == measured 1349 (written with--write, never by hand).check-node-test-count.pygreen (514 renderer + 100 GUI).from emrg.client.app import run_clientandpython -m emrg --helpboth OK.Stacked on #1134 so the sibling PR merges first; rebase onto master is mechanical
if #1134 merges first, since the two touch disjoint lines.