What's wrong
CreateStartInfo redirects standard output and standard error but never touches standard input, so RedirectStandardInput stays false and the command inherits the calling process's handle. A command that reads standard input then blocks on the caller's handle until something writes to it or it closes — which, in a host that is not a console, is never. The run ends only when the caller cancels it.
CommandOptions exposes WorkingDirectory, EnvironmentVariables and Elevation, so there is no way to ask for anything else.
Reproduction
Measured at main (d06609e), .NET SDK 10.0.401, Linux, against a console app that calls ExecuteAsync with an 8-second cancellation token:
await RunCommand.ExecuteAsync("/bin/sh", ["-c", "read x; echo \"got:[$x]\""], handler, cts.Token);
| caller's standard input |
result |
a pipe held open with no data (sleep 30 | probe) |
TaskCanceledException after 8.0s — the command sat in read |
/dev/null (probe < /dev/null) |
exit=0 in 0.1s, output got:[] |
The first row is the failure. Nothing was wrong with the command; it was waiting on a handle it should never have been given.
Why it matters
This is a hang rather than an error, and it lands on exactly the callers least able to notice: long-running services, daemons and background workers, where standard input is a socket or an idle pipe rather than a terminal. The RunCommand.Test host is itself an example — its standard input is a live socket, so a command reading standard input waits there indefinitely.
Redirecting standard output and standard error while inheriting standard input is also internally inconsistent: a command that prompts cannot be answered, because its prompt goes to the OutputHandler rather than to a terminal, yet it is still handed a stream to wait on.
Environment settings such as GIT_TERMINAL_PROMPT=0 cover a tool that deliberately prompts, but not a command that reads standard input for its own reasons.
Where this was found
ktsu-dev/GitBranchStateCache#27 (delegate git process invocation to ktsu.RunCommand) is blocked on this. Its GitRunner sets RedirectStandardInput = true and calls process.StandardInput.Close() by hand, with a comment recording that a child holding an open standard input it is waiting on "is a hang rather than an error". Adopting this library as it stands would reintroduce the hang that code deliberately guards against, in a long-running service.
Suggested fix
Add a standard-input setting to CommandOptions, as that issue's triage asks for. Redirecting is not sufficient on its own — a redirected but unclosed stream leaves the command holding a pipe nobody writes to, which is the same wait as inheriting. The stream has to be closed after Process.Start for a read to report end of stream.
Keeping Inherit as the default makes this additive rather than breaking; whether the default should change is a separate call, since an interactive caller may be relying on inheritance today.
Elevation.Elevated on Windows forces UseShellExecute, which offers no stream to redirect, so that combination should throw up front the way EnvironmentVariables already does.
Acceptance criteria
- A caller can ask for a command's standard input to be closed, and a command that reads it then reports end of stream instead of waiting.
- The behaviour is pinned by a test that fails if the stream is redirected but left open, and by one that fails if the option is ignored altogether. The second matters: a test runner whose own standard input is already at end of stream hands a child the same answer by inheritance, so a behavioural test alone passes on such a machine even with the option ignored.
What's wrong
CreateStartInforedirects standard output and standard error but never touches standard input, soRedirectStandardInputstaysfalseand the command inherits the calling process's handle. A command that reads standard input then blocks on the caller's handle until something writes to it or it closes — which, in a host that is not a console, is never. The run ends only when the caller cancels it.CommandOptionsexposesWorkingDirectory,EnvironmentVariablesandElevation, so there is no way to ask for anything else.Reproduction
Measured at
main(d06609e), .NET SDK 10.0.401, Linux, against a console app that callsExecuteAsyncwith an 8-second cancellation token:sleep 30 | probe)TaskCanceledExceptionafter 8.0s — the command sat inread/dev/null(probe < /dev/null)exit=0in 0.1s, outputgot:[]The first row is the failure. Nothing was wrong with the command; it was waiting on a handle it should never have been given.
Why it matters
This is a hang rather than an error, and it lands on exactly the callers least able to notice: long-running services, daemons and background workers, where standard input is a socket or an idle pipe rather than a terminal. The
RunCommand.Testhost is itself an example — its standard input is a live socket, so a command reading standard input waits there indefinitely.Redirecting standard output and standard error while inheriting standard input is also internally inconsistent: a command that prompts cannot be answered, because its prompt goes to the
OutputHandlerrather than to a terminal, yet it is still handed a stream to wait on.Environment settings such as
GIT_TERMINAL_PROMPT=0cover a tool that deliberately prompts, but not a command that reads standard input for its own reasons.Where this was found
ktsu-dev/GitBranchStateCache#27(delegate git process invocation toktsu.RunCommand) is blocked on this. ItsGitRunnersetsRedirectStandardInput = trueand callsprocess.StandardInput.Close()by hand, with a comment recording that a child holding an open standard input it is waiting on "is a hang rather than an error". Adopting this library as it stands would reintroduce the hang that code deliberately guards against, in a long-running service.Suggested fix
Add a standard-input setting to
CommandOptions, as that issue's triage asks for. Redirecting is not sufficient on its own — a redirected but unclosed stream leaves the command holding a pipe nobody writes to, which is the same wait as inheriting. The stream has to be closed afterProcess.Startfor a read to report end of stream.Keeping
Inheritas the default makes this additive rather than breaking; whether the default should change is a separate call, since an interactive caller may be relying on inheritance today.Elevation.Elevatedon Windows forcesUseShellExecute, which offers no stream to redirect, so that combination should throw up front the wayEnvironmentVariablesalready does.Acceptance criteria