diff --git a/SlipeServer.Server.Tests/Integration/Elements/PlayerTests.cs b/SlipeServer.Server.Tests/Integration/Elements/PlayerTests.cs index 43a603da..6dfe1f0d 100644 --- a/SlipeServer.Server.Tests/Integration/Elements/PlayerTests.cs +++ b/SlipeServer.Server.Tests/Integration/Elements/PlayerTests.cs @@ -5,8 +5,12 @@ using SlipeServer.Server.Loggers; using SlipeServer.Server.PacketHandling.Handlers.Player; using SlipeServer.Server.PacketHandling.Handlers.QueueHandlers; +using SlipeServer.Server.Resources; using SlipeServer.Server.TestTools; using System.Linq; +using System; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace SlipeServer.Server.Tests.Integration.Elements; @@ -67,4 +71,71 @@ public void KickingPlayerShouldDestroyAndDisconnectPlayer() .Select(x => x.EventName) .Should().BeEquivalentTo(["Kicked", "Disconnected", "Destroyed"]); } + + [Fact] + public async Task StartForAsyncShouldBeCancellable() + { + var server = new TestingServer(); + var player = server.AddFakePlayer(); + + var resource = new Resource(server, server.RootElement, "test"); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var act = async () => await resource.StartForAsync(player, cts.Token); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task StartForAsyncShouldBeCancellable2() + { + var server = new TestingServer(); + var player = server.AddFakePlayer(); + + var resource = new Resource(server, server.RootElement, "test"); + + var cts = new CancellationTokenSource(); + + var act = async () => await resource.StartForAsync(player, cts.Token); + + var waitHandle = new ManualResetEvent(false); + var cancelled = false; + var _ = Task.Run(async () => + { + await act.Should().ThrowAsync(); + waitHandle.Set(); + cancelled = true; + }); + + await Task.Delay(2000); // Let StartForAsync execute for a while to block on "await source.Task" + await cts.CancelAsync(); + waitHandle.WaitOne(TimeSpan.FromSeconds(5)); + cancelled.Should().BeTrue(); + } + + [Fact] + public void ResourceShouldStopStartingWhenPlayerDisconnectFromTheServer() + { + var server = new TestingServer(); + var player = server.AddFakePlayer(); + var resource = new Resource(server, server.RootElement, "test"); + var cts = new CancellationTokenSource(); + + var act = async () => await resource.StartForAsync(player, cts.Token); + + var waitHandle = new ManualResetEvent(false); + var cancelled = false; + var _ = Task.Run(async () => + { + await act.Should().ThrowAsync(); + waitHandle.Set(); + cancelled = true; + }); + + player.Kick(); + waitHandle.WaitOne(TimeSpan.FromSeconds(5)); + cancelled.Should().BeTrue(); + } } diff --git a/SlipeServer.Server/Resources/Resource.cs b/SlipeServer.Server/Resources/Resource.cs index cb6a84f9..3281251d 100644 --- a/SlipeServer.Server/Resources/Resource.cs +++ b/SlipeServer.Server/Resources/Resource.cs @@ -3,6 +3,7 @@ using SlipeServer.Server.Elements; using SlipeServer.Server.Elements.Events; using SlipeServer.Server.Extensions; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -83,46 +84,62 @@ public void StartFor(Player player) .SendTo(player); } - public Task StartForAsync(Player player, CancellationToken cancelationToken = default) + public async Task StartForAsync(Player player, CancellationToken cancelationToken = default) { - cancelationToken.ThrowIfCancellationRequested(); + if (player.IsDestroyed || !player.Client.IsConnected) + throw new InvalidOperationException("Player is not connected to the server."); + using var cts = new CancellationTokenSource(300_000); + using var cts2 = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancelationToken); var source = new TaskCompletionSource(); - cancelationToken.Register(() => + cts2.Token.Register(() => { - player.ResourceStarted -= HandleResourceStart; - player.Disconnected -= HandlePlayerDisconnected; + source.SetException(new TaskCanceledException()); }); - player.ResourceStarted += HandleResourceStart; - player.Disconnected += HandlePlayerDisconnected; - void HandleResourceStart(Player sender, PlayerResourceStartedEventArgs e) { - if (e.NetId != this.NetId) + if (e.NetId != this.NetId || cts2.Token.IsCancellationRequested) return; - player.ResourceStarted -= HandleResourceStart; - player.Disconnected -= HandlePlayerDisconnected; - source.SetResult(); } void HandlePlayerDisconnected(Player disconnectingPlayer, PlayerQuitEventArgs e) { - if(player != disconnectingPlayer) + if(player != disconnectingPlayer || cts2.Token.IsCancellationRequested) return; - player.ResourceStarted -= HandleResourceStart; - player.Disconnected -= HandlePlayerDisconnected; + source.SetException(new Exception("Player disconnected.")); + } + + void HandleDestroed(Element destroyedElement) + { + if(player != destroyedElement || cts2.Token.IsCancellationRequested) + return; - source.SetException(new System.Exception("Player disconnected.")); + source.SetException(new Exception("Player destroyed.")); } + cts2.Token.ThrowIfCancellationRequested(); + + player.ResourceStarted += HandleResourceStart; + player.Disconnected += HandlePlayerDisconnected; + player.Destroyed += HandleDestroed; + StartFor(player); - return source.Task; + try + { + await source.Task; + } + finally + { + player.ResourceStarted -= HandleResourceStart; + player.Disconnected -= HandlePlayerDisconnected; + player.Destroyed -= HandleDestroed; + } } public void StopFor(Player player)