Along the same lines of my last post, we might want to be smarter about launching a process and waiting for it to exit.

Particularly if that process is part of a critical system such as your build process, you don't want a hung process to be able to cause your entire build system to stop dead in it's tracks.

Fortunately, Process has an overload of WaitForExit that takes an integer timeout, and returns a boolean - true if the process exited, or false if the process did not.

Using this function, we can write a method like this:

static int GetNumOutputChars(Process process, int timeoutInMilliseconds)
{
    var counter = 0;
    process.OutputDataReceived += (sender, e) =>
    {
        if (e.Data != null)
        {
            counter += e.Data.Length;
        }
    };

    process.BeginOutputReadLine();

    if (!process.WaitForExit(timeoutInMilliseconds))
    {
        process.Kill();
        return 0;
    }
            
    return counter;
}

In this snippet, OutputDataReceived is an event that fires when the process writes data to standard output, and BeginOutputReadLine signals to .NET that we've set up our event and are ready to begin listening for process output.

Using the event form in this case means that we don't need to have the full process output text in memory at any one point in time. As the process writes a line of text, we will have just that line in memory, and it will get garbage-collected after we update our counter.

There are two "gotcha"s in this snippet, though.

The first is process.Kill(). This is a classic race condition - in between the time our process returns from process.WaitForExit() and invokes process.Kill(), the target process can exit. In this case, MSDN states that if you call .Kill() on a process that has already exited, you will get an InvalidOperationException.

We can fix this by handling the exception, like so:

if (!process.WaitForExit(timeoutInMilliseconds))
{
    try
    {
        process.Kill();
    }
    catch (InvalidOperationException)
    {
        // The process already finished by itself, so use the counter value.
        return counter;
    }

    // We killed the process, it had not finished.
    return 0;
}
            
return counter;

The second "gotcha" is very subtle, and you probably won't find it until either the function doesn't return the result you expect, or until you sit down and read the entire documentation page for Process.WaitForExit(int). MDSN states that:

When standard output has been redirected to asynchronous event handlers, it is possible that output processing will not have completed when this method returns. To ensure that asynchronous event handling has been completed, call the WaitForExit() overload that takes no parameter after receiving a true from this overload.

In this case, it is possible for our function GetNumOutputChars to return a value containing less characters than the actual process output, unless we call WaitForExit() again.

With this in mind, our final safe function now looks like this:

static int GetNumOutputChars(Process process, int timeoutInMilliseconds)
{
    var counter = 0;
    process.OutputDataReceived += (sender, e) =>
    {
        if (e.Data != null)
        {
            counter += e.Data.Length;
        }
    };

    process.BeginOutputReadLine();

    if (!process.WaitForExit(timeoutInMilliseconds))
    {
        try
        {
            process.Kill();
        }
        catch (InvalidOperationException)
        {
            // The process already finished by itself, so use the counter value.
            process.WaitForExit();
            return counter;
        }

        // We killed the process, it had not finished.
        return 0;
    }

    process.WaitForExit();
    return counter;
}