Providing async functionality for System.Diagnostics.Process

Spread the love

If you need to interact with processes in .NET, you could do worse than System.Diagnostics.Process. But you could also do a bit better, especially since Process provides no out-of-the-box asynchronous methods. But we can fix that!

Let’s devise a ProcessEx class that provides async versions of the Start and WaitForExit methods. Here is the class overview:

public sealed class ProcessEx : IDisposable
{
    private ProcessEx(Process process);

    public Process Inner { get; }

    public static Task<ProcessEx> StartAsync(ProcessStartInfo psi);

    public Task WaitForExitAsync();

    public Task<bool> WaitForExitAsync(TimeSpan timeout);

    public void Dispose();
}

The constructor is private since we will only support instantiating ProcessEx via the StartAsync factory method:

public static Task<ProcessEx> StartAsync(ProcessStartInfo psi)
{
    return Task.Factory.StartNew(i => new ProcessEx(Process.Start((ProcessStartInfo)i)), psi);
}

Unfortunately, there is no Windows API to asynchronously start a process so we are stuck offloading the blocking Process.Start call to a worker thread. Okay, so what do we do now that we have our hands on a Process instance? Well, we need to ensure that EnableRaisingEvents is set to true so that we can be notified via the Exited event when the process exits. This will enable us to build a simple async version of WaitForExit, using our old friend TaskCompletionSource. Here is the relevant code:

private readonly Process process;
private readonly TaskCompletionSource<bool> exited;

private ProcessEx(Process process)
{
    this.process = process;
    this.exited = new TaskCompletionSource<bool>();
    this.process.EnableRaisingEvents = true;
    this.process.Exited += this.OnProcessExited;
    if (this.process.HasExited)
    {
        this.exited.TrySetResult(false);
    }
}

public Process Inner
{
    get { return this.process; }
}

public Task WaitForExitAsync()
{
    return this.exited.Task;
}

private void OnProcessExited(object sender, EventArgs e)
{
    this.exited.TrySetResult(false);
}

Note the use of TrySetResult rather than SetResult and the explicit check of HasExited. This is to handle two potential race conditions:

  1. The Exited event may never be raised since the process could have already exited by the time we subscribe.
  2. The check of HasExited may occur at the same time the OnProcessExited handler is invoked.

Let’s get Dispose out of the way. To avoid WaitForExitAsync from blocking forever, we’ll attempt to complete it with an ObjectDisposedException:

public void Dispose()
{
    this.Dispose(true);
    GC.SuppressFinalize(this);
}

private void Dispose(bool disposing)
{
    if (disposing)
    {
        this.process.EnableRaisingEvents = false;
        this.process.Exited -= this.OnProcessExited;
        this.exited.TrySetException(new ObjectDisposedException("ProcessEx"));
        this.process.Dispose();
    }
}

Now we need to implement the overload of WaitForExitAsync that accepts a timeout:

public async Task<bool> WaitForExitAsync(TimeSpan timeout)
{
    TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
    using (CancellationTokenSource cts = new CancellationTokenSource())
    {
        Task exitedTask = this.exited.Task;
        Task completedTask;
        using (cts.Token.Register(o => ((TaskCompletionSource<bool>)o).SetResult(false), tcs))
        {
            cts.CancelAfter(timeout);
            completedTask = await Task.WhenAny(tcs.Task, exitedTask);
        }

        bool result = false;
        if (completedTask == exitedTask)
        {
            await exitedTask;
            result = true;
        }

        return result;
    }
}

Basically, we need to await the result of two operations, one tracking the timeout (tcs above) and the other tracking process exit (this.exited). The timeout task will only complete if the timeout expires before the process actually exits. To manage this, we use CancellationTokenSource.CancelAfter and a CancellationTokenRegistration which will complete the timeout task. (Note that if the CancellationToken is already in a canceled state, Register will run the callback immediately, so there is no race here.) We use Task.WhenAny to notify us when either task completes. (Since we are good citizens, we Dispose the registration after this point since we don’t need it anymore.) If the first task to complete indicates that the process exited, we await the task (in case it completed with an exception) and return true. Otherwise, the process is still running as far as we know and we return false.

That’s all there is to it. Here is a simple use case of creating a file, opening a Notepad window to display it, and waiting for the user to close it:

internal sealed class Program
{
    private static void Main(string[] args)
    {
        MainAsync().Wait();
    }

    private static async Task MainAsync()
    {
        string fileName = Environment.ExpandEnvironmentVariables(@"%TEMP%\SampleFile.txt");
        using (FileStream stream = CreateAsyncStream(fileName))
        {
            string text = "Close this window to unblock app.";
            byte[] buffer = Encoding.ASCII.GetBytes(text);
            await stream.WriteAsync(buffer, 0, buffer.Length);
        }

        using (ProcessEx process = await ProcessEx.StartAsync(new ProcessStartInfo("notepad.exe", fileName)))
        {
            bool exited;
            do
            {
                Console.WriteLine("Waiting for process to exit...");
                exited = await process.WaitForExitAsync(TimeSpan.FromSeconds(1.0d));
            }
            while (!exited);
        }

        Console.WriteLine("Done.");
    }

    private static FileStream CreateAsyncStream(string fileName)
    {
        return new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Read, 65536);
    }
}

2 thoughts on “Providing async functionality for System.Diagnostics.Process

  1. badcom
    bool exited;
    do
    {
        Console.WriteLine("Waiting for process to exit...");
        exited = await process.WaitForExitAsync(TimeSpan.FromSeconds(1.0d));
    }
    while (!exited);
    

    ***************
    This is bad!

Leave a Reply

Your email address will not be published. Required fields are marked *