-
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement some more extension functions
- Loading branch information
Showing
10 changed files
with
502 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// <auto-generated /> | ||
|
||
#nullable enable | ||
|
||
#pragma warning disable | ||
|
||
using System; | ||
using System.Threading; | ||
using Link = System.ComponentModel.DescriptionAttribute; | ||
|
||
static partial class PolyfillExtensions | ||
{ | ||
|
||
#if !NETCOREAPP3_0_OR_GREATER | ||
|
||
/// <summary> | ||
/// Registers a delegate that will be called when this | ||
/// <see cref="CancellationToken">CancellationToken</see> is canceled. | ||
/// </summary> | ||
/// <remarks> | ||
/// <para> | ||
/// If this token is already in the canceled state, the delegate will be run immediately and synchronously. | ||
/// Any exception the delegate generates will be propagated out of this method call. | ||
/// </para> | ||
/// <para> | ||
/// <see cref="ExecutionContext">ExecutionContext</see> is not captured nor flowed | ||
/// to the callback's invocation. | ||
/// </para> | ||
/// </remarks> | ||
/// <param name="callback">The delegate to be executed when the <see cref="CancellationToken">CancellationToken</see> is canceled.</param> | ||
/// <param name="state">The state to pass to the <paramref name="callback"/> when the delegate is invoked. This may be null.</param> | ||
/// <returns>The <see cref="CancellationTokenRegistration"/> instance that can | ||
/// be used to unregister the callback.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception> | ||
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object))-system-object)")] | ||
public static CancellationTokenRegistration UnsafeRegister(this CancellationToken target, Action<object?> callback, object? state) | ||
{ | ||
if (callback is null) | ||
{ | ||
throw new ArgumentNullException(nameof(callback)); | ||
} | ||
|
||
// The main difference between UnsafeRegister and Register appears to be that UnsafeRegister callbacks don't capture and use the execution context. | ||
// So to emulate that here, let's suppress the execution context if needed before calling Register. | ||
// This idea was taken from UniTask and how they implemented their RegisterWithoutCaptureExecutionContext extension methods: | ||
// https://github.com/Cysharp/UniTask/blob/master/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenExtensions.cs | ||
|
||
var restoreFlow = false; | ||
if (!ExecutionContext.IsFlowSuppressed()) | ||
{ | ||
ExecutionContext.SuppressFlow(); | ||
restoreFlow = true; | ||
} | ||
|
||
try | ||
{ | ||
return target.Register(callback, state, false); | ||
} | ||
finally | ||
{ | ||
if (restoreFlow) | ||
{ | ||
ExecutionContext.RestoreFlow(); | ||
} | ||
} | ||
} | ||
|
||
#endif | ||
|
||
#if !NET6_0_OR_GREATER | ||
|
||
/// <summary>Registers a delegate that will be called when this <see cref="CancellationToken">CancellationToken</see> is canceled.</summary> | ||
/// <remarks> | ||
/// If this token is already in the canceled state, the delegate will be run immediately and synchronously. Any exception the delegate | ||
/// generates will be propagated out of this method call. The current <see cref="ExecutionContext">ExecutionContext</see>, if one exists, | ||
/// will be captured along with the delegate and will be used when executing it. The current <see cref="SynchronizationContext"/> is not captured. | ||
/// </remarks> | ||
/// <param name="callback">The delegate to be executed when the <see cref="CancellationToken">CancellationToken</see> is canceled.</param> | ||
/// <param name="state">The state to pass to the <paramref name="callback"/> when the delegate is invoked. This may be null.</param> | ||
/// <returns>The <see cref="CancellationTokenRegistration"/> instance that can be used to unregister the callback.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception> | ||
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.register#system-threading-cancellationtoken-register(system-action((system-object-system-threading-cancellationtoken))-system-object)")] | ||
public static CancellationTokenRegistration Register(this CancellationToken target, Action<object?, CancellationToken> callback, object? state) | ||
{ | ||
if (callback is null) | ||
{ | ||
throw new ArgumentNullException(nameof(callback)); | ||
} | ||
|
||
return target.Register((data) => callback(data, target), state, useSynchronizationContext: false); | ||
} | ||
|
||
/// <summary>Registers a delegate that will be called when this <see cref="CancellationToken">CancellationToken</see> is canceled.</summary> | ||
/// <remarks> | ||
/// If this token is already in the canceled state, the delegate will be run immediately and synchronously. Any exception the delegate | ||
/// generates will be propagated out of this method call. <see cref="ExecutionContext"/> is not captured nor flowed to the callback's invocation. | ||
/// </remarks> | ||
/// <param name="callback">The delegate to be executed when the <see cref="CancellationToken">CancellationToken</see> is canceled.</param> | ||
/// <param name="state">The state to pass to the <paramref name="callback"/> when the delegate is invoked. This may be null.</param> | ||
/// <returns>The <see cref="CancellationTokenRegistration"/> instance that can be used to unregister the callback.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception> | ||
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object-system-threading-cancellationtoken))-system-object)")] | ||
public static CancellationTokenRegistration UnsafeRegister(this CancellationToken target, Action<object?, CancellationToken> callback, object? state) | ||
{ | ||
if (callback is null) | ||
{ | ||
throw new ArgumentNullException(nameof(callback)); | ||
} | ||
|
||
// The main difference between UnsafeRegister and Register appears to be that UnsafeRegister callbacks don't capture and use the execution context. | ||
// So to emulate that here, let's suppress the execution context if needed before calling Register. | ||
// This idea was taken from UniTask and how they implemented their RegisterWithoutCaptureExecutionContext extension methods: | ||
// https://github.com/Cysharp/UniTask/blob/master/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenExtensions.cs | ||
|
||
var restoreFlow = false; | ||
if (!ExecutionContext.IsFlowSuppressed()) | ||
{ | ||
ExecutionContext.SuppressFlow(); | ||
restoreFlow = true; | ||
} | ||
|
||
try | ||
{ | ||
Action<object> internalCallback = (data) => callback(data, target); | ||
return target.Register(internalCallback, state, false); | ||
} | ||
finally | ||
{ | ||
if (restoreFlow) | ||
{ | ||
ExecutionContext.RestoreFlow(); | ||
} | ||
} | ||
} | ||
|
||
#endif | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// <auto-generated /> | ||
|
||
#pragma warning disable | ||
|
||
#if !NET5_0_OR_GREATER | ||
|
||
using System; | ||
using System.Diagnostics; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Link = System.ComponentModel.DescriptionAttribute; | ||
|
||
static partial class PolyfillExtensions | ||
{ | ||
/// <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> | ||
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync")] | ||
public static async Task WaitForExitAsync(this Process target, 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 register | ||
// 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 (!target.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) | ||
target.EnableRaisingEvents = true; | ||
} | ||
catch (InvalidOperationException) | ||
{ | ||
// CASE 2: If the process has exited, our work is done, otherwise bubble the | ||
// exception up to the user | ||
if (target.HasExited) | ||
{ | ||
return; | ||
} | ||
|
||
throw; | ||
} | ||
|
||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
|
||
EventHandler handler = (_, _) => tcs.TrySetResult(null); | ||
target.Exited += handler; | ||
|
||
try | ||
{ | ||
if (target.HasExited) | ||
{ | ||
// CASE 1.2 & CASE 3.2: Handle race where the process exits before registering the handler | ||
} | ||
else | ||
{ | ||
// CASE 1.1 & CASE 3.1: Process exits or is canceled here | ||
using (cancellationToken.UnsafeRegister(static (s, cancellationToken) => ((TaskCompletionSource<object>)s!).TrySetCanceled(cancellationToken), tcs)) | ||
{ | ||
await tcs.Task.ConfigureAwait(false); | ||
} | ||
} | ||
} | ||
finally | ||
{ | ||
target.Exited -= handler; | ||
} | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.