What's wrong
When a stream reaches end of stream, StreamReader.ReadAsync returns 0 immediately. ReadCallback (RunCommand/AsyncProcessStreamReader.cs:~160) ignores the 0, so the task completes. The loop at :79-96 sees IsCompleted and issues another read, which also returns 0 immediately. Task.WhenAny therefore never actually waits, and the loop busy-spins until process.HasExited.
Reproduction (Linux, net10.0, parent process CPU time)
| Command |
Wall |
CPU |
sh -c "sleep 3" (baseline) |
3069 ms |
73 ms |
sh -c "exec 2>/dev/null; sleep 3" |
3056 ms |
3021 ms |
sh -c "exec 1>&- 2>&-; sleep 3" |
3048 ms |
3030 ms |
Why it matters
Any child that closes or redirects one of its streams and keeps running burns a full core in the caller for the whole run. Examples include daemons, wrappers that do exec 2>/dev/null, and tools that close stdout early. The cost scales with the command's duration.
Suggested fix
Have ReadAndCallback report end of stream (for example, return Task<bool>). Once a stream has ended, stop issuing reads for it: leave it out of WhenAny, or substitute a task that never completes. When both streams have ended, fall through to waiting for exit. The post-exit drain (:103-106) should also skip streams that have already ended.
Acceptance criteria
sh -c "exec 2>/dev/null; sleep 3" uses roughly baseline CPU, and output is still delivered completely for normal commands.
What's wrong
When a stream reaches end of stream,
StreamReader.ReadAsyncreturns 0 immediately.ReadCallback(RunCommand/AsyncProcessStreamReader.cs:~160) ignores the 0, so the task completes. The loop at:79-96seesIsCompletedand issues another read, which also returns 0 immediately.Task.WhenAnytherefore never actually waits, and the loop busy-spins untilprocess.HasExited.Reproduction (Linux, net10.0, parent process CPU time)
sh -c "sleep 3"(baseline)sh -c "exec 2>/dev/null; sleep 3"sh -c "exec 1>&- 2>&-; sleep 3"Why it matters
Any child that closes or redirects one of its streams and keeps running burns a full core in the caller for the whole run. Examples include daemons, wrappers that do
exec 2>/dev/null, and tools that close stdout early. The cost scales with the command's duration.Suggested fix
Have
ReadAndCallbackreport end of stream (for example, returnTask<bool>). Once a stream has ended, stop issuing reads for it: leave it out ofWhenAny, or substitute a task that never completes. When both streams have ended, fall through to waiting for exit. The post-exit drain (:103-106) should also skip streams that have already ended.Acceptance criteria
sh -c "exec 2>/dev/null; sleep 3"uses roughly baseline CPU, and output is still delivered completely for normal commands.