using System.Runtime.Serialization;
using System.Text;
using System.Threading;
+using System.Threading.Tasks;
namespace System.Diagnostics
{
return exited;
}
+ /// <summary>
+ /// Instructs the Process component to wait for the associated process to exit, or
+ /// for the <paramref name="cancellationToken"/> to be canceled.
+ /// </summary>
+ /// <remarks>
+ /// Calling this method will set <see cref="EnableRaisingEvents"/> to <see langword="true" />.
+ /// </remarks>
+ /// <returns>
+ /// A task that will complete when the process has exited, cancellation has been requested,
+ /// or an error occurs.
+ /// </returns>
+ public async Task WaitForExitAsync(CancellationToken cancellationToken = default)
+ {
+ // Because the process has already started by the time this method is called,
+ // we're in a race against the process to set up our exit handlers before the process
+ // exits. As a result, there are several different flows that must be handled:
+ //
+ // CASE 1: WE ENABLE EVENTS
+ // This is the "happy path". In this case we enable events.
+ //
+ // CASE 1.1: PROCESS EXITS OR IS CANCELED AFTER REGISTERING HANDLER
+ // This case continues the "happy path". The process exits or waiting is canceled after
+ // registering the handler and no special cases are needed.
+ //
+ // CASE 1.2: PROCESS EXITS BEFORE REGISTERING HANDLER
+ // It's possible that the process can exit after we enable events but before we reigster
+ // the handler. In that case we must check for exit after registering the handler.
+ //
+ //
+ // CASE 2: PROCESS EXITS BEFORE ENABLING EVENTS
+ // The process may exit before we attempt to enable events. In that case EnableRaisingEvents
+ // will throw an exception like this:
+ // System.InvalidOperationException : Cannot process request because the process (42) has exited.
+ // In this case we catch the InvalidOperationException. If the process has exited, our work
+ // is done and we return. If for any reason (now or in the future) enabling events fails
+ // and the process has not exited, bubble the exception up to the user.
+ //
+ //
+ // CASE 3: USER ALREADY ENABLED EVENTS
+ // In this case the user has already enabled raising events. Re-enabling events is a no-op
+ // as the value hasn't changed. However, no-op also means that if the process has already
+ // exited, EnableRaisingEvents won't throw an exception.
+ //
+ // CASE 3.1: PROCESS EXITS OR IS CANCELED AFTER REGISTERING HANDLER
+ // (See CASE 1.1)
+ //
+ // CASE 3.2: PROCESS EXITS BEFORE REGISTERING HANDLER
+ // (See CASE 1.2)
+
+ if (!Associated)
+ {
+ throw new InvalidOperationException(SR.NoAssociatedProcess);
+ }
+
+ if (!HasExited)
+ {
+ // Early out for cancellation before doing more expensive work
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ try
+ {
+ // CASE 1: We enable events
+ // CASE 2: Process exits before enabling events (and throws an exception)
+ // CASE 3: User already enabled events (no-op)
+ EnableRaisingEvents = true;
+ }
+ catch (InvalidOperationException)
+ {
+ // CASE 2: If the process has exited, our work is done, otherwise bubble the
+ // exception up to the user
+ if (HasExited)
+ {
+ return;
+ }
+
+ throw;
+ }
+
+ var tcs = new TaskCompletionSourceWithCancellation<object>();
+
+ EventHandler handler = (s, e) => tcs.TrySetResult(null);
+ Exited += handler;
+
+ try
+ {
+ if (HasExited)
+ {
+ // CASE 1.2 & CASE 3.2: Handle race where the process exits before registering the handler
+ return;
+ }
+
+ // CASE 1.1 & CASE 3.1: Process exits or is canceled here
+ await tcs.WaitWithCancellationAsync(cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ Exited -= handler;
+ }
+ }
+
/// <devdoc>
/// <para>
/// Instructs the <see cref='System.Diagnostics.Process'/> component to start
}
[Fact]
+ public async Task MultipleProcesses_StartAllKillAllWaitAllAsync()
+ {
+ const int Iters = 10;
+ Process[] processes = Enumerable.Range(0, Iters).Select(_ => CreateProcessLong()).ToArray();
+
+ foreach (Process p in processes) p.Start();
+ foreach (Process p in processes) p.Kill();
+ foreach (Process p in processes)
+ {
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await p.WaitForExitAsync(cts.Token);
+ Assert.True(p.HasExited);
+ }
+ }
+ }
+
+ [Fact]
public void MultipleProcesses_SerialStartKillWait()
{
const int Iters = 10;
Process p = CreateProcessLong();
p.Start();
p.Kill();
- p.WaitForExit(WaitInMS);
+ Assert.True(p.WaitForExit(WaitInMS));
+ }
+ }
+
+ [Fact]
+ public async Task MultipleProcesses_SerialStartKillWaitAsync()
+ {
+ const int Iters = 10;
+ for (int i = 0; i < Iters; i++)
+ {
+ Process p = CreateProcessLong();
+ p.Start();
+ p.Kill();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await p.WaitForExitAsync(cts.Token);
+ Assert.True(p.HasExited);
+ }
}
}
Task.WaitAll(Enumerable.Range(0, Tasks).Select(_ => Task.Run(work)).ToArray());
}
+ [Fact]
+ public async Task MultipleProcesses_ParallelStartKillWaitAsync()
+ {
+ const int Tasks = 4, ItersPerTask = 10;
+ Func<Task> work = async () =>
+ {
+ for (int i = 0; i < ItersPerTask; i++)
+ {
+ Process p = CreateProcessLong();
+ p.Start();
+ p.Kill();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await p.WaitForExitAsync(cts.Token);
+ Assert.True(p.HasExited);
+ }
+ }
+ };
+
+ await Task.WhenAll(Enumerable.Range(0, Tasks).Select(_ => Task.Run(work)).ToArray());
+ }
+
[Theory]
[InlineData(0)] // poll
[InlineData(10)] // real timeout
Assert.False(Process.GetCurrentProcess().WaitForExit(milliseconds));
}
+ [Theory]
+ [InlineData(0)] // poll
+ [InlineData(10)] // real timeout
+ public async Task CurrentProcess_WaitAsyncNeverCompletes(int milliseconds)
+ {
+ using (var cts = new CancellationTokenSource(milliseconds))
+ {
+ CancellationToken token = cts.Token;
+ Process process = Process.GetCurrentProcess();
+ OperationCanceledException ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => process.WaitForExitAsync(token));
+ Assert.Equal(token, ex.CancellationToken);
+ Assert.False(process.HasExited);
+ }
+ }
+
[Fact]
public void SingleProcess_TryWaitMultipleTimesBeforeCompleting()
{
Assert.True(p.WaitForExit(0));
}
+ [Fact]
+ public async Task SingleProcess_TryWaitAsyncMultipleTimesBeforeCompleting()
+ {
+ Process p = CreateProcessLong();
+ p.Start();
+
+ // Verify we can try to wait for the process to exit multiple times
+
+ // First test with an already canceled token. Because the token is already canceled,
+ // WaitForExitAsync should complete synchronously
+ for (int i = 0; i < 2; i++)
+ {
+ var token = new CancellationToken(canceled: true);
+ Task t = p.WaitForExitAsync(token);
+
+ Assert.Equal(TaskStatus.Canceled, t.Status);
+
+ OperationCanceledException ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => t);
+ Assert.Equal(token, ex.CancellationToken);
+ Assert.False(p.HasExited);
+ }
+
+ // Next, test with a token that is canceled after the task is created to
+ // exercise event hookup and async cancellation
+ using (var cts = new CancellationTokenSource())
+ {
+ CancellationToken token = cts.Token;
+ Task t = p.WaitForExitAsync(token);
+ cts.Cancel();
+
+ OperationCanceledException ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => t);
+ Assert.Equal(token, ex.CancellationToken);
+ Assert.False(p.HasExited);
+ }
+
+ // Then wait until it exits and concurrently kill it.
+ // There's a race condition here, in that we really want to test
+ // killing it while we're waiting, but we could end up killing it
+ // before hand, in which case we're simply not testing exactly
+ // what we wanted to test, but everything should still work.
+ _ = Task.Delay(10).ContinueWith(_ => p.Kill());
+
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await p.WaitForExitAsync(cts.Token);
+ Assert.True(p.HasExited);
+ }
+
+ // Waiting on an already exited process should complete synchronously
+ Assert.True(p.HasExited);
+ Task task = p.WaitForExitAsync();
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ }
+
[Theory]
[InlineData(false)]
[InlineData(true)]
}
[Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task SingleProcess_WaitAsyncAfterExited(bool addHandlerBeforeStart)
+ {
+ Process p = CreateProcessLong();
+ p.EnableRaisingEvents = true;
+
+ var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
+ if (addHandlerBeforeStart)
+ {
+ p.Exited += delegate
+ { tcs.SetResult(true); };
+ }
+ p.Start();
+ if (!addHandlerBeforeStart)
+ {
+ p.Exited += delegate
+ { tcs.SetResult(true); };
+ }
+
+ p.Kill();
+ Assert.True(await tcs.Task);
+
+ var token = new CancellationToken(canceled: true);
+ await p.WaitForExitAsync(token);
+ Assert.True(p.HasExited);
+
+ await p.WaitForExitAsync();
+ Assert.True(p.HasExited);
+ }
+
+ [Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(127)]
}
[Fact]
+ public async Task SingleProcess_CopiesShareExitAsyncInformation()
+ {
+ using Process p = CreateProcessLong();
+ p.Start();
+
+ Process[] copies = Enumerable.Range(0, 3).Select(_ => Process.GetProcessById(p.Id)).ToArray();
+
+ using (var cts = new CancellationTokenSource(millisecondsDelay: 0))
+ {
+ CancellationToken token = cts.Token;
+ OperationCanceledException ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => p.WaitForExitAsync(token));
+ Assert.Equal(token, ex.CancellationToken);
+ Assert.False(p.HasExited);
+ }
+ p.Kill();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await p.WaitForExitAsync(cts.Token);
+ Assert.True(p.HasExited);
+ }
+
+ using (var cts = new CancellationTokenSource(millisecondsDelay: 0))
+ {
+ foreach (Process copy in copies)
+ {
+ // Since the process has already exited, waiting again does not throw (even if the token is canceled) because
+ // there's no work to do.
+ await copy.WaitForExitAsync(cts.Token);
+ Assert.True(copy.HasExited);
+ }
+ }
+ }
+
+ [Fact]
public void WaitForPeerProcess()
{
Process child1 = CreateProcessLong();
}
[Fact]
+ public async Task WaitAsyncForPeerProcess()
+ {
+ using Process child1 = CreateProcessLong();
+ child1.Start();
+
+ using Process child2 = CreateProcess(async peerId =>
+ {
+ Process peer = Process.GetProcessById(int.Parse(peerId));
+ Console.WriteLine("Signal");
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await peer.WaitForExitAsync(cts.Token);
+ Assert.True(peer.HasExited);
+ }
+ return RemoteExecutor.SuccessExitCode;
+ }, child1.Id.ToString());
+ child2.StartInfo.RedirectStandardOutput = true;
+ child2.Start();
+ char[] output = new char[6];
+ child2.StandardOutput.Read(output, 0, output.Length);
+ Assert.Equal("Signal", new string(output)); // wait for the signal before killing the peer
+
+ child1.Kill();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await child1.WaitForExitAsync(cts.Token);
+ Assert.True(child1.HasExited);
+ }
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await child2.WaitForExitAsync(cts.Token);
+ Assert.True(child2.HasExited);
+ }
+
+ Assert.Equal(RemoteExecutor.SuccessExitCode, child2.ExitCode);
+ }
+
+ [Fact]
public void WaitForSignal()
{
- const string expectedSignal = "Signal";
- const string successResponse = "Success";
+ const string ExpectedSignal = "Signal";
+ const string SuccessResponse = "Success";
const int timeout = 30 * 1000; // 30 seconds, to allow for very slow machines
Process p = CreateProcessPortable(RemotelyInvokable.WriteLineReadLine);
{
linesReceived++;
- if (e.Data == expectedSignal)
+ if (e.Data == ExpectedSignal)
{
mre.Set();
}
using (StreamWriter writer = p.StandardInput)
{
- writer.WriteLine(successResponse);
+ writer.WriteLine(SuccessResponse);
}
Assert.True(p.WaitForExit(timeout), "Process has not exited");
}
[Fact]
+ public async Task WaitAsyncForSignal()
+ {
+ const string expectedSignal = "Signal";
+ const string successResponse = "Success";
+ const int timeout = 5 * 1000;
+
+ using Process p = CreateProcessPortable(RemotelyInvokable.WriteLineReadLine);
+ p.StartInfo.RedirectStandardInput = true;
+ p.StartInfo.RedirectStandardOutput = true;
+ using var mre = new ManualResetEventSlim(false);
+
+ int linesReceived = 0;
+ p.OutputDataReceived += (s, e) =>
+ {
+ if (e.Data != null)
+ {
+ linesReceived++;
+
+ if (e.Data == expectedSignal)
+ {
+ mre.Set();
+ }
+ }
+ };
+
+ p.Start();
+ p.BeginOutputReadLine();
+
+ Assert.True(mre.Wait(timeout));
+ Assert.Equal(1, linesReceived);
+
+ // Wait a little bit to make sure process didn't exit on itself
+ Thread.Sleep(1);
+ Assert.False(p.HasExited, "Process has prematurely exited");
+
+ using (StreamWriter writer = p.StandardInput)
+ {
+ writer.WriteLine(successResponse);
+ }
+
+ using (var cts = new CancellationTokenSource(timeout))
+ {
+ await p.WaitForExitAsync(cts.Token);
+ Assert.True(p.HasExited, "Process has not exited");
+ }
+ Assert.Equal(RemotelyInvokable.SuccessExitCode, p.ExitCode);
+ }
+
+ [Fact]
public void WaitChain()
{
Process root = CreateProcess(() =>
}
[Fact]
+ public async Task WaitAsyncChain()
+ {
+ Process root = CreateProcess(async () =>
+ {
+ Process child1 = CreateProcess(async () =>
+ {
+ Process child2 = CreateProcess(async () =>
+ {
+ Process child3 = CreateProcess(() => RemoteExecutor.SuccessExitCode);
+ child3.Start();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await child3.WaitForExitAsync(cts.Token);
+ Assert.True(child3.HasExited);
+ }
+
+ return child3.ExitCode;
+ });
+ child2.Start();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await child2.WaitForExitAsync(cts.Token);
+ Assert.True(child2.HasExited);
+ }
+
+ return child2.ExitCode;
+ });
+ child1.Start();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await child1.WaitForExitAsync(cts.Token);
+ Assert.True(child1.HasExited);
+ }
+
+ return child1.ExitCode;
+ });
+ root.Start();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await root.WaitForExitAsync(cts.Token);
+ Assert.True(root.HasExited);
+ }
+ Assert.Equal(RemoteExecutor.SuccessExitCode, root.ExitCode);
+ }
+
+ [Fact]
public void WaitForSelfTerminatingChild()
{
Process child = CreateProcessPortable(RemotelyInvokable.SelfTerminate);
}
[Fact]
+ public async Task WaitAsyncForSelfTerminatingChild()
+ {
+ Process child = CreateProcessPortable(RemotelyInvokable.SelfTerminate);
+ child.Start();
+ using (var cts = new CancellationTokenSource(WaitInMS))
+ {
+ await child.WaitForExitAsync(cts.Token);
+ Assert.True(child.HasExited);
+ }
+ Assert.NotEqual(RemoteExecutor.SuccessExitCode, child.ExitCode);
+ }
+
+ [Fact]
+ public async Task WaitAsyncForProcess()
+ {
+ Process p = CreateSleepProcess(WaitInMS);
+ p.Start();
+
+ Task processTask = p.WaitForExitAsync();
+ Task delayTask = Task.Delay(WaitInMS * 2);
+
+ Task result = await Task.WhenAny(processTask, delayTask);
+ Assert.Equal(processTask, result);
+ Assert.True(p.HasExited);
+ }
+
+ [Fact]
public void WaitForInputIdle_NotDirected_ThrowsInvalidOperationException()
{
var process = new Process();
var process = new Process();
Assert.Throws<InvalidOperationException>(() => process.WaitForExit());
}
+
+ [Fact]
+ public async Task WaitForExitAsync_NotDirected_ThrowsInvalidOperationException()
+ {
+ var process = new Process();
+ await Assert.ThrowsAsync<InvalidOperationException>(() => process.WaitForExitAsync());
+ }
}
}