diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/TaskCompletionSourceWithCancellation.cs b/src/libraries/Common/src/System/Threading/Tasks/TaskCompletionSourceWithCancellation.cs
similarity index 73%
rename from src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/TaskCompletionSourceWithCancellation.cs
rename to src/libraries/Common/src/System/Threading/Tasks/TaskCompletionSourceWithCancellation.cs
index 4d2cba1a6750e8..a5d667e7180840 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/TaskCompletionSourceWithCancellation.cs
+++ b/src/libraries/Common/src/System/Threading/Tasks/TaskCompletionSourceWithCancellation.cs
@@ -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
{
+ ///
+ /// A that supports cancellation registration so that any
+ /// s contain the relevant ,
+ /// while also avoiding unnecessary allocations for closure captures.
+ ///
internal class TaskCompletionSourceWithCancellation : TaskCompletionSource
{
private CancellationToken _cancellationToken;
diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
index 6d411cac3506fe..67e2d5151ece5a 100644
--- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
+++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
@@ -124,6 +124,7 @@ public void Refresh() { }
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; }
}
diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
index 511c111c15bc07..cbbb5cb4d5085e 100644
--- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
+++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
@@ -44,6 +44,9 @@
Common\Interop\Windows\Interop.Errors.cs
+
+ Common\System\Threading\Tasks\TaskCompletionSourceWithCancellation.cs
+
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
index e4de4f76f4f1d0..46c23d33bc8c8c 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
@@ -10,6 +10,7 @@
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
+using System.Threading.Tasks;
namespace System.Diagnostics
{
@@ -1340,6 +1341,107 @@ public bool WaitForExit(int milliseconds)
return exited;
}
+ ///
+ /// Instructs the Process component to wait for the associated process to exit, or
+ /// for the to be canceled.
+ ///
+ ///
+ /// Calling this method will set to .
+ ///
+ ///
+ /// A task that will complete when the process has exited, cancellation has been requested,
+ /// or an error occurs.
+ ///
+ 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