Skip to content

Fix runaway os-open stderr logging loop - #126

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

Fix runaway os-open stderr logging loop#126
samuelpatro wants to merge 2 commits into
manaflow-ai:mainfrom
samuelpatro:fix/os-open-log-loop

Conversation

@samuelpatro

@samuelpatro samuelpatro commented Jul 18, 2026

Copy link
Copy Markdown

Summary

  • consume stderr delimiters correctly in the asynchronous open process reader
  • stop the reader at EOF so exe.wait() reaps the child process
  • add a regression test covering multiple newline-delimited records followed by EOF

Root cause

Ghostty is pinned to Zig 0.15.2. Its Reader.takeDelimiterExclusive regression leaves the delimiter buffered, so after the first stderr line the loop repeatedly returns an empty slice. Each affected open call then creates a permanent logging thread and an unreaped child process, driving both cmux and macOS logd CPU usage.

Testing

  • Added the regression test in a first commit and confirmed it fails by returning an empty second line.
  • Switched the reader to takeDelimiter; the same Zig 0.15.2 test now passes.
  • Result: 1/1 targeted tests passed.

A full local GhosttyKit build is blocked on macOS 26.5 because Zig 0.15.2 cannot link against that SDK; the isolated test was compiled with the pinned Zig toolchain to WASI and executed successfully.


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


Summary by cubic

Fixes a runaway stderr logging loop in os.open by correctly consuming newline delimiters and stopping at EOF. Prevents permanent logging threads and unreaped child processes that could spike CPU usage.

  • Bug Fixes
    • Use open_reader.takeLine (based on takeDelimiter) instead of takeDelimiterExclusive to consume newline delimiters and handle long lines.
    • Break the reader loop at EOF so exe.wait() can reap the child process.
    • Add a regression test for multiple newline-delimited stderr records followed by EOF.
    • Notes the Zig 0.15.2 takeDelimiterExclusive regression that left delimiters buffered, causing empty-line loops.

Written for commit 71ef84e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved stderr line reading during open operations.
    • Ensured streams are handled cleanly when reaching end-of-file or encountering read errors.
    • Preserved warning logs for collected stderr messages.
  • Tests
    • Added coverage for reading multiple lines and correctly handling end-of-stream.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a reusable takeLine helper for newline-delimited reads, tests EOF behavior, and updates openThread to use the helper when collecting stderr output.

Changes

Stderr line reading

Layer / File(s) Summary
Reader helper and openThread integration
src/os/open_reader.zig, src/os/open.zig
takeLine handles newline-delimited reads, EOF, read failures, and oversized streams; tests verify sequential consumption, and openThread uses the helper for stderr parsing.

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

Suggested reviewers: jcollie

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing the runaway stderr logging loop in os-open.
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.
✨ 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.

@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a runaway stderr-logging loop in openThread caused by a Zig 0.15.2 regression in takeDelimiterExclusive, which leaves the delimiter unconsumed and causes the loop to spin forever on empty slices. The fix switches to takeDelimiter (which consumes the delimiter) and extracts the reader logic into a new, separately-testable open_reader.zig module.

  • src/os/open.zig: The inline delimiter-reading loop is replaced with a single call to open_reader.takeLine; EOF and read-failure handling is delegated to the helper, keeping openThread clean.
  • src/os/open_reader.zig: New helper that wraps takeDelimiter, falls back to a fixed-size take on StreamTooLong, and includes a regression test confirming both delimiter consumption and correct EOF detection.

Confidence Score: 4/5

Safe to merge — the core loop fix is correct and the child process is properly reaped via exe.wait().

The delimiter switch from takeDelimiterExclusive to takeDelimiter is the right fix for Zig 0.15.2 and the regression test directly covers the previously broken path. The only open question is whether a final stderr line lacking a trailing newline is silently dropped at EOF, which the new test does not exercise.

src/os/open_reader.zig — the takeLine EOF-without-delimiter path deserves an additional test case.

Important Files Changed

Filename Overview
src/os/open.zig Replaces the inline takeDelimiterExclusive loop with a call to open_reader.takeLine; null/error handling is clean and the child-process reap via exe.wait() is preserved.
src/os/open_reader.zig New helper module extracting the stderr line-reading logic; switches to takeDelimiter to consume delimiters correctly, but the test only covers newline-terminated input — content that reaches EOF without a trailing newline may be silently dropped.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([openThread starts]) --> B{exe.stderr piped?}
    B -- No --> W[exe.wait]
    B -- Yes --> C[readerStreaming with 256-byte buffer]
    C --> D[loop: call takeLine]
    D --> E{takeDelimiter result}
    E -- "null (EOF)" --> W
    E -- "error.ReadFailed" --> W
    E -- "[]u8 line" --> F[log.warn open stderr=line]
    F --> D
    E -- "error.StreamTooLong" --> G[take chunk_size bytes]
    G -- "error.ReadFailed" --> W
    G -- "error.EndOfStream" --> W
    G -- "[]u8 chunk" --> F
    W([exe.wait reaps child]) --> Z([thread exits])
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A([openThread starts]) --> B{exe.stderr piped?}
    B -- No --> W[exe.wait]
    B -- Yes --> C[readerStreaming with 256-byte buffer]
    C --> D[loop: call takeLine]
    D --> E{takeDelimiter result}
    E -- "null (EOF)" --> W
    E -- "error.ReadFailed" --> W
    E -- "[]u8 line" --> F[log.warn open stderr=line]
    F --> D
    E -- "error.StreamTooLong" --> G[take chunk_size bytes]
    G -- "error.ReadFailed" --> W
    G -- "error.EndOfStream" --> W
    G -- "[]u8 chunk" --> F
    W([exe.wait reaps child]) --> Z([thread exits])
Loading

Reviews (1): Last reviewed commit: "fix: stop open stderr reader at EOF" | Re-trigger Greptile

Comment thread src/os/open_reader.zig
Comment on lines +4 to +10
return reader.takeDelimiter('\n') catch |outer| switch (outer) {
error.ReadFailed => error.ReadFailed,
error.StreamTooLong => reader.take(chunk_size) catch |inner| switch (inner) {
error.ReadFailed => error.ReadFailed,
error.EndOfStream => null,
},
};

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 Last line without trailing newline silently dropped

If the child process writes a final stderr message that doesn't end with \n (e.g. the process exits mid-line or writes "error: bad url" without a newline), takeDelimiter('\n') will reach EOF before finding the delimiter. In Zig's std.Io.Reader, hitting EOF without a delimiter causes takeDelimiter to return null rather than the partial bytes it already read, so that last message is never logged. The same scenario in the StreamTooLong branch returns null via the EndOfStream case, also silently discarding content. The existing test only exercises fully-terminated lines; adding a case like "first\nno-newline-at-end" would clarify actual behavior and surface any silent data loss.

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/os/open.zig (1)

81-82: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Close stderr to prevent file descriptor leaks.

The .Pipe file descriptors are not closed automatically by exe.wait(). Ensure stderr is explicitly closed when the thread completes reading to avoid exhausting file descriptors.

🔧 Proposed fix to close the pipe
     if (exe.stderr) |stderr| {
+        defer stderr.close();
         var buffer: [256]u8 = undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/os/open.zig` around lines 81 - 82, Update the stderr-reading branch
around exe.stderr to explicitly close the pipe after the reader thread finishes
consuming it, ensuring the descriptor is released while preserving the existing
stderr capture behavior.
🧹 Nitpick comments (2)
src/os/open.zig (1)

72-73: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle thread spawn failure to prevent leaks.

If std.Thread.spawn fails, the child process is never reaped, leaving a zombie process and leaking the stderr pipe. Consider handling the error by killing and reaping the process before returning.

🛠️ Proposed refactor for error handling
-    const thread = try std.Thread.spawn(.{}, openThread, .{exe});
+    const thread = std.Thread.spawn(.{}, openThread, .{exe}) catch |err| {
+        if (exe.stderr) |stderr| stderr.close();
+        _ = exe.kill() catch {};
+        _ = exe.wait() catch {};
+        return err;
+    };
     thread.detach();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/os/open.zig` around lines 72 - 73, Handle errors from std.Thread.spawn in
the openThread launch path by terminating and reaping the child process and
closing the stderr pipe before propagating the failure. Preserve the existing
detached-thread behavior when spawning succeeds, and reuse the process and pipe
cleanup mechanisms already available in the surrounding open logic.
src/os/open_reader.zig (1)

13-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add test coverage for the fallback path.

Consider adding a regression test that exercises the error.StreamTooLong fallback logic by reading a line longer than the chunk size (e.g., using a small buffer). This ensures the fallback chunking logic remains correct during future refactors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/os/open_reader.zig` around lines 13 - 19, Add regression coverage
alongside the existing “open stderr reader consumes delimiters and reaches EOF”
test that forces takeLine to hit error.StreamTooLong by using a line longer than
the supplied small chunk size. Verify the fallback path returns the complete
line, then confirm subsequent reads still consume delimiters and reach EOF
correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/os/open.zig`:
- Around line 81-82: Update the stderr-reading branch around exe.stderr to
explicitly close the pipe after the reader thread finishes consuming it,
ensuring the descriptor is released while preserving the existing stderr capture
behavior.

---

Nitpick comments:
In `@src/os/open_reader.zig`:
- Around line 13-19: Add regression coverage alongside the existing “open stderr
reader consumes delimiters and reaches EOF” test that forces takeLine to hit
error.StreamTooLong by using a line longer than the supplied small chunk size.
Verify the fallback path returns the complete line, then confirm subsequent
reads still consume delimiters and reach EOF correctly.

In `@src/os/open.zig`:
- Around line 72-73: Handle errors from std.Thread.spawn in the openThread
launch path by terminating and reaping the child process and closing the stderr
pipe before propagating the failure. Preserve the existing detached-thread
behavior when spawning succeeds, and reuse the process and pipe cleanup
mechanisms already available in the surrounding open logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7bf3245-a7e0-4674-990e-0dcf71b9ad01

📥 Commits

Reviewing files that changed from the base of the PR and between bb30526 and 71ef84e.

📒 Files selected for processing (2)
  • src/os/open.zig
  • src/os/open_reader.zig

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.

1 participant