fix: consume the newline delimiter in open stderr logging loop - #129
fix: consume the newline delimiter in open stderr logging loop#129vcolombo wants to merge 2 commits into
Conversation
std.Io.Reader.takeDelimiterExclusive does not advance past the delimiter byte. Once the spawned open/xdg-open command wrote a newline to stderr, the loop in openThread peeked that same newline forever, returning an empty line each iteration and logging it at an unbounded rate: a spinning thread pinned near 100% CPU and tens of thousands of os_log messages per second until logd throttled the entire process. Switch to takeDelimiter, which consumes the delimiter and returns null at end of stream, and skip logging empty lines. Also wire src/os/open.zig into the os test block so its regression test runs.
📝 WalkthroughWalkthrough
ChangesStderr logging
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR fixes a high-CPU infinite busy loop in src/os/open.zig when logging stderr from open/xdg-open by ensuring newline delimiters are consumed while iterating. It also adds a regression test and ensures the open.zig tests are actually executed by zig build test via the src/os/main.zig test block.
Changes:
- Replace
takeDelimiterExclusive('\n')withtakeDelimiter('\n')and skip empty lines to prevent spinning on a buffered newline. - Extract the stderr logging loop into
logStderrLinesand add a regression test asserting termination and full consumption. - Include
open.zigin thesrc/os/main.zigtest {}block so its tests run underzig build test.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/os/open.zig | Fixes newline-delimited stderr consumption to prevent infinite logging loops; adds helper + regression test. |
| src/os/main.zig | Ensures open.zig tests are included in the OS package test aggregation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile SummaryThis PR fixes the stderr logging loop used by spawned URL-opening commands. The main changes are:
Confidence Score: 5/5This looks safe to merge for the supported desktop test targets, with a non-blocking target guard to consider.
src/os/main.zig Important Files Changed
Reviews (1): Last reviewed commit: "fix: consume the newline delimiter in op..." | Re-trigger Greptile |
| test { | ||
| _ = file; | ||
| _ = i18n; | ||
| _ = openpkg; |
There was a problem hiding this comment.
Unsupported Targets Enter Open Module
The unconditional test reference compiles open.zig for every zig build test target. When tests target an OS outside the module's Linux, FreeBSD, Windows, macOS, and iOS cases, its existing unsupported-OS compile error is reached even though opening URLs is not used, so that test build fails.
Context Used: AGENTS.md (source)
There was a problem hiding this comment.
Addressed in 7bf51a5 — the import is now gated to open.zig's supported OS set, matching this test block's existing per-OS pattern (kernel_info on Linux, mach on Darwin).
For the record on mechanism: the @compileError sits inside open()'s function body, and Zig analyzes function bodies lazily, so _ = openpkg alone wouldn't have reached it (the test only references the portable logStderrLines). The gate is defense-in-depth + style consistency rather than a required fix.
Match the existing per-OS conditional imports in this test block (kernel_info on Linux, mach on Darwin) so test targets outside open.zig's supported set never analyze the module.
Problem
openThreadinsrc/os/open.ziglogs each stderr line from the spawnedopen/xdg-opencommand usingstd.Io.Reader.takeDelimiterExclusive, which returns the line but does not advance past the delimiter byte. Once the child writes a single newline to stderr, every subsequent iteration peeks the same buffered newline, returns an empty slice, and logs it — an infinite busy loop.Observed in production (cmux 0.64.20, macOS): two leaked threads pinned at ~85% CPU each for days, emitting ~44,000
os-open: open stderr=messages/second until logd throttled the whole process (__FIREHOSE_CLIENT_THROTTLED_DUE_TO_HEAVY_LOGGING__on-stack), which in turn made unrelatedos_logcalls block and the UI jank.The same loop exists in upstream ghostty-org/ghostty (introduced in
bb375a2f, "deal with large outputs from xdg-open/rundll32/open"), so this is upstreamable.Fix
takeDelimiter, which consumes the delimiter and returnsnullat end of stream.logStderrLinesand add a regression test. On the old code the test spins forever at ~93% CPU (reproducing the production hang); on the new code it passes in seconds and asserts the stream is fully consumed.open.ziginto thesrc/os/main.zigtest block — its tests were not previously part ofzig build test.Verified with
zig build test -Dtest-filter=logStderrLineson zig 0.15.2, plus a full ReleaseFast universal GhosttyKit.xcframework build and a cmux Debug app build on top of it.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Consume the newline delimiter when reading
open/xdg-openstderr to stop an infinite loop that spiked CPU and flooded logs.takeDelimiterto consume newlines and stop on EOF.logStderrLinesand add a regression test; importopen.zigtests only on supported OSes insrc/os/main.zig.Written for commit 7bf51a5. Summary will update on new commits.
Summary by CodeRabbit
Bug Fixes
Tests