Skip to content

Fix burn subprocess leak in BurnLedger.capture#481

Merged
willwashburn merged 1 commit into
mainfrom
macos/fix-subprocess-leak
Jun 17, 2026
Merged

Fix burn subprocess leak in BurnLedger.capture#481
willwashburn merged 1 commit into
mainfrom
macos/fix-subprocess-leak

Conversation

@willwashburn

@willwashburn willwashburn commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

The macOS app's live-burn tab was leaking burn summary subprocesses — dozens accumulated (one per ~1.5s poll, none exiting), which hammered the machine. This fix landed on the #480 branch after #480 was squash-merged, so it didn't reach main; this PR brings it in.

Root cause

BurnLedger is an actor, so capture() calls are serialized — the only way burn processes pile up is if capture() returns while the child is still alive. The previous implementation used a terminationHandler + semaphore (which has a race: if the process exits before the handler is wired, the wait misbehaves) and only sent SIGTERM on timeout, which a busy burn can ignore. So each poll's subprocess got orphaned and the next spawned on top.

Fix

Rewrite capture() to read stdout to EOF + waitUntilExit on a background queue, bounded by the timeout, and SIGKILL (not just SIGTERM) on timeout. It now blocks the actor until the process truly exits or is killed, so the actor's serialization guarantees at most one burn subprocess alive at a time — no pile-up possible.

🤖 Generated with Claude Code

Review in cubic

Root cause of the runaway `burn summary` processes: capture() used a
terminationHandler+semaphore that could return while the child was still
alive (handler race), and only sent SIGTERM on timeout. Since BurnLedger is
an actor, a capture() that returns early frees the actor to spawn the next
poll's subprocess — so the live tab's 1.5s poll piled up dozens of orphaned
`burn summary` processes.

Rewrite capture() to read stdout to EOF + waitUntilExit on a background queue,
bounded by the timeout, and SIGKILL (not just SIGTERM) on timeout. This blocks
the actor until the process truly exits or is killed, so the actor's
serialization guarantees at most one `burn` subprocess alive at a time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@willwashburn willwashburn merged commit 2584f23 into main Jun 17, 2026
3 of 4 checks passed
@willwashburn willwashburn deleted the macos/fix-subprocess-leak branch June 17, 2026 23:12
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e1225f1-b0f9-4793-b293-248a127fe051

📥 Commits

Reviewing files that changed from the base of the PR and between 88fa939 and 77233d5.

📒 Files selected for processing (1)
  • apps/macos/Sources/Burn/BurnLedger.swift

📝 Walkthrough

Walkthrough

BurnLedger.capture(_:timeout:) is rewritten to use a DispatchGroup that concurrently tracks stdout-to-EOF reading and process.waitUntilExit(). On timeout, it calls terminate(), sleeps briefly, then escalates to SIGKILL if the process is still alive, returning nil.

Changes

BurnLedger capture timeout refactor

Layer / File(s) Summary
DispatchGroup-based stdout drain and process exit with SIGTERM/SIGKILL escalation
apps/macos/Sources/Burn/BurnLedger.swift
Removes the dedicated queue, terminationHandler, semaphore, and separate drain synchronization. Replaces them with a DispatchGroup that leaves when stdout reaches EOF and when waitUntilExit() returns. On group-wait timeout, sends SIGTERM, sleeps, then conditionally sends SIGKILL, and returns nil.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hoppity-hop, I tossed the semaphore away,
A DispatchGroup now keeps the chaos at bay.
SIGTERM whispers first, then SIGKILL if need be,
Stdout drains to EOF, the process runs free.
No more tangled queues — just clean concurrency! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch macos/fix-subprocess-leak

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 and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77233d588b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

process.terminate() // SIGTERM…
usleep(200_000)
if process.isRunning { // …then SIGKILL if it ignores it
kill(process.processIdentifier, SIGKILL)

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 Badge Kill the actual burn child on PATH timeouts

When resolveTool() falls back to .path, loginShell makes this Process be /bin/zsh -lc ..., so signaling process.processIdentifier here only targets the shell. If burn summary is the hung process, killing the shell can orphan the burn/node child while capture() returns and the actor accepts the next poll, so the subprocess pile-up still occurs for PATH fallback builds. Start the shell in its own process group and kill the group, or avoid the shell after resolving the executable.

Useful? React with 👍 / 👎.

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