You can use the Exited event and the good old ManualResetEvent / ManualResetEventSlim :
class Program { static void Main(string[] args) { var reset1 = new ManualResetEventSlim(); var reset2 = new ManualResetEventSlim(); var p1 = new Process(); p1.StartInfo = new ProcessStartInfo(@"c:\windows\system32\notepad.exe"); p1.EnableRaisingEvents = true; p1.Exited += (s, e) => reset1.Set(); p1.Start(); var p2 = new Process(); p2.StartInfo = new ProcessStartInfo(@"c:\windows\system32\notepad.exe"); p2.EnableRaisingEvents = true; p2.Exited += (s, e) => reset2.Set(); p2.Start(); var onTime = WaitHandle.WaitAll(new[] { reset1.WaitHandle, reset2.WaitHandle }, 10000); Console.WriteLine($"Completed on time: {onTime}"); } }
Also now in the trend to expect using the modern API: wrapping events in TaskCompletionSource (although, of course, sometimes this is too sophisticated). You can use the Exited event, TaskCompletionSource and the Task.WaitAny() method:
class Program { static void Main(string[] args) { var tcs1 = new TaskCompletionSource<byte>(); var tcs2 = new TaskCompletionSource<byte>(); var p1 = new Process(); p1.StartInfo = new ProcessStartInfo(@"c:\windows\system32\notepad.exe"); p1.EnableRaisingEvents = true; p1.Exited += (s, e) => tcs1.SetResult(0); p1.Start(); var p2 = new Process(); p2.StartInfo = new ProcessStartInfo(@"c:\windows\system32\notepad.exe"); p2.EnableRaisingEvents = true; p2.Exited += (s, e) => tcs2.SetResult(0); p2.Start(); var completedTaskIndex = Task.WaitAny(Task.Delay(10000), tcs1.Task, tcs2.Task); Console.WriteLine($"Completed on time: {completedTaskIndex > 0}"); } }