Skip to content

Never run the standard output and standard error delegates at once [patch] - #123

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/95-serialise-delivery
Oct 27, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/95-serialise-delivery

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #95

What was wrong

AsyncProcessStreamReader reads stdout and stderr in two independent loops, and each loop called its delegate from its own read continuation. So onStandardOutput and onStandardError could run inside each other on different thread-pool threads. If both delegates added to one List<string> (the most natural way to capture a command's whole output), lines disappeared with no exception.

Change

This is option 1 from the triage: serialise delivery.

  • AsyncProcessStreamReader holds one Lock per run and takes it around each delegate call. The two pipe reads still run concurrently. Only the hand-off to the caller is serialised, and each stream's chunks still arrive in the order they were written.
  • The OutputHandler XML docs now state the contract: the two actions never run at the same time, so they can share state without locking.

A caller whose delegates already take their own lock is unaffected, apart from a lock that is now redundant.

Test

ExecuteAsyncShouldNeverRunTheStandardOutputAndStandardErrorDelegatesAtOnce interleaves 5,000 lines on each stream into a single non-thread-safe List<string> through both delegates, five times over. On every attempt it asserts that:

  • the delegates never overlap,
  • all 10,000 lines arrive,
  • each stream's lines are in order.

It is skipped (Assert.Inconclusive) on Windows, like the neighbouring shell-based test, because the code it covers is platform independent.

  • With the fix reverted, the test fails on Assert.AreEqual(0, overlaps).
  • With the fix, the full suite passes on Linux/net10.0: 64 passed, 2 skipped (the existing elevation skips), 0 failed.

Generated by Claude Code

… once

The two pipes are read concurrently, and each read loop called its delegate
from its own continuation, so the two delegates ran inside each other on
different thread-pool threads. A caller sharing one List<T> or StringBuilder
between them lost lines with no exception.

Delivery now takes one lock per run, so the reads stay concurrent while the
delegates run one at a time, and each stream keeps its own order.

Fixes #95

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UuQscGFrjteUf9Ui3jWdd
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit cf103d8 into main Oct 27, 2026
15 checks passed
@matt-edmondson
matt-edmondson deleted the fix/95-serialise-delivery branch October 27, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants