Fix deadlock in PipeTarget.Merge when a sub-target reads past end-of-stream - #347
Conversation
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
It's a dormant bug, not a regression. The one-shot EOF semaphore design has been there since |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## prime #347 +/- ##
==========================================
+ Coverage 92.77% 92.80% +0.03%
==========================================
Files 26 26
Lines 1162 1167 +5
Branches 73 76 +3
==========================================
+ Hits 1078 1083 +5
Misses 64 64
Partials 20 20 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a deadlock in PipeTarget.Merge caused by SimplexStream only signaling end-of-stream once, which can legitimately trigger a second EOF read (e.g., via StreamReader) and hang ExecuteAsync.
Changes:
- Added an end-of-stream completion latch in
SimplexStreamso reads past EOF reliably return0without blocking. - Adjusted EOF handling so the write lock is not released after the closing (empty) buffer is consumed.
- Added regression tests covering both a minimal over-read scenario and a
StreamReader/UTF-8 boundary case.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| CliWrap/Utils/SimplexStream.cs | Latches completion and adjusts semaphore/lock behavior to avoid post-EOF blocking. |
| CliWrap.Tests/PipingSpecs.cs | Adds regression tests that reproduce the hang and validate the fix under realistic conditions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
SimplexStream(used internally byPipeTarget.Merge) can only deliver its end-of-stream signal once. Per theStreamcontract, every read at end-of-stream must return0, but a second read after the first EOF read blocks forever on a semaphore that's never released again.StreamReader-based targets (ToStringBuilder,ToDelegate, and transitivelyExecuteBufferedAsync) can trigger this legitimately when a multibyte UTF-8 sequence causes an extra probe read, hangingExecuteAsyncentirely.Fix
_isCompletedlatch toSimplexStream, set when the closing (empty) buffer is written.ReadAsyncshort-circuits to return0immediately for any subsequent read, without waiting on the read semaphore.Tests
Added regression tests in
PipingSpecs:PipeTarget.Mergesub-target that reads once past end-of-stream.PipeTarget.ToStringBuildermerged with another target, fed exactly 1024 bytes ending in a multibyte UTF-8 character (matchesStreamReader's internal buffer size, the exact trigger condition from the report).