1

Consider the following program. Here I start a simple process and want to deal with it's output. I assumed that would be the case after WaitForExit has returned, but it turns out, that I have to wait upto a full second until that output actually arrives in my program.

static void Main(string[] args)
{
    using var p = new Process();
    p.StartInfo.FileName = "echo";
    p.StartInfo.Arguments = "I apologize for being late";
    p.StartInfo.CreateNoWindow = false;
    p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
    p.StartInfo.UseShellExecute = false;
    p.StartInfo.RedirectStandardOutput = true;
    p.StartInfo.RedirectStandardError = true;
    var stdError = new StringBuilder();
    var stdOutput = new StringBuilder();
    p.ErrorDataReceived += (sender, args) => stdError.AppendLine(args.Data);
    p.OutputDataReceived += (sender, args) => stdOutput.AppendLine(args.Data);
    p.Start();
    p.BeginErrorReadLine();
    p.BeginOutputReadLine();
    // without the int-parameter here, it works
    while (!p.WaitForExit(10000))
        Console.WriteLine("still waiting...");
    string a = stdOutput.ToString();
    string b = stdError.ToString();
    Thread.Sleep(1000);
    string c = stdOutput.ToString();
    string d = stdError.ToString();
    Console.WriteLine("output before sleep: " + a);
    Console.WriteLine("error  before sleep: " + b);
    Console.WriteLine("output after  sleep: " + c);
    Console.WriteLine("error  after  sleep: " + d);
}

output

output before sleep:
error  before sleep:
output after  sleep: I apologize for being late


error  after  sleep:

Here I would expect, that a and c have the exact same value. But that is not the case. How would I modify this example such that I reliable receive the full output of the process, but without calling Thread.Sleep(1000)?

Notes:

  • I want the reliable complete output of both stdOut and stdErr
  • when I use p.WaitForExit() instead p.WaitForExit(10000) everything seems to work
  • When using p.StandardOutput.ReadToEnd() for both streams it seems to work. But I am told by the official documentation, that this would lead to deadlocks
  • when using p.StandardError.ReadToEnd() while using the async solution for the output, than the output still arrives late.
  • this is not a duplicate of Process WaitForExit not waiting because for them p.WaitForExit()without any parameter already doesn't work. Also they are not interested in the output at all.
wotanii
  • 2,470
  • 20
  • 38

1 Answers1

3

There is an awkward implementation detail that is at play here.

Calling

p.WaitForExit();

and

p.WaitForExit(10000);

do slightly different things when the actual native processhandle gets signaled.

Internally p.WaitForExit(); calls p.WaitForExit(-1);. The -1 is significant here. Let's see what we have (code is simplified/ paraphrased to show the essence):

public bool WaitForExit(int milliseconds)
{
    // init stuff removed
    bool flag;
    try
    {
        flag = processWaitHandle.WaitOne(milliseconds, false);
    }
    finally
    {
        // here we see our -1 return
        if (this.output != null && milliseconds == -1)
        {
            this.output.WaitUtilEOF();
        }
    }
    return flag;
}

In the above snippet you see this.output.WaitUtilEOF(); and that calls into an internal AsyncStreamReader that employs a queue. The call to WaitUtilEOF(); basically waits on the stream for the EOF event to be raised.

There is no other way that I could find to force the Process class to make a call to wait for those EOF events. The only option is to call WaitForExit() without a parameter. There is however no penalty in calling WaitForExit(); after a call to WaitForExit(10000) returned.

So if your timeout on the first WaitForExit(10000) was reached but you're sure you rather wait a bit longer for the AsyncStreamReader to hand you all data it has, call WaitForExit() without a parameter to have both AsyncStreamReaders empty their queue and then return control to you. This does mean that if your process didn't end you're now stuck in a wait that won't ever resolve itself unless you kill the child process by yourself.

rene
  • 41,474
  • 78
  • 114
  • 152
  • wow. That explanation is both interesting and disturbing. Thank you for this. I am assuming, that once `WaitForExit(10000)` returns `true`, the process has finished (or at least is very close to it), and I can call `WaitForExit()` without having to worry about it running ad-infinitum. I guess the most reliable way would be to call only `WaitForExit()`, but in a different thread, and to handle the timeout and childmurder elsewhere. – wotanii Jul 23 '21 at 12:20
  • @wotanii yeah, if you expect that waitforexit might block forever then you have to take care of that case by yourself. What you propose is what I would do. – rene Jul 23 '21 at 12:22
  • That's a really a pretty lame and wrong implementation by Microsoft :-( I made a workaround by calling process.WaitForExit() inside a background worker thread and then waiting for that while doing what I needed to do (Application.DoEvents in my case). But probably won't fit your case... – obartelt Dec 22 '22 at 14:28