Skip to content

Reader spins at 100% CPU for the rest of the run once a command closes stdout or stderr but keeps running #90

Description

@matt-edmondson

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingreadyFully specified; implement as written

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions