19

I use Process.Start to start a batch file. The batch file uses the "START" command to start several programs in parallel and then exits.

Once the batch file is done Process.HasExited becomes true and Process.ExitCode contains the correct exit code.

But when I call Process.WaitForExit() it hangs / never returns.

The following piece of code demonstrates the problem. It creates a batch file, starts it and then prints:

Process is still running...
Batch file is done!
Process has exited. Exit code: 123
Calling WaitForExit()...

It should then print:

WaitForExit returned.

... but it never does (even though HasExited is true and we already have an ExitCode).

open System.IO
open System.Diagnostics
open System.Threading

let foobat = """
  START ping -t localhost
  START ping -t google.com
  ECHO Batch file is done!
  EXIT /B 123
"""

File.WriteAllText("foo.bat", foobat)

use p = new Process(StartInfo = ProcessStartInfo("foo.bat",
                                                 UseShellExecute = false,
                                                 RedirectStandardOutput = true,
                                                 RedirectStandardError = true))

let onOutput = DataReceivedEventHandler(fun _ args -> printfn "%s" args.Data)

p.OutputDataReceived.AddHandler onOutput
p.ErrorDataReceived.AddHandler onOutput

p.Start() |> ignore

p.BeginErrorReadLine()
p.BeginOutputReadLine()

while not p.HasExited do
  printfn "Process is still running..."
  Thread.Sleep(1000)

printfn "Process has exited. Exit code: %d" p.ExitCode

printfn "Calling WaitForExit()..."
p.WaitForExit()|> ignore
printfn "WaitForExit returned."

I noticed that this only happens when the batch file contains "START" commands and when standard output and/or standard error are redirected.

Why does WaitForExit() never return?

What's the right way to wait for such a process to exit?

Is it safe to just poll Process.HasExited or can that result in other problems?

PS.: I just noticed that calling WaitForExit(100000) with a huge timeout (that definitely doesn't expire) returns immediately when the process exits. Wierd. Without timeout it hangs.

ildjarn
  • 62,044
  • 9
  • 127
  • 211
stmax
  • 6,506
  • 4
  • 28
  • 45
  • 2
    At a guess, as STDOUT and STDERR are redirected, they are not closed. As the streams are still open, the file hasn't completely exited. – John Palmer Nov 03 '14 at 11:36
  • Shouldn't these streams be closed when the process exits? The cmd.exe process that's running foo.bat even disappears from the task manager.. – stmax Nov 03 '14 at 11:53
  • Why do you call wait _after_ it exited? – fejesjoco Nov 03 '14 at 11:57
  • 1
    There is a fundamental difference when you call WaitForExit() without a time-out, it ensures that the redirected stdout/err have returned EOF. This makes sure that you've read *all* the output that was produced by the process. We can't see what "onOutput" does, but high odds that it deadlocks your program because it does something nasty like assuming that your main thread is idle when it is actually stuck in WaitForExit(). – Hans Passant Nov 03 '14 at 12:06
  • @fejesjoco only to demonstrate the problem in the example above. – stmax Nov 03 '14 at 12:49
  • @HansPassant What do you mean with "can't see what onOutput does"? It's defined in the example above (the example is complete & runnable). onOutput just prints whatever it receives. That shouldn't deadlock? I also tried replacing the "print" with a completely empty function body... still hangs. – stmax Nov 03 '14 at 12:54
  • I asked about this issue separately (https://stackoverflow.com/questions/53231519/freeze-when-programmatically-launching-a-batch-file-with-output-redirection-w), because I didn't have commenting privileges at the time, but mostly because the accepted answer here leaks threads. I found two more related questions, neither having an answer: 1) https://stackoverflow.com/questions/20849558/issue-with-output-redirection-in-batch 2) https://stackoverflow.com/questions/36091748/how-do-i-use-the-start-command-without-inheriting-handles-in-the-child-process For a workaround, see my own question. – kostasvs Nov 14 '18 at 18:15

1 Answers1

31

This seems to be an artifact (I'd say "bug") in the specific implementation of the event-based asynchronous handling of StandardOutput and StandardError.

I noticed that while I was able to easily reproduce your problem, simply by running the code you provided (excellent code example, by the way! :) ), the process did not actually hang indefinitely. Rather, it returned from WaitForExit() once both of the child processes that had been started had themselves exited.

This seems to be an intentional part of the implementation of the Process class. In particular, in the Process.WaitForExit() method, once it has finished waiting on the process handle itself, it checks to see if a reader for either stdout or stderr has been created; if so, and if the timeout value for the WaitForExit() call is "infinite" (i.e. -1), the code actually waits for the end-of-stream on the reader(s).

Each respective reader is created only when the BeginOutputReadLine() or BeginErrorReadLine() method is called. The stdout and stderr streams are themselves not closed until the child processes have closed. So waiting on the end of those streams will block until that happens.

That WaitForExit() should behave differently depending on whether one has called either of the methods that start the event-based reading of the streams or not, and especially given that reading those streams directly does not cause WaitForExit() to behave that way, creates an inconsistency in the API that makes it much more difficult to understand and use. While I'd personally call this a bug, I suppose it's possible that the implementor(s) of the Process class are aware of this inconsistency and created it on purpose.

In any case, the work-around would be to read StandardOutput and StandardError directly instead of using the event-based part of the API. (Though of course, if one's code were to wait on those streams, one would see the same blocking behavior until the child processes close.)

For example (C#, because I don't know F# well enough to slap a code example like this together quickly :) ):

using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace TestSO26713374WaitForExit
{
    class Program
    {
        static void Main(string[] args)
        {
            string foobat =
@"START ping -t localhost
START ping -t google.com
ECHO Batch file is done!
EXIT /B 123
";

            File.WriteAllText("foo.bat", foobat);

            Process p = new Process { StartInfo =
                new ProcessStartInfo("foo.bat")
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true
                } };

            p.Start();

            var _ = ConsumeReader(p.StandardOutput);
            _ = ConsumeReader(p.StandardError);

            Console.WriteLine("Calling WaitForExit()...");
            p.WaitForExit();
            Console.WriteLine("Process has exited. Exit code: {0}", p.ExitCode);
            Console.WriteLine("WaitForExit returned.");
        }

        async static Task ConsumeReader(TextReader reader)
        {
            string text;

            while ((text = await reader.ReadLineAsync()) != null)
            {
                Console.WriteLine(text);
            }
        }
    }
}

Hopefully the above work-around or something similar will address the basic issue you've run into. My thanks to commenter Niels Vorgaard Christensen for directing me to the problematic lines in the WaitForExit() method, so that I could improve this answer.

Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Sorry for the delay - I tried this today and it seems to work. I actually like this method with tasks/asyncs better than the ugly event code I used before (+ it works). Thanks – stmax Nov 19 '14 at 10:14
  • Glad it helped. It was an interesting problem, and I learned something myself, thanks to your well-researched question. :) – Peter Duniho Nov 19 '14 at 17:45
  • 1
    +1. This definitely is a pitfall - I wouldn't call it a bug though. I'm pretty sure the reason for this behavior is so that one can use `BeginOutputReadLine/BeginErrorReadLine` without having to write complicated *"have I processed all output data yet?"* synchronization code. – Paul Groke Jan 04 '16 at 22:21
  • I do not think it is a bug. The problem is that the child processes inherit the redirected handles and keep them open. WaitForExit() blocks until the handles are closed. – Niels Bliddal Christensen Feb 16 '16 at 10:22
  • @PeterDuniho: `WaitForExit()` will only wait for EOF on a stream if the corresponding `BeginOutputReadLine()/BeginErrorReadLine()` has been called. Try waiting on one of the `Task`s returned by `ConsumeReader()`. I think it will block in the same way. – Niels Bliddal Christensen Feb 17 '16 at 08:08
  • @Peter: Look at line 2458-2465. The members `output` and `error` are both `null` unless `BeginOutputReadLine()/BeginErrorReadLine()` has been called. I agree that the API is too hard to use. – Niels Bliddal Christensen Feb 17 '16 at 08:57
  • @Niels: ah, thanks...I see. I've updated the answer to provide that detail. – Peter Duniho Feb 17 '16 at 09:33
  • 1
    I believe this solution will leak worker threads, which matters if the launcher program is not designed to exit so quickly. See my question here: https://stackoverflow.com/questions/53231519/freeze-when-programmatically-launching-a-batch-file-with-output-redirection-w (which I posted back when I didn't have commenting privileges). – kostasvs Nov 14 '18 at 18:07
  • Replacing WaitForExit() with WaitForExit(timeoutInMilliSeconds) that includes a timeout certainly helped me with resolving Process objects hanging behaviour when a process prematurely exits before reaching this method call. – CyrilDex Jun 03 '20 at 05:17