Skip to content

fix(run): start pipe drain before waitForProcess to avoid 64KB deadlock#6

Merged
poolcamacho merged 1 commit intomasterfrom
fix/run-pipe-deadlock
Apr 15, 2026
Merged

fix(run): start pipe drain before waitForProcess to avoid 64KB deadlock#6
poolcamacho merged 1 commit intomasterfrom
fix/run-pipe-deadlock

Conversation

@poolcamacho
Copy link
Copy Markdown
Owner

Closes #4.

Reported by @just-doit. Reproducible on any repo whose git log output exceeds the macOS pipe buffer (~64 KB).

Regression

When GitService.run was decomposed into helpers, the read tasks were moved inside drainAndClose, which run called only after waitForProcess returned:

let completed = await waitForProcess(process, timeout: timeout)  // <- blocks here
let (outData, errData) = await drainAndClose(...)                 // <- reads start here

While the process was alive, nothing was reading the pipes. Once the kernel pipe buffer filled (~64 KB), git blocked on write, process.isRunning stayed true, and the timeout eventually fired.

Fix

Split drainAndClose into:

  • startDraining (synchronous, returns the two Task handles) — called right after process.run().
  • awaitDrain (async, awaits the values and closes the read ends) — called after waitForProcess.

The detached read tasks now live for the whole lifetime of the child process, so the pipe buffer can't deadlock git regardless of output size.

Verification

  • xcodebuild build clean
  • swiftlint --strict clean
  • Manual reproduction: opened a repo whose git log --all exceeds 64 KB on b0a0a02, confirmed the timeout. Re-tested on this branch, loads in normal time with no timeout.

Closes #4.

Reported by @just-doit. Reproducible on any repo whose 'git log' output
exceeds the macOS pipe buffer (~64 KB).

Regression introduced when GitService.run was decomposed into helpers:
the read tasks were moved inside drainAndClose, which the run method
called only AFTER waitForProcess returned. While the process was alive
nothing was reading the pipes, so once the kernel buffer filled git
blocked on write, never exited, and the timeout fired.

Split the helper into startDraining (synchronous, returns the task
handles) and awaitDrain (async, awaits values and closes the read
ends). The run method now starts the drain immediately after
process.run() and awaits it after waitForProcess, restoring the
behaviour that was lost in the decomposition.
@poolcamacho poolcamacho merged commit b5f7a78 into master Apr 15, 2026
3 checks passed
@poolcamacho poolcamacho deleted the fix/run-pipe-deadlock branch April 15, 2026 09:34
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.

Loading a repository may timeout

1 participant