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:
- The
Exited
event may never be raised since the process could have already exited by the time we subscribe. - The check of
HasExited
may occur at the same time theOnProcessExited
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); } }
***************
This is bad!
I’m not sure I follow… can you elaborate? I do agree that this code in general could be better designed, which is what I wrote about in my follow up to this post, Async Process functionality – take 2.