Skip to content

Commit

Permalink
Fix ToSignal to throw OperationCanceledException
Browse files Browse the repository at this point in the history
  • Loading branch information
Atlinx committed Mar 28, 2024
1 parent f698a1f commit 7da860a
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 80 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ public Test : Node
var cts = new CancellationTokenSource();
WaitAndEmitMySignal(TimeSpan.FromSeconds(2)).Forget();
WaitAndCancelToken(TimeSpan.FromSeconds(1), cts).Forget();
var signalResults = await GDTask.ToSignal(this, nameof(MySignal), cts.Token);
// signalResults = null, since we cancelled awaiting the signal before the signal could fire.
try
{
var signalResults = await GDTask.ToSignal(this, nameof(MySignal), cts.Token);
}
catch (OperationCanceledException _)
{
GD.Print("Awaiting MySignal cancelled!");
}

// Waiting a single frame
await GDTask.Yield();
Expand Down
84 changes: 10 additions & 74 deletions addons/GDTask/GDTask.ToSignal.cs
Original file line number Diff line number Diff line change
@@ -1,89 +1,25 @@
using Godot;
using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Fractural.Tasks
{
/// <summary>
/// A cancellable signal awaiter that wraps the Godot <see cref="SignalAwaiter"/>. Using ToSignal
/// with an additional <see cref="CancellationToken"/> as parameter automatically returns this awaiter.
/// See <see cref="GodotObjectExtensions.ToSignal(GodotObject, GodotObject, StringName, CancellationToken)"/>.
///
/// Originally from <see href="https://github.com/altamkp/GodotEx/blob/master/src/GodotEx.Async/src/Core/CancellableSignalAwaiter.cs">GodotEx</see>
/// </summary>
public class CancellableSignalAwaiter : IAwaiter<Variant[]>, INotifyCompletion, IAwaitable<Variant[]>
{
private readonly SignalAwaiter _signalAwaiter;
private readonly CancellationToken _cancellationToken;
private readonly CancellationTokenRegistration _cancellationTokenRegistration;

private Action? _continuation;
private bool _isCancelled;

/// <summary>
/// Creates a new <see cref="CancellableSignalAwaiter"/> that wraps the Godot <see cref="SignalAwaiter"/>.
/// </summary>
/// <param name="signalAwaiter">Godot <see cref="SignalAwaiter"/>.</param>
/// <param name="cancellationToken">Cancellation token for cancellation request.</param>
public CancellableSignalAwaiter(SignalAwaiter signalAwaiter, CancellationToken cancellationToken)
{
_signalAwaiter = signalAwaiter;
_cancellationToken = cancellationToken;
_cancellationTokenRegistration = _cancellationToken.Register(() =>
{
_cancellationTokenRegistration.Dispose();
_isCancelled = true;
OnAwaiterCompleted();
});
}

/// <summary>
/// Completion status of the awaiter. True if canceled or if signal is emitted.
/// </summary>
public bool IsCompleted => _isCancelled || _signalAwaiter.IsCompleted;

/// <summary>
/// Registers action delegate upon completion.
/// </summary>
/// <param name="continuation">Action delegate up completion.</param>
public void OnCompleted(Action continuation)
{
_continuation = continuation;
_signalAwaiter.OnCompleted(OnAwaiterCompleted);
}

/// <summary>
/// Returns current awaiter as <see cref="IAwaiter"/> that can be used with the await keyword.
/// </summary>
/// <returns>Current awaiter as <see cref="IAwaiter"/>.</returns>
public IAwaiter<Variant[]> GetAwaiter() => this;

/// <summary>
/// Returns result upon completion.
/// </summary>
/// <returns>Result upon completion.</returns>
public Variant[] GetResult() => _signalAwaiter.GetResult();

private void OnAwaiterCompleted()
{
var continuation = _continuation;
_continuation = null;
continuation?.Invoke();
}
}

public partial struct GDTask
{
public static async GDTask<Variant[]> ToSignal(GodotObject self, string signal)
public static async GDTask<Variant[]> ToSignal(GodotObject self, StringName signal)
{
return await self.ToSignal(self, signal);
}

public static async GDTask<Variant[]> ToSignal(GodotObject self, string signal, CancellationToken ct)
public static async GDTask<Variant[]> ToSignal(GodotObject self, StringName signal, CancellationToken ct)
{
var cancellableSignalAwaiter = new CancellableSignalAwaiter(self.ToSignal(self, signal), ct);
return await cancellableSignalAwaiter;
var tcs = new GDTaskCompletionSource<Variant[]>();
ct.Register(() => tcs.TrySetCanceled(ct));
Create(async () =>
{
var result = await self.ToSignal(self, signal);
tcs.TrySetResult(result);
}).Forget();
return await tcs.Task;
}
}
}
22 changes: 18 additions & 4 deletions tests/manual/Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,29 @@ private async GDTaskVoid Run()
GD.Print("Run: Await Cancellable MyEmptySignal");
WaitAndEmitMyEmptySignal(TimeSpan.FromSeconds(3)).Forget();
WaitAndCancelToken(TimeSpan.FromSeconds(0.5), cts).Forget();
signalResult = await GDTask.ToSignal(this, nameof(MyEmptySignal), cts.Token);
GD.Print("Run: Await Cancellable MyEmptySignal Cancelled, result: ", signalResult);
try
{
signalResult = await GDTask.ToSignal(this, nameof(MyEmptySignal), cts.Token);
GD.Print("Run: Await Cancellable MyEmptySignal ran with result: ", signalResult);
}
catch (OperationCanceledException _)
{
GD.Print("Run: Await Cancellable MyEmptySignal Cancelled");
}

cts = new CancellationTokenSource();
GD.Print("Run: Await Cancellable MyArgSignal");
WaitAndEmitMyArgSignal(TimeSpan.FromSeconds(3)).Forget();
WaitAndCancelToken(TimeSpan.FromSeconds(0.5), cts).Forget();
signalResult = await GDTask.ToSignal(this, nameof(MyArgSignal), cts.Token);
GD.Print("Run: Await Cancellable MyArgSignal Cancelled, result: ", signalResult);
try
{
signalResult = await GDTask.ToSignal(this, nameof(MyArgSignal), cts.Token);
GD.Print("Run: Await Cancellable MyArgSignal ran with result: ", signalResult);
}
catch (OperationCanceledException _)
{
GD.Print("Run: Await Cancellable MyArgSignal Cancelled");
}

GD.Print("Run: Pre RunWithResult");
string runResult = await RunWithResult();
Expand Down

0 comments on commit 7da860a

Please sign in to comment.