Friday, October 23, 2009

Capturing StandardOutput AND StandardError from a Process

I recently revived a program I had written to do an automated build and deploy of a solution containing a Visual Studio Database Edition project as well as a client UI in WPF. The build program runs a bunch of command-line programs, capturing all the output along the way and keeping track of errors, etc. When I was going through the code, I noticed a bug. I am using ProcessStartInfo and Process and I thought I was capturing both StandardOutput and StandardError but I wasn’t. My code looked like this:

      psi.UseShellExecute = false;
psi.RedirectStandardOutput
= true;
psi.RedirectStandardError
= true;
Process p
= Process.Start(psi);
string output = p.StandardOutput.ReadToEnd();
string error = p.StandardOutput.ReadToEnd();
p.WaitForExit();


So I “fixed” the code so now it looked like this:



      ...
string error = p.StandardError.ReadToEnd();
...


But when I ran it, one of the programs seemed to hang forever. I was able to determine that the program did in fact finish, and was actually my code that hung forever. It turns out that if you use ReadToEnd() on both StandardOutput and StandardError, you can reach a blocking situation that hangs your code. This is even documented on MSDN here:




“A deadlock condition results if the parent process calls p.StandardOutput.ReadToEnd followed by p.StandardError.ReadToEnd and the child process writes enough text to fill its error stream. The parent process would wait indefinitely for the child process to close its StandardOutput stream. The child process would wait indefinitely for the parent to read from the full StandardError stream.”




This is exactly what was happening in my code. I was up until 4am updating the Build program, and at this point I wanted a quick solution. The MSDN documentation hinted at the solution (asynchronous threads) but did not provide code outright. I did some searching on the Internet and found some solutions out there, but they were all much heavier than I wanted. I wanted something simple, preferably just a few lines of code and very modular, so after I couple hours of sleep and some coffee, I came up with a solution. First, I created a very simple class, which could be called from a new Thread, to capture the output of a stream:



  class MyStreamReader
{
StreamReader _sr
= null;
string _text = null;
public string Text { get { return _text; } }

public MyStreamReader(StreamReader sr)
{
_sr
= sr;
}

public void Go()
{
_text
= _sr.ReadToEnd();
}
}


Then I used that class to capture the standard error and standard output from my newly launched process, as follows:



      ProcessStartInfo psi = new ProcessStartInfo(commandToRun);
psi.UseShellExecute
= false;
psi.RedirectStandardOutput
= true;
psi.RedirectStandardError
= true;
Process p
= Process.Start(psi);

// Create my objects to capture output asynchronously
MyStreamReader msr_stdout = new MyStreamReader(p.StandardOutput);
MyStreamReader msr_stderr
= new MyStreamReader(p.StandardError);

// Create the thread objects to run the code asynchronously
Thread t_stdout = new Thread(msr_stdout.Go);
Thread t_stderr
= new Thread(msr_stderr.Go);

// Launch both threads
t_stdout.Start();
t_stderr.Start();

// Wait for both output and error streams to finish
t_stdout.Join();
t_stderr.Join();

p.WaitForExit();

// retrieve the output and error text
string output = msr_stdout.Text;
string error = msr_stderr.Text;

Console.WriteLine(output);
Console.WriteLine(error);


I tested out my new code now everything works properly – now I just need to thank the developer who checked in the post deployment scripts that caused the errors, otherwise I might not have caught this problem to begin with!

8 comments:

  1. Hello, I was creating a couple of windows services which launch perl scripts trough batch files, and I crash with the same trouble as you... after a little research I found you post... I'm coding replicating into my processes... Thanks for your post is very useful!

    ReplyDelete
  2. Thanks for the comment! I'm glad to have helped -- that is the main reason I decided to start this blog and share some of my experiences. A colleague of mine also used this same code last week to capture all the output from SQLMetal when auto-generating the DBML from the database for a larger Linq to SQL project.

    ReplyDelete
  3. Thanks for posting your solution! I had a similar issue when running wzzip.exe and running ReadToEnd() on both output and error streams, the process hung.

    ReplyDelete
  4. Hey dev3001,

    Thank you for this excellent post!
    I have two comment:
    1. for simple batch you can use the 2>&1 as argument, then you need to redirect only the stdout. For instance:

    private static int Make(string argument)
    {
    Process make = new Process();
    make.StartInfo.FileName ="Build.bat";
    make.StartInfo.Arguments = argument + " 2>&1";
    make.StartInfo.UseShellExecute = false;
    make.StartInfo.RedirectStandardOutput = true;
    make.StartInfo.CreateNoWindow = true;
    make.Start();

    MyStreamReader msr_stdout = new MyStreamReader(make.StandardOutput);
    Thread t_stdout = new Thread(msr_stdout.Go);
    t_stdout.Start();
    t_stdout.Join();

    make.WaitForExit();
    return make.ExitCode;
    }

    2. For me the continous logging on the screen was an important point. Using a while-readline combo made it:
    class MyStreamReader
    {
    StreamReader _sr = null;
    public MyStreamReader(StreamReader sr)
    {
    _sr = sr;
    }
    public void Go()
    {
    while (!_sr.EndOfStream)
    {
    Console.WriteLine(_sr.ReadLine());
    }
    }
    }

    Thank you again!

    ReplyDelete
  5. Hi Andras, thank you for adding your comments to this post! I didn't realize you could use the command-line redirection with the Process class (the 2>&1). Excellent short cut!

    Your other code is also very useful -- I was recently thinking about adding a way to monitor the output of one of my programs, and this would help. Thanks for sharing -- I think this is my most read post, so now everyone who reads the post can use your methods as well.

    ReplyDelete
  6. Thanks for posting your solution and comment on MSDN topic about StandardOutput property. Earlier I workarounded this problem by using redirection, but in current project this is not possible, so I needed working solution, which was found just in minute from MSDN page.

    ReplyDelete
  7. Here is the complete solution for the problem.

    Reading StandardOutput synchronously and StandardError asynchronously

    http://dotnetthread.com/articles/9-CExecuteprocessandreadbothstandardoutputandstandarderroroutput.aspx

    ReplyDelete
  8. More succinct code using the TPL

    Task stdOutReadTask = Task.Run(() => { return process.StandardOutput.ReadToEnd(); });
    Task stdErrReadTask = Task.Run(() => { return process.StandardError.ReadToEnd(); });

    String stdErr = stdErrReadTask.Result;
    String stdOut = stdOutReadTask.Result;

    ReplyDelete