Skip to content

fix: non-blocking ShellExecutor — deadlock-safe, with tests#11

Merged
SeanMcTex merged 2 commits into
mainfrom
fix/shell-executor-deadlock
May 11, 2026
Merged

fix: non-blocking ShellExecutor — deadlock-safe, with tests#11
SeanMcTex merged 2 commits into
mainfrom
fix/shell-executor-deadlock

Conversation

@SeanMcTex
Copy link
Copy Markdown
Owner

Summary

Ports the ShellExecutor rewrite from PR #8 (credit: @zhengbuqian) with additional fixes and test coverage identified during review.

What the original rewrite fixed (from #8):

  • waitUntilExit() blocked Swift's cooperative thread pool → replaced with terminationHandler + withCheckedThrowingContinuation
  • Pipe buffer deadlock on output >64KB → readabilityHandler streams data incrementally via OSAllocatedUnfairLock<Data>
  • actor ShellExecutor serialized concurrent task-group calls → converted to final class for true parallelism

Additional fixes in this PR:

  • Timeout Task was never cancelled after normal process exit — leaked for the full timeout duration (30s by default). Fixed via OSAllocatedUnfairLock<Task<Void, Never>?> so terminationHandler cancels it immediately.
  • networkErrorPatterns was duplicated between execute() and checkGHAuthenticated(), and "failed to connect" was missing from the latter copy. Extracted to a single static let.
  • Pre-existing Swift 6 build errors in PRMonitorViewModelTests: four test structs were calling @MainActor-inferred types from non-isolated contexts. Added @MainActor to ParsePRURLTests, OtherPRsServiceTests, PRTypeDisplayTitleTests, and CustomNamesServiceTests.

Test plan

  • New ShellExecutorTests suite: 6 parseHosts unit tests + 1 timeout integration test (sleep 10 with 0.1s timeout asserts executionFailed("…timed out…"))
  • Full test suite: 95 tests, 0 failures
  • Swift 6 strict concurrency: 0 errors, 0 warnings

Attribution

Core ShellExecutor rewrite originally authored by @zhengbuqian in #8.

🤖 Generated with Claude Code

SeanMcTex and others added 2 commits May 11, 2026 09:09
…implementation

Three bugs fixed:

1. `waitUntilExit()` blocked Swift's cooperative thread pool — replaced
   with `withCheckedThrowingContinuation` + `terminationHandler` so no
   thread is held while waiting for the subprocess.

2. Timeout error was silently swallowed — the throw inside the old timeout
   `Task` never escaped to the caller. Now both the termination handler and
   the timeout race via `OSAllocatedUnfairLock<Bool>`; first one wins and
   resumes the continuation correctly.

3. Pipe buffer deadlock — `readDataToEndOfFile()` after `waitUntilExit()`
   deadlocks if output exceeds ~64 KB. Fixed with `readabilityHandler`
   draining into `OSAllocatedUnfairLock<Data>` buffers as data arrives.

Also converts `actor ShellExecutor` to `final class` so concurrent
`execute()` calls across multiple users can run in true parallel rather
than serializing through the actor.

Co-Authored-By: Buqian Zheng <zhengbuqian@gmail.com>
…tterns, add ShellExecutor tests

- Cancel the timeout Task via OSAllocatedUnfairLock<Task?> when terminationHandler
  fires so it doesn't linger for the full timeout duration after fast commands
- Extract networkErrorPatterns to a static constant; was duplicated between
  execute() and checkGHAuthenticated(), with "failed to connect" missing from
  the latter
- Make parseHosts internal (was private) so @testable import can reach it
- Add ShellExecutorTests: 6 parseHosts unit tests + 1 timeout integration test
- Fix pre-existing Swift 6 build errors in PRMonitorViewModelTests: add
  @mainactor to ParsePRURLTests, OtherPRsServiceTests, PRTypeDisplayTitleTests,
  and CustomNamesServiceTests (all test @MainActor-inferred types)

Co-Authored-By: Buqian Zheng <zhengbuqian@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@SeanMcTex SeanMcTex merged commit 94fb7ed into main May 11, 2026
@SeanMcTex SeanMcTex deleted the fix/shell-executor-deadlock branch May 11, 2026 14:39
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