Skip to content

fix: consume the newline delimiter in open stderr logging loop - #129

Open
vcolombo wants to merge 2 commits into
manaflow-ai:mainfrom
vcolombo:fix/os-open-log-spin
Open

fix: consume the newline delimiter in open stderr logging loop#129
vcolombo wants to merge 2 commits into
manaflow-ai:mainfrom
vcolombo:fix/os-open-log-spin

Conversation

@vcolombo

@vcolombo vcolombo commented Jul 22, 2026

Copy link
Copy Markdown

Problem

openThread in src/os/open.zig logs each stderr line from the spawned open/xdg-open command using std.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 unrelated os_log calls 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

  • Switch to takeDelimiter, which consumes the delimiter and returns null at end of stream.
  • Skip logging empty lines.
  • Extract the loop into logStderrLines and 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.
  • Wire open.zig into the src/os/main.zig test block — its tests were not previously part of zig build test.

Verified with zig build test -Dtest-filter=logStderrLines on zig 0.15.2, plus a full ReleaseFast universal GhosttyKit.xcframework build and a cmux Debug app build on top of it.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Consume the newline delimiter when reading open/xdg-open stderr to stop an infinite loop that spiked CPU and flooded logs.

  • Bug Fixes
    • Switch to takeDelimiter to consume newlines and stop on EOF.
    • Skip empty lines; chunk long lines to the buffer size.
    • Extract logStderrLines and add a regression test; import open.zig tests only on supported OSes in src/os/main.zig.

Written for commit 7bf51a5. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved process output handling so consecutive blank lines no longer cause logging to stall.
    • Ensured error output is fully consumed and handled reliably, including oversized or interrupted streams.
  • Tests

    • Added regression coverage verifying that delimited error output completes successfully without leaving unread data.

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.
Copilot AI review requested due to automatic review settings July 22, 2026 16:49
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

openThread now delegates stderr processing to logStderrLines, which consumes delimiters safely, handles read failures and oversized streams, and is covered by a regression test. The open module is also referenced in main.zig’s test block.

Changes

Stderr logging

Layer / File(s) Summary
Stderr consumption and validation
src/os/open.zig, src/os/main.zig
openThread uses logStderrLines for stderr output; the helper handles newline-delimited reads, failures, and oversized input, with regression coverage for consecutive newlines and compile-time module usage.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: copilot, jcollie

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the core fix: consuming newline delimiters in the open stderr logging loop.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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') with takeDelimiter('\n') and skip empty lines to prevent spinning on a buffered newline.
  • Extract the stderr logging loop into logStderrLines and add a regression test asserting termination and full consumption.
  • Include open.zig in the src/os/main.zig test {} block so its tests run under zig 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-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes the stderr logging loop used by spawned URL-opening commands. The main changes are:

  • Consume newline delimiters while reading stderr.
  • Skip empty stderr lines and retain bounded handling for long lines.
  • Add a stream-consumption test to the OS test surface.

Confidence Score: 5/5

This looks safe to merge for the supported desktop test targets, with a non-blocking target guard to consider.

  • The production loop now consumes delimiters and terminates at end of stream.
  • Test builds for unsupported OS targets can fail when the new module reference reaches its compile-time OS check.

src/os/main.zig

Important Files Changed

Filename Overview
src/os/open.zig Extracts stderr line logging, consumes delimiters correctly, and adds a focused termination test.
src/os/main.zig Adds the open module to all OS test builds, including targets that the module rejects at compile time.

Reviews (1): Last reviewed commit: "fix: consume the newline delimiter in op..." | Re-trigger Greptile

Comment thread src/os/main.zig Outdated
test {
_ = file;
_ = i18n;
_ = openpkg;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants