Friday, 18 November 2011

Process.WaitForExit (Int32) hangs problem

I recently encountered this problem while using a method I use to call commands from the shell via the cmd.exe process.

The code looks like almost this:



code 1:
   1 using System;
   2 using System.Collections.Generic;
   3 using System.Linq;
   4 using System.Text;
   5 using System.Diagnostics;
   6 
   7 namespace TestWaitForExit
   8 {
   9     class Program
  10     {
  11         static int Main(string[] args)
  12         {
  13             CommandResult Result = ExecuteShellCommandSync(@"ping -t 127.0.0.1", 1000);
  14 
  15             return 0;
  16         }
  17 
  18         public static CommandResult ExecuteShellCommandSync(String CommandString, int Timeout)
  19         {
  20             CommandResult ToReturn = new CommandResult();
  21 
  22             Process process = new Process();
  23         
  24             System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
  25                     
  26             startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
  27                     
  28             startInfo.UseShellExecute = false;
  29             startInfo.RedirectStandardError = true;
  30             startInfo.RedirectStandardOutput = true;
  31         
  32             startInfo.FileName = "cmd.exe";
  33         
  34             String MyCommand = @"/C " + CommandString;
  35        
  36             startInfo.Arguments = MyCommand;
  37 
  38             process.StartInfo = startInfo;
  39         
  40             process.Start();
  41 
  42             String Output = "";
  43             String Error = "";
  44             try
  45             {            
  46                 Output = process.StandardOutput.ReadToEnd();
  47                 Error = process.StandardError.ReadToEnd();
  48             }
  49             catch
  50             {
  51                 Output = "Can't read stdout";
  52                 Error = "Can't read stderr";
  53             }
  54 
  55             process.WaitForExit(Timeout);       
  56                     
  57             int Exit = 0;
  58 
  59             if (process.HasExited == true)
  60             {
  61                 ToReturn.ProcessNotExited = false;
  62                 Exit = process.ExitCode;
  63             }
  64             else
  65             {
  66                 ToReturn.ProcessNotExited = true;
  67             }
  68         
  69             process.Dispose();
  70 
  71             ToReturn.ReturnedCode = Exit;
  72             ToReturn.OutputString = Output;
  73             ToReturn.ErrorString = Error;
  74 
  75             return ToReturn;        
  76         }
  77     }
  78 
  79     public class CommandResult
  80     {
  81         public int ReturnedCode { get; set; }
  82         public String OutputString { get; set; }
  83         public String ErrorString { get; set; }
  84         public Boolean ProcessNotExited { get; set; }
  85     }
  86 }

As you can see, the code starts a "cmd.exe" process and passes to it the command I want to be executed.


I redirect StandardError and StandarOutput in order to read them from the code.
The code reads them before the process.WaitForExit(Timeout) call as recommended by Microsoft (more on this later).
The problem arises if the command I send to "cmd.exe" never terminates or hangs indefinitely.
In the code I used the command ping -t 8.8.8.8 which, because of the -t option, pings the host without stopping. What happens? The "cmd.exe" process along with the ping -t command never exits and never closes the stdout stream and so our code hangs at the Output = process.StandardOutput.ReadToEnd(); line because it can't succeed reading all the stream.


The same happens also if a command in a batch file hangs for any reason and so the above code could work continuously for years and then hang suddenly without any apparent reason.

Before I wrote that it's recommended to read redirected streams before the process.WaitForExit(Timeout) call, well this is especially true if you use the WaitForExit signature without the Timeout.

If you call process.WaitForExit() before reading the redirected streams:

code 2:
   1 process.Start();
   2 process.WaitForExit();  
   3 
   4 String Output = "";
   5 String Error = "";
   6 
   7 try
   8 {                
   9 Output = process.StandardOutput.ReadToEnd();
  10 Error = process.StandardError.ReadToEnd();
  11 }
  12 catch
  13 {
  14 Output = "Can't read stdout";
  15 Error = "Can't read stderr";
  16 }

 you can experience a deadlock if  the command you attach to "cmd.exe" or the process you are calling fills the standard output or standard error. This because our code can't reach the lines
Output = process.StandardOutput.ReadToEnd();
     Error = process.StandardError.ReadToEnd();.
As a matter of fact the child process (the ping command or a batch file or whatever process you are executing) can't go on if our program doesn't read the filled buffers of the streams and this can't happen because the code is hanging at the line with process.WaitForExit() which will wait forever for the child project to exit.


The default size of both streams is 4096 bytes. You can test this two sizes with these batch files:

code 3:
   1 @ECHO OFF
   2 for /l %%X in (1,1,409) do ( echo|set /p=0123456789)
   3 echo 0123

code 4:
   1 @ECHO OFF
   2 for /l %%X in (1,1,409) do (echo|set /p=0123456789)  1>&2
   3 (echo 0123) 1>&2

The first script writes 4096 bytes to standard output and the second to standard error.

Save one of these into "C:\testbuffsize.bat" and run our program calling process.WaitForExit() before 
Output = process.StandardOutput.ReadToEnd();
     Error = process.StandardError.ReadToEnd();
as in code 2, you can do it writing
CommandResult Result = ExecuteShellCommandSync(@"c:\testbuffsize.bat", 1000);
at line 13 of code 1.

 The code won't hang but if you write one more byte in any of the two streams it will overflow the buffer size making the program hang.

If you need to redirect and read the standard output or standar error the best solution is to read them asynchronously. An excellent way to do this is proposed by Mark Byers in this stackoverflow thread

As the last thing please notice that if the child process exits only because you use the process.WaitForExit(Timeout) signature and it actually goes in timeout you should kill the "cmd.exe" process and its possible children.