diff --git a/Modules/Websockets/Handler/WebsocketConnection.cs b/Modules/Websockets/Handler/WebsocketConnection.cs index a504f155c..32fa42322 100644 --- a/Modules/Websockets/Handler/WebsocketConnection.cs +++ b/Modules/Websockets/Handler/WebsocketConnection.cs @@ -47,51 +47,47 @@ public sealed class WebsocketConnection : IWebSocketConnection, IWebsocketConnec #region Initialization public WebsocketConnection(ISocket socket, IRequest request, List supportedProtocols, - Action? onOpen, - Action? onClose, - Action? onMessage, - Action? onBinary, - Action? onPing, - Action? onPong, - Action? onError) + Func? onOpen, + Func? onClose, + Func? onMessage, + Func? onBinary, + Func? onPing, + Func? onPong, + Func? onError) { Socket = socket; Request = request; SupportedProtocols = supportedProtocols; - OnOpen = (onOpen != null) ? () => onOpen(this) : () => { }; - OnClose = (onClose != null) ? () => onClose(this) : () => { }; - OnMessage = (onMessage != null) ? x => onMessage(this, x) : x => { }; - OnBinary = (onBinary != null) ? x => onBinary(this, x) : x => { }; - OnPing = (onPing != null) ? x => onPing(this, x) : x => SendPong(x); - OnPong = (onPong != null) ? x => onPong(this, x) : x => { }; - OnError = (onError != null) ? x => onError(this, x) : x => { }; + OnOpen = (onOpen != null) ? () => WebsocketDispatcher.Schedule(() => onOpen(this)) : () => { }; + OnClose = (onClose != null) ? () => WebsocketDispatcher.Schedule(() => onClose(this)) : () => { }; + OnMessage = (onMessage != null) ? x => WebsocketDispatcher.Schedule(() => onMessage(this, x)) : x => { }; + OnBinary = (onBinary != null) ? x => WebsocketDispatcher.Schedule(() => onBinary(this, x)) : x => { }; + OnPing = (onPing != null) ? x => WebsocketDispatcher.Schedule(() => onPing(this, x)) : x => WebsocketDispatcher.Schedule(() => SendPongAsync(x)); + OnPong = (onPong != null) ? x => WebsocketDispatcher.Schedule(() => onPong(this, x)) : x => { }; + OnError = (onError != null) ? x => WebsocketDispatcher.Schedule(() => onError(this, x)) : x => { }; } #endregion #region Functionality - public Task Send(string message) - { - return Send(message, GetHandler().FrameText); - } + public Task Send(string message) => SendAsync(message); - public Task Send(byte[] message) - { - return Send(message, GetHandler().FrameBinary); - } + public Task Send(byte[] message) => SendAsync(message); - public Task SendPing(byte[] message) - { - return Send(message, GetHandler().FramePing); - } + public Task SendPing(byte[] message) => SendPingAsync(message); - public Task SendPong(byte[] message) - { - return Send(message, GetHandler().FramePong); - } + public Task SendPong(byte[] message) => SendPongAsync(message); + + public Task SendAsync(string message) => Send(message, GetHandler().FrameText); + + public Task SendAsync(byte[] message) => Send(message, GetHandler().FrameBinary); + + public Task SendPingAsync(byte[] message) => Send(message, GetHandler().FramePing); + + public Task SendPongAsync(byte[] message) => Send(message, GetHandler().FramePong); private Task Send(T message, Func createFrame) { @@ -109,6 +105,7 @@ private Task Send(T message, Func createFrame) } var bytes = createFrame(message); + return SendBytes(bytes); } diff --git a/Modules/Websockets/Handler/WebsocketDispatcher.cs b/Modules/Websockets/Handler/WebsocketDispatcher.cs new file mode 100644 index 000000000..2d9976863 --- /dev/null +++ b/Modules/Websockets/Handler/WebsocketDispatcher.cs @@ -0,0 +1,37 @@ +using Fleck; + +namespace GenHTTP.Modules.Websockets.Handler; + +/// +/// Flecks uses synchronous callbacks for integration of user +/// logic, but provides asynchronous methods for interacting +/// with the websocket connection, which is counter-intuitive +/// and leads to errors. GenHTTP´s integration forces asynchronous +/// callbacks to be supplied by the user which requires us to dispatch +/// them on the synchronous callbacks invoked by Fleck. +/// +public static class WebsocketDispatcher +{ + + /// + /// Schedules the given piece of work on a background + /// thread and logs any error using the regular Fleck + /// logging mechanism. + /// + /// The actual piece of work to be executed + public static void Schedule(Func work) + { + _ = Task.Run(async () => + { + try + { + await work().ConfigureAwait(false); + } + catch (Exception e) + { + FleckLog.Error("Failed to run asynchronous event handler.", e); + } + }); + } + +} diff --git a/Modules/Websockets/Handler/WebsocketHandler.cs b/Modules/Websockets/Handler/WebsocketHandler.cs index 9b37c9f3a..0c8047280 100644 --- a/Modules/Websockets/Handler/WebsocketHandler.cs +++ b/Modules/Websockets/Handler/WebsocketHandler.cs @@ -10,19 +10,19 @@ public sealed class WebsocketHandler : IHandler #region Get-/Setters - public Action? OnOpen { get; } + public Func? OnOpen { get; } - public Action? OnClose { get; } + public Func? OnClose { get; } - public Action? OnMessage { get; } + public Func? OnMessage { get; } - public Action? OnBinary { get; } + public Func? OnBinary { get; } - public Action? OnPing { get; } + public Func? OnPing { get; } - public Action? OnPong { get; } + public Func? OnPong { get; } - public Action? OnError { get; } + public Func? OnError { get; } public List SupportedProtocols { get; } @@ -31,13 +31,13 @@ public sealed class WebsocketHandler : IHandler #region Initialization public WebsocketHandler(List supportedProtocols, - Action? onOpen, - Action? onClose, - Action? onMessage, - Action? onBinary, - Action? onPing, - Action? onPong, - Action? onError) + Func? onOpen, + Func? onClose, + Func? onMessage, + Func? onBinary, + Func? onPing, + Func? onPong, + Func? onError) { SupportedProtocols = supportedProtocols; diff --git a/Modules/Websockets/Handler/WebsocketHandlerBuilder.cs b/Modules/Websockets/Handler/WebsocketHandlerBuilder.cs index fa5e8e736..033719f3a 100644 --- a/Modules/Websockets/Handler/WebsocketHandlerBuilder.cs +++ b/Modules/Websockets/Handler/WebsocketHandlerBuilder.cs @@ -8,13 +8,13 @@ public class WebsocketHandlerBuilder : IHandlerBuilder private readonly List _SupportedProtocols = []; - private Action? _OnOpen; - private Action? _OnClose; - private Action? _OnMessage; - private Action? _OnBinary; - private Action? _OnPing; - private Action? _OnPong; - private Action? _OnError; + private Func? _OnOpen; + private Func? _OnClose; + private Func? _OnMessage; + private Func? _OnBinary; + private Func? _OnPing; + private Func? _OnPong; + private Func? _OnError; #region Functionality @@ -38,7 +38,7 @@ public WebsocketHandlerBuilder Protocol(string supportedProtocol) /// Will be executed if a new websocket client connected. /// /// The method to be executed - public WebsocketHandlerBuilder OnOpen(Action handler) + public WebsocketHandlerBuilder OnOpen(Func handler) { _OnOpen = handler; return this; @@ -48,7 +48,7 @@ public WebsocketHandlerBuilder OnOpen(Action handler) /// Will be executed if a websocket client disconnects. /// /// The method to be executed - public WebsocketHandlerBuilder OnClose(Action handler) + public WebsocketHandlerBuilder OnClose(Func handler) { _OnClose = handler; return this; @@ -58,7 +58,7 @@ public WebsocketHandlerBuilder OnClose(Action handler) /// Will be executed if a string message has been received from the client. /// /// The method to be executed - public WebsocketHandlerBuilder OnMessage(Action handler) + public WebsocketHandlerBuilder OnMessage(Func handler) { _OnMessage = handler; return this; @@ -68,7 +68,7 @@ public WebsocketHandlerBuilder OnMessage(Action ha /// Will be executed if a binary message has been received from the client. /// /// The method to be executed - public WebsocketHandlerBuilder OnBinary(Action handler) + public WebsocketHandlerBuilder OnBinary(Func handler) { _OnBinary = handler; return this; @@ -78,7 +78,7 @@ public WebsocketHandlerBuilder OnBinary(Action han /// Will be executed if the client sends a ping request. /// /// The method to be executed - public WebsocketHandlerBuilder OnPing(Action handler) + public WebsocketHandlerBuilder OnPing(Func handler) { _OnPing = handler; return this; @@ -88,7 +88,7 @@ public WebsocketHandlerBuilder OnPing(Action handl /// Will be executed if the client sends a pong request. /// /// The method to be executed - public WebsocketHandlerBuilder OnPong(Action handler) + public WebsocketHandlerBuilder OnPong(Func handler) { _OnPong = handler; return this; @@ -98,7 +98,7 @@ public WebsocketHandlerBuilder OnPong(Action handl /// Will be executed if there is some client connection error. /// /// The method to be executed - public WebsocketHandlerBuilder OnError(Action handler) + public WebsocketHandlerBuilder OnError(Func handler) { _OnError = handler; return this; diff --git a/Modules/Websockets/IWebsocketConnection.cs b/Modules/Websockets/IWebsocketConnection.cs index f62b686e8..5a174d358 100644 --- a/Modules/Websockets/IWebsocketConnection.cs +++ b/Modules/Websockets/IWebsocketConnection.cs @@ -22,25 +22,25 @@ public interface IWebsocketConnection /// Sends a text message to the connected client. /// /// The message to be sent - Task Send(string message); + Task SendAsync(string message); /// /// Sends a binary message to the connected client. /// /// The message to be sent - Task Send(byte[] message); + Task SendAsync(byte[] message); /// /// Sends a ping message to the connected client. /// /// The message to be sent - Task SendPing(byte[] message); + Task SendPingAsync(byte[] message); /// /// Sends a pong message to the connected client. /// /// The message to be sent - Task SendPong(byte[] message); + Task SendPongAsync(byte[] message); /// /// Gracefully closes the connection to the client. @@ -52,7 +52,7 @@ public interface IWebsocketConnection /// /// The code to be sent to the connected client /// - /// See Flex.WebSocketStatusCodes. + /// See . /// void Close(int code); diff --git a/Testing/Acceptance/Modules/Websockets/ErrorHandlingTests.cs b/Testing/Acceptance/Modules/Websockets/ErrorHandlingTests.cs index 920f72a28..2a167791b 100644 --- a/Testing/Acceptance/Modules/Websockets/ErrorHandlingTests.cs +++ b/Testing/Acceptance/Modules/Websockets/ErrorHandlingTests.cs @@ -1,5 +1,6 @@ using System.Net; -using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Websocket.Client; using WS = GenHTTP.Modules.Websockets.Websocket; @@ -19,4 +20,19 @@ public async Task TestInvalidRequest() Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); } + [TestMethod] + public async Task TestErrorHandling() + { + var server = WS.Create() + .OnOpen(_ => throw new InvalidOperationException("Ooops")); + + await using var host = await TestHost.RunAsync(server); + + using var client = new WebsocketClient(new Uri("ws://localhost:" + host.Port)); + + await client.Start(); + + await Task.Delay(1000); + } + } diff --git a/Testing/Acceptance/Modules/Websockets/InitializerTest.cs b/Testing/Acceptance/Modules/Websockets/InitializerTest.cs index b62510f10..8a7cfc031 100644 --- a/Testing/Acceptance/Modules/Websockets/InitializerTest.cs +++ b/Testing/Acceptance/Modules/Websockets/InitializerTest.cs @@ -1,4 +1,3 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; using WS = GenHTTP.Modules.Websockets; namespace GenHTTP.Testing.Acceptance.Modules.Websockets; @@ -11,13 +10,13 @@ public class InitializerTest public void InitializeAll() { WS.Websocket.Create() - .OnOpen(s => { }) - .OnClose(s => { }) - .OnPing((s, b) => { s.SendPong(b); }) - .OnPong((s, b) => { }) - .OnMessage((s, x) => { }) - .OnBinary((s, x) => { }) - .OnError((s, x) => { }) + .OnOpen(s => Task.CompletedTask) + .OnClose(s => Task.CompletedTask) + .OnPing(async (s, b) => { await s.SendPongAsync(b); }) + .OnPong((s, b) => Task.CompletedTask) + .OnMessage((s, x) => Task.CompletedTask) + .OnBinary((s, x) => Task.CompletedTask) + .OnError((s, x) => Task.CompletedTask) .Protocol("chat"); } diff --git a/Testing/Acceptance/Modules/Websockets/IntegrationTest.cs b/Testing/Acceptance/Modules/Websockets/IntegrationTest.cs index 7d6ee191b..c3bdf5977 100644 --- a/Testing/Acceptance/Modules/Websockets/IntegrationTest.cs +++ b/Testing/Acceptance/Modules/Websockets/IntegrationTest.cs @@ -2,8 +2,6 @@ using WS = GenHTTP.Modules.Websockets; -using Microsoft.VisualStudio.TestTools.UnitTesting; - using Websocket.Client; namespace GenHTTP.Testing.Acceptance.Modules.Websockets; @@ -20,10 +18,12 @@ public async Task TestServer() var length = 0; var server = WS.Websocket.Create() - .OnMessage((socket, msg) => + .OnMessage(async (socket, msg) => { length += msg.Length; - socket.Send(msg); + + await socket.SendAsync(msg); + socket.Close(); }); @@ -54,23 +54,20 @@ public async Task TestDataTypes() var waitEvent = new ManualResetEvent(false); var server = WS.Websocket.Create() - .OnOpen((socket) => + .OnOpen(async (socket) => { - Task.Run(async () => - { - await socket.SendPing([42]); - await socket.SendPong([42]); + await socket.SendPingAsync([42]); + await socket.SendPongAsync([42]); - await socket.Send([42]); + await socket.SendAsync([42]); - Assert.IsTrue(socket.IsAvailable); + Assert.IsTrue(socket.IsAvailable); - Assert.IsTrue(socket.Request.Headers.Count > 0); + Assert.IsTrue(socket.Request.Headers.Count > 0); - socket.Close(42); + socket.Close(42); - waitEvent.Set(); - }); + waitEvent.Set(); }); await using var host = await TestHost.RunAsync(server);