Skip to content

Commit

Permalink
Implement some more extension functions
Browse files Browse the repository at this point in the history
  • Loading branch information
sbeca committed Nov 6, 2023
1 parent ef3f0c4 commit 7b41188
Show file tree
Hide file tree
Showing 10 changed files with 502 additions and 4 deletions.
12 changes: 12 additions & 0 deletions api_list.include.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
* `Boolean TryFormat(Span<Char>, Int32&, ReadOnlySpan<Char>, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.decimal.tryformat)


### Process

* `Task WaitForExitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync)


### Double

* `Boolean TryFormat(Span<Char>, Int32&, ReadOnlySpan<Char>, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.double.tryformat)
Expand Down Expand Up @@ -208,6 +213,13 @@
* `Boolean Equals(ReadOnlySpan<Char>)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.equals#system-text-stringbuilder-equals(system-readonlyspan((system-char))))


### CancellationToken

* `CancellationTokenRegistration Register(Action<Object,CancellationToken>, Object)` [reference](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))
* `CancellationTokenRegistration UnsafeRegister(Action<Object>, Object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object))-system-object))
* `CancellationTokenRegistration UnsafeRegister(Action<Object,CancellationToken>, Object)` [reference](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))


### CancellationTokenSource

* `Task CancelAsync()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.cancelasync)
Expand Down
12 changes: 12 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,11 @@ The class `PolyfillExtensions` includes the following extension methods:
* `Boolean TryFormat(Span<Char>, Int32&, ReadOnlySpan<Char>, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.decimal.tryformat)


### Process

* `Task WaitForExitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync)


### Double

* `Boolean TryFormat(Span<Char>, Int32&, ReadOnlySpan<Char>, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.double.tryformat)
Expand Down Expand Up @@ -563,6 +568,13 @@ The class `PolyfillExtensions` includes the following extension methods:
* `Boolean Equals(ReadOnlySpan<Char>)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.equals#system-text-stringbuilder-equals(system-readonlyspan((system-char))))


### CancellationToken

* `CancellationTokenRegistration Register(Action<Object,CancellationToken>, Object)` [reference](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))
* `CancellationTokenRegistration UnsafeRegister(Action<Object>, Object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object))-system-object))
* `CancellationTokenRegistration UnsafeRegister(Action<Object,CancellationToken>, Object)` [reference](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))


### CancellationTokenSource

* `Task CancelAsync()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.cancelasync)
Expand Down
14 changes: 14 additions & 0 deletions src/Consume/Consume.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ async Task CancellationTokenSource()
await source.CancelAsync();
}

void CancellationTokenUnsafeRegister()
{
var source = new CancellationTokenSource();
var token = source.Token;
token.UnsafeRegister((state) => {}, null);
token.UnsafeRegister((state, token) => {}, null);
}

async Task ProcessWaitForExitAsync()
{
var process = new Process();
await process.WaitForExitAsync();
}

async Task StreamReaderReadToEndAsync()
{
var reader = new StreamReader(new MemoryStream());
Expand Down
137 changes: 137 additions & 0 deletions src/Polyfill/PolyfillExtensions_CancellationToken.cs
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
}
41 changes: 39 additions & 2 deletions src/Polyfill/PolyfillExtensions_CancellationTokenSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,48 @@ static partial class PolyfillExtensions
{
#if !NET8_0_OR_GREATER

/// <summary>Communicates a request for cancellation asynchronously.</summary>
/// <remarks>
/// <para>
/// The associated <see cref="CancellationToken" /> will be notified of the cancellation
/// and will synchronously transition to a state where <see cref="CancellationToken.IsCancellationRequested"/> returns true.
/// Any callbacks or cancelable operations registered with the <see cref="CancellationToken"/> will be executed asynchronously,
/// with the returned <see cref="Task"/> representing their eventual completion.
/// </para>
/// <para>
/// Callbacks registered with the token should not throw exceptions.
/// However, any such exceptions that are thrown will be aggregated into an <see cref="AggregateException"/>,
/// such that one callback throwing an exception will not prevent other registered callbacks from being executed.
/// </para>
/// <para>
/// The <see cref="ExecutionContext"/> that was captured when each callback was registered
/// will be reestablished when the callback is invoked.
/// </para>
/// </remarks>
/// <exception cref="ObjectDisposedException">This <see cref="CancellationTokenSource"/> has been disposed.</exception>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.cancelasync")]
public static Task CancelAsync(this CancellationTokenSource target)
{
target.Cancel();
return Task.CompletedTask;
if (target.IsCancellationRequested)
{
// If cancellation has already been requested then we can just return immediately
return Task.CompletedTask;
}
else
{
// Run sync Cancel call in Task to avoid possible deadlock.
// As an example, the CancellationTokenSource_CancelAsync_CallbacksInvokedAsynchronously test
// will hit a deadlock if we try to just call Cancel directly without it being run in a task
Task task = Task.Run(() => target.Cancel());

while (!target.IsCancellationRequested)
{
// Don't return until we know that the cancellation request has started, to match the
// state that the real implemenation for CancelAsync would be in after being called
}

return task;
}
}

#endif
Expand Down
117 changes: 117 additions & 0 deletions src/Polyfill/PolyfillExtensions_Process.cs
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
5 changes: 3 additions & 2 deletions src/Tests/BuildApiTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class BuildApiTest
{
static string[] namespacesToClean =
{
"System.Diagnostics.",
"System.Collections.Generic.",
"System.Threading.Tasks.",
"System.Threading.",
Expand All @@ -17,8 +18,8 @@ class BuildApiTest
public void Run()
{
var solutionDirectory = SolutionDirectoryFinder.Find();
var path = Path.Combine(solutionDirectory, @"Consume\bin\Debug\netstandard2.0\Consume.dll");
var md = Path.Combine(solutionDirectory, @"..\api_list.include.md");
var path = Path.Combine(solutionDirectory, "Consume", "bin", "Debug", "netstandard2.0", "Consume.dll");
var md = Path.Combine(solutionDirectory, "..", "api_list.include.md");
File.Delete(md);
using var module = Mono.Cecil.ModuleDefinition.ReadModule(path);
var extensions = module.GetTypes().Single(_ => _.Name == nameof(PolyfillExtensions));
Expand Down
Loading

0 comments on commit 7b41188

Please sign in to comment.