What's wrong
LineOutputHandler holds unterminated text in outputBuffer / errorBuffer (RunCommand/LineOutputHandler.cs:16-21). The only thing that clears them is Complete() (LineOutputHandler.cs:57-63), and its doc comment says it "leaves both buffers empty so a later run on this handler starts clean". Reusing a handler is therefore supported.
RunAsync calls Complete() only on the success path (RunCommand/RunCommand.cs:493-500). When a run is cancelled, cancellationToken.ThrowIfCancellationRequested() throws first, and the buffers keep the partial line. A run that faults, for example because a callback throws, keeps them too. Nothing resets the buffers when the next run starts, so the leftover text is glued onto that run's first line.
There's also a thread-safety problem. If a cancelled run's readers were abandoned because a descendant still holds the pipe, those readers can keep appending to the same unsynchronized buffers while the next run is using them.
Repro (verified on Linux, net10.0, at 12afce7)
var lines = new List<string>();
var h = new LineOutputHandler(l => lines.Add(l));
using (var cts = new CancellationTokenSource(1500))
{
try { await RunCommand.ExecuteAsync("sh", ["-c", "printf partial; sleep 10"], h, cts.Token); }
catch (OperationCanceledException) { }
}
await RunCommand.ExecuteAsync("sh", ["-c", "echo hello"], h);
// observed: lines == ["partialhello"]
// expected: lines == ["hello"]
Why it matters
A common pattern is to keep one handler, often a field, that forwards lines to a logger or UI, and run commands through it with a timeout or a cancel button. After any cancelled or failed run, the next command's first line is corrupted. A caller that parses output, such as git rev-parse or a version probe, gets a wrong value and no error.
Suggested fix
- Reset handler state at the start of each run: add an
internal virtual void Reset() on OutputHandler, override it in LineOutputHandler to clear both buffers and any pending-CR state, and call it from RunAsync before Start. Alternatively, clear the buffers in a finally when a run does not complete normally. Either way, discard the leftover text rather than flushing it, because a cancelled run's partial line isn't a real line.
- An abandoned reader from a cancelled run must not be able to write into the buffers of a later run. For example, give each run its own buffer state, or have the reader check a per-run generation token.
Acceptance criteria
- The repro above yields
["hello"].
- A test covers reusing a handler after a cancelled run, and another covers reuse after a run whose callback threw.
What's wrong
LineOutputHandlerholds unterminated text inoutputBuffer/errorBuffer(RunCommand/LineOutputHandler.cs:16-21). The only thing that clears them isComplete()(LineOutputHandler.cs:57-63), and its doc comment says it "leaves both buffers empty so a later run on this handler starts clean". Reusing a handler is therefore supported.RunAsynccallsComplete()only on the success path (RunCommand/RunCommand.cs:493-500). When a run is cancelled,cancellationToken.ThrowIfCancellationRequested()throws first, and the buffers keep the partial line. A run that faults, for example because a callback throws, keeps them too. Nothing resets the buffers when the next run starts, so the leftover text is glued onto that run's first line.There's also a thread-safety problem. If a cancelled run's readers were abandoned because a descendant still holds the pipe, those readers can keep appending to the same unsynchronized buffers while the next run is using them.
Repro (verified on Linux, net10.0, at 12afce7)
Why it matters
A common pattern is to keep one handler, often a field, that forwards lines to a logger or UI, and run commands through it with a timeout or a cancel button. After any cancelled or failed run, the next command's first line is corrupted. A caller that parses output, such as
git rev-parseor a version probe, gets a wrong value and no error.Suggested fix
internal virtual void Reset()onOutputHandler, override it inLineOutputHandlerto clear both buffers and any pending-CR state, and call it fromRunAsyncbeforeStart. Alternatively, clear the buffers in afinallywhen a run does not complete normally. Either way, discard the leftover text rather than flushing it, because a cancelled run's partial line isn't a real line.Acceptance criteria
["hello"].