Skip to content

A command that reads standard input hangs, because standard input is neither redirected nor closed #81

Description

@matt-edmondson

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions