From f949ef2c1ae1506feb00faea201de6ce62892aa1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 22:27:46 +0000 Subject: [PATCH] Pin that a command closing its output does not make the reader spin [patch] Before #87, a stream at end of stream was read again and again until the process exited, burning a full core for as long as a command that had closed stdout or stderr kept running. #87's read-to-end loops stop at end of stream, which removed the spin. This adds the issue's repro as a regression test: it fails against the pre-#87 reader, using over 6 s of CPU during a 2 s run, and passes now. The measurement is process-wide CPU, so the test opts out of parallel execution. Fixes #90 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TRHs5nFW38XRh3KGTHYjE6 --- RunCommand.Test/RunCommandTests.cs | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/RunCommand.Test/RunCommandTests.cs b/RunCommand.Test/RunCommandTests.cs index 3a34051..c7c0d3c 100644 --- a/RunCommand.Test/RunCommandTests.cs +++ b/RunCommand.Test/RunCommandTests.cs @@ -13,6 +13,7 @@ public class RunCommandTests { private static readonly string[] ExpectedALast = ["a", "last"]; private static readonly string[] ExpectedALastNext = ["a", "last", "next"]; + private static readonly string[] ExpectedDone = ["done"]; private static string GetCopyCommand(string source, string destination) => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) @@ -971,6 +972,50 @@ public async Task ExecuteAsyncShouldDeliverOutputStillInThePipeWhenTheProcessExi } } + [TestMethod] + [DoNotParallelize] + public async Task ExecuteAsyncShouldNotSpinWhileACommandThatClosedItsOutputKeepsRunning() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Inconclusive("Needs a shell that can close its own output streams and stay alive. The read loop this covers is in platform independent code, so the other legs cover it."); + } + + // Standard error reaches end of stream at once while the command runs on. A reader that + // keeps reading a stream after end of stream gets 0 back straight away every time, so it + // spins on a core for the whole run instead of waiting. + const int runMilliseconds = 2000; + List output = []; + + using Process self = Process.GetCurrentProcess(); + TimeSpan before = self.TotalProcessorTime; + + int exitCode = await RunCommand.ExecuteAsync( + "sh", + ["-c", $"exec 2>&-; sleep {runMilliseconds / 1000}; echo done"], + new LineOutputHandler(onStandardOutput: line => + { + lock (output) + { + output.Add(line); + } + })).ConfigureAwait(false); + + self.Refresh(); + TimeSpan used = self.TotalProcessorTime - before; + + Assert.AreEqual(0, exitCode); + Assert.AreSequenceEqual(ExpectedDone, output, "Expected standard output to be delivered in full."); + + // This is process-wide CPU, which is why the test opts out of running alongside the others. + // A spinning reader costs at least a full core for the whole run, so half of the run is + // clear of the test host's own background work and still well short of a spin. + Assert.IsLessThan( + runMilliseconds / 2, + used.TotalMilliseconds, + $"Expected the reader to wait rather than spin, but the process used {used.TotalMilliseconds:F0} ms of CPU during a {runMilliseconds} ms run."); + } + /// /// Returns a command that prints a file's contents unchanged. ///