Implement WaitForExitAsync for System.Diagnostics.Process (#1278)
authorMatt Kotsenas <Matt.Kotsenas@gmail.com>
Thu, 13 Feb 2020 02:16:21 +0000 (21:16 -0500)
committerGitHub <noreply@github.com>
Thu, 13 Feb 2020 02:16:21 +0000 (18:16 -0800)
* Add Process.WaitForExitAsync() and associated unit tests

Add a task-based `WaitForExitAsync()` for the `Process` class, and
duplicate existing `WaitForExit()` tests to add async versions.

In order to ensure that the `Exited` event is never lost, it is the
callers responsibility to ensure that `EnableRaisingEvents` is `true`
_prior_ to calling `WaitForExitAsync()`. A new
`InvalidOperationException` is introduced to enforce those semantics.

* Review feedback: Change WaitForExitAsync to return Task

Per review feedback, change `WaitForExitAsync` to return a `Task`
instead of `Task<bool>`. Now, to determine if the process has exited,
callers should check `HasExited` after the await, and cancellation
follows the async pattern of setting canceled on the task.

* Remove asserts on Task properties

Per code review feedback, remove the asserts that verify that the Task
returned by WaitForExitAsync is completed successfully / canceled, as
they're essentially Task tests and not relevant to WaitForExitAsync.

* Fix unit test to not create async void

Per code review feedback, fix the
MultipleProcesses_ParallelStartKillWaitAsync test to make work a
`Func<Task>` instead of an `Action` since we're await-ing it.

* Remove implicit delegate creation for ExitHandler

Per code review feedback, converting ExitHandler from a local function
to an `EventHandler` to avoid the extra delegate allocation to convert
between the types.

* Flow CancellationToken to OperationCanceledExceptions

Per code review, register the `TaskCompletionSource` with the
`CancellationToken` so that if an `OperationCanceledException` is thrown
the relevant token is available on
`OperationCanceledException.CancellationToken`.

To accomplish that the `TaskCompletionSourceWithCancellation` class
that's internal to `System.Net.Http` is moved to Common and consumed in
both places.

Unit tests are also updated to verify that the cancellation token passed
in is the same one returned from
`OperationCanceledException.CancellationToken`.

* Do not require EnableRaisingEvents to already be true

Per code review feedback, update `WaitForExitAsync` to not require the
user to call `EnableRaisingEvents` first.

Setting `EnableRaisingEvents` ourselves introduces the chance for an
exception in the case where the process has already exited, so I've
added a comment to the top of the method to detail the possible paths
through the code and added comment annotations for each case.

Lastly, I updated the unit tests to remove `EnableRaisingEvents` calls.

* Follow style guidelines in ProcessWaitingTests

Per code review feedback, update the new unit tests in
ProcessWaitingTests to follow style guidelines.

* Update tests to follow coding guidelines

Per code review feedback, update tests to follow coding guidelines,
simplify creating cancellation tokens, and verify synchronous completion
of tasks in the exited / canceled case.

* Update WaitForExitAsync to early out in canceled case

Per code review feedback, add an early out for a non-exited process when
canceled.

* Add a test for completion without a cancellation token

Per code review feedback, add a test for a process that completes
normally and doesn't use a canellation token.

* Address PR feedback in xmldocs

Per code review feedback, update the xmldocs for `WaitForExitAsync` to
specify the language keyword for "true", and add a `<returns>` element.

* Update xmldocs per code review feedback

Per code review feedback, update xmldocs to list other conditions that
can cause the function to return.

* Update function guards per code review feedback

Per code review feedback, update the method guards to span multiple
lines to adhere to style guidelines.

* Refactor test to verify async as well as sync cancellation

Per code review feedback, update
SingleProcess_TryWaitAsyncMultipleTimesBeforeCompleting to test both the
case where the token is canceled before creating the Task as well as
after the task is already created.

src/libraries/Common/src/System/Threading/Tasks/TaskCompletionSourceWithCancellation.cs [moved from src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/TaskCompletionSourceWithCancellation.cs with 73% similarity]
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
src/libraries/System.Diagnostics.Process/tests/ProcessTestBase.cs
src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs
src/libraries/System.Net.Http/src/System.Net.Http.csproj

@@ -2,11 +2,13 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace System.Net.Http
+namespace System.Threading.Tasks
 {
+    /// <summary>
+    /// A <see cref="TaskCompletionSource{TResult}"/> that supports cancellation registration so that any
+    /// <seealso cref="OperationCanceledException"/>s contain the relevant <see cref="CancellationToken"/>,
+    /// while also avoiding unnecessary allocations for closure captures.
+    /// </summary>
     internal class TaskCompletionSourceWithCancellation<T> : TaskCompletionSource<T>
     {
         private CancellationToken _cancellationToken;
index 95c3aa4..49cb6c1 100644 (file)
@@ -123,6 +123,7 @@ namespace System.Diagnostics
         public override string ToString() { throw null; }
         public void WaitForExit() { }
         public bool WaitForExit(int milliseconds) { throw null; }
+        public System.Threading.Tasks.Task WaitForExitAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; }
         public bool WaitForInputIdle() { throw null; }
         public bool WaitForInputIdle(int milliseconds) { throw null; }
     }
index 24550f4..c0e27a2 100644 (file)
@@ -42,6 +42,9 @@
     <Compile Include="$(CommonPath)Interop\Windows\Interop.Errors.cs">
       <Link>Common\Interop\Windows\Interop.Errors.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)System\Threading\Tasks\TaskCompletionSourceWithCancellation.cs">
+      <Link>Common\System\Threading\Tasks\TaskCompletionSourceWithCancellation.cs</Link>
+    </Compile>
   </ItemGroup>
   <ItemGroup Condition=" '$(TargetsWindows)' == 'true'">
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.EnumProcessModules.cs">
index 03ab377..074f4ad 100644 (file)
@@ -10,6 +10,7 @@ using System.IO;
 using System.Runtime.Serialization;
 using System.Text;
 using System.Threading;
+using System.Threading.Tasks;
 
 namespace System.Diagnostics
 {
@@ -1346,6 +1347,107 @@ 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
index 9b15ab9..9933c3e 100644 (file)
@@ -60,6 +60,18 @@ namespace System.Diagnostics.Tests
             return p;
         }
 
+        protected Process CreateProcess(Func<Task<int>> method)
+        {
+            Process p = null;
+            using (RemoteInvokeHandle handle = RemoteExecutor.Invoke(method, new RemoteInvokeOptions { Start = false }))
+            {
+                p = handle.Process;
+                handle.Process = null;
+            }
+            AddProcessForDispose(p);
+            return p;
+        }
+
         protected Process CreateProcess(Func<string, int> method, string arg, bool autoDispose = true)
         {
             Process p = null;
index f57a5ce..f48abd4 100644 (file)
@@ -25,6 +25,24 @@ namespace System.Diagnostics.Tests
         }
 
         [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;
@@ -33,7 +51,24 @@ namespace System.Diagnostics.Tests
                 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);
+                }
             }
         }
 
@@ -54,6 +89,28 @@ namespace System.Diagnostics.Tests
             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
@@ -62,6 +119,21 @@ namespace System.Diagnostics.Tests
             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()
         {
@@ -82,6 +154,60 @@ namespace System.Diagnostics.Tests
             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)]
@@ -108,6 +234,38 @@ namespace System.Diagnostics.Tests
         }
 
         [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)]
@@ -144,6 +302,40 @@ namespace System.Diagnostics.Tests
         }
 
         [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();
@@ -170,10 +362,48 @@ namespace System.Diagnostics.Tests
         }
 
         [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);
@@ -188,7 +418,7 @@ namespace System.Diagnostics.Tests
                 {
                     linesReceived++;
 
-                    if (e.Data == expectedSignal)
+                    if (e.Data == ExpectedSignal)
                     {
                         mre.Set();
                     }
@@ -207,7 +437,7 @@ namespace System.Diagnostics.Tests
 
             using (StreamWriter writer = p.StandardInput)
             {
-                writer.WriteLine(successResponse);
+                writer.WriteLine(SuccessResponse);
             }
 
             Assert.True(p.WaitForExit(timeout), "Process has not exited");
@@ -215,6 +445,55 @@ namespace System.Diagnostics.Tests
         }
 
         [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(() =>
@@ -242,6 +521,52 @@ namespace System.Diagnostics.Tests
         }
 
         [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);
@@ -251,6 +576,33 @@ namespace System.Diagnostics.Tests
         }
 
         [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();
@@ -263,5 +615,12 @@ namespace System.Diagnostics.Tests
             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());
+        }
     }
 }
index dc84be1..cbcd8ca 100644 (file)
     <Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpHandler.cs" />
     <Compile Include="System\Net\Http\SocketsHttpHandler\RawConnectionStream.cs" />
     <Compile Include="System\Net\Http\SocketsHttpHandler\RedirectHandler.cs" />
-    <Compile Include="System\Net\Http\SocketsHttpHandler\TaskCompletionSourceWithCancellation.cs" />
     <Compile Include="System\Net\Http\SocketsHttpHandler\FailedProxyCache.cs" />
     <Compile Include="System\Net\Http\SocketsHttpHandler\IMultiWebProxy.cs" />
     <Compile Include="System\Net\Http\SocketsHttpHandler\MultiProxy.cs" />
     <Compile Include="$(CommonPath)System\Net\Mail\WhitespaceReader.cs">
       <Link>Common\System\Net\Mail\WhitespaceReader.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)System\Threading\Tasks\TaskCompletionSourceWithCancellation.cs">
+      <Link>Common\System\Threading\Tasks\TaskCompletionSourceWithCancellation.cs</Link>
+    </Compile>
   </ItemGroup>
   <ItemGroup Condition=" '$(TargetsUnix)' == 'true' ">
     <Compile Include="$(CommonPath)System\StrongToWeakReference.cs">