From 029bad159ef07451f2a80daa3c7dc7ef2daa7879 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 17 Apr 2023 19:24:17 +0200 Subject: [PATCH 01/20] Lib refactoring (#18) * editorconfig and codeformatting * rewrite * docs * throttle-event-args * another check to not reconnect while disconnecting/closing * naming * docs * changelog * reusable TaskHelper * docs * docs * downgrade from version 7 to version 5 of Microsoft.Extensions.Logging.Abstractions * downgrade from net7.0 to net5.0 * fix from integration-test * upgrade to net6 * reduced some overheaad, as mentioned/suggested by Bukk94 * ChangeLog * whispers are obsolete * ChangeLog * ChangeLog: Consideration/Proposal * fixing reconnectionpolicy-issue * fixing reconnectionpolicy-issue * ThrottlingPeriod * get MessageType-values outside while-loop the values wont change at runtime * logging * ToString() * moved and added tests * tests * usings * naming * diagnostic messages * suppress diagnostic messages * OnData has never been used * docs * todo removed, cause it got unnecessary * checked * naming * use build variable * remove * remove * remove * formatting * visibility * removed OnReconnectedEventArgs OnConnectedEventArgs is used, cause what happens is indicated by the EventHandler that is called * update version from 1.0.6 to 1.1.0 * push up ThrottlerService to TwitchClient and update version to 2.2.0 PubSub has its own PING/PONG-Timers PubSub only subscribes to Events PubSub only receives push-messages from twitch * WaitOneDuration * return-value for throttlerservice re-introduced... * changelog * assertion * remove whisper stuff * started changelog * rename method SendIRC to SpecificClientSend changed visibility from internal to protected comments updated * Send can be and is called by several methods so send has to be synchronized/locked * changelog * missed the important thing - sry * removed: event EventHandler OnStateChanged * removed comments locking is done in ABaseClient.Send() * give MonitorTaskAction a chance to catch cancellation * Addressed PR comments, renamed properties based on guidelines * Formatted code, updated logic for ConnectionWatchDog * Code formatting for unit tests * Added github workflow and improvements from Syzuna * Updated SSL port for TCP Client, added logging abstractions * Properly updated properties, improved naming, removed duplicate close on streams * Remove lingering option from TCP Client * Fixed code path for net6 and higher * Reworked NoReconnection policy, further code improvements * Removed broken test --------- Co-authored-by: CMR --- .github/workflows/check-buildstatus.yml | 9 +- .github/workflows/tests-linux.yml | 25 + .github/workflows/tests-windows.yml | 25 + .gitignore | 5 +- ChangeLog.md | 101 ++++ TwitchLib.Communication.sln | 6 +- .../Clients/ClientTestsBase.cs | 185 +++++++ .../Clients/TcpClientTests.cs | 10 + .../Clients/WebSocketClientTests.cs | 8 + .../Helpers/TestLogHelper.cs | 69 +++ .../Logs/TcpClient/.gitkeep | 0 .../Logs/WebSocketClient/.gitkeep | 0 .../Models/ReconnectionPolicyTests.cs | 40 ++ .../TcpClientTests.cs | 113 ----- .../TwitchLib.Communication.Tests.csproj | 15 +- .../WebSocketClientTests.cs | 113 ----- .../Clients/ClientBase.cs | 375 ++++++++++++++ .../Clients/TcpClient.cs | 454 +++++------------ .../Clients/WebsocketClient.cs | 478 ++++++------------ .../Enums/ClientType.cs | 2 +- .../Events/OnConnectedEventArgs.cs | 3 +- .../Events/OnDataEventArgs.cs | 9 - .../Events/OnDisconnectedEventArgs.cs | 3 +- .../Events/OnFatalErrorEventArgs.cs | 14 +- .../Events/OnMessageThrottledEventArgs.cs | 12 - .../Events/OnReconnectedEventArgs.cs | 7 - .../Events/OnStateChangedEventArgs.cs | 10 - .../Events/OnWhisperThrottledEventArgs.cs | 12 - .../Extensions/LogExtensions.cs | 53 ++ .../Helpers/TaskHelper.cs | 18 + .../Interfaces/IClient.cs | 138 +++-- .../Interfaces/IClientOptions.cs | 54 +- .../Models/ClientOptions.cs | 49 +- .../Models/NoReconnectionPolicy.cs | 15 + .../Models/ReconnectionPolicy.cs | 167 ++++-- .../Services/ConnectionWatchDog.cs | 122 +++++ .../Services/NetworkServices.cs | 54 ++ .../Services/Throttlers.cs | 203 -------- .../TwitchLib.Communication.csproj | 47 +- 39 files changed, 1663 insertions(+), 1360 deletions(-) create mode 100644 .github/workflows/tests-linux.yml create mode 100644 .github/workflows/tests-windows.yml create mode 100644 ChangeLog.md create mode 100644 src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs create mode 100644 src/TwitchLib.Communication.Tests/Clients/TcpClientTests.cs create mode 100644 src/TwitchLib.Communication.Tests/Clients/WebSocketClientTests.cs create mode 100644 src/TwitchLib.Communication.Tests/Helpers/TestLogHelper.cs create mode 100644 src/TwitchLib.Communication.Tests/Logs/TcpClient/.gitkeep create mode 100644 src/TwitchLib.Communication.Tests/Logs/WebSocketClient/.gitkeep create mode 100644 src/TwitchLib.Communication.Tests/Models/ReconnectionPolicyTests.cs delete mode 100644 src/TwitchLib.Communication.Tests/TcpClientTests.cs delete mode 100644 src/TwitchLib.Communication.Tests/WebSocketClientTests.cs create mode 100644 src/TwitchLib.Communication/Clients/ClientBase.cs delete mode 100644 src/TwitchLib.Communication/Events/OnDataEventArgs.cs delete mode 100644 src/TwitchLib.Communication/Events/OnMessageThrottledEventArgs.cs delete mode 100644 src/TwitchLib.Communication/Events/OnReconnectedEventArgs.cs delete mode 100644 src/TwitchLib.Communication/Events/OnStateChangedEventArgs.cs delete mode 100644 src/TwitchLib.Communication/Events/OnWhisperThrottledEventArgs.cs create mode 100644 src/TwitchLib.Communication/Extensions/LogExtensions.cs create mode 100644 src/TwitchLib.Communication/Helpers/TaskHelper.cs create mode 100644 src/TwitchLib.Communication/Models/NoReconnectionPolicy.cs create mode 100644 src/TwitchLib.Communication/Services/ConnectionWatchDog.cs create mode 100644 src/TwitchLib.Communication/Services/NetworkServices.cs delete mode 100644 src/TwitchLib.Communication/Services/Throttlers.cs diff --git a/.github/workflows/check-buildstatus.yml b/.github/workflows/check-buildstatus.yml index 917a7c0..3ce8c7d 100644 --- a/.github/workflows/check-buildstatus.yml +++ b/.github/workflows/check-buildstatus.yml @@ -8,14 +8,19 @@ jobs: check-buildstatus: runs-on: ubuntu-latest + strategy: + matrix: + dotnet-version: [ '6.0.x' ] steps: - uses: actions/checkout@v2 - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: ${{ matrix.dotnet-version }} - name: Restore dependencies run: dotnet restore - name: Build TwitchLib.Communication - run: dotnet build --no-restore + run: dotnet build --no-restore --configuration Release + - name: Test + run: dotnet test --no-restore --verbosity normal \ No newline at end of file diff --git a/.github/workflows/tests-linux.yml b/.github/workflows/tests-linux.yml new file mode 100644 index 0000000..9c3768b --- /dev/null +++ b/.github/workflows/tests-linux.yml @@ -0,0 +1,25 @@ +name: Test TwitchLib.Communication Linux + +on: + [push] + +jobs: + tests: + + runs-on: ubuntu-latest + strategy: + matrix: + dotnet-version: [ '6.0.x' ] + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: ${{ matrix.dotnet-version }} + - name: Restore dependencies + run: dotnet restore + - name: Build TwitchLib.Communication + run: dotnet build --no-restore --configuration Release + - name: Test + run: dotnet test --no-restore --verbosity normal diff --git a/.github/workflows/tests-windows.yml b/.github/workflows/tests-windows.yml new file mode 100644 index 0000000..022ce06 --- /dev/null +++ b/.github/workflows/tests-windows.yml @@ -0,0 +1,25 @@ +name: Test TwitchLib.Communication Windows + +on: + [push] + +jobs: + tests: + + runs-on: windows-latest + strategy: + matrix: + dotnet-version: [ '6.0.x' ] + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: ${{ matrix.dotnet-version }} + - name: Restore dependencies + run: dotnet restore + - name: Build TwitchLib.Communication + run: dotnet build --no-restore --configuration Release + - name: Test + run: dotnet test --no-restore --verbosity normal diff --git a/.gitignore b/.gitignore index 89ad0e0..cc7fed1 100644 --- a/.gitignore +++ b/.gitignore @@ -233,4 +233,7 @@ $RECYCLE.BIN/ *.msp # Windows shortcuts -*.lnk \ No newline at end of file +*.lnk + +# Rider files +.idea/* \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..dcef01d --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,101 @@ +# Changelog + +## Version 2.0.0 +### Addresses +##### Issues +- https://github.com/TwitchLib/TwitchLib/issues/1093 +- https://github.com/TwitchLib/TwitchLib.Client/issues/206 +- https://github.com/TwitchLib/TwitchLib/issues/1104 +- https://github.com/TwitchLib/TwitchLib.Communication/issues/13 +- https://github.com/TwitchLib/TwitchLib.Communication/issues/7 + +##### Pull Requests +- none + +--- + +### Changes + +--- + +#### IClient +##### Changed +- now extends `IDisposable` +- `event EventHandler OnReconnected;` + - to `event EventHandler OnReconnected;` + - now the `event`handlers argument is `OnConnectedEventArgs` instead of `OnReconnectedEventArgs` + - the specific `event`handler itself, determines wether the args are in context of connect or reconnect +- `IClient.Send(string message)` is now synchronized because + - `ThrottlerService` got removed + - https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream?view=netstandard-2.0#remarks +##### Added +- none +##### Removed +- see also: https://discuss.dev.twitch.tv/t/deprecation-of-chat-commands-through-irc/40486 + - `bool SendWhisper(string message);` + - `void WhisperThrottled(OnWhisperThrottledEventArgs eventArgs);` +- `event EventHandler OnData;` + - as far as i got it right, + - binary data is not received + - it has never ever been used/raised +- `event EventHandler OnMessageThrottled;` + - because `ThrottlerService` is now part of `TwitchLib.Client` +- `event EventHandler OnStateChanged;` + - neither used by `TwitchLib.Client` nor by `TwitchLib.PubSub` +--- + +#### ClientOptions +##### Changed +- `value`s for properties can only be passed by `ctor` +- `ctor` also takes an argument for `ReconnectionPolicy` + - by leaving it `null`, a `default` `ReconnectionPolicy` is created, that attempts to reconnect every 3_000 milliseconds for ten times +- `DisconnectWait` became an unsigned integer (`uint`), to ensure only positive values are used for it +##### Removed +- see also: https://discuss.dev.twitch.tv/t/deprecation-of-chat-commands-through-irc/40486 + - `TimeSpan WhisperThrottlingPeriod { get; set; }` + - `int WhispersAllowedInPeriod { get; set; }` + - `int WhisperQueueCapacity { get; set; }` +##### Moved +- the following properties went to `TwitchLib.Client.Models.SendOptions` + - `int SendQueueCapacity { get; set; }` + - `TimeSpan SendCacheItemTimeout { get; set; }` + - `ushort SendDelay { get; set; }` + - `TimeSpan ThrottlingPeriod { get; set; }` + - `int MessagesAllowedInPeriod { get; set; }` + +--- + +#### ConnectionWatchDog +- now the `ConnectionWatchDog` enforces reconnect according to the `ReconnectionPolicy` +- `ConnectionWatchDog` does not send `PING :tmi.twitch.tv`-messages anymore + - `TwitchLib.Client` receives `PING :tmi.twitch.tv`-messages and has to reply with `PONG :tmi.twitch.tv` + - https://dev.twitch.tv/docs/irc/#keepalive-messages + - `TwitchLib.Client` does so + - it handles received PING-messages + - `TwitchLib.PubSub` has to send `PING :tmi.twitch.tv` within at least every five minutes + - https://dev.twitch.tv/docs/pubsub/#connection-management + - `TwitchLib.PubSub` does so + - it has its own PING- and PONG-Timer + +--- + +#### Throttling/ThrottlerService +- `TwitchLib.Communication.IClient` doesnt throttle messages anymore + - `TwitchLib.PubSub` does not need it + - only `TwitchLib.Client` needs it + - so, throttling went to `TwitchLib.Client.Services.ThrottlerService` in combination with `TwitchLib.Client.Services.Throttler` +- everything related to throttling got removed + - `TwitchLib.Communication.Events.OnMessageThrottledEventArgs` + - `TwitchLib.Communication.Interfaces.IClientOptions` + - see also [ClientOptions.Moved](#ClientOptions.Moved) + - `int SendQueueCapacity { get; set; }` + - `TimeSpan SendCacheItemTimeout { get; set; }` + - `ushort SendDelay { get; set; }` + - `TimeSpan ThrottlingPeriod { get; set; }` + - `int MessagesAllowedInPeriod { get; set; }` + +--- + +#### OnStateChangedEventArgs +- removed +- neither used by `TwitchLib.Client` nor by `TwitchLib.PubSub` \ No newline at end of file diff --git a/TwitchLib.Communication.sln b/TwitchLib.Communication.sln index 793ca03..a635a11 100644 --- a/TwitchLib.Communication.sln +++ b/TwitchLib.Communication.sln @@ -1,11 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27703.2035 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Communication", "src\TwitchLib.Communication\TwitchLib.Communication.csproj", "{5DBA3070-744D-45EF-84EA-D5ECF3C71CFE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchLib.Communication.Tests", "src\TwitchLib.Communication.Tests\TwitchLib.Communication.Tests.csproj", "{8945B40A-7E9A-423E-964F-E215C112DA56}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Communication.Tests", "src\TwitchLib.Communication.Tests\TwitchLib.Communication.Tests.csproj", "{8945B40A-7E9A-423E-964F-E215C112DA56}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs new file mode 100644 index 0000000..1fd81de --- /dev/null +++ b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs @@ -0,0 +1,185 @@ +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using TwitchLib.Communication.Events; +using TwitchLib.Communication.Interfaces; +using TwitchLib.Communication.Models; +using TwitchLib.Communication.Tests.Helpers; +using Xunit; + +namespace TwitchLib.Communication.Tests.Clients +{ + /// + /// bundles -Tests in one container + /// + /// + /// + /// + /// + /// + /// + /// + /// + public abstract class ClientTestsBase where T : IClient + { + private static uint WaitAfterDispose => 3; + private static TimeSpan WaitOneDuration => TimeSpan.FromSeconds(5); + private static IClientOptions Options; + + public ClientTestsBase(IClientOptions options = null) + { + Options = options; + } + + [Fact] + public void Client_Raises_OnConnected_EventArgs() + { + // create one logger per test-method! - cause one file per test-method is generated + ILogger logger = TestLogHelper.GetLogger(); + T? client = GetClient(logger, Options); + Assert.NotNull(client); + try + { + ManualResetEvent pauseConnected = new ManualResetEvent(false); + + Assert.Raises( + h => client.OnConnected += h, + h => client.OnConnected -= h, + () => + { + client.OnConnected += (sender, e) => pauseConnected.Set(); + client.Open(); + Assert.True(pauseConnected.WaitOne(WaitOneDuration)); + }); + } + catch (Exception e) + { + logger.LogError(e.ToString()); + Assert.Fail(e.ToString()); + } + finally + { + Cleanup(client); + } + } + + [Fact] + public void Client_Raises_OnDisconnected_EventArgs() + { + // create one logger per test-method! - cause one file per test-method is generated + ILogger logger = TestLogHelper.GetLogger(); + T? client = GetClient(logger, Options); + Assert.NotNull(client); + try + { + ManualResetEvent pauseDisconnected = new ManualResetEvent(false); + + Assert.Raises( + h => client.OnDisconnected += h, + h => client.OnDisconnected -= h, + () => + { + client.OnConnected += (sender, e) => + { + Task.Delay(WaitOneDuration).GetAwaiter().GetResult(); + client.Close(); + }; + client.OnDisconnected += (sender, e) => pauseDisconnected.Set(); + client.Open(); + Assert.True(pauseDisconnected.WaitOne(WaitOneDuration)); + }); + } + catch (Exception e) + { + logger.LogError(e.ToString()); + Assert.Fail(e.ToString()); + } + finally + { + Cleanup(client); + } + } + + [Fact] + public void Client_Raises_OnReconnected_EventArgs() + { + // create one logger per test-method! - cause one file per test-method is generated + ILogger logger = TestLogHelper.GetLogger(); + T? client = GetClient(logger, Options); + Assert.NotNull(client); + try + { + ManualResetEvent pauseReconnected = new ManualResetEvent(false); + + Assert.Raises( + h => client.OnReconnected += h, + h => client.OnReconnected -= h, + () => + { + client.OnConnected += (s, e) => client.Reconnect(); + + client.OnReconnected += (s, e) => pauseReconnected.Set(); + client.Open(); + + Assert.True(pauseReconnected.WaitOne(WaitOneDuration)); + }); + } + catch (Exception e) + { + logger.LogError(e.ToString()); + Assert.Fail(e.ToString()); + } + finally + { + Cleanup(client); + } + } + + [Fact] + public void Dispose_Client_Before_Connecting_IsOK() + { + // create one logger per test-method! - cause one file per test-method is generated + ILogger logger = TestLogHelper.GetLogger(); + IClient? client = null; + try + { + client = GetClient(logger, Options); + Assert.NotNull(client); + client.Dispose(); + } + catch (Exception e) + { + logger.LogError(e.ToString()); + Assert.Fail(e.ToString()); + } + finally + { + Cleanup((T?)client); + } + } + + private static void Cleanup(T? client) + { + client?.Dispose(); + Task.Delay(TimeSpan.FromSeconds(WaitAfterDispose)).GetAwaiter().GetResult(); + } + + private static TClient? GetClient(ILogger logger, IClientOptions? options = null) + { + Type[] constructorParameterTypes = new Type[] + { + typeof(IClientOptions), + typeof(ILogger) + }; + ConstructorInfo? constructor = typeof(TClient).GetConstructor(constructorParameterTypes); + object[] constructorParameters = new object[] + { + options ?? new ClientOptions(), + logger + }; + return (TClient?)constructor?.Invoke(constructorParameters); + } + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication.Tests/Clients/TcpClientTests.cs b/src/TwitchLib.Communication.Tests/Clients/TcpClientTests.cs new file mode 100644 index 0000000..007f058 --- /dev/null +++ b/src/TwitchLib.Communication.Tests/Clients/TcpClientTests.cs @@ -0,0 +1,10 @@ +using TwitchLib.Communication.Clients; +using TwitchLib.Communication.Models; + +namespace TwitchLib.Communication.Tests.Clients +{ + public class TcpClientTests : ClientTestsBase + { + public TcpClientTests() : base(new ClientOptions(useSsl: false)) { } + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication.Tests/Clients/WebSocketClientTests.cs b/src/TwitchLib.Communication.Tests/Clients/WebSocketClientTests.cs new file mode 100644 index 0000000..fe9a507 --- /dev/null +++ b/src/TwitchLib.Communication.Tests/Clients/WebSocketClientTests.cs @@ -0,0 +1,8 @@ +using TwitchLib.Communication.Clients; + +namespace TwitchLib.Communication.Tests.Clients +{ + public class WebSocketClientTests : ClientTestsBase + { + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication.Tests/Helpers/TestLogHelper.cs b/src/TwitchLib.Communication.Tests/Helpers/TestLogHelper.cs new file mode 100644 index 0000000..ce5f44a --- /dev/null +++ b/src/TwitchLib.Communication.Tests/Helpers/TestLogHelper.cs @@ -0,0 +1,69 @@ +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using Serilog.Exceptions; + +namespace TwitchLib.Communication.Tests.Helpers +{ + internal static class TestLogHelper + { + private static readonly string OUTPUT_TEMPLATE = + "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u}] {Message:lj}{NewLine}{Exception}{NewLine}"; + + private static readonly string NEW_TEST_RUN_INDICATOR; + + static TestLogHelper() + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine(); + builder.AppendLine(new string('-', 80)); + builder.Append(new string(' ', 34)); + builder.AppendLine("new Test-Run"); + builder.AppendLine(new string('-', 80)); + NEW_TEST_RUN_INDICATOR = builder.ToString(); + } + + internal static Microsoft.Extensions.Logging.ILogger GetLogger( + LogEventLevel logEventLevel = LogEventLevel.Verbose, + [CallerMemberName] string callerMemberName = "TestMethod") + { + Serilog.ILogger logger = GetSerilogLogger(typeof(T).Name, + callerMemberName, + logEventLevel); + Microsoft.Extensions.Logging.ILoggerFactory loggerFactory = + new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); + return loggerFactory.CreateLogger(); + } + + private static Serilog.ILogger GetSerilogLogger(string typeName, + string callerMemberName, + LogEventLevel logEventLevel) + { + Serilog.LoggerConfiguration loggerConfiguration = GetConfiguration(typeName, + callerMemberName, + logEventLevel); + Serilog.ILogger logger = loggerConfiguration.CreateLogger().ForContext(); + logger.Information(NEW_TEST_RUN_INDICATOR); + return logger; + } + + private static Serilog.LoggerConfiguration GetConfiguration(string typeName, + string callerMemberName, + LogEventLevel logEventLevel) + { + Serilog.LoggerConfiguration loggerConfiguration = new Serilog.LoggerConfiguration(); + loggerConfiguration.MinimumLevel.Verbose(); + string path = $"../../../Logs/{typeName}/{callerMemberName}.log"; + loggerConfiguration.WriteTo.File( + path: path, + restrictedToMinimumLevel: logEventLevel, + outputTemplate: OUTPUT_TEMPLATE + ); + loggerConfiguration.Enrich.WithExceptionDetails(); + loggerConfiguration.Enrich.FromLogContext(); + return loggerConfiguration; + } + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication.Tests/Logs/TcpClient/.gitkeep b/src/TwitchLib.Communication.Tests/Logs/TcpClient/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/TwitchLib.Communication.Tests/Logs/WebSocketClient/.gitkeep b/src/TwitchLib.Communication.Tests/Logs/WebSocketClient/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/TwitchLib.Communication.Tests/Models/ReconnectionPolicyTests.cs b/src/TwitchLib.Communication.Tests/Models/ReconnectionPolicyTests.cs new file mode 100644 index 0000000..e67a286 --- /dev/null +++ b/src/TwitchLib.Communication.Tests/Models/ReconnectionPolicyTests.cs @@ -0,0 +1,40 @@ +using System; +using TwitchLib.Communication.Models; +using Xunit; + +namespace TwitchLib.Communication.Tests.Models +{ + public class ReconnectionPolicyTests + { + /// + /// Checks + ///

+ /// + ///

+ /// + ///
+ [Fact] + public void ReconnectionPolicy_OmitReconnect() + { + try + { + ReconnectionPolicy reconnectionPolicy = new NoReconnectionPolicy(); + Assert.False(reconnectionPolicy.AreAttemptsComplete()); + reconnectionPolicy.ProcessValues(); + Assert.True(reconnectionPolicy.AreAttemptsComplete()); + // in case of a normal connect, we expect the ReconnectionPolicy to be reset + reconnectionPolicy.Reset(false); + Assert.False(reconnectionPolicy.AreAttemptsComplete()); + reconnectionPolicy.ProcessValues(); + Assert.True(reconnectionPolicy.AreAttemptsComplete()); + // in case of a reconnect, we expect the ReconnectionPolicy not to be reset + reconnectionPolicy.Reset(true); + Assert.True(reconnectionPolicy.AreAttemptsComplete()); + } + catch (Exception e) + { + Assert.Fail(e.ToString()); + } + } + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication.Tests/TcpClientTests.cs b/src/TwitchLib.Communication.Tests/TcpClientTests.cs deleted file mode 100644 index dce736d..0000000 --- a/src/TwitchLib.Communication.Tests/TcpClientTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using TwitchLib.Communication.Clients; -using TwitchLib.Communication.Events; -using TwitchLib.Communication.Models; -using Xunit; - -namespace TwitchLib.Communication.Tests -{ - public class TcpClientTests - { - [Fact] - public void Client_Raises_OnConnected_EventArgs() - { - - var client = new TcpClient(); - var pauseConnected = new ManualResetEvent(false); - - Assert.Raises( - h => client.OnConnected += h, - h => client.OnConnected -= h, - () => - { - client.OnConnected += (sender, e) => { pauseConnected.Set(); }; - client.Open(); - Assert.True(pauseConnected.WaitOne(5000)); - }); - } - - [Fact] - public void Client_Raises_OnDisconnected_EventArgs() - { - var client = new TcpClient(new ClientOptions() {DisconnectWait = 100}); - var pauseDisconnected = new ManualResetEvent(false); - - Assert.Raises( - h => client.OnDisconnected += h, - h => client.OnDisconnected -= h, - () => - { - client.OnConnected += async (sender, e) => - { - await Task.Delay(2000); - client.Close(); - }; - client.OnDisconnected += (sender, e) => - { - pauseDisconnected.Set(); - }; - client.Open(); - Assert.True(pauseDisconnected.WaitOne(20000)); - }); - } - - [Fact] - public void Client_Raises_OnReconnected_EventArgs() - { - var client = new TcpClient(new ClientOptions(){ReconnectionPolicy = null}); - var pauseReconnected = new ManualResetEvent(false); - - Assert.Raises( - h => client.OnReconnected += h, - h => client.OnReconnected -= h, - () => - { - client.OnConnected += async (s, e) => - { - await Task.Delay(2000); - client.Reconnect(); - }; - - client.OnReconnected += (s, e) => { pauseReconnected.Set(); }; - client.Open(); - - Assert.True(pauseReconnected.WaitOne(20000)); - }); - } - - [Fact] - public void Dispose_Client_Before_Connecting_IsOK() - { - var tcpClient = new TcpClient(); - tcpClient.Dispose(); - } - - [Fact] - public void Client_Can_SendAndReceive_Messages() - { - var client = new TcpClient(); - var pauseConnected = new ManualResetEvent(false); - var pauseReadMessage = new ManualResetEvent(false); - - Assert.Raises( - h => client.OnMessage += h, - h => client.OnMessage -= h, - () => - { - client.OnConnected += (sender, e) => { pauseConnected.Set(); }; - - client.OnMessage += (sender, e) => - { - pauseReadMessage.Set(); - Assert.Equal("PONG :tmi.twitch.tv", e.Message); - }; - - client.Open(); - client.Send("PING"); - Assert.True(pauseConnected.WaitOne(5000)); - Assert.True(pauseReadMessage.WaitOne(5000)); - }); - } - } -} diff --git a/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj b/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj index 859c2cc..baed66a 100644 --- a/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj +++ b/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj @@ -3,13 +3,21 @@ net6.0 false + enable + disable + + + + + + - all + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -19,4 +27,9 @@ + + + + + diff --git a/src/TwitchLib.Communication.Tests/WebSocketClientTests.cs b/src/TwitchLib.Communication.Tests/WebSocketClientTests.cs deleted file mode 100644 index b4736ba..0000000 --- a/src/TwitchLib.Communication.Tests/WebSocketClientTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using TwitchLib.Communication.Clients; -using TwitchLib.Communication.Events; -using TwitchLib.Communication.Models; -using Xunit; - -namespace TwitchLib.Communication.Tests -{ - public class WebSocketClientTests - { - [Fact] - public void Client_Raises_OnConnected_EventArgs() - { - var client = new WebSocketClient(); - var pauseConnected = new ManualResetEvent(false); - - Assert.Raises( - h => client.OnConnected += h, - h => client.OnConnected -= h, - () => - { - client.OnConnected += (sender, e) => { pauseConnected.Set(); }; - client.Open(); - Assert.True(pauseConnected.WaitOne(5000)); - }); - } - - [Fact] - public void Client_Raises_OnDisconnected_EventArgs() - { - var client = new WebSocketClient(new ClientOptions() {DisconnectWait = 5000}); - var pauseDisconnected = new ManualResetEvent(false); - - Assert.Raises( - h => client.OnDisconnected += h, - h => client.OnDisconnected -= h, - () => - { - client.OnConnected += async (sender, e) => - { - await Task.Delay(2000); - client.Close(); - }; - client.OnDisconnected += (sender, e) => - { - pauseDisconnected.Set(); - }; - client.Open(); - Assert.True(pauseDisconnected.WaitOne(200000)); - }); - } - - [Fact] - public void Client_Raises_OnReconnected_EventArgs() - { - var client = new WebSocketClient(new ClientOptions(){ReconnectionPolicy = null}); - var pauseReconnected = new ManualResetEvent(false); - - Assert.Raises( - h => client.OnReconnected += h, - h => client.OnReconnected -= h, - () => - { - client.OnConnected += async (s, e) => - { - await Task.Delay(2000); - client.Reconnect(); - }; - - client.OnReconnected += (s, e) => { pauseReconnected.Set(); }; - client.Open(); - - Assert.True(pauseReconnected.WaitOne(20000)); - }); - } - - [Fact] - public void Dispose_Client_Before_Connecting_IsOK() - { - var client = new WebSocketClient(); - client.Dispose(); - } - - - [Fact] - public void Client_Can_SendAndReceive_Messages() - { - var client = new WebSocketClient(); - var pauseConnected = new ManualResetEvent(false); - var pauseReadMessage = new ManualResetEvent(false); - - Assert.Raises( - h => client.OnMessage += h, - h => client.OnMessage -= h, - () => - { - client.OnConnected += (sender, e) => { pauseConnected.Set(); }; - - client.OnMessage += (sender, e) => - { - pauseReadMessage.Set(); - Assert.Equal("PONG :tmi.twitch.tv", e.Message); - }; - - client.Open(); - client.Send("PING"); - Assert.True(pauseConnected.WaitOne(5000)); - Assert.True(pauseReadMessage.WaitOne(5000)); - }); - } - } -} diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs new file mode 100644 index 0000000..6b75074 --- /dev/null +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -0,0 +1,375 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using TwitchLib.Communication.Events; +using TwitchLib.Communication.Extensions; +using TwitchLib.Communication.Interfaces; +using TwitchLib.Communication.Models; +using TwitchLib.Communication.Services; + +namespace TwitchLib.Communication.Clients +{ + /// + /// This bundles almost everything that and have in common + /// to be able to + /// + /// + /// pass instances of this to and + /// + /// + /// and to access Methods of this instance within and + /// + /// + /// + public abstract class ClientBase : IClient where T : IDisposable + { + private static readonly object Lock = new object(); + private readonly NetworkServices _networkServices; + private CancellationTokenSource _cancellationTokenSource; + + /// + /// This is used for + /// whenever a call to is made + /// + internal CancellationToken Token => _cancellationTokenSource.Token; + + internal static TimeSpan TimeOutEstablishConnection => TimeSpan.FromSeconds(15); + + protected ILogger Logger { get; } + + protected abstract string Url { get; } + + /// + /// The underlying client. + /// + public T Client { get; private set; } + + public abstract bool IsConnected { get; } + + public IClientOptions Options { get; } + + public event EventHandler OnConnected; + public event EventHandler OnDisconnected; + public event EventHandler OnError; + public event EventHandler OnFatality; + public event EventHandler OnMessage; + public event EventHandler OnSendFailed; + public event EventHandler OnReconnected; + + internal ClientBase( + IClientOptions options = null, + ILogger logger = null) + { + Logger = logger; + _cancellationTokenSource = new CancellationTokenSource(); + Options = options ?? new ClientOptions(); + _networkServices = new NetworkServices(this, logger); + } + + /// + /// Wont raise the given if .IsCancellationRequested + /// + private void RaiseSendFailed(OnSendFailedEventArgs eventArgs) + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; + } + + OnSendFailed?.Invoke(this, eventArgs); + } + + /// + /// Wont raise the given if .IsCancellationRequested + /// + internal void RaiseError(OnErrorEventArgs eventArgs) + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; + } + + OnError?.Invoke(this, eventArgs); + } + + /// + /// Wont raise the given if .IsCancellationRequested + /// + private void RaiseReconnected() + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; + } + + OnReconnected?.Invoke(this, new OnConnectedEventArgs()); + } + + /// + /// Wont raise the given if .IsCancellationRequested + /// + internal void RaiseMessage(OnMessageEventArgs eventArgs) + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; + } + + OnMessage?.Invoke(this, eventArgs); + } + + /// + /// Wont raise the given if .IsCancellationRequested + /// + internal void RaiseFatal(Exception e = null) + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; + } + + var onFatalErrorEventArgs = new OnFatalErrorEventArgs("Fatal network error."); + if (e != null) + { + onFatalErrorEventArgs = new OnFatalErrorEventArgs(e); + } + + OnFatality?.Invoke(this, onFatalErrorEventArgs); + } + + private void RaiseDisconnected() + { + Logger?.TraceMethodCall(GetType()); + OnDisconnected?.Invoke(this, new OnDisconnectedEventArgs()); + } + + private void RaiseConnected() + { + Logger?.TraceMethodCall(GetType()); + OnConnected?.Invoke(this, new OnConnectedEventArgs()); + } + + public bool Send(string message) + { + Logger?.TraceMethodCall(GetType()); + try + { + lock (Lock) + { + ClientSend(message); + return true; + } + } + catch (Exception e) + { + RaiseSendFailed(new OnSendFailedEventArgs() { Exception = e, Data = message }); + return false; + } + } + + public bool Open() + { + Logger?.TraceMethodCall(GetType()); + return OpenPrivate(false); + } + + public void Close() + { + Logger?.TraceMethodCall(GetType()); + + // Network services has to be stopped first so that it wont reconnect + _networkServices.Stop(); + + // ClosePrivate() also handles IClientOptions.DisconnectWait + ClosePrivate(); + } + + /// + /// + /// + public void Dispose() + { + Logger?.TraceMethodCall(GetType()); + Close(); + GC.SuppressFinalize(this); + } + + public bool Reconnect() + { + Logger?.TraceMethodCall(GetType()); + + // Stops everything (including NetworkServices) + if (IsConnected) + { + Close(); + } + + // interface IClient doesnt declare a return value for Reconnect() + // so we can suppress IDE0058 of ReconnectInternal() + return ReconnectInternal(); + } + + private bool OpenPrivate(bool isReconnect) + { + Logger?.TraceMethodCall(GetType()); + try + { + if (Token.IsCancellationRequested) + { + return false; + } + + if (IsConnected) + { + return true; + } + + // Always create new client when opening new connection + Client = CreateClient(); + + var first = true; + Options.ReconnectionPolicy.Reset(isReconnect); + while (!IsConnected + && !Options.ReconnectionPolicy.AreAttemptsComplete()) + { + Logger?.TraceAction(GetType(), "try to connect"); + if (!first) + { + Task.Delay(Options.ReconnectionPolicy.GetReconnectInterval(), CancellationToken.None) + .GetAwaiter().GetResult(); + } + + ConnectClient(); + Options.ReconnectionPolicy.ProcessValues(); + first = false; + } + + if (!IsConnected) + { + Logger?.TraceAction(GetType(), "Client couldn't establish a connection"); + RaiseFatal(); + return false; + } + + Logger?.TraceAction(GetType(), "Client established a connection"); + _networkServices.Start(); + if (!isReconnect) + { + RaiseConnected(); + } + + return true; + } + catch (Exception ex) + { + Logger?.LogExceptionAsError(GetType(), ex); + RaiseError(new OnErrorEventArgs { Exception = ex }); + RaiseFatal(); + return false; + } + } + + /// + /// Stops + /// by calling + ///

+ /// and enforces the + ///

+ /// afterwards it waits for the via given amount of milliseconds + ///

+ ///

+ /// will keep running, + /// because itself issued this call by calling + ///
+ private void ClosePrivate() + { + Logger?.TraceMethodCall(GetType()); + + // This cancellation traverse up to NetworkServices.ListenTask + _cancellationTokenSource.Cancel(); + Logger?.TraceAction(GetType(), + $"{nameof(_cancellationTokenSource)}.{nameof(_cancellationTokenSource.Cancel)} is called"); + + CloseClient(); + RaiseDisconnected(); + _cancellationTokenSource = new CancellationTokenSource(); + + Task.Delay(TimeSpan.FromMilliseconds(Options.DisconnectWait), CancellationToken.None) + .GetAwaiter().GetResult(); + } + + /// + /// Send method for the client. + /// + /// + /// Message to be send + /// + protected abstract void ClientSend(string message); + + /// + /// Instantiate the underlying client. + /// + protected abstract T CreateClient(); + + /// + /// one of the following specific methods + /// + /// + /// + /// + /// + /// + /// + /// + /// calls to one of the methods mentioned above, + /// also Dispose() the respective client, + /// so no additional Dispose() is needed + /// + protected abstract void CloseClient(); + + /// + /// Connect the client. + /// + protected abstract void ConnectClient(); + + /// + /// To issue a reconnect + ///

+ /// especially for the + ///

+ /// it stops all but ! + ///

+ ///

+ /// see also : + ///

+ /// + ///
+ /// + /// if a connection could be established, otherwise + /// + internal bool ReconnectInternal() + { + Logger?.TraceMethodCall(GetType()); + ClosePrivate(); + var reconnected = OpenPrivate(true); + if (reconnected) + { + RaiseReconnected(); + } + + return reconnected; + } + + /// + /// just the Action that listens for new Messages + /// the corresponding is held by + /// + internal abstract void ListenTaskAction(); + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Clients/TcpClient.cs b/src/TwitchLib.Communication/Clients/TcpClient.cs index 3f39532..7467280 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -1,393 +1,179 @@ using System; using System.IO; -using System.Linq; using System.Net.Security; -using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using TwitchLib.Communication.Events; +using TwitchLib.Communication.Extensions; using TwitchLib.Communication.Interfaces; -using TwitchLib.Communication.Models; -using TwitchLib.Communication.Services; namespace TwitchLib.Communication.Clients { - public class TcpClient : IClient + public class TcpClient : ClientBase { - private int NotConnectedCounter; - public TimeSpan DefaultKeepAliveInterval { get; set; } - public int SendQueueLength => _throttlers.SendQueue.Count; - public int WhisperQueueLength => _throttlers.WhisperQueue.Count; - public bool IsConnected => Client?.Connected ?? false; - public IClientOptions Options { get; } + protected override string Url => "irc.chat.twitch.tv"; - public event EventHandler OnConnected; - public event EventHandler OnData; - public event EventHandler OnDisconnected; - public event EventHandler OnError; - public event EventHandler OnFatality; - public event EventHandler OnMessage; - public event EventHandler OnMessageThrottled; - public event EventHandler OnWhisperThrottled; - public event EventHandler OnSendFailed; - public event EventHandler OnStateChanged; - public event EventHandler OnReconnected; + private int Port => Options.UseSsl ? 6697 : 6667; + private StreamReader Reader { get; set; } + private StreamWriter Writer { get; set; } - private readonly string _server = "irc.chat.twitch.tv"; - private int Port => Options != null ? Options.UseSsl ? 443 : 80 : 0; - public System.Net.Sockets.TcpClient Client { get; private set; } - private StreamReader _reader; - private StreamWriter _writer; - private readonly Throttlers _throttlers; - private CancellationTokenSource _tokenSource = new CancellationTokenSource(); - private bool _stopServices; - private bool _networkServicesRunning; - private Task[] _networkTasks; - private Task _monitorTask; + public override bool IsConnected => Client?.Connected ?? false; - public TcpClient(IClientOptions options = null) + public TcpClient( + IClientOptions options = null, + ILogger logger = null) + : base(options, logger) { - Options = options ?? new ClientOptions(); - _throttlers = - new Throttlers(this, Options.ThrottlingPeriod, Options.WhisperThrottlingPeriod) - { - TokenSource = _tokenSource - }; - InitializeClient(); } - private void InitializeClient() + internal override void ListenTaskAction() { - // check if services should stop - if (_stopServices) { return; } - - Client = new System.Net.Sockets.TcpClient(); - - if (_monitorTask == null) + Logger?.TraceMethodCall(GetType()); + if (Reader == null) { - _monitorTask = StartMonitorTask(); - return; + Exception ex = new InvalidOperationException($"{nameof(Reader)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + RaiseFatal(ex); + throw ex; } - if (_monitorTask.IsCompleted) _monitorTask = StartMonitorTask(); - } - - public bool Open() - { - // reset some boolean values - // especially _stopServices - Reset(); - // now using private _Open() - return _Open(); - } - - /// - /// for private use only, - /// to be able to check at the beginning - /// - private bool _Open() - { - // check if services should stop - if (_stopServices) { return false; } - - try + while (IsConnected) { - if (IsConnected) return true; - - Task.Run(() => { - InitializeClient(); - Client.Connect(_server, Port); - if (Options.UseSsl) - { - var ssl = new SslStream(Client.GetStream(), false); - ssl.AuthenticateAsClient(_server); - _reader = new StreamReader(ssl); - _writer = new StreamWriter(ssl); - } - else + try + { + var input = Reader.ReadLine(); + if (input is null) { - _reader = new StreamReader(Client.GetStream()); - _writer = new StreamWriter(Client.GetStream()); + continue; } - }).Wait(10000); - - if (!IsConnected) return _Open(); - - StartNetworkServices(); - return true; - - } - catch (Exception) - { - InitializeClient(); - return false; - } - } - - public void Close(bool callDisconnect = true) - { - _reader?.Dispose(); - _writer?.Dispose(); - Client?.Close(); - _stopServices = callDisconnect; - CleanupServices(); - InitializeClient(); - OnDisconnected?.Invoke(this, new OnDisconnectedEventArgs()); - } - - public void Reconnect() - { - // reset some boolean values - // especially _stopServices - Reset(); - // now using private _Reconnect() - _Reconnect(); - } - - /// - /// for private use only, - /// to be able to check at the beginning - /// - private void _Reconnect() - { - // check if services should stop - if (_stopServices) { return; } - - Task.Run(() => - { - Task.Delay(20).Wait(); - Close(); - if(Open()) + RaiseMessage(new OnMessageEventArgs { Message = input }); + } + catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || + ex.GetType() == typeof(OperationCanceledException)) { - OnReconnected?.Invoke(this, new OnReconnectedEventArgs()); + // occurs if the Tasks are canceled by the CancellationTokenSource.Token + Logger?.LogExceptionAsInformation(GetType(), ex); } - }); - } - - public bool Send(string message) - { - try - { - if (!IsConnected || SendQueueLength >= Options.SendQueueCapacity) + catch (Exception ex) { - return false; + Logger?.LogExceptionAsError(GetType(), ex); + RaiseError(new OnErrorEventArgs { Exception = ex }); + break; } - - _throttlers.SendQueue.Add(new Tuple(DateTime.UtcNow, message)); - - return true; - } - catch (Exception ex) - { - OnError?.Invoke(this, new OnErrorEventArgs {Exception = ex}); - throw; } } - public bool SendWhisper(string message) + protected override void ClientSend(string message) { - try - { - if (!IsConnected || WhisperQueueLength >= Options.WhisperQueueCapacity) - { - return false; - } + Logger?.TraceMethodCall(GetType()); - _throttlers.WhisperQueue.Add(new Tuple(DateTime.UtcNow, message)); - - return true; - } - catch (Exception ex) + // this is not thread safe + // this method should only be called from 'ClientBase.Send()' + // where its call gets synchronized/locked + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream?view=netstandard-2.0#remarks + if (Writer == null) { - OnError?.Invoke(this, new OnErrorEventArgs {Exception = ex}); - throw; + Exception ex = new InvalidOperationException($"{nameof(Writer)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + RaiseFatal(ex); + throw ex; } - } - private void StartNetworkServices() - { - _networkServicesRunning = true; - _networkTasks = new[] - { - StartListenerTask(), - _throttlers.StartSenderTask(), - _throttlers.StartWhisperSenderTask() - }.ToArray(); - - if (!_networkTasks.Any(c => c.IsFaulted)) return; - _networkServicesRunning = false; - CleanupServices(); + Writer.WriteLine(message); + Writer.Flush(); } - public Task SendAsync(string message) + protected override void ConnectClient() { - return Task.Run(async () => + Logger?.TraceMethodCall(GetType()); + if (Client == null) { - await _writer.WriteLineAsync(message); - await _writer.FlushAsync(); - }); - } + Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + throw ex; + } - private Task StartListenerTask() - { - return Task.Run(async () => + try { - while (IsConnected && _networkServicesRunning) + // https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios +#if NET6_0_OR_GREATER + // within the following thread: + // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout + // the following answer + // NET6_0_OR_GREATER: https://stackoverflow.com/a/68998339 + + var connectTask = Client.ConnectAsync(Url, + Port); + var waitTask = connectTask.WaitAsync(TimeOutEstablishConnection, + Token); + Task.WhenAny(connectTask, waitTask).GetAwaiter().GetResult(); +#else + // within the following thread: + // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout + // the following two answers: + // https://stackoverflow.com/a/11191070 + // https://stackoverflow.com/a/22078975 + + using (var delayTaskCancellationTokenSource = new System.Threading.CancellationTokenSource()) { - try - { - var input = await _reader.ReadLineAsync(); - - if (input is null && IsConnected) - { - Send("PING"); - Task.Delay(500).Wait(); - } - - OnMessage?.Invoke(this, new OnMessageEventArgs {Message = input}); - } - catch (Exception ex) - { - OnError?.Invoke(this, new OnErrorEventArgs {Exception = ex}); - } + var connectTask = Client.ConnectAsync(Url, Port); + var delayTask = Task.Delay((int)TimeOutEstablishConnection.TotalMilliseconds, + delayTaskCancellationTokenSource.Token); + + Task.WhenAny(connectTask, delayTask).GetAwaiter().GetResult(); + delayTaskCancellationTokenSource?.Cancel(); } - }); - } - - private Task StartMonitorTask() - { - return Task.Run(() => - { - var needsReconnect = false; - var checkConnectedCounter = 0; - try +#endif + if (!Client.Connected) { - var lastState = IsConnected; - while (!_tokenSource.IsCancellationRequested) - { - if (lastState == IsConnected) - { - Thread.Sleep(200); - - if (!IsConnected) - NotConnectedCounter++; - else - checkConnectedCounter++; - - if (checkConnectedCounter >= 300) //Check every 60s for Response - { - Send("PING"); - checkConnectedCounter = 0; - } - - switch (NotConnectedCounter) - { - case 25: //Try Reconnect after 5s - case 75: //Try Reconnect after extra 10s - case 150: //Try Reconnect after extra 15s - case 300: //Try Reconnect after extra 30s - case 600: //Try Reconnect after extra 60s - _Reconnect(); - break; - default: - { - if (NotConnectedCounter >= 1200 && NotConnectedCounter % 600 == 0) //Try Reconnect after every 120s from this point - _Reconnect(); - break; - } - } - - if (NotConnectedCounter != 0 && IsConnected) - NotConnectedCounter = 0; - - continue; - } - OnStateChanged?.Invoke(this, new OnStateChangedEventArgs { IsConnected = IsConnected, WasConnected = lastState }); - - if (IsConnected) - OnConnected?.Invoke(this, new OnConnectedEventArgs()); - - if (!IsConnected && !_stopServices) - { - if (lastState && Options.ReconnectionPolicy != null && !Options.ReconnectionPolicy.AreAttemptsComplete()) - { - needsReconnect = true; - break; - } - OnDisconnected?.Invoke(this, new OnDisconnectedEventArgs()); - } - - lastState = IsConnected; - } + Logger?.TraceAction(GetType(), "Client couldn't establish connection"); + return; } - catch (Exception ex) + + Logger?.TraceAction(GetType(), "Client established connection successfully"); + if (Options.UseSsl) { - OnError?.Invoke(this, new OnErrorEventArgs {Exception = ex}); + SslStream ssl = new SslStream(Client.GetStream(), false); + ssl.AuthenticateAsClient(Url); + Reader = new StreamReader(ssl); + Writer = new StreamWriter(ssl); } - - if (needsReconnect && !_stopServices) - _Reconnect(); - }, _tokenSource.Token); - } - - private void CleanupServices() - { - _tokenSource.Cancel(); - _tokenSource = new CancellationTokenSource(); - _throttlers.TokenSource = _tokenSource; - - if (!_stopServices) return; - if (!(_networkTasks?.Length > 0)) return; - if (Task.WaitAll(_networkTasks, 15000)) return; - - OnFatality?.Invoke(this, - new OnFatalErrorEventArgs + else { - Reason = "Fatal network error. Network services fail to shut down." - }); - - // moved to Reset() - //_stopServices = false; - //_throttlers.Reconnecting = false; - //_networkServicesRunning = false; - } - - private void Reset() - { - _stopServices = false; - _throttlers.Reconnecting = false; - _networkServicesRunning = false; - } - - public void WhisperThrottled(OnWhisperThrottledEventArgs eventArgs) - { - OnWhisperThrottled?.Invoke(this, eventArgs); - } - - public void MessageThrottled(OnMessageThrottledEventArgs eventArgs) - { - OnMessageThrottled?.Invoke(this, eventArgs); + Reader = new StreamReader(Client.GetStream()); + Writer = new StreamWriter(Client.GetStream()); + } + } + catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || + ex.GetType() == typeof(OperationCanceledException)) + { + // occurs if the Tasks are canceled by the CancellationTokenSource.Token + Logger?.LogExceptionAsInformation(GetType(), ex); + } + catch (Exception ex) + { + Logger?.LogExceptionAsError(GetType(), ex); + } } - public void SendFailed(OnSendFailedEventArgs eventArgs) + protected override System.Net.Sockets.TcpClient CreateClient() { - OnSendFailed?.Invoke(this, eventArgs); - } + Logger?.TraceMethodCall(GetType()); - public void Error(OnErrorEventArgs eventArgs) - { - OnError?.Invoke(this, eventArgs); + return new System.Net.Sockets.TcpClient + { + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient.lingerstate?view=netstandard-2.0#remarks + LingerState = new System.Net.Sockets.LingerOption(true, 0) + }; } - public void Dispose() + protected override void CloseClient() { - Close(); - _throttlers.ShouldDispose = true; - _tokenSource.Cancel(); - Thread.Sleep(500); - _tokenSource.Dispose(); + Logger?.TraceMethodCall(GetType()); + Reader?.Dispose(); + Writer?.Dispose(); Client?.Dispose(); - GC.Collect(); } } -} +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index 93c5777..d88dd90 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -1,51 +1,27 @@ using System; -using System.Linq; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using TwitchLib.Communication.Enums; using TwitchLib.Communication.Events; +using TwitchLib.Communication.Extensions; using TwitchLib.Communication.Interfaces; -using TwitchLib.Communication.Models; -using TwitchLib.Communication.Services; namespace TwitchLib.Communication.Clients { - public class WebSocketClient : IClient + public class WebSocketClient : ClientBase { - private int NotConnectedCounter; - public TimeSpan DefaultKeepAliveInterval { get; set; } - public int SendQueueLength => _throttlers.SendQueue.Count; - public int WhisperQueueLength => _throttlers.WhisperQueue.Count; - public bool IsConnected => Client?.State == WebSocketState.Open; - public IClientOptions Options { get; } - public ClientWebSocket Client { get; private set; } + protected override string Url { get; } - public event EventHandler OnConnected; - public event EventHandler OnData; - public event EventHandler OnDisconnected; - public event EventHandler OnError; - public event EventHandler OnFatality; - public event EventHandler OnMessage; - public event EventHandler OnMessageThrottled; - public event EventHandler OnWhisperThrottled; - public event EventHandler OnSendFailed; - public event EventHandler OnStateChanged; - public event EventHandler OnReconnected; + public override bool IsConnected => Client?.State == WebSocketState.Open; - private string Url { get; } - private readonly Throttlers _throttlers; - private CancellationTokenSource _tokenSource = new CancellationTokenSource(); - private bool _stopServices; - private bool _networkServicesRunning; - private Task[] _networkTasks; - private Task _monitorTask; - - public WebSocketClient(IClientOptions options = null) + public WebSocketClient( + IClientOptions options = null, + ILogger logger = null) + : base(options, logger) { - Options = options ?? new ClientOptions(); - switch (Options.ClientType) { case ClientType.Chat: @@ -55,348 +31,176 @@ public WebSocketClient(IClientOptions options = null) Url = Options.UseSsl ? "wss://pubsub-edge.twitch.tv:443" : "ws://pubsub-edge.twitch.tv:80"; break; default: - throw new ArgumentOutOfRangeException(); + Exception ex = new ArgumentOutOfRangeException(nameof(Options.ClientType)); + Logger?.LogExceptionAsError(GetType(), ex); + throw ex; } - - _throttlers = new Throttlers(this, Options.ThrottlingPeriod, Options.WhisperThrottlingPeriod) { TokenSource = _tokenSource }; } - private void InitializeClient() + internal override void ListenTaskAction() { - // check if services should stop - if (_stopServices) { return; } - - Client?.Abort(); - Client = new ClientWebSocket(); - - if (_monitorTask == null) + Logger?.TraceMethodCall(GetType()); + if (Client == null) { - _monitorTask = StartMonitorTask(); - return; + Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + RaiseFatal(ex); + throw ex; } - if (_monitorTask.IsCompleted) _monitorTask = StartMonitorTask(); - } - public bool Open() - { - // reset some boolean values - // especially _stopServices - Reset(); - // now using private _Open() - return _Open(); - } - - /// - /// for private use only, - /// to be able to check at the beginning - /// - private bool _Open() - { - // check if services should stop - if (_stopServices) { return false; } - - try - { - if (IsConnected) return true; - - InitializeClient(); - Client.ConnectAsync(new Uri(Url), _tokenSource.Token).Wait(10000); - if (!IsConnected) return _Open(); - - StartNetworkServices(); - return true; - } - catch (WebSocketException) + var message = ""; + while (IsConnected) { - InitializeClient(); - return false; - } - } - - public void Close(bool callDisconnect = true) - { - Client?.Abort(); - _stopServices = callDisconnect; - CleanupServices(); - - if (!callDisconnect) - InitializeClient(); - - OnDisconnected?.Invoke(this, new OnDisconnectedEventArgs()); - } - - public void Reconnect() - { - // reset some boolean values - // especially _stopServices - Reset(); - // now using private _Reconnect() - _Reconnect(); - } - - /// - /// for private use only, - /// to be able to check at the beginning - /// - private void _Reconnect() - { - // check if services should stop - if (_stopServices) { return; } - - Task.Run(() => - { - Task.Delay(20).Wait(); - Close(); - if(Open()) + WebSocketReceiveResult result; + var buffer = new byte[1024]; + try { - OnReconnected?.Invoke(this, new OnReconnectedEventArgs()); + result = Client.ReceiveAsync(new ArraySegment(buffer), Token).GetAwaiter().GetResult(); + if (result == null) + { + continue; + } } - }); - } - - public bool Send(string message) - { - try - { - if (!IsConnected || SendQueueLength >= Options.SendQueueCapacity) + catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || + ex.GetType() == typeof(OperationCanceledException)) { - return false; + // occurs if the Tasks are canceled by the CancellationTokenSource.Token + Logger?.LogExceptionAsInformation(GetType(), ex); + break; } - - _throttlers.SendQueue.Add(new Tuple(DateTime.UtcNow, message)); - - return true; - } - catch (Exception ex) - { - OnError?.Invoke(this, new OnErrorEventArgs { Exception = ex }); - throw; - } - } - - public bool SendWhisper(string message) - { - try - { - if (!IsConnected || WhisperQueueLength >= Options.WhisperQueueCapacity) + catch (Exception ex) { - return false; + Logger?.LogExceptionAsError(GetType(), ex); + RaiseError(new OnErrorEventArgs { Exception = ex }); + break; } - _throttlers.WhisperQueue.Add(new Tuple(DateTime.UtcNow, message)); + switch (result.MessageType) + { + case WebSocketMessageType.Close: + Close(); + break; + case WebSocketMessageType.Text when !result.EndOfMessage: + message += Encoding.UTF8.GetString(buffer).TrimEnd('\0'); - return true; - } - catch (Exception ex) - { - OnError?.Invoke(this, new OnErrorEventArgs { Exception = ex }); - throw; - } - } - - private void StartNetworkServices() - { - _networkServicesRunning = true; - _networkTasks = new[] - { - StartListenerTask(), - _throttlers.StartSenderTask(), - _throttlers.StartWhisperSenderTask() - }.ToArray(); + // continue while, to receive more message-parts + continue; - if (!_networkTasks.Any(c => c.IsFaulted)) return; - _networkServicesRunning = false; - CleanupServices(); - } + case WebSocketMessageType.Text: + message += Encoding.UTF8.GetString(buffer).TrimEnd('\0'); + RaiseMessage(new OnMessageEventArgs() { Message = message }); + break; + case WebSocketMessageType.Binary: + break; + default: + Exception ex = new ArgumentOutOfRangeException(); + Logger?.LogExceptionAsError(GetType(), ex); + throw ex; + } - public Task SendAsync(byte[] message) - { - return Client.SendAsync(new ArraySegment(message), WebSocketMessageType.Text, true, _tokenSource.Token); + // clear/reset message + message = ""; + } } - private Task StartListenerTask() + protected override void ClientSend(string message) { - return Task.Run(async () => - { - var message = ""; + Logger?.TraceMethodCall(GetType()); - while (IsConnected && _networkServicesRunning) - { - WebSocketReceiveResult result; - var buffer = new byte[1024]; + // this is not thread safe + // this method should only be called from 'ClientBase.Send()' + // where its call gets synchronized/locked + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream?view=netstandard-2.0#remarks - try - { - result = await Client.ReceiveAsync(new ArraySegment(buffer), _tokenSource.Token); - } - catch - { - InitializeClient(); - break; - } - - if (result == null) continue; + // https://stackoverflow.com/a/59619916 + // links from within this thread: + // the 4th point: https://www.codetinkerer.com/2018/06/05/aspnet-core-websockets.html + // https://github.com/dotnet/corefx/blob/d6b11250b5113664dd3701c25bdf9addfacae9cc/src/Common/src/System/Net/WebSockets/ManagedWebSocket.cs#L22-L28 + if (Client == null) + { + Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + RaiseFatal(ex); + throw ex; + } - switch (result.MessageType) - { - case WebSocketMessageType.Close: - Close(); - break; - case WebSocketMessageType.Text when !result.EndOfMessage: - message += Encoding.UTF8.GetString(buffer).TrimEnd('\0'); - continue; - case WebSocketMessageType.Text: - message += Encoding.UTF8.GetString(buffer).TrimEnd('\0'); - OnMessage?.Invoke(this, new OnMessageEventArgs(){Message = message}); - break; - case WebSocketMessageType.Binary: - break; - default: - throw new ArgumentOutOfRangeException(); - } - - message = ""; - } - }); + var bytes = Encoding.UTF8.GetBytes(message); + var sendTask = Client.SendAsync(new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + Token); + sendTask.GetAwaiter().GetResult(); } - private Task StartMonitorTask() + protected override void ConnectClient() { - return Task.Run(() => + Logger?.TraceMethodCall(GetType()); + if (Client == null) { - var needsReconnect = false; - var checkConnectedCounter = 0; - try - { - var lastState = IsConnected; - while (!_tokenSource.IsCancellationRequested) - { - if (lastState == IsConnected) - { - Thread.Sleep(200); - - if (!IsConnected) - NotConnectedCounter++; - else - checkConnectedCounter++; - - if (checkConnectedCounter >= 300) //Check every 60s for Response - { - Send("PING"); - checkConnectedCounter = 0; - } - - switch (NotConnectedCounter) - { - case 25: //Try Reconnect after 5s - case 75: //Try Reconnect after extra 10s - case 150: //Try Reconnect after extra 15s - case 300: //Try Reconnect after extra 30s - case 600: //Try Reconnect after extra 60s - _Reconnect(); - break; - default: - { - if (NotConnectedCounter >= 1200 && NotConnectedCounter % 600 == 0) //Try Reconnect after every 120s from this point - _Reconnect(); - break; - } - } - - if (NotConnectedCounter != 0 && IsConnected) - NotConnectedCounter = 0; - - continue; - } - OnStateChanged?.Invoke(this, new OnStateChangedEventArgs { IsConnected = Client.State == WebSocketState.Open, WasConnected = lastState}); - - if (IsConnected) - OnConnected?.Invoke(this, new OnConnectedEventArgs()); - - if (!IsConnected && !_stopServices) - { - if (lastState && Options.ReconnectionPolicy != null && !Options.ReconnectionPolicy.AreAttemptsComplete()) - { - needsReconnect = true; - break; - } - - OnDisconnected?.Invoke(this, new OnDisconnectedEventArgs()); - if (Client.CloseStatus != null && Client.CloseStatus != WebSocketCloseStatus.NormalClosure) - OnError?.Invoke(this, new OnErrorEventArgs { Exception = new Exception(Client.CloseStatus + " " + Client.CloseStatusDescription) }); - } + Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + RaiseFatal(ex); + throw ex; + } - lastState = IsConnected; - } - } - catch (Exception ex) + try + { + // https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios +#if NET6_0_OR_GREATER + // within the following thread: + // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout + // the following answer + // NET6_0_OR_GREATER: https://stackoverflow.com/a/68998339 + var connectTask = Client.ConnectAsync(new Uri(Url), Token); + var waitTask = connectTask.WaitAsync(TimeOutEstablishConnection, Token); + // GetAwaiter().GetResult() to avoid async in method-signature 'protected override void SpecificClientConnect()'; + Task.WhenAny(connectTask, waitTask).GetAwaiter().GetResult(); +#else + // within the following thread: + // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout + // the following two answers: + // https://stackoverflow.com/a/11191070 + // https://stackoverflow.com/a/22078975 + + using (var delayTaskCancellationTokenSource = new CancellationTokenSource()) { - OnError?.Invoke(this, new OnErrorEventArgs { Exception = ex }); + var connectTask = Client.ConnectAsync(new Uri(Url), + Token); + var delayTask = Task.Delay((int)TimeOutEstablishConnection.TotalMilliseconds, + delayTaskCancellationTokenSource.Token); + + Task.WhenAny(connectTask, delayTask).GetAwaiter().GetResult(); + delayTaskCancellationTokenSource.Cancel(); } - - if (needsReconnect && !_stopServices) - _Reconnect(); - }, _tokenSource.Token); - } - - private void CleanupServices() - { - _tokenSource.Cancel(); - _tokenSource = new CancellationTokenSource(); - _throttlers.TokenSource = _tokenSource; - - if (!_stopServices) return; - if (!(_networkTasks?.Length > 0)) return; - if (Task.WaitAll(_networkTasks, 15000)) return; - - OnFatality?.Invoke(this, - new OnFatalErrorEventArgs +#endif + if (!IsConnected) { - Reason = "Fatal network error. Network services fail to shut down." - }); - - // moved to Reset() - //_stopServices = false; - //_throttlers.Reconnecting = false; - //_networkServicesRunning = false; - } - - private void Reset() - { - this._stopServices = false; - this._throttlers.Reconnecting = false; - this._networkServicesRunning = false; - } - - public void WhisperThrottled(OnWhisperThrottledEventArgs eventArgs) - { - OnWhisperThrottled?.Invoke(this, eventArgs); - } - - public void MessageThrottled(OnMessageThrottledEventArgs eventArgs) - { - OnMessageThrottled?.Invoke(this, eventArgs); - } - - public void SendFailed(OnSendFailedEventArgs eventArgs) - { - OnSendFailed?.Invoke(this, eventArgs); + Logger?.TraceAction(GetType(), "Client couldn't establish connection"); + } + } + catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || + ex.GetType() == typeof(OperationCanceledException)) + { + // occurs if the Tasks are canceled by the CancellationTokenSource.Token + Logger?.LogExceptionAsInformation(GetType(), ex); + } + catch (Exception ex) + { + Logger?.LogExceptionAsError(GetType(), ex); + } } - public void Error(OnErrorEventArgs eventArgs) + protected override ClientWebSocket CreateClient() { - OnError?.Invoke(this, eventArgs); + Logger?.TraceMethodCall(GetType()); + return new ClientWebSocket(); } - public void Dispose() + protected override void CloseClient() { - Close(); - _throttlers.ShouldDispose = true; - _tokenSource.Cancel(); - Thread.Sleep(500); - _tokenSource.Dispose(); + Logger?.TraceMethodCall(GetType()); + Client?.Abort(); Client?.Dispose(); - GC.Collect(); } } -} +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Enums/ClientType.cs b/src/TwitchLib.Communication/Enums/ClientType.cs index 706cdcc..dee4b24 100644 --- a/src/TwitchLib.Communication/Enums/ClientType.cs +++ b/src/TwitchLib.Communication/Enums/ClientType.cs @@ -5,4 +5,4 @@ public enum ClientType Chat, PubSub } -} +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Events/OnConnectedEventArgs.cs b/src/TwitchLib.Communication/Events/OnConnectedEventArgs.cs index dc1e053..7d96d52 100644 --- a/src/TwitchLib.Communication/Events/OnConnectedEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnConnectedEventArgs.cs @@ -2,6 +2,5 @@ namespace TwitchLib.Communication.Events { - public class OnConnectedEventArgs : EventArgs - { } + public class OnConnectedEventArgs : EventArgs { } } diff --git a/src/TwitchLib.Communication/Events/OnDataEventArgs.cs b/src/TwitchLib.Communication/Events/OnDataEventArgs.cs deleted file mode 100644 index a470734..0000000 --- a/src/TwitchLib.Communication/Events/OnDataEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace TwitchLib.Communication.Events -{ - public class OnDataEventArgs : EventArgs - { - public byte[] Data; - } -} diff --git a/src/TwitchLib.Communication/Events/OnDisconnectedEventArgs.cs b/src/TwitchLib.Communication/Events/OnDisconnectedEventArgs.cs index 9beb085..da3b830 100644 --- a/src/TwitchLib.Communication/Events/OnDisconnectedEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnDisconnectedEventArgs.cs @@ -2,6 +2,5 @@ namespace TwitchLib.Communication.Events { - public class OnDisconnectedEventArgs : EventArgs - { } + public class OnDisconnectedEventArgs : EventArgs { } } diff --git a/src/TwitchLib.Communication/Events/OnFatalErrorEventArgs.cs b/src/TwitchLib.Communication/Events/OnFatalErrorEventArgs.cs index f473d1d..4f51fd8 100644 --- a/src/TwitchLib.Communication/Events/OnFatalErrorEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnFatalErrorEventArgs.cs @@ -4,6 +4,16 @@ namespace TwitchLib.Communication.Events { public class OnFatalErrorEventArgs : EventArgs { - public string Reason; + public string Reason { get; } + + public OnFatalErrorEventArgs(string reason) + { + Reason = reason; + } + + public OnFatalErrorEventArgs(Exception e) + { + Reason = e.ToString(); + } } -} +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Events/OnMessageThrottledEventArgs.cs b/src/TwitchLib.Communication/Events/OnMessageThrottledEventArgs.cs deleted file mode 100644 index ce7e692..0000000 --- a/src/TwitchLib.Communication/Events/OnMessageThrottledEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace TwitchLib.Communication.Events -{ - public class OnMessageThrottledEventArgs : EventArgs - { - public string Message { get; set; } - public int SentMessageCount { get; set; } - public TimeSpan Period { get; set; } - public int AllowedInPeriod { get; set; } - } -} diff --git a/src/TwitchLib.Communication/Events/OnReconnectedEventArgs.cs b/src/TwitchLib.Communication/Events/OnReconnectedEventArgs.cs deleted file mode 100644 index 89a496c..0000000 --- a/src/TwitchLib.Communication/Events/OnReconnectedEventArgs.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System; - -namespace TwitchLib.Communication.Events -{ - public class OnReconnectedEventArgs : EventArgs - { } -} diff --git a/src/TwitchLib.Communication/Events/OnStateChangedEventArgs.cs b/src/TwitchLib.Communication/Events/OnStateChangedEventArgs.cs deleted file mode 100644 index f3bf3f1..0000000 --- a/src/TwitchLib.Communication/Events/OnStateChangedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace TwitchLib.Communication.Events -{ - public class OnStateChangedEventArgs : EventArgs - { - public bool IsConnected; - public bool WasConnected; - } -} diff --git a/src/TwitchLib.Communication/Events/OnWhisperThrottledEventArgs.cs b/src/TwitchLib.Communication/Events/OnWhisperThrottledEventArgs.cs deleted file mode 100644 index 4c985de..0000000 --- a/src/TwitchLib.Communication/Events/OnWhisperThrottledEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace TwitchLib.Communication.Events -{ - public class OnWhisperThrottledEventArgs : EventArgs - { - public string Message { get; set; } - public int SentWhisperCount { get; set; } - public TimeSpan Period { get; set; } - public int AllowedInPeriod { get; set; } - } -} diff --git a/src/TwitchLib.Communication/Extensions/LogExtensions.cs b/src/TwitchLib.Communication/Extensions/LogExtensions.cs new file mode 100644 index 0000000..997a1a0 --- /dev/null +++ b/src/TwitchLib.Communication/Extensions/LogExtensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; + +namespace TwitchLib.Communication.Extensions +{ + /// + /// expensive Extensions of the + /// + internal static class LogExtensions + { + public static void TraceMethodCall(this ILogger logger, + Type type, + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0) + { + // because of the code-formatting, 2 line is subtracted from the callerLineNumber + // cant be done inline! + callerLineNumber -= 2; + logger?.LogTrace("{FullName}.{callerMemberName} at line {callerLineNumber} is called", + type.FullName, callerMemberName, callerLineNumber); + } + public static void LogExceptionAsError(this ILogger logger, + Type type, + Exception exception, + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger?.LogError(exception, + "Exception in {FullName}.{callerMemberName} at line {callerLineNumber}:", + type.FullName, callerMemberName, callerLineNumber); + } + public static void LogExceptionAsInformation(this ILogger logger, + Type type, + Exception exception, + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger?.LogInformation(exception, + "Exception in {FullName}.{callerMemberName} at line {callerLineNumber}:", + type.FullName, callerMemberName, callerLineNumber); + } + public static void TraceAction(this ILogger logger, + Type type, + string action, + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger?.LogTrace("{FullName}.{callerMemberName} at line {callerLineNumber}: {action}", + type.FullName, callerMemberName, callerLineNumber, action); + } + } +} diff --git a/src/TwitchLib.Communication/Helpers/TaskHelper.cs b/src/TwitchLib.Communication/Helpers/TaskHelper.cs new file mode 100644 index 0000000..015a48c --- /dev/null +++ b/src/TwitchLib.Communication/Helpers/TaskHelper.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace TwitchLib.Communication.Helpers +{ + internal static class TaskHelper + { + internal static bool IsTaskRunning(this Task task) + { + return task != null + && !task.IsFaulted + && !task.IsCompleted +#if NET + && !task.IsCompletedSuccessfully +#endif + && !task.IsCanceled; + } + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Interfaces/IClient.cs b/src/TwitchLib.Communication/Interfaces/IClient.cs index 691be31..da77570 100644 --- a/src/TwitchLib.Communication/Interfaces/IClient.cs +++ b/src/TwitchLib.Communication/Interfaces/IClient.cs @@ -3,127 +3,103 @@ namespace TwitchLib.Communication.Interfaces { - public interface IClient + public interface IClient : IDisposable { /// - /// Keep alive period for the Connection. Not needed in TCP. - /// - TimeSpan DefaultKeepAliveInterval { get; set; } - - /// - /// The current number of items waiting to be sent. - /// - int SendQueueLength { get; } - - /// - /// The current number of Whispers waiting to be sent. - /// - int WhisperQueueLength { get; } - - /// - /// The current state of the connection. + /// The current state of the connection. /// bool IsConnected { get; } /// - /// Client Configuration Options + /// Client Configuration Options /// - IClientOptions Options {get;} + IClientOptions Options { get; } /// - /// Fires when the Client has connected + /// Fires when the Client has connected /// event EventHandler OnConnected; - - /// - /// Fires when Data (ByteArray) is received. - /// - event EventHandler OnData; /// - /// Fires when the Client disconnects + /// Fires when the Client disconnects /// event EventHandler OnDisconnected; /// - /// Fires when An Exception Occurs in the client + /// Fires when An Exception Occurs in the client /// event EventHandler OnError; /// - /// Fires when a Fatal Error Occurs. + /// Fires when a Fatal Error Occurs. /// event EventHandler OnFatality; /// - /// Fires when a Message/ group of messages is received. + /// Fires when a Message/ group of messages is received. /// event EventHandler OnMessage; /// - /// Fires when a Message has been throttled. - /// - event EventHandler OnMessageThrottled; - - /// - /// Fires when a Whisper has been throttled. - /// - event EventHandler OnWhisperThrottled; - - /// - /// Fires when a message Send event failed. + /// Fires when a message Send event failed. /// event EventHandler OnSendFailed; /// - /// Fires when the connection state changes + /// Fires when the client reconnects automatically /// - event EventHandler OnStateChanged; + event EventHandler OnReconnected; /// - /// Fires when the client reconnects automatically + /// tries to connect to twitch according to ! /// - event EventHandler OnReconnected; - - /// - /// Disconnect the Client from the Server - /// Set disconnect called in the client. Used in test cases. (default true) - /// - void Close(bool callDisconnect = true); - - /// - /// Dispose the Client. Forces the Send Queue to be destroyed, resulting in Message Loss. - /// - void Dispose(); - - /// - /// Connect the Client to the requested Url. - /// - /// Returns True if Connected, False if Failed to Connect. + /// + /// if a connection could be established, otherwise + /// bool Open(); /// - /// Queue a Message to Send to the server as a String. - /// - /// The Message To Queue - /// Returns True if was successfully queued. False if it fails. + /// if the underlying Client is connected, + ///

+ /// is invoked + ///

+ /// before it makes a call to and + ///

+ ///

+ /// this Method is also used by 'TwitchLib.Client.TwitchClient' + ///

+ /// whenever it receives a Reconnect-Message + ///

+ ///

+ /// so, if the twitch-servers want us to reconnect, + ///

+ /// we have to close the connection and establish a new ones + ///

+ ///

+ /// can also be used for a manual reconnect + /// + /// + /// , if the client reconnected; otherwise + /// + bool Reconnect(); + + /// + /// stops everything + /// and waits for the via given amount of milliseconds + /// + void Close(); + + /// + /// sends the given irc- + /// + /// + /// irc-message to send + /// + /// + /// , if the message should be sent + ///

+ /// otherwise + ///
bool Send(string message); - - /// - /// Queue a Whisper to Send to the server as a String. - /// - /// The Whisper To Queue - /// Returns True if was successfully queued. False if it fails. - bool SendWhisper(string message); - - /// - /// Manually reconnects the client. - /// - void Reconnect(); - - void MessageThrottled(OnMessageThrottledEventArgs eventArgs); - void SendFailed(OnSendFailedEventArgs eventArgs); - void Error(OnErrorEventArgs eventArgs); - void WhisperThrottled(OnWhisperThrottledEventArgs eventArgs); } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Interfaces/IClientOptions.cs b/src/TwitchLib.Communication/Interfaces/IClientOptions.cs index 20d373f..fc8a049 100644 --- a/src/TwitchLib.Communication/Interfaces/IClientOptions.cs +++ b/src/TwitchLib.Communication/Interfaces/IClientOptions.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Communication.Enums; +using TwitchLib.Communication.Enums; using TwitchLib.Communication.Models; namespace TwitchLib.Communication.Interfaces @@ -9,63 +8,22 @@ public interface IClientOptions /// /// Type of the Client to Create. Possible Types Chat or PubSub. /// - ClientType ClientType { get; set; } + ClientType ClientType { get; } /// - /// How long to wait on a clean disconnect [in ms] (default 20000ms). + /// How long to wait on a clean disconnect [in ms] (default 1_500ms). /// - int DisconnectWait { get; set; } - - /// - /// Number of Messages Allowed Per Instance of the Throttling Period. (default 100) - /// - int MessagesAllowedInPeriod { get; set; } + uint DisconnectWait { get; } /// /// Reconnection Policy Settings. Reconnect without Losing data etc. /// The Default Policy applied is 10 reconnection attempts with 3 seconds between each attempt. /// - ReconnectionPolicy ReconnectionPolicy { get; set; } - - /// - /// The amount of time an object can wait to be sent before it is considered dead, and should be skipped (default 30 minutes). - /// A dead item will be ignored and removed from the send queue when it is hit. - /// - TimeSpan SendCacheItemTimeout { get; set; } - - /// - /// Minimum time between sending items from the queue [in ms] (default 50ms). - /// - ushort SendDelay { get; set; } - - /// - /// Maximum number of Queued outgoing messages (default 10000). - /// - int SendQueueCapacity { get; set; } - - /// - /// Period Between each reset of the throttling instance window. (default 30s) - /// - TimeSpan ThrottlingPeriod { get; set; } + ReconnectionPolicy ReconnectionPolicy { get; } /// /// Use Secure Connection [SSL] (default: true) /// - bool UseSsl { get; set; } - - /// - /// Period Between each reset of the whisper throttling instance window. (default 60s) - /// - TimeSpan WhisperThrottlingPeriod { get; set; } - - /// - /// Number of Whispers Allowed to be sent Per Instance of the Throttling Period. (default 100) - /// - int WhispersAllowedInPeriod { get; set; } - - /// - /// Maximum number of Queued outgoing Whispers (default 10000). - /// - int WhisperQueueCapacity { get; set; } + bool UseSsl { get; } } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Models/ClientOptions.cs b/src/TwitchLib.Communication/Models/ClientOptions.cs index 6ad91f7..8dd1e96 100644 --- a/src/TwitchLib.Communication/Models/ClientOptions.cs +++ b/src/TwitchLib.Communication/Models/ClientOptions.cs @@ -1,22 +1,41 @@ -using System; -using TwitchLib.Communication.Enums; +using TwitchLib.Communication.Enums; using TwitchLib.Communication.Interfaces; namespace TwitchLib.Communication.Models { public class ClientOptions : IClientOptions { - public int SendQueueCapacity { get; set; } = 10000; - public TimeSpan SendCacheItemTimeout { get; set; } = TimeSpan.FromMinutes(30); - public ushort SendDelay { get; set; } = 50; - public ReconnectionPolicy ReconnectionPolicy { get; set; } = new ReconnectionPolicy(3000, maxAttempts: 10); - public bool UseSsl { get; set; } = true; - public int DisconnectWait { get; set; } = 20000; - public ClientType ClientType { get; set; } = ClientType.Chat; - public TimeSpan ThrottlingPeriod { get; set; } = TimeSpan.FromSeconds(30); - public int MessagesAllowedInPeriod { get; set; } = 100; - public TimeSpan WhisperThrottlingPeriod { get; set; } = TimeSpan.FromSeconds(60); - public int WhispersAllowedInPeriod { get; set; } = 100; - public int WhisperQueueCapacity { get; set; } = 10000; + public ReconnectionPolicy ReconnectionPolicy { get; } + public bool UseSsl { get; } + public uint DisconnectWait { get; } + public ClientType ClientType { get; } + + /// + /// + /// + /// your own + ///

+ /// by leaving it , a , that makes every 3_000ms one attempt to connect for ten times, is going to be applied + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public ClientOptions( + ReconnectionPolicy reconnectionPolicy = null, + bool useSsl = true, + uint disconnectWait = 1_500, + ClientType clientType = ClientType.Chat) + { + ReconnectionPolicy = reconnectionPolicy ?? new ReconnectionPolicy(3_000, maxAttempts: 10); + UseSsl = useSsl; + DisconnectWait = disconnectWait; + ClientType = clientType; + } } -} +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Models/NoReconnectionPolicy.cs b/src/TwitchLib.Communication/Models/NoReconnectionPolicy.cs new file mode 100644 index 0000000..63e3bfd --- /dev/null +++ b/src/TwitchLib.Communication/Models/NoReconnectionPolicy.cs @@ -0,0 +1,15 @@ +namespace TwitchLib.Communication.Models +{ + /// + /// This policy should be used to omit reconnect-attempts. + /// + public class NoReconnectionPolicy : ReconnectionPolicy + { + public NoReconnectionPolicy() + : base( + reconnectInterval: 0, + maxAttempts: 1) + { + } + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Models/ReconnectionPolicy.cs b/src/TwitchLib.Communication/Models/ReconnectionPolicy.cs index 1041670..b81a5cd 100644 --- a/src/TwitchLib.Communication/Models/ReconnectionPolicy.cs +++ b/src/TwitchLib.Communication/Models/ReconnectionPolicy.cs @@ -1,42 +1,82 @@ namespace TwitchLib.Communication.Models { + /// + /// Connection/Reconnection-Policy + ///

+ ///

+ /// controls the attempts to make to connect and to reconnect to twitch + ///

+ ///

+ /// to omit reconnects and to only make one attempt to connect to twitch, please use + ///
public class ReconnectionPolicy { private readonly int _reconnectStepInterval; private readonly int? _initMaxAttempts; - private int _minReconnectInterval; + private int _currentReconnectInterval; private readonly int _maxReconnectInterval; private int? _maxAttempts; private int _attemptsMade; + /// + /// the or + /// infinitely + /// attempts to reconnect + ///

+ ///

+ /// with each attempt, the reconnect interval increases by 3_000 milliseconds + /// until it reaches 30_000 milliseconds + ///

+ /// + ///

+ ///

+ /// Example: + ///

+ /// try to connect -> couldn't connect -> wait 3_000 milliseconds -> try to connect -> couldn't connect -> wait 6_000 milliseconds -> and so on + ///
public ReconnectionPolicy() { - _reconnectStepInterval = 3000; - _minReconnectInterval = 3000; - _maxReconnectInterval = 30000; + _reconnectStepInterval = 3_000; + _currentReconnectInterval = _reconnectStepInterval; + _maxReconnectInterval = 30_000; _maxAttempts = null; _initMaxAttempts = null; _attemptsMade = 0; } - public void SetMaxAttempts(int attempts) - { - _maxAttempts = attempts; - } - - public void Reset() - { - _attemptsMade = 0; - _minReconnectInterval = _reconnectStepInterval; - _maxAttempts = _initMaxAttempts; - } - - public void SetAttemptsMade(int count) => _attemptsMade = count; - - public ReconnectionPolicy(int minReconnectInterval, int maxReconnectInterval, int? maxAttempts) + /// + /// the or + /// attempts to reconnect for times + ///

+ ///

+ /// with each attempt, the reconnect interval increases by the amount of + /// until it reaches + ///

+ ///

+ /// Example: + ///

+ /// = 3_000 + ///

+ /// = 30_000 + ///

+ /// try to connect -> couldnt connect -> wait 3_000 milliseconds -> try to connect -> couldnt connect -> wait 6_000 milliseconds -> and so on + ///
+ /// + /// minimum interval in milliseconds + /// + /// + /// maximum interval in milliseconds + /// + /// + /// means infinite; it never stops to try to reconnect + /// + public ReconnectionPolicy( + int minReconnectInterval, + int maxReconnectInterval, + int maxAttempts) { _reconnectStepInterval = minReconnectInterval; - _minReconnectInterval = minReconnectInterval > maxReconnectInterval + _currentReconnectInterval = minReconnectInterval > maxReconnectInterval ? maxReconnectInterval : minReconnectInterval; _maxReconnectInterval = maxReconnectInterval; @@ -45,10 +85,36 @@ public ReconnectionPolicy(int minReconnectInterval, int maxReconnectInterval, in _attemptsMade = 0; } - public ReconnectionPolicy(int minReconnectInterval, int maxReconnectInterval) + /// + /// the or + /// infinitely + /// attempts to reconnect + ///

+ ///

+ /// with each attempt, the reconnect interval increases by the amount of + /// until it reaches + ///

+ ///

+ /// Example: + ///

+ /// = 3_000 + ///

+ /// = 30_000 + ///

+ /// try to connect -> couldn't connect -> wait 3_000 milliseconds -> try to connect -> couldn't connect -> wait 6_000 milliseconds -> and so on + ///
+ /// + /// minimum interval in milliseconds + /// + /// + /// maximum interval in milliseconds + /// + public ReconnectionPolicy( + int minReconnectInterval, + int maxReconnectInterval) { _reconnectStepInterval = minReconnectInterval; - _minReconnectInterval = minReconnectInterval > maxReconnectInterval + _currentReconnectInterval = minReconnectInterval > maxReconnectInterval ? maxReconnectInterval : minReconnectInterval; _maxReconnectInterval = maxReconnectInterval; @@ -57,37 +123,76 @@ public ReconnectionPolicy(int minReconnectInterval, int maxReconnectInterval) _attemptsMade = 0; } + /// + /// the or + /// infinitely + /// attempts to reconnect every -milliseconds + /// + /// + /// Interval in milliseconds between trying to reconnect + /// public ReconnectionPolicy(int reconnectInterval) { _reconnectStepInterval = reconnectInterval; - _minReconnectInterval = reconnectInterval; + _currentReconnectInterval = reconnectInterval; _maxReconnectInterval = reconnectInterval; _maxAttempts = null; _initMaxAttempts = null; _attemptsMade = 0; } - public ReconnectionPolicy(int reconnectInterval, int? maxAttempts) + /// + /// the or + /// attempts to reconnect every -milliseconds for times + /// + /// + /// Interval in milliseconds between trying to reconnect + /// + /// + /// means infinite; it never stops to try to reconnect + /// + public ReconnectionPolicy( + int reconnectInterval, + int? maxAttempts) { _reconnectStepInterval = reconnectInterval; - _minReconnectInterval = reconnectInterval; + _currentReconnectInterval = reconnectInterval; _maxReconnectInterval = reconnectInterval; _maxAttempts = maxAttempts; _initMaxAttempts = maxAttempts; _attemptsMade = 0; } + internal void Reset(bool isReconnect) + { + if (isReconnect) return; + _attemptsMade = 0; + _currentReconnectInterval = _reconnectStepInterval; + _maxAttempts = _initMaxAttempts; + } + internal void ProcessValues() { _attemptsMade++; - if (_minReconnectInterval < _maxReconnectInterval) - _minReconnectInterval += _reconnectStepInterval; - if (_minReconnectInterval > _maxReconnectInterval) - _minReconnectInterval = _maxReconnectInterval; + if (_currentReconnectInterval < _maxReconnectInterval) + { + _currentReconnectInterval += _reconnectStepInterval; + } + + if (_currentReconnectInterval > _maxReconnectInterval) + { + _currentReconnectInterval = _maxReconnectInterval; + } } - public int GetReconnectInterval() => _minReconnectInterval; + public int GetReconnectInterval() + { + return _currentReconnectInterval; + } - public bool AreAttemptsComplete() => _attemptsMade == _maxAttempts; + public bool AreAttemptsComplete() + { + return _attemptsMade == _maxAttempts; + } } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs new file mode 100644 index 0000000..c2b8a9e --- /dev/null +++ b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs @@ -0,0 +1,122 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using TwitchLib.Communication.Clients; +using TwitchLib.Communication.Events; +using TwitchLib.Communication.Extensions; + +namespace TwitchLib.Communication.Services +{ + /// + /// Service that checks connection state. + /// + internal class ConnectionWatchDog where T : IDisposable + { + private readonly ILogger _logger; + private readonly ClientBase _client; + + /// + /// + /// + /// should only be set to a new instance in + /// + /// + /// should only be set to in + /// + /// + /// + private CancellationTokenSource _cancellationTokenSource; + + private const int MonitorTaskDelayInMilliseconds = 200; + + internal ConnectionWatchDog( + ClientBase client, + ILogger logger = null) + { + _logger = logger; + _client = client; + } + + internal Task StartMonitorTask() + { + _logger?.TraceMethodCall(GetType()); + // We dont want to start more than one WatchDog + if (_cancellationTokenSource != null) + { + Exception ex = new InvalidOperationException("Monitor Task cant be started more than once!"); + _logger?.LogExceptionAsError(GetType(), ex); + throw ex; + } + + // This should be the only place where a new instance of CancellationTokenSource is set + _cancellationTokenSource = new CancellationTokenSource(); + + return Task.Run(MonitorTaskAction, _cancellationTokenSource.Token); + } + + internal void Stop() + { + _logger?.TraceMethodCall(GetType()); + _cancellationTokenSource?.Cancel(); + // give MonitorTaskAction a chance to catch cancellation + // otherwise it may result in an Exception + Task.Delay(MonitorTaskDelayInMilliseconds * 2).GetAwaiter().GetResult(); + _cancellationTokenSource?.Dispose(); + // set it to null for the check within this.StartMonitorTask() + _cancellationTokenSource = null; + } + + private void MonitorTaskAction() + { + _logger?.TraceMethodCall(GetType()); + try + { + while (_cancellationTokenSource != null && !_cancellationTokenSource.Token.IsCancellationRequested) + { + // we expect the client is connected, + // when this monitor task starts + // cause BaseClient.Open() starts NetworkServices after a connection could be established + if (!_client.IsConnected) + { + _logger?.TraceAction(GetType(), "Client isn't connected anymore"); + // no call to close needed, + // ReconnectInternal() calls the correct Close-Method within the Client + // ReconnectInternal() makes attempts to reconnect according to the ReconnectionPolicy within the IClientOptions + _logger?.TraceAction(GetType(), "Try to reconnect"); + var connected = _client.ReconnectInternal(); + if (!connected) + { + _logger?.TraceAction(GetType(), "Client couldn't reconnect"); + // if the ReconnectionPolicy is set up to be finite + // and no connection could be established + // a call to Client.Close() is made + // that public Close() also shuts down this ConnectionWatchDog + _client.Close(); + break; + } + + _logger?.TraceAction(GetType(), "Client reconnected"); + } + + Task.Delay(MonitorTaskDelayInMilliseconds).GetAwaiter().GetResult(); + } + } + catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || + ex.GetType() == typeof(OperationCanceledException)) + { + // Occurs if the Tasks are canceled by the CancellationTokenSource.Token + _logger?.LogExceptionAsInformation(GetType(), ex); + } + catch (Exception ex) + { + _logger?.LogExceptionAsError(GetType(), ex); + _client.RaiseError(new OnErrorEventArgs { Exception = ex }); + _client.RaiseFatal(); + + // To ensure CancellationTokenSource is set to null again call Stop(); + Stop(); + } + } + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Services/NetworkServices.cs b/src/TwitchLib.Communication/Services/NetworkServices.cs new file mode 100644 index 0000000..db27298 --- /dev/null +++ b/src/TwitchLib.Communication/Services/NetworkServices.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using TwitchLib.Communication.Clients; +using TwitchLib.Communication.Extensions; +using TwitchLib.Communication.Helpers; + +namespace TwitchLib.Communication.Services +{ + /// + /// to bundle Network-Service-s + /// + internal class NetworkServices where T : IDisposable + { + private Task _listenTask; + private Task _monitorTask; + private readonly ClientBase _client; + private readonly ILogger _logger; + private readonly ConnectionWatchDog _connectionWatchDog; + + private CancellationToken Token => _client.Token; + + internal NetworkServices( + ClientBase client, + ILogger logger = null) + { + _logger = logger; + _client = client; + _connectionWatchDog = new ConnectionWatchDog(_client, logger); + } + + internal void Start() + { + _logger?.TraceMethodCall(GetType()); + if (_monitorTask == null || !_monitorTask.IsTaskRunning()) + { + // this task is probably still running + // may be in case of a network connection loss + // all other Tasks haven't been started or have been canceled! + // ConnectionWatchDog is the only one, that has a seperate CancellationTokenSource! + _monitorTask = _connectionWatchDog.StartMonitorTask(); + } + + _listenTask = Task.Run(_client.ListenTaskAction, Token); + } + + internal void Stop() + { + _logger?.TraceMethodCall(GetType()); + _connectionWatchDog.Stop(); + } + } +} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Services/Throttlers.cs b/src/TwitchLib.Communication/Services/Throttlers.cs deleted file mode 100644 index 10a98b7..0000000 --- a/src/TwitchLib.Communication/Services/Throttlers.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using TwitchLib.Communication.Clients; -using TwitchLib.Communication.Events; -using TwitchLib.Communication.Interfaces; - -namespace TwitchLib.Communication.Services -{ - public class Throttlers - { - public readonly BlockingCollection> SendQueue = - new BlockingCollection>(); - - public readonly BlockingCollection> WhisperQueue = - new BlockingCollection>(); - - public bool Reconnecting { get; set; } = false; - public bool ShouldDispose { get; set; } = false; - public CancellationTokenSource TokenSource { get; set; } - public bool ResetThrottlerRunning; - public bool ResetWhisperThrottlerRunning; - public int SentCount = 0; - public int WhispersSent = 0; - public Task ResetThrottler; - public Task ResetWhisperThrottler; - - private readonly TimeSpan _throttlingPeriod; - private readonly TimeSpan _whisperThrottlingPeriod; - private readonly IClient _client; - - public Throttlers(IClient client, TimeSpan throttlingPeriod, TimeSpan whisperThrottlingPeriod) - { - _throttlingPeriod = throttlingPeriod; - _whisperThrottlingPeriod = whisperThrottlingPeriod; - _client = client; - } - - public void StartThrottlingWindowReset() - { - ResetThrottler = Task.Run(async () => - { - ResetThrottlerRunning = true; - while (!ShouldDispose && !Reconnecting) - { - Interlocked.Exchange(ref SentCount, 0); - await Task.Delay(_throttlingPeriod, TokenSource.Token); - } - - ResetThrottlerRunning = false; - return Task.CompletedTask; - }); - } - - public void StartWhisperThrottlingWindowReset() - { - ResetWhisperThrottler = Task.Run(async () => - { - ResetWhisperThrottlerRunning = true; - while (!ShouldDispose && !Reconnecting) - { - Interlocked.Exchange(ref WhispersSent, 0); - await Task.Delay(_whisperThrottlingPeriod, TokenSource.Token); - } - - ResetWhisperThrottlerRunning = false; - return Task.CompletedTask; - }); - } - - public void IncrementSentCount() - { - Interlocked.Increment(ref SentCount); - } - - public void IncrementWhisperCount() - { - Interlocked.Increment(ref WhispersSent); - } - - public Task StartSenderTask() - { - StartThrottlingWindowReset(); - - return Task.Run(async () => - { - try - { - while (!ShouldDispose) - { - await Task.Delay(_client.Options.SendDelay); - - if (SentCount == _client.Options.MessagesAllowedInPeriod) - { - _client.MessageThrottled(new OnMessageThrottledEventArgs - { - Message = - "Message Throttle Occured. Too Many Messages within the period specified in WebsocketClientOptions.", - AllowedInPeriod = _client.Options.MessagesAllowedInPeriod, - Period = _client.Options.ThrottlingPeriod, - SentMessageCount = Interlocked.CompareExchange(ref SentCount, 0, 0) - }); - - continue; - } - - if (!_client.IsConnected || ShouldDispose) continue; - - var msg = SendQueue.Take(TokenSource.Token); - if (msg.Item1.Add(_client.Options.SendCacheItemTimeout) < DateTime.UtcNow) continue; - - try - { - switch (_client) - { - case WebSocketClient ws: - await ws.SendAsync(Encoding.UTF8.GetBytes(msg.Item2)); - break; - case TcpClient tcp: - await tcp.SendAsync(msg.Item2); - break; - } - - IncrementSentCount(); - } - catch (Exception ex) - { - _client.SendFailed(new OnSendFailedEventArgs {Data = msg.Item2, Exception = ex}); - break; - } - } - } - catch (Exception ex) - { - _client.SendFailed(new OnSendFailedEventArgs {Data = "", Exception = ex}); - _client.Error(new OnErrorEventArgs {Exception = ex}); - } - }); - } - - public Task StartWhisperSenderTask() - { - StartWhisperThrottlingWindowReset(); - - return Task.Run(async () => - { - try - { - while (!ShouldDispose) - { - await Task.Delay(_client.Options.SendDelay); - - if (WhispersSent == _client.Options.WhispersAllowedInPeriod) - { - _client.WhisperThrottled(new OnWhisperThrottledEventArgs() - { - Message = - "Whisper Throttle Occured. Too Many Whispers within the period specified in ClientOptions.", - AllowedInPeriod = _client.Options.WhispersAllowedInPeriod, - Period = _client.Options.WhisperThrottlingPeriod, - SentWhisperCount = Interlocked.CompareExchange(ref WhispersSent, 0, 0) - }); - - continue; - } - - if (!_client.IsConnected || ShouldDispose) continue; - - var msg = WhisperQueue.Take(TokenSource.Token); - if (msg.Item1.Add(_client.Options.SendCacheItemTimeout) < DateTime.UtcNow) continue; - - try - { - switch (_client) - { - case WebSocketClient ws: - await ws.SendAsync(Encoding.UTF8.GetBytes(msg.Item2)); - break; - case TcpClient tcp: - await tcp.SendAsync(msg.Item2); - break; - } - - IncrementWhisperCount(); - } - catch (Exception ex) - { - _client.SendFailed(new OnSendFailedEventArgs {Data = msg.Item2, Exception = ex}); - break; - } - } - } - catch (Exception ex) - { - _client.SendFailed(new OnSendFailedEventArgs {Data = "", Exception = ex}); - _client.Error(new OnErrorEventArgs {Exception = ex}); - } - }); - } - } -} diff --git a/src/TwitchLib.Communication/TwitchLib.Communication.csproj b/src/TwitchLib.Communication/TwitchLib.Communication.csproj index 6f7f1a9..25c76c5 100644 --- a/src/TwitchLib.Communication/TwitchLib.Communication.csproj +++ b/src/TwitchLib.Communication/TwitchLib.Communication.csproj @@ -1,24 +1,29 @@ - + - - netstandard2.0 - 1.0.6 - $(VersionSuffix) - swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin - swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin + + netstandard2.0 + 2.0.0 + $(VersionSuffix) + swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin + swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin Connection library used throughout TwitchLib to replace third party depedencies. - Copyright 2022 - https://opensource.org/licenses/MIT - https://github.com/TwitchLib/TwitchLib.Communication - https://cdn.syzuna-programs.de/images/twitchlib.png - https://github.com/TwitchLib/TwitchLib.Communication - Git - twitch twitchlib library irc chat c# csharp api events pubsub net standard 2.0 - Fix reconnect loop on disconnect - en-US - 1.0.6 - 1.0.6 - true - - + Copyright 2022 + https://opensource.org/licenses/MIT + https://github.com/TwitchLib/TwitchLib.Communication + https://cdn.syzuna-programs.de/images/twitchlib.png + https://github.com/TwitchLib/TwitchLib.Communication + Git + twitch twitchlib library irc chat c# csharp api events pubsub net standard 2.0 + Fix reconnect loop on disconnect + en-US + 2.0.0 + 2.0.0 + true + + + + + + + From 6cc076d87969e75c536fb86616f9cab9debfb44c Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 17 Apr 2023 23:55:29 +0200 Subject: [PATCH 02/20] Async/Await refactoring (#19) * Removed all GetAwaiter().GetResult() and replaced it with proper async/await Renamed all methods that handles tasks with Async postfix Changed IClient interface to be async Replaced lock with semaphore Fixed tests to not wait after disposing * Disable tests parallelization --- .../Clients/ClientTestsBase.cs | 86 +++++++++---------- .../Clients/ClientBase.cs | 86 ++++++++++--------- .../Clients/TcpClient.cs | 59 ++++++------- .../Clients/WebsocketClient.cs | 38 ++++---- .../Interfaces/IClient.cs | 17 ++-- .../Services/ConnectionWatchDog.cs | 24 +++--- .../Services/NetworkServices.cs | 12 +-- 7 files changed, 163 insertions(+), 159 deletions(-) diff --git a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs index 1fd81de..b0bacec 100644 --- a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs +++ b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs @@ -9,6 +9,7 @@ using TwitchLib.Communication.Tests.Helpers; using Xunit; +[assembly: CollectionBehavior(DisableTestParallelization = true)] namespace TwitchLib.Communication.Tests.Clients { /// @@ -24,33 +25,33 @@ namespace TwitchLib.Communication.Tests.Clients /// public abstract class ClientTestsBase where T : IClient { - private static uint WaitAfterDispose => 3; private static TimeSpan WaitOneDuration => TimeSpan.FromSeconds(5); - private static IClientOptions Options; + private readonly IClientOptions? _options; - public ClientTestsBase(IClientOptions options = null) + protected ClientTestsBase(IClientOptions? options = null) { - Options = options; + _options = options; } [Fact] - public void Client_Raises_OnConnected_EventArgs() + public async Task Client_Raises_OnConnected_EventArgs() { // create one logger per test-method! - cause one file per test-method is generated - ILogger logger = TestLogHelper.GetLogger(); - T? client = GetClient(logger, Options); + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); Assert.NotNull(client); try { - ManualResetEvent pauseConnected = new ManualResetEvent(false); + var pauseConnected = new ManualResetEvent(false); - Assert.Raises( + await Assert.RaisesAsync( h => client.OnConnected += h, h => client.OnConnected -= h, - () => + async () => { client.OnConnected += (sender, e) => pauseConnected.Set(); - client.Open(); + await client.OpenAsync(); + Assert.True(pauseConnected.WaitOne(WaitOneDuration)); }); } @@ -61,33 +62,34 @@ public void Client_Raises_OnConnected_EventArgs() } finally { - Cleanup(client); + client.Dispose(); } } [Fact] - public void Client_Raises_OnDisconnected_EventArgs() + public async Task Client_Raises_OnDisconnected_EventArgs() { // create one logger per test-method! - cause one file per test-method is generated - ILogger logger = TestLogHelper.GetLogger(); - T? client = GetClient(logger, Options); + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); Assert.NotNull(client); try { - ManualResetEvent pauseDisconnected = new ManualResetEvent(false); + var pauseDisconnected = new ManualResetEvent(false); - Assert.Raises( + await Assert.RaisesAsync( h => client.OnDisconnected += h, h => client.OnDisconnected -= h, - () => + async () => { - client.OnConnected += (sender, e) => + client.OnConnected += async (sender, e) => { - Task.Delay(WaitOneDuration).GetAwaiter().GetResult(); - client.Close(); + await client.CloseAsync(); }; + client.OnDisconnected += (sender, e) => pauseDisconnected.Set(); - client.Open(); + await client.OpenAsync(); + Assert.True(pauseDisconnected.WaitOne(WaitOneDuration)); }); } @@ -98,30 +100,30 @@ public void Client_Raises_OnDisconnected_EventArgs() } finally { - Cleanup(client); + client.Dispose(); } } [Fact] - public void Client_Raises_OnReconnected_EventArgs() + public async Task Client_Raises_OnReconnected_EventArgs() { // create one logger per test-method! - cause one file per test-method is generated - ILogger logger = TestLogHelper.GetLogger(); - T? client = GetClient(logger, Options); + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); Assert.NotNull(client); try { - ManualResetEvent pauseReconnected = new ManualResetEvent(false); + var pauseReconnected = new ManualResetEvent(false); - Assert.Raises( + await Assert.RaisesAsync( h => client.OnReconnected += h, h => client.OnReconnected -= h, - () => + async () => { - client.OnConnected += (s, e) => client.Reconnect(); + client.OnConnected += async (s, e) => await client.ReconnectAsync(); client.OnReconnected += (s, e) => pauseReconnected.Set(); - client.Open(); + await client.OpenAsync(); Assert.True(pauseReconnected.WaitOne(WaitOneDuration)); }); @@ -133,7 +135,7 @@ public void Client_Raises_OnReconnected_EventArgs() } finally { - Cleanup(client); + client.Dispose(); } } @@ -141,11 +143,11 @@ public void Client_Raises_OnReconnected_EventArgs() public void Dispose_Client_Before_Connecting_IsOK() { // create one logger per test-method! - cause one file per test-method is generated - ILogger logger = TestLogHelper.GetLogger(); + var logger = TestLogHelper.GetLogger(); IClient? client = null; try { - client = GetClient(logger, Options); + client = GetClient(logger, _options); Assert.NotNull(client); client.Dispose(); } @@ -156,29 +158,25 @@ public void Dispose_Client_Before_Connecting_IsOK() } finally { - Cleanup((T?)client); + client?.Dispose(); } } - private static void Cleanup(T? client) - { - client?.Dispose(); - Task.Delay(TimeSpan.FromSeconds(WaitAfterDispose)).GetAwaiter().GetResult(); - } - private static TClient? GetClient(ILogger logger, IClientOptions? options = null) { - Type[] constructorParameterTypes = new Type[] + var constructorParameterTypes = new Type[] { typeof(IClientOptions), typeof(ILogger) }; - ConstructorInfo? constructor = typeof(TClient).GetConstructor(constructorParameterTypes); - object[] constructorParameters = new object[] + + var constructor = typeof(TClient).GetConstructor(constructorParameterTypes); + var constructorParameters = new object[] { options ?? new ClientOptions(), logger }; + return (TClient?)constructor?.Invoke(constructorParameters); } } diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index 6b75074..578b1a1 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -22,9 +22,10 @@ namespace TwitchLib.Communication.Clients /// /// /// - public abstract class ClientBase : IClient where T : IDisposable + public abstract class ClientBase : IClient + where T : IDisposable { - private static readonly object Lock = new object(); + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); private readonly NetworkServices _networkServices; private CancellationTokenSource _cancellationTokenSource; @@ -126,7 +127,7 @@ internal void RaiseMessage(OnMessageEventArgs eventArgs) /// /// Wont raise the given if .IsCancellationRequested /// - internal void RaiseFatal(Exception e = null) + internal void RaiseFatal(Exception ex = null) { Logger?.TraceMethodCall(GetType()); if (Token.IsCancellationRequested) @@ -135,9 +136,9 @@ internal void RaiseFatal(Exception e = null) } var onFatalErrorEventArgs = new OnFatalErrorEventArgs("Fatal network error."); - if (e != null) + if (ex != null) { - onFatalErrorEventArgs = new OnFatalErrorEventArgs(e); + onFatalErrorEventArgs = new OnFatalErrorEventArgs(ex); } OnFatality?.Invoke(this, onFatalErrorEventArgs); @@ -155,67 +156,68 @@ private void RaiseConnected() OnConnected?.Invoke(this, new OnConnectedEventArgs()); } - public bool Send(string message) + public async Task SendAsync(string message) { Logger?.TraceMethodCall(GetType()); + + await _semaphore.WaitAsync(Token); try { - lock (Lock) - { - ClientSend(message); - return true; - } + await ClientSendAsync(message); + return true; } catch (Exception e) { - RaiseSendFailed(new OnSendFailedEventArgs() { Exception = e, Data = message }); + RaiseSendFailed(new OnSendFailedEventArgs { Exception = e, Data = message }); return false; } + finally + { + _semaphore.Release(); + } } - public bool Open() + public Task OpenAsync() { Logger?.TraceMethodCall(GetType()); - return OpenPrivate(false); + return OpenPrivateAsync(false); } - public void Close() + public async Task CloseAsync() { Logger?.TraceMethodCall(GetType()); // Network services has to be stopped first so that it wont reconnect - _networkServices.Stop(); + await _networkServices.StopAsync(); // ClosePrivate() also handles IClientOptions.DisconnectWait - ClosePrivate(); + await ClosePrivateAsync(); } /// - /// + /// /// public void Dispose() { Logger?.TraceMethodCall(GetType()); - Close(); + CloseAsync().GetAwaiter().GetResult(); GC.SuppressFinalize(this); } - public bool Reconnect() + public async Task ReconnectAsync() { Logger?.TraceMethodCall(GetType()); // Stops everything (including NetworkServices) if (IsConnected) { - Close(); + await CloseAsync(); } - // interface IClient doesnt declare a return value for Reconnect() - // so we can suppress IDE0058 of ReconnectInternal() - return ReconnectInternal(); + return await ReconnectInternalAsync(); } - private bool OpenPrivate(bool isReconnect) + private async Task OpenPrivateAsync(bool isReconnect) { Logger?.TraceMethodCall(GetType()); try @@ -235,17 +237,17 @@ private bool OpenPrivate(bool isReconnect) var first = true; Options.ReconnectionPolicy.Reset(isReconnect); - while (!IsConnected - && !Options.ReconnectionPolicy.AreAttemptsComplete()) + + while (!IsConnected && + !Options.ReconnectionPolicy.AreAttemptsComplete()) { Logger?.TraceAction(GetType(), "try to connect"); if (!first) { - Task.Delay(Options.ReconnectionPolicy.GetReconnectInterval(), CancellationToken.None) - .GetAwaiter().GetResult(); + await Task.Delay(Options.ReconnectionPolicy.GetReconnectInterval(), CancellationToken.None); } - ConnectClient(); + await ConnectClientAsync(); Options.ReconnectionPolicy.ProcessValues(); first = false; } @@ -259,6 +261,7 @@ private bool OpenPrivate(bool isReconnect) Logger?.TraceAction(GetType(), "Client established a connection"); _networkServices.Start(); + if (!isReconnect) { RaiseConnected(); @@ -276,7 +279,7 @@ private bool OpenPrivate(bool isReconnect) } /// - /// Stops + /// Stops /// by calling ///

/// and enforces the @@ -285,9 +288,9 @@ private bool OpenPrivate(bool isReconnect) ///

///

/// will keep running, - /// because itself issued this call by calling + /// because itself issued this call by calling ///
- private void ClosePrivate() + private async Task ClosePrivateAsync() { Logger?.TraceMethodCall(GetType()); @@ -299,9 +302,8 @@ private void ClosePrivate() CloseClient(); RaiseDisconnected(); _cancellationTokenSource = new CancellationTokenSource(); - - Task.Delay(TimeSpan.FromMilliseconds(Options.DisconnectWait), CancellationToken.None) - .GetAwaiter().GetResult(); + + await Task.Delay(TimeSpan.FromMilliseconds(Options.DisconnectWait), CancellationToken.None); } /// @@ -310,7 +312,7 @@ private void ClosePrivate() /// /// Message to be send /// - protected abstract void ClientSend(string message); + protected abstract Task ClientSendAsync(string message); /// /// Instantiate the underlying client. @@ -336,7 +338,7 @@ private void ClosePrivate() /// /// Connect the client. /// - protected abstract void ConnectClient(); + protected abstract Task ConnectClientAsync(); /// /// To issue a reconnect @@ -353,11 +355,11 @@ private void ClosePrivate() /// /// if a connection could be established, otherwise /// - internal bool ReconnectInternal() + internal async Task ReconnectInternalAsync() { Logger?.TraceMethodCall(GetType()); - ClosePrivate(); - var reconnected = OpenPrivate(true); + await ClosePrivateAsync(); + var reconnected = await OpenPrivateAsync(true); if (reconnected) { RaiseReconnected(); @@ -370,6 +372,6 @@ internal bool ReconnectInternal() /// just the Action that listens for new Messages /// the corresponding is held by /// - internal abstract void ListenTaskAction(); + internal abstract Task ListenTaskActionAsync(); } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Clients/TcpClient.cs b/src/TwitchLib.Communication/Clients/TcpClient.cs index 7467280..be18a5e 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Net.Security; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using TwitchLib.Communication.Events; @@ -11,11 +12,12 @@ namespace TwitchLib.Communication.Clients { public class TcpClient : ClientBase { + private StreamReader _reader; + private StreamWriter _writer; + protected override string Url => "irc.chat.twitch.tv"; private int Port => Options.UseSsl ? 6697 : 6667; - private StreamReader Reader { get; set; } - private StreamWriter Writer { get; set; } public override bool IsConnected => Client?.Connected ?? false; @@ -26,12 +28,12 @@ public TcpClient( { } - internal override void ListenTaskAction() + internal override async Task ListenTaskActionAsync() { Logger?.TraceMethodCall(GetType()); - if (Reader == null) + if (_reader == null) { - Exception ex = new InvalidOperationException($"{nameof(Reader)} was null!"); + var ex = new InvalidOperationException($"{nameof(_reader)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); RaiseFatal(ex); throw ex; @@ -41,7 +43,7 @@ internal override void ListenTaskAction() { try { - var input = Reader.ReadLine(); + var input = await _reader.ReadLineAsync(); if (input is null) { continue; @@ -64,7 +66,7 @@ internal override void ListenTaskAction() } } - protected override void ClientSend(string message) + protected override async Task ClientSendAsync(string message) { Logger?.TraceMethodCall(GetType()); @@ -72,19 +74,19 @@ protected override void ClientSend(string message) // this method should only be called from 'ClientBase.Send()' // where its call gets synchronized/locked // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream?view=netstandard-2.0#remarks - if (Writer == null) + if (_writer == null) { - Exception ex = new InvalidOperationException($"{nameof(Writer)} was null!"); + var ex = new InvalidOperationException($"{nameof(_writer)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); RaiseFatal(ex); throw ex; } - Writer.WriteLine(message); - Writer.Flush(); + await _writer.WriteLineAsync(message); + await _writer.FlushAsync(); } - protected override void ConnectClient() + protected override async Task ConnectClientAsync() { Logger?.TraceMethodCall(GetType()); if (Client == null) @@ -103,11 +105,9 @@ protected override void ConnectClient() // the following answer // NET6_0_OR_GREATER: https://stackoverflow.com/a/68998339 - var connectTask = Client.ConnectAsync(Url, - Port); - var waitTask = connectTask.WaitAsync(TimeOutEstablishConnection, - Token); - Task.WhenAny(connectTask, waitTask).GetAwaiter().GetResult(); + var connectTask = Client.ConnectAsync(Url, Port); + var waitTask = connectTask.WaitAsync(TimeOutEstablishConnection, Token); + await Task.WhenAny(connectTask, waitTask); #else // within the following thread: // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout @@ -115,14 +115,15 @@ protected override void ConnectClient() // https://stackoverflow.com/a/11191070 // https://stackoverflow.com/a/22078975 - using (var delayTaskCancellationTokenSource = new System.Threading.CancellationTokenSource()) + using (var delayTaskCancellationTokenSource = new CancellationTokenSource()) { var connectTask = Client.ConnectAsync(Url, Port); - var delayTask = Task.Delay((int)TimeOutEstablishConnection.TotalMilliseconds, + var delayTask = Task.Delay( + (int)TimeOutEstablishConnection.TotalMilliseconds, delayTaskCancellationTokenSource.Token); - Task.WhenAny(connectTask, delayTask).GetAwaiter().GetResult(); - delayTaskCancellationTokenSource?.Cancel(); + await Task.WhenAny(connectTask, delayTask); + delayTaskCancellationTokenSource.Cancel(); } #endif if (!Client.Connected) @@ -134,15 +135,15 @@ protected override void ConnectClient() Logger?.TraceAction(GetType(), "Client established connection successfully"); if (Options.UseSsl) { - SslStream ssl = new SslStream(Client.GetStream(), false); - ssl.AuthenticateAsClient(Url); - Reader = new StreamReader(ssl); - Writer = new StreamWriter(ssl); + var ssl = new SslStream(Client.GetStream(), false); + await ssl.AuthenticateAsClientAsync(Url); + _reader = new StreamReader(ssl); + _writer = new StreamWriter(ssl); } else { - Reader = new StreamReader(Client.GetStream()); - Writer = new StreamWriter(Client.GetStream()); + _reader = new StreamReader(Client.GetStream()); + _writer = new StreamWriter(Client.GetStream()); } } catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || @@ -171,8 +172,8 @@ protected override System.Net.Sockets.TcpClient CreateClient() protected override void CloseClient() { Logger?.TraceMethodCall(GetType()); - Reader?.Dispose(); - Writer?.Dispose(); + _reader?.Dispose(); + _writer?.Dispose(); Client?.Dispose(); } } diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index d88dd90..a3d9c40 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -31,31 +31,31 @@ public WebSocketClient( Url = Options.UseSsl ? "wss://pubsub-edge.twitch.tv:443" : "ws://pubsub-edge.twitch.tv:80"; break; default: - Exception ex = new ArgumentOutOfRangeException(nameof(Options.ClientType)); + var ex = new ArgumentOutOfRangeException(nameof(Options.ClientType)); Logger?.LogExceptionAsError(GetType(), ex); throw ex; } } - internal override void ListenTaskAction() + internal override async Task ListenTaskActionAsync() { Logger?.TraceMethodCall(GetType()); if (Client == null) { - Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); + var ex = new InvalidOperationException($"{nameof(Client)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); RaiseFatal(ex); throw ex; } - var message = ""; + var message = string.Empty; while (IsConnected) { WebSocketReceiveResult result; var buffer = new byte[1024]; try { - result = Client.ReceiveAsync(new ArraySegment(buffer), Token).GetAwaiter().GetResult(); + result = await Client.ReceiveAsync(new ArraySegment(buffer), Token); if (result == null) { continue; @@ -78,7 +78,7 @@ internal override void ListenTaskAction() switch (result.MessageType) { case WebSocketMessageType.Close: - Close(); + await CloseAsync(); break; case WebSocketMessageType.Text when !result.EndOfMessage: message += Encoding.UTF8.GetString(buffer).TrimEnd('\0'); @@ -99,11 +99,11 @@ internal override void ListenTaskAction() } // clear/reset message - message = ""; + message = string.Empty; } } - protected override void ClientSend(string message) + protected override async Task ClientSendAsync(string message) { Logger?.TraceMethodCall(GetType()); @@ -118,26 +118,25 @@ protected override void ClientSend(string message) // https://github.com/dotnet/corefx/blob/d6b11250b5113664dd3701c25bdf9addfacae9cc/src/Common/src/System/Net/WebSockets/ManagedWebSocket.cs#L22-L28 if (Client == null) { - Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); + var ex = new InvalidOperationException($"{nameof(Client)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); RaiseFatal(ex); throw ex; } var bytes = Encoding.UTF8.GetBytes(message); - var sendTask = Client.SendAsync(new ArraySegment(bytes), + await Client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, Token); - sendTask.GetAwaiter().GetResult(); } - protected override void ConnectClient() + protected override async Task ConnectClientAsync() { Logger?.TraceMethodCall(GetType()); if (Client == null) { - Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); + var ex = new InvalidOperationException($"{nameof(Client)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); RaiseFatal(ex); throw ex; @@ -153,8 +152,7 @@ protected override void ConnectClient() // NET6_0_OR_GREATER: https://stackoverflow.com/a/68998339 var connectTask = Client.ConnectAsync(new Uri(Url), Token); var waitTask = connectTask.WaitAsync(TimeOutEstablishConnection, Token); - // GetAwaiter().GetResult() to avoid async in method-signature 'protected override void SpecificClientConnect()'; - Task.WhenAny(connectTask, waitTask).GetAwaiter().GetResult(); + await Task.WhenAny(connectTask, waitTask); #else // within the following thread: // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout @@ -164,12 +162,12 @@ protected override void ConnectClient() using (var delayTaskCancellationTokenSource = new CancellationTokenSource()) { - var connectTask = Client.ConnectAsync(new Uri(Url), - Token); - var delayTask = Task.Delay((int)TimeOutEstablishConnection.TotalMilliseconds, + var connectTask = Client.ConnectAsync(new Uri(Url), Token); + var delayTask = Task.Delay( + (int)TimeOutEstablishConnection.TotalMilliseconds, delayTaskCancellationTokenSource.Token); - - Task.WhenAny(connectTask, delayTask).GetAwaiter().GetResult(); + + await Task.WhenAny(connectTask, delayTask); delayTaskCancellationTokenSource.Cancel(); } #endif diff --git a/src/TwitchLib.Communication/Interfaces/IClient.cs b/src/TwitchLib.Communication/Interfaces/IClient.cs index da77570..dff63fb 100644 --- a/src/TwitchLib.Communication/Interfaces/IClient.cs +++ b/src/TwitchLib.Communication/Interfaces/IClient.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using TwitchLib.Communication.Events; namespace TwitchLib.Communication.Interfaces @@ -56,14 +57,14 @@ public interface IClient : IDisposable /// /// if a connection could be established, otherwise /// - bool Open(); + Task OpenAsync(); /// /// if the underlying Client is connected, ///

- /// is invoked + /// is invoked ///

- /// before it makes a call to and + /// before it makes a call to and ///

///

/// this Method is also used by 'TwitchLib.Client.TwitchClient' @@ -81,25 +82,25 @@ public interface IClient : IDisposable /// /// , if the client reconnected; otherwise /// - bool Reconnect(); + Task ReconnectAsync(); /// /// stops everything /// and waits for the via given amount of milliseconds /// - void Close(); + Task CloseAsync(); /// - /// sends the given irc- + /// Sends the given irc- /// /// /// irc-message to send /// /// - /// , if the message should be sent + /// , if the message was sent ///

/// otherwise ///
- bool Send(string message); + Task SendAsync(string message); } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs index c2b8a9e..6bf4034 100644 --- a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs +++ b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs @@ -22,7 +22,7 @@ internal class ConnectionWatchDog where T : IDisposable /// should only be set to a new instance in /// /// - /// should only be set to in + /// should only be set to in /// /// ///
@@ -38,7 +38,7 @@ internal ConnectionWatchDog( _client = client; } - internal Task StartMonitorTask() + internal Task StartMonitorTaskAsync() { _logger?.TraceMethodCall(GetType()); // We dont want to start more than one WatchDog @@ -52,27 +52,28 @@ internal Task StartMonitorTask() // This should be the only place where a new instance of CancellationTokenSource is set _cancellationTokenSource = new CancellationTokenSource(); - return Task.Run(MonitorTaskAction, _cancellationTokenSource.Token); + return Task.Run(MonitorTaskActionAsync, _cancellationTokenSource.Token); } - internal void Stop() + internal async Task StopAsync() { _logger?.TraceMethodCall(GetType()); _cancellationTokenSource?.Cancel(); // give MonitorTaskAction a chance to catch cancellation // otherwise it may result in an Exception - Task.Delay(MonitorTaskDelayInMilliseconds * 2).GetAwaiter().GetResult(); + await Task.Delay(MonitorTaskDelayInMilliseconds * 2); _cancellationTokenSource?.Dispose(); // set it to null for the check within this.StartMonitorTask() _cancellationTokenSource = null; } - private void MonitorTaskAction() + private async Task MonitorTaskActionAsync() { _logger?.TraceMethodCall(GetType()); try { - while (_cancellationTokenSource != null && !_cancellationTokenSource.Token.IsCancellationRequested) + while (_cancellationTokenSource != null && + !_cancellationTokenSource.Token.IsCancellationRequested) { // we expect the client is connected, // when this monitor task starts @@ -84,7 +85,8 @@ private void MonitorTaskAction() // ReconnectInternal() calls the correct Close-Method within the Client // ReconnectInternal() makes attempts to reconnect according to the ReconnectionPolicy within the IClientOptions _logger?.TraceAction(GetType(), "Try to reconnect"); - var connected = _client.ReconnectInternal(); + + var connected = await _client.ReconnectInternalAsync(); if (!connected) { _logger?.TraceAction(GetType(), "Client couldn't reconnect"); @@ -92,14 +94,14 @@ private void MonitorTaskAction() // and no connection could be established // a call to Client.Close() is made // that public Close() also shuts down this ConnectionWatchDog - _client.Close(); + await _client.CloseAsync(); break; } _logger?.TraceAction(GetType(), "Client reconnected"); } - Task.Delay(MonitorTaskDelayInMilliseconds).GetAwaiter().GetResult(); + await Task.Delay(MonitorTaskDelayInMilliseconds); } } catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || @@ -115,7 +117,7 @@ private void MonitorTaskAction() _client.RaiseFatal(); // To ensure CancellationTokenSource is set to null again call Stop(); - Stop(); + await StopAsync(); } } } diff --git a/src/TwitchLib.Communication/Services/NetworkServices.cs b/src/TwitchLib.Communication/Services/NetworkServices.cs index db27298..590e0ae 100644 --- a/src/TwitchLib.Communication/Services/NetworkServices.cs +++ b/src/TwitchLib.Communication/Services/NetworkServices.cs @@ -38,17 +38,19 @@ internal void Start() // this task is probably still running // may be in case of a network connection loss // all other Tasks haven't been started or have been canceled! - // ConnectionWatchDog is the only one, that has a seperate CancellationTokenSource! - _monitorTask = _connectionWatchDog.StartMonitorTask(); + // ConnectionWatchDog is the only one, that has a separate CancellationTokenSource! + + // Let those tasks run in the background, do not await them + _monitorTask = _connectionWatchDog.StartMonitorTaskAsync(); } - _listenTask = Task.Run(_client.ListenTaskAction, Token); + _listenTask = Task.Run(_client.ListenTaskActionAsync, Token); } - internal void Stop() + internal async Task StopAsync() { _logger?.TraceMethodCall(GetType()); - _connectionWatchDog.Stop(); + await _connectionWatchDog.StopAsync(); } } } \ No newline at end of file From 717c3f2d3f066ec8236cc8e02ec0bfcf1b7f5c64 Mon Sep 17 00:00:00 2001 From: ondru Date: Tue, 18 Apr 2023 22:06:37 +0200 Subject: [PATCH 03/20] code improvement and bug fix in `WebSocketClient` --- .../Clients/ClientBase.cs | 8 ++-- .../Clients/TcpClient.cs | 17 ++++---- .../Clients/WebsocketClient.cs | 40 ++++++++++--------- .../Extensions/LogExtensions.cs | 8 ++-- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index 578b1a1..7fccb59 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -135,11 +135,9 @@ internal void RaiseFatal(Exception ex = null) return; } - var onFatalErrorEventArgs = new OnFatalErrorEventArgs("Fatal network error."); - if (ex != null) - { - onFatalErrorEventArgs = new OnFatalErrorEventArgs(ex); - } + var onFatalErrorEventArgs = ex != null + ? new OnFatalErrorEventArgs(ex) + : new OnFatalErrorEventArgs("Fatal network error."); OnFatality?.Invoke(this, onFatalErrorEventArgs); } diff --git a/src/TwitchLib.Communication/Clients/TcpClient.cs b/src/TwitchLib.Communication/Clients/TcpClient.cs index be18a5e..1fbfa13 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -114,14 +114,14 @@ protected override async Task ConnectClientAsync() // the following two answers: // https://stackoverflow.com/a/11191070 // https://stackoverflow.com/a/22078975 - + using (var delayTaskCancellationTokenSource = new CancellationTokenSource()) { var connectTask = Client.ConnectAsync(Url, Port); var delayTask = Task.Delay( (int)TimeOutEstablishConnection.TotalMilliseconds, delayTaskCancellationTokenSource.Token); - + await Task.WhenAny(connectTask, delayTask); delayTaskCancellationTokenSource.Cancel(); } @@ -133,18 +133,15 @@ protected override async Task ConnectClientAsync() } Logger?.TraceAction(GetType(), "Client established connection successfully"); + Stream stream = Client.GetStream(); if (Options.UseSsl) { - var ssl = new SslStream(Client.GetStream(), false); + var ssl = new SslStream(stream, false); await ssl.AuthenticateAsClientAsync(Url); - _reader = new StreamReader(ssl); - _writer = new StreamWriter(ssl); - } - else - { - _reader = new StreamReader(Client.GetStream()); - _writer = new StreamWriter(Client.GetStream()); + stream = ssl; } + _reader = new StreamReader(stream); + _writer = new StreamWriter(stream); } catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || ex.GetType() == typeof(OperationCanceledException)) diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index a3d9c40..984a4a9 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -48,18 +49,15 @@ internal override async Task ListenTaskActionAsync() throw ex; } - var message = string.Empty; + var memoryStream = new MemoryStream(); + var bytes = new byte[1024]; + var buffer = new ArraySegment(bytes); + WebSocketReceiveResult result; while (IsConnected) { - WebSocketReceiveResult result; - var buffer = new byte[1024]; try { - result = await Client.ReceiveAsync(new ArraySegment(buffer), Token); - if (result == null) - { - continue; - } + result = await Client.ReceiveAsync(buffer, Token); } catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || ex.GetType() == typeof(OperationCanceledException)) @@ -80,26 +78,30 @@ internal override async Task ListenTaskActionAsync() case WebSocketMessageType.Close: await CloseAsync(); break; - case WebSocketMessageType.Text when !result.EndOfMessage: - message += Encoding.UTF8.GetString(buffer).TrimEnd('\0'); - - // continue while, to receive more message-parts - continue; - case WebSocketMessageType.Text: - message += Encoding.UTF8.GetString(buffer).TrimEnd('\0'); - RaiseMessage(new OnMessageEventArgs() { Message = message }); + if (result.EndOfMessage && memoryStream.Position == 0) + { + //optimization when we can read the whole message at once + var message = Encoding.UTF8.GetString(bytes, 0, result.Count); + RaiseMessage(new OnMessageEventArgs() { Message = message }); + break; + } + memoryStream.Write(bytes, 0, result.Count); + if (result.EndOfMessage) + { + var message = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Position); + RaiseMessage(new OnMessageEventArgs() { Message = message }); + memoryStream.Position = 0; + } break; case WebSocketMessageType.Binary: + //todo break; default: Exception ex = new ArgumentOutOfRangeException(); Logger?.LogExceptionAsError(GetType(), ex); throw ex; } - - // clear/reset message - message = string.Empty; } } diff --git a/src/TwitchLib.Communication/Extensions/LogExtensions.cs b/src/TwitchLib.Communication/Extensions/LogExtensions.cs index 997a1a0..ccf4741 100644 --- a/src/TwitchLib.Communication/Extensions/LogExtensions.cs +++ b/src/TwitchLib.Communication/Extensions/LogExtensions.cs @@ -17,7 +17,7 @@ public static void TraceMethodCall(this ILogger logger, // because of the code-formatting, 2 line is subtracted from the callerLineNumber // cant be done inline! callerLineNumber -= 2; - logger?.LogTrace("{FullName}.{callerMemberName} at line {callerLineNumber} is called", + logger.LogTrace("{FullName}.{callerMemberName} at line {callerLineNumber} is called", type.FullName, callerMemberName, callerLineNumber); } public static void LogExceptionAsError(this ILogger logger, @@ -26,7 +26,7 @@ public static void LogExceptionAsError(this ILogger logger, [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0) { - logger?.LogError(exception, + logger.LogError(exception, "Exception in {FullName}.{callerMemberName} at line {callerLineNumber}:", type.FullName, callerMemberName, callerLineNumber); } @@ -36,7 +36,7 @@ public static void LogExceptionAsInformation(this ILogger logger, [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0) { - logger?.LogInformation(exception, + logger.LogInformation(exception, "Exception in {FullName}.{callerMemberName} at line {callerLineNumber}:", type.FullName, callerMemberName, callerLineNumber); } @@ -46,7 +46,7 @@ public static void TraceAction(this ILogger logger, [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0) { - logger?.LogTrace("{FullName}.{callerMemberName} at line {callerLineNumber}: {action}", + logger.LogTrace("{FullName}.{callerMemberName} at line {callerLineNumber}: {action}", type.FullName, callerMemberName, callerLineNumber, action); } } From 3e6d2baf6add7b1a50848a32d5cf3c15cd863a23 Mon Sep 17 00:00:00 2001 From: ondru Date: Wed, 19 Apr 2023 18:03:37 +0200 Subject: [PATCH 04/20] enable --- .../Clients/ClientBase.cs | 28 +++++++++---------- .../Clients/TcpClient.cs | 14 +++++----- .../Clients/WebsocketClient.cs | 10 +++---- .../Events/OnErrorEventArgs.cs | 7 ++++- .../Events/OnMessageEventArgs.cs | 7 ++++- .../Events/OnSendFailedEventArgs.cs | 10 +++++-- .../Helpers/TaskHelper.cs | 2 +- .../Interfaces/IClient.cs | 14 +++++----- .../Models/ClientOptions.cs | 2 +- .../Services/ConnectionWatchDog.cs | 8 +++--- .../Services/NetworkServices.cs | 8 +++--- .../TwitchLib.Communication.csproj | 4 ++- 12 files changed, 66 insertions(+), 48 deletions(-) diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index 7fccb59..0b9c937 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -37,30 +37,30 @@ public abstract class ClientBase : IClient internal static TimeSpan TimeOutEstablishConnection => TimeSpan.FromSeconds(15); - protected ILogger Logger { get; } + protected ILogger? Logger { get; } protected abstract string Url { get; } /// /// The underlying client. /// - public T Client { get; private set; } + public T? Client { get; private set; } public abstract bool IsConnected { get; } public IClientOptions Options { get; } - public event EventHandler OnConnected; - public event EventHandler OnDisconnected; - public event EventHandler OnError; - public event EventHandler OnFatality; - public event EventHandler OnMessage; - public event EventHandler OnSendFailed; - public event EventHandler OnReconnected; + public event EventHandler? OnConnected; + public event EventHandler? OnDisconnected; + public event EventHandler? OnError; + public event EventHandler? OnFatality; + public event EventHandler? OnMessage; + public event EventHandler? OnSendFailed; + public event EventHandler? OnReconnected; internal ClientBase( - IClientOptions options = null, - ILogger logger = null) + IClientOptions? options, + ILogger? logger) { Logger = logger; _cancellationTokenSource = new CancellationTokenSource(); @@ -127,7 +127,7 @@ internal void RaiseMessage(OnMessageEventArgs eventArgs) /// /// Wont raise the given if .IsCancellationRequested /// - internal void RaiseFatal(Exception ex = null) + internal void RaiseFatal(Exception? ex = null) { Logger?.TraceMethodCall(GetType()); if (Token.IsCancellationRequested) @@ -166,7 +166,7 @@ public async Task SendAsync(string message) } catch (Exception e) { - RaiseSendFailed(new OnSendFailedEventArgs { Exception = e, Data = message }); + RaiseSendFailed(new OnSendFailedEventArgs(e, message)); return false; } finally @@ -270,7 +270,7 @@ private async Task OpenPrivateAsync(bool isReconnect) catch (Exception ex) { Logger?.LogExceptionAsError(GetType(), ex); - RaiseError(new OnErrorEventArgs { Exception = ex }); + RaiseError(new OnErrorEventArgs(ex)); RaiseFatal(); return false; } diff --git a/src/TwitchLib.Communication/Clients/TcpClient.cs b/src/TwitchLib.Communication/Clients/TcpClient.cs index 1fbfa13..5ffb9a2 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -12,9 +12,9 @@ namespace TwitchLib.Communication.Clients { public class TcpClient : ClientBase { - private StreamReader _reader; - private StreamWriter _writer; - + private StreamReader? _reader; + private StreamWriter? _writer; + protected override string Url => "irc.chat.twitch.tv"; private int Port => Options.UseSsl ? 6697 : 6667; @@ -22,8 +22,8 @@ public class TcpClient : ClientBase public override bool IsConnected => Client?.Connected ?? false; public TcpClient( - IClientOptions options = null, - ILogger logger = null) + IClientOptions? options = null, + ILogger? logger = null) : base(options, logger) { } @@ -49,7 +49,7 @@ internal override async Task ListenTaskActionAsync() continue; } - RaiseMessage(new OnMessageEventArgs { Message = input }); + RaiseMessage(new OnMessageEventArgs(input)); } catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || ex.GetType() == typeof(OperationCanceledException)) @@ -60,7 +60,7 @@ internal override async Task ListenTaskActionAsync() catch (Exception ex) { Logger?.LogExceptionAsError(GetType(), ex); - RaiseError(new OnErrorEventArgs { Exception = ex }); + RaiseError(new OnErrorEventArgs(ex)); break; } } diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index 984a4a9..4a7d4fd 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -19,8 +19,8 @@ public class WebSocketClient : ClientBase public override bool IsConnected => Client?.State == WebSocketState.Open; public WebSocketClient( - IClientOptions options = null, - ILogger logger = null) + IClientOptions? options = null, + ILogger? logger = null) : base(options, logger) { switch (Options.ClientType) @@ -69,7 +69,7 @@ internal override async Task ListenTaskActionAsync() catch (Exception ex) { Logger?.LogExceptionAsError(GetType(), ex); - RaiseError(new OnErrorEventArgs { Exception = ex }); + RaiseError(new OnErrorEventArgs(ex)); break; } @@ -83,14 +83,14 @@ internal override async Task ListenTaskActionAsync() { //optimization when we can read the whole message at once var message = Encoding.UTF8.GetString(bytes, 0, result.Count); - RaiseMessage(new OnMessageEventArgs() { Message = message }); + RaiseMessage(new OnMessageEventArgs(message)); break; } memoryStream.Write(bytes, 0, result.Count); if (result.EndOfMessage) { var message = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Position); - RaiseMessage(new OnMessageEventArgs() { Message = message }); + RaiseMessage(new OnMessageEventArgs(message)); memoryStream.Position = 0; } break; diff --git a/src/TwitchLib.Communication/Events/OnErrorEventArgs.cs b/src/TwitchLib.Communication/Events/OnErrorEventArgs.cs index 3d630dc..07752f0 100644 --- a/src/TwitchLib.Communication/Events/OnErrorEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnErrorEventArgs.cs @@ -4,6 +4,11 @@ namespace TwitchLib.Communication.Events { public class OnErrorEventArgs : EventArgs { - public Exception Exception { get; set; } + public Exception Exception { get; } + + public OnErrorEventArgs(Exception exception) + { + Exception = exception; + } } } diff --git a/src/TwitchLib.Communication/Events/OnMessageEventArgs.cs b/src/TwitchLib.Communication/Events/OnMessageEventArgs.cs index ab56142..9fe7282 100644 --- a/src/TwitchLib.Communication/Events/OnMessageEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnMessageEventArgs.cs @@ -4,6 +4,11 @@ namespace TwitchLib.Communication.Events { public class OnMessageEventArgs : EventArgs { - public string Message; + public string Message { get; } + + public OnMessageEventArgs(string message) + { + Message = message; + } } } diff --git a/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs b/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs index 79812e7..e1c1db1 100644 --- a/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs @@ -4,7 +4,13 @@ namespace TwitchLib.Communication.Events { public class OnSendFailedEventArgs : EventArgs { - public string Data; - public Exception Exception; + public string Data { get; } + public Exception Exception { get; } + + public OnSendFailedEventArgs(Exception exception, string data) + { + Exception = exception; + Data = data; + } } } diff --git a/src/TwitchLib.Communication/Helpers/TaskHelper.cs b/src/TwitchLib.Communication/Helpers/TaskHelper.cs index 015a48c..790d5b0 100644 --- a/src/TwitchLib.Communication/Helpers/TaskHelper.cs +++ b/src/TwitchLib.Communication/Helpers/TaskHelper.cs @@ -4,7 +4,7 @@ namespace TwitchLib.Communication.Helpers { internal static class TaskHelper { - internal static bool IsTaskRunning(this Task task) + internal static bool IsTaskRunning(this Task? task) { return task != null && !task.IsFaulted diff --git a/src/TwitchLib.Communication/Interfaces/IClient.cs b/src/TwitchLib.Communication/Interfaces/IClient.cs index dff63fb..5c71b7c 100644 --- a/src/TwitchLib.Communication/Interfaces/IClient.cs +++ b/src/TwitchLib.Communication/Interfaces/IClient.cs @@ -19,37 +19,37 @@ public interface IClient : IDisposable /// /// Fires when the Client has connected /// - event EventHandler OnConnected; + event EventHandler? OnConnected; /// /// Fires when the Client disconnects /// - event EventHandler OnDisconnected; + event EventHandler? OnDisconnected; /// /// Fires when An Exception Occurs in the client /// - event EventHandler OnError; + event EventHandler? OnError; /// /// Fires when a Fatal Error Occurs. /// - event EventHandler OnFatality; + event EventHandler? OnFatality; /// /// Fires when a Message/ group of messages is received. /// - event EventHandler OnMessage; + event EventHandler? OnMessage; /// /// Fires when a message Send event failed. /// - event EventHandler OnSendFailed; + event EventHandler? OnSendFailed; /// /// Fires when the client reconnects automatically /// - event EventHandler OnReconnected; + event EventHandler? OnReconnected; /// /// tries to connect to twitch according to ! diff --git a/src/TwitchLib.Communication/Models/ClientOptions.cs b/src/TwitchLib.Communication/Models/ClientOptions.cs index 8dd1e96..c97dd89 100644 --- a/src/TwitchLib.Communication/Models/ClientOptions.cs +++ b/src/TwitchLib.Communication/Models/ClientOptions.cs @@ -27,7 +27,7 @@ public class ClientOptions : IClientOptions /// /// public ClientOptions( - ReconnectionPolicy reconnectionPolicy = null, + ReconnectionPolicy? reconnectionPolicy = null, bool useSsl = true, uint disconnectWait = 1_500, ClientType clientType = ClientType.Chat) diff --git a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs index 6bf4034..2bac625 100644 --- a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs +++ b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs @@ -13,7 +13,7 @@ namespace TwitchLib.Communication.Services /// internal class ConnectionWatchDog where T : IDisposable { - private readonly ILogger _logger; + private readonly ILogger? _logger; private readonly ClientBase _client; /// @@ -26,13 +26,13 @@ internal class ConnectionWatchDog where T : IDisposable /// /// /// - private CancellationTokenSource _cancellationTokenSource; + private CancellationTokenSource? _cancellationTokenSource; private const int MonitorTaskDelayInMilliseconds = 200; internal ConnectionWatchDog( ClientBase client, - ILogger logger = null) + ILogger? logger = null) { _logger = logger; _client = client; @@ -113,7 +113,7 @@ private async Task MonitorTaskActionAsync() catch (Exception ex) { _logger?.LogExceptionAsError(GetType(), ex); - _client.RaiseError(new OnErrorEventArgs { Exception = ex }); + _client.RaiseError(new OnErrorEventArgs(ex)); _client.RaiseFatal(); // To ensure CancellationTokenSource is set to null again call Stop(); diff --git a/src/TwitchLib.Communication/Services/NetworkServices.cs b/src/TwitchLib.Communication/Services/NetworkServices.cs index 590e0ae..098a20b 100644 --- a/src/TwitchLib.Communication/Services/NetworkServices.cs +++ b/src/TwitchLib.Communication/Services/NetworkServices.cs @@ -13,17 +13,17 @@ namespace TwitchLib.Communication.Services ///
internal class NetworkServices where T : IDisposable { - private Task _listenTask; - private Task _monitorTask; + private Task? _listenTask; + private Task? _monitorTask; private readonly ClientBase _client; - private readonly ILogger _logger; + private readonly ILogger? _logger; private readonly ConnectionWatchDog _connectionWatchDog; private CancellationToken Token => _client.Token; internal NetworkServices( ClientBase client, - ILogger logger = null) + ILogger? logger = null) { _logger = logger; _client = client; diff --git a/src/TwitchLib.Communication/TwitchLib.Communication.csproj b/src/TwitchLib.Communication/TwitchLib.Communication.csproj index 25c76c5..047c530 100644 --- a/src/TwitchLib.Communication/TwitchLib.Communication.csproj +++ b/src/TwitchLib.Communication/TwitchLib.Communication.csproj @@ -1,7 +1,9 @@ - + netstandard2.0 + enable + latest 2.0.0 $(VersionSuffix) swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin From 668cb061c21ac30904b2e70adfeced9f6022960a Mon Sep 17 00:00:00 2001 From: ondru Date: Sat, 29 Apr 2023 17:28:23 +0200 Subject: [PATCH 05/20] improve logging performance --- .../Clients/ClientBase.cs | 2 +- .../Extensions/LogExtensions.cs | 56 ++++++------------- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index 0b9c937..2097e70 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -37,7 +37,7 @@ public abstract class ClientBase : IClient internal static TimeSpan TimeOutEstablishConnection => TimeSpan.FromSeconds(15); - protected ILogger? Logger { get; } + protected readonly ILogger? Logger; protected abstract string Url { get; } diff --git a/src/TwitchLib.Communication/Extensions/LogExtensions.cs b/src/TwitchLib.Communication/Extensions/LogExtensions.cs index ccf4741..04409ec 100644 --- a/src/TwitchLib.Communication/Extensions/LogExtensions.cs +++ b/src/TwitchLib.Communication/Extensions/LogExtensions.cs @@ -1,53 +1,33 @@ -using System; -using System.Runtime.CompilerServices; +#pragma warning disable SYSLIB1006 // Multiple logging methods cannot use the same event id within a class using Microsoft.Extensions.Logging; +using System; +using System.Runtime.CompilerServices; namespace TwitchLib.Communication.Extensions { /// /// expensive Extensions of the /// - internal static class LogExtensions + internal static partial class LogExtensions { - public static void TraceMethodCall(this ILogger logger, - Type type, - [CallerMemberName] string callerMemberName = "", - [CallerLineNumber] int callerLineNumber = 0) + public static void TraceMethodCall(this ILogger logger, Type type, [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0) { // because of the code-formatting, 2 line is subtracted from the callerLineNumber // cant be done inline! callerLineNumber -= 2; - logger.LogTrace("{FullName}.{callerMemberName} at line {callerLineNumber} is called", - type.FullName, callerMemberName, callerLineNumber); - } - public static void LogExceptionAsError(this ILogger logger, - Type type, - Exception exception, - [CallerMemberName] string callerMemberName = "", - [CallerLineNumber] int callerLineNumber = 0) - { - logger.LogError(exception, - "Exception in {FullName}.{callerMemberName} at line {callerLineNumber}:", - type.FullName, callerMemberName, callerLineNumber); - } - public static void LogExceptionAsInformation(this ILogger logger, - Type type, - Exception exception, - [CallerMemberName] string callerMemberName = "", - [CallerLineNumber] int callerLineNumber = 0) - { - logger.LogInformation(exception, - "Exception in {FullName}.{callerMemberName} at line {callerLineNumber}:", - type.FullName, callerMemberName, callerLineNumber); - } - public static void TraceAction(this ILogger logger, - Type type, - string action, - [CallerMemberName] string callerMemberName = "", - [CallerLineNumber] int callerLineNumber = 0) - { - logger.LogTrace("{FullName}.{callerMemberName} at line {callerLineNumber}: {action}", - type.FullName, callerMemberName, callerLineNumber, action); + TraceMethodCallCore(logger, type, callerMemberName, callerLineNumber); } + + [LoggerMessage(0, LogLevel.Trace, "{type}.{callerMemberName} at line {callerLineNumber} is called")] + static partial void TraceMethodCallCore(this ILogger logger, Type type, string callerMemberName, int callerLineNumber); + + [LoggerMessage(0, LogLevel.Error, "Exception in {type}.{callerMemberName} at line {callerLineNumber}")] + public static partial void LogExceptionAsError(this ILogger logger, Type type, Exception exception, [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage(0, LogLevel.Information, "Exception in {type}.{callerMemberName} at line {callerLineNumber}")] + public static partial void LogExceptionAsInformation(this ILogger logger, Type type, Exception exception, [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage(0, LogLevel.Trace, "{type}.{callerMemberName} at line {callerLineNumber}: {action}")] + public static partial void TraceAction(this ILogger logger, Type type, string action, [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0); } } From eb5093245e675e6780b3c93982b65bd9af1eef1a Mon Sep 17 00:00:00 2001 From: ondru Date: Mon, 1 May 2023 16:38:03 +0200 Subject: [PATCH 06/20] change lLogger to ILogger --- src/TwitchLib.Communication/Clients/ClientBase.cs | 4 ++-- src/TwitchLib.Communication/Clients/TcpClient.cs | 2 +- src/TwitchLib.Communication/Clients/WebsocketClient.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index 2097e70..62a5bcd 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -37,7 +37,7 @@ public abstract class ClientBase : IClient internal static TimeSpan TimeOutEstablishConnection => TimeSpan.FromSeconds(15); - protected readonly ILogger? Logger; + protected readonly ILogger>? Logger; protected abstract string Url { get; } @@ -60,7 +60,7 @@ public abstract class ClientBase : IClient internal ClientBase( IClientOptions? options, - ILogger? logger) + ILogger>? logger) { Logger = logger; _cancellationTokenSource = new CancellationTokenSource(); diff --git a/src/TwitchLib.Communication/Clients/TcpClient.cs b/src/TwitchLib.Communication/Clients/TcpClient.cs index 5ffb9a2..b316afa 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -23,7 +23,7 @@ public class TcpClient : ClientBase public TcpClient( IClientOptions? options = null, - ILogger? logger = null) + ILogger? logger = null) : base(options, logger) { } diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index 4a7d4fd..ce697f1 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -20,7 +20,7 @@ public class WebSocketClient : ClientBase public WebSocketClient( IClientOptions? options = null, - ILogger? logger = null) + ILogger? logger = null) : base(options, logger) { switch (Options.ClientType) From db65596953960088c55ee09274e1072ad0e19de3 Mon Sep 17 00:00:00 2001 From: Gimli_CZ Date: Sat, 6 May 2023 15:11:59 +0200 Subject: [PATCH 07/20] Async Task Events Reference: https://medium.com/@a.lyskawa/the-hitchhiker-guide-to-asynchronous-events-in-c-e9840109fb53 --- .../Clients/ClientTestsBase.cs | 666 +++++++++++++++++- .../Clients/ClientBase.cs | 57 +- .../Clients/TcpClient.cs | 8 +- .../Clients/WebsocketClient.cs | 12 +- .../Events/CoreEvents.cs | 17 + .../Interfaces/IClient.cs | 15 +- 6 files changed, 721 insertions(+), 54 deletions(-) create mode 100644 src/TwitchLib.Communication/Events/CoreEvents.cs diff --git a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs index b0bacec..656f097 100644 --- a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs +++ b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs @@ -1,5 +1,5 @@ using System; -using System.Reflection; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -8,6 +8,8 @@ using TwitchLib.Communication.Models; using TwitchLib.Communication.Tests.Helpers; using Xunit; +using static TwitchLib.Communication.Events.CoreEvents; +using Xunit.Sdk; [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace TwitchLib.Communication.Tests.Clients @@ -49,9 +51,12 @@ await Assert.RaisesAsync( h => client.OnConnected -= h, async () => { - client.OnConnected += (sender, e) => pauseConnected.Set(); + client.OnConnected += async(sender, e) => + { + pauseConnected.Set(); + }; + await client.OpenAsync(); - Assert.True(pauseConnected.WaitOne(WaitOneDuration)); }); } @@ -86,10 +91,13 @@ await Assert.RaisesAsync( { await client.CloseAsync(); }; - - client.OnDisconnected += (sender, e) => pauseDisconnected.Set(); + + client.OnDisconnected += async (sender, e) => + { + pauseDisconnected.Set(); + }; await client.OpenAsync(); - + Assert.True(pauseDisconnected.WaitOne(WaitOneDuration)); }); } @@ -122,9 +130,11 @@ await Assert.RaisesAsync( { client.OnConnected += async (s, e) => await client.ReconnectAsync(); - client.OnReconnected += (s, e) => pauseReconnected.Set(); + client.OnReconnected += async (s, e) => + { + pauseReconnected.Set(); + }; await client.OpenAsync(); - Assert.True(pauseReconnected.WaitOne(WaitOneDuration)); }); } @@ -180,4 +190,642 @@ public void Dispose_Client_Before_Connecting_IsOK() return (TClient?)constructor?.Invoke(constructorParameters); } } -} \ No newline at end of file + + + #region Modified Assert + //TL;DR: Extracted version of XUNIT with + //modification to accept new event Handler + + public partial class Assert + { + + /// + /// Verifies that a event with the exact event args (and not a derived type) is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAsync(Action> attach, Action> detach, Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw new RaisesException(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw new RaisesException(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + /// + /// Verifies that an event with the exact or a derived event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAnyAsync(Action> attach, Action> detach, Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw new RaisesException(typeof(T)); + + return raisedEvent; + } + +#if XUNIT_NULLABLE + static async Task?> RaisesAsyncInternal(Action> attach, Action> detach, Func testCode) +#else + static async Task> RaisesAsyncInternal(Action> attach, Action> detach, Func testCode) +#endif + { + NotNull(attach); + NotNull(detach); + NotNull(testCode); + +#if XUNIT_NULLABLE + RaisedEvent? raisedEvent = null; + void handler(object? s, T args) => raisedEvent = new RaisedEvent(s, args); +#else + RaisedEvent raisedEvent = null; + AsyncEventHandler value = (object s, T args) => + { + raisedEvent = new RaisedEvent(s, args); + return Task.CompletedTask; + }; + AsyncEventHandler handler = value; +#endif + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + + /// + /// Represents a raised event after the fact. + /// + /// The type of the event arguments. + public class RaisedEvent + { + /// + /// The sender of the event. + /// +#if XUNIT_NULLABLE + public object? Sender { get; } +#else + public object Sender { get; } +#endif + + /// + /// The event arguments. + /// + public T Arguments { get; } + + /// + /// Creates a new instance of the class. + /// + /// The sender of the event. + /// The event arguments +#if XUNIT_NULLABLE + public RaisedEvent(object? sender, T args) +#else + public RaisedEvent(object sender, T args) +#endif + { + Sender = sender; + Arguments = args; + } + } + + +#if XUNIT_NULLABLE + public static void False([DoesNotReturnIf(parameterValue: true)] bool condition) +#else + public static void False(bool condition) +#endif + { + False((bool?)condition, null); + } + + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// Thrown if the condition is not false +#if XUNIT_NULLABLE + public static void False([DoesNotReturnIf(parameterValue: true)] bool? condition) +#else + public static void False(bool? condition) +#endif + { + False(condition, null); + } + + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// The message to show when the condition is not false + /// Thrown if the condition is not false +#if XUNIT_NULLABLE + public static void False([DoesNotReturnIf(parameterValue: true)] bool condition, string? userMessage) +#else + public static void False(bool condition, string userMessage) +#endif + { + False((bool?)condition, userMessage); + } + + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// The message to show when the condition is not false + /// Thrown if the condition is not false +#if XUNIT_NULLABLE + public static void False([DoesNotReturnIf(parameterValue: true)] bool? condition, string? userMessage) +#else + public static void False(bool? condition, string userMessage) +#endif + { + if (!condition.HasValue || condition.GetValueOrDefault()) + throw new FalseException(userMessage, condition); + } + + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// Thrown when the condition is false +#if XUNIT_NULLABLE + public static void True([DoesNotReturnIf(parameterValue: false)] bool condition) +#else + public static void True(bool condition) +#endif + { + True((bool?)condition, null); + } + + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// Thrown when the condition is false +#if XUNIT_NULLABLE + public static void True([DoesNotReturnIf(parameterValue: false)] bool? condition) +#else + public static void True(bool? condition) +#endif + { + True(condition, null); + } + + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// The message to be shown when the condition is false + /// Thrown when the condition is false +#if XUNIT_NULLABLE + public static void True([DoesNotReturnIf(parameterValue: false)] bool condition, string? userMessage) +#else + public static void True(bool condition, string userMessage) +#endif + { + True((bool?)condition, userMessage); + } + + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// The message to be shown when the condition is false + /// Thrown when the condition is false +#if XUNIT_NULLABLE + public static void True([DoesNotReturnIf(parameterValue: false)] bool? condition, string? userMessage) +#else + public static void True(bool? condition, string userMessage) +#endif + { + if (!condition.HasValue || !condition.GetValueOrDefault()) + throw new TrueException(userMessage, condition); + } + + /// + /// Verifies that a string contains a given sub-string, using the current culture. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// Thrown when the sub-string is not present inside the string +#if XUNIT_NULLABLE + public static void Contains(string expectedSubstring, string? actualString) +#else + public static void Contains(string expectedSubstring, string actualString) +#endif + { + Contains(expectedSubstring, actualString, StringComparison.CurrentCulture); + } + + /// + /// Verifies that a string contains a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is not present inside the string +#if XUNIT_NULLABLE + public static void Contains(string expectedSubstring, string? actualString, StringComparison comparisonType) +#else + public static void Contains(string expectedSubstring, string actualString, StringComparison comparisonType) +#endif + { + NotNull(expectedSubstring); + + if (actualString == null || actualString.IndexOf(expectedSubstring, comparisonType) < 0) + throw new ContainsException(expectedSubstring, actualString); + } + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string which is expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string +#if XUNIT_NULLABLE + public static void DoesNotContain(string expectedSubstring, string? actualString) +#else + public static void DoesNotContain(string expectedSubstring, string actualString) +#endif + { + DoesNotContain(expectedSubstring, actualString, StringComparison.CurrentCulture); + } + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string which is expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the given string +#if XUNIT_NULLABLE + public static void DoesNotContain(string expectedSubstring, string? actualString, StringComparison comparisonType) +#else + public static void DoesNotContain(string expectedSubstring, string actualString, StringComparison comparisonType) +#endif + { + NotNull(expectedSubstring); + + if (actualString != null && actualString.IndexOf(expectedSubstring, comparisonType) >= 0) + throw new DoesNotContainException(expectedSubstring, actualString); + } + + /// + /// Verifies that a string starts with a given string, using the current culture. + /// + /// The string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected string +#if XUNIT_NULLABLE + public static void StartsWith(string? expectedStartString, string? actualString) +#else + public static void StartsWith(string expectedStartString, string actualString) +#endif + { + StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); + } + + /// + /// Verifies that a string starts with a given string, using the given comparison type. + /// + /// The string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected string +#if XUNIT_NULLABLE + public static void StartsWith(string? expectedStartString, string? actualString, StringComparison comparisonType) +#else + public static void StartsWith(string expectedStartString, string actualString, StringComparison comparisonType) +#endif + { + if (expectedStartString == null || actualString == null || !actualString.StartsWith(expectedStartString, comparisonType)) + throw new StartsWithException(expectedStartString, actualString); + } + + /// + /// Verifies that a string ends with a given string, using the current culture. + /// + /// The string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected string +#if XUNIT_NULLABLE + public static void EndsWith(string? expectedEndString, string? actualString) +#else + public static void EndsWith(string expectedEndString, string actualString) +#endif + { + EndsWith(expectedEndString, actualString, StringComparison.CurrentCulture); + } + + /// + /// Verifies that a string ends with a given string, using the given comparison type. + /// + /// The string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected string +#if XUNIT_NULLABLE + public static void EndsWith(string? expectedEndString, string? actualString, StringComparison comparisonType) +#else + public static void EndsWith(string expectedEndString, string actualString, StringComparison comparisonType) +#endif + { + if (expectedEndString == null || actualString == null || !actualString.EndsWith(expectedEndString, comparisonType)) + throw new EndsWithException(expectedEndString, actualString); + } + + /// + /// Verifies that a string matches a regular expression. + /// + /// The regex pattern expected to match + /// The string to be inspected + /// Thrown when the string does not match the regex pattern +#if XUNIT_NULLABLE + public static void Matches(string expectedRegexPattern, string? actualString) +#else + public static void Matches(string expectedRegexPattern, string actualString) +#endif + { + NotNull(expectedRegexPattern); + + if (actualString == null || !Regex.IsMatch(actualString, expectedRegexPattern)) + throw new MatchesException(expectedRegexPattern, actualString); + } + + /// + /// Verifies that a string matches a regular expression. + /// + /// The regex expected to match + /// The string to be inspected + /// Thrown when the string does not match the regex +#if XUNIT_NULLABLE + public static void Matches(Regex expectedRegex, string? actualString) +#else + public static void Matches(Regex expectedRegex, string actualString) +#endif + { + NotNull(expectedRegex); + + if (actualString == null || !expectedRegex.IsMatch(actualString)) + throw new MatchesException(expectedRegex.ToString(), actualString); + } + + /// + /// Verifies that a string does not match a regular expression. + /// + /// The regex pattern expected not to match + /// The string to be inspected + /// Thrown when the string matches the regex pattern +#if XUNIT_NULLABLE + public static void DoesNotMatch(string expectedRegexPattern, string? actualString) +#else + public static void DoesNotMatch(string expectedRegexPattern, string actualString) +#endif + { + NotNull(expectedRegexPattern); + + if (actualString != null && Regex.IsMatch(actualString, expectedRegexPattern)) + throw new DoesNotMatchException(expectedRegexPattern, actualString); + } + + /// + /// Verifies that a string does not match a regular expression. + /// + /// The regex expected not to match + /// The string to be inspected + /// Thrown when the string matches the regex +#if XUNIT_NULLABLE + public static void DoesNotMatch(Regex expectedRegex, string? actualString) +#else + public static void DoesNotMatch(Regex expectedRegex, string actualString) +#endif + { + NotNull(expectedRegex); + + if (actualString != null && expectedRegex.IsMatch(actualString)) + throw new DoesNotMatchException(expectedRegex.ToString(), actualString); + } + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. +#if XUNIT_NULLABLE + public static void Equal(string? expected, string? actual) +#else + public static void Equal(string expected, string actual) +#endif + { + Equal(expected, actual, false, false, false); + } + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// Thrown when the strings are not equivalent. +#if XUNIT_NULLABLE + public static void Equal( + string? expected, + string? actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false) +#else + public static void Equal( + string expected, + string actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false) +#endif + { +#if XUNIT_SPAN + if (expected == null && actual == null) + return; + if (expected == null || actual == null) + throw new EqualException(expected, actual, -1, -1); + + Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences); +#else + // Start out assuming the one of the values is null + int expectedIndex = -1; + int actualIndex = -1; + int expectedLength = 0; + int actualLength = 0; + + if (expected == null) + { + if (actual == null) + return; + } + else if (actual != null) + { + // Walk the string, keeping separate indices since we can skip variable amounts of + // data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences. + expectedIndex = 0; + actualIndex = 0; + expectedLength = expected.Length; + actualLength = actual.Length; + + while (expectedIndex < expectedLength && actualIndex < actualLength) + { + char expectedChar = expected[expectedIndex]; + char actualChar = actual[actualIndex]; + + if (ignoreLineEndingDifferences && IsLineEnding(expectedChar) && IsLineEnding(actualChar)) + { + expectedIndex = SkipLineEnding(expected, expectedIndex); + actualIndex = SkipLineEnding(actual, actualIndex); + } + else if (ignoreWhiteSpaceDifferences && IsWhiteSpace(expectedChar) && IsWhiteSpace(actualChar)) + { + expectedIndex = SkipWhitespace(expected, expectedIndex); + actualIndex = SkipWhitespace(actual, actualIndex); + } + else + { + if (ignoreCase) + { + expectedChar = Char.ToUpperInvariant(expectedChar); + actualChar = Char.ToUpperInvariant(actualChar); + } + + if (expectedChar != actualChar) + { + break; + } + + expectedIndex++; + actualIndex++; + } + } + } + + if (expectedIndex < expectedLength || actualIndex < actualLength) + { + throw new EqualException(expected, actual, expectedIndex, actualIndex); + } +#endif + } + static bool IsLineEnding(char c) + { + return c == '\r' || c == '\n'; + } + + static bool IsWhiteSpace(char c) + { + return c == ' ' || c == '\t'; + } + + static int SkipLineEnding(string value, int index) + { + if (value[index] == '\r') + { + ++index; + } + if (index < value.Length && value[index] == '\n') + { + ++index; + } + + return index; + } + + static int SkipWhitespace(string value, int index) + { + while (index < value.Length) + { + switch (value[index]) + { + case ' ': + case '\t': + index++; + break; + + default: + return index; + } + } + + return index; + } + + /// + /// Verifies that an object reference is not null. + /// + /// The object to be validated + /// Thrown when the object reference is null +#if XUNIT_NULLABLE + public static void NotNull([NotNull] object? @object) +#else + public static void NotNull(object @object) +#endif + { + if (@object == null) + throw new NotNullException(); + } + + /// + /// Verifies that an object reference is null. + /// + /// The object to be inspected + /// Thrown when the object reference is not null +#if XUNIT_NULLABLE + public static void Null([MaybeNull] object? @object) +#else + public static void Null(object @object) +#endif + { + if (@object != null) + throw new NullException(@object); + } + + /// + /// Indicates that the test should immediately fail. + /// + /// The failure message +#if XUNIT_NULLABLE + [DoesNotReturn] +#endif + public static void Fail(string message) + { + NotNull( message); + + throw new FailException(message); + } + + + + + } + #endregion + + +} + + + + diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index 0b9c937..05c7157 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -7,6 +7,7 @@ using TwitchLib.Communication.Interfaces; using TwitchLib.Communication.Models; using TwitchLib.Communication.Services; +using static TwitchLib.Communication.Events.CoreEvents; namespace TwitchLib.Communication.Clients { @@ -50,13 +51,13 @@ public abstract class ClientBase : IClient public IClientOptions Options { get; } - public event EventHandler? OnConnected; - public event EventHandler? OnDisconnected; - public event EventHandler? OnError; - public event EventHandler? OnFatality; - public event EventHandler? OnMessage; - public event EventHandler? OnSendFailed; - public event EventHandler? OnReconnected; + public event AsyncEventHandler? OnConnected; + public event AsyncEventHandler? OnDisconnected; + public event AsyncEventHandler? OnError; + public event AsyncEventHandler? OnFatality; + public event AsyncEventHandler? OnMessage; + public event AsyncEventHandler? OnSendFailed; + public event AsyncEventHandler? OnReconnected; internal ClientBase( IClientOptions? options, @@ -71,7 +72,7 @@ internal ClientBase( /// /// Wont raise the given if .IsCancellationRequested /// - private void RaiseSendFailed(OnSendFailedEventArgs eventArgs) + private async Task RaiseSendFailed(OnSendFailedEventArgs eventArgs) { Logger?.TraceMethodCall(GetType()); if (Token.IsCancellationRequested) @@ -79,13 +80,13 @@ private void RaiseSendFailed(OnSendFailedEventArgs eventArgs) return; } - OnSendFailed?.Invoke(this, eventArgs); + if(OnSendFailed != null) await OnSendFailed.Invoke(this, eventArgs); } /// /// Wont raise the given if .IsCancellationRequested /// - internal void RaiseError(OnErrorEventArgs eventArgs) + internal async Task RaiseError(OnErrorEventArgs eventArgs) { Logger?.TraceMethodCall(GetType()); if (Token.IsCancellationRequested) @@ -93,13 +94,13 @@ internal void RaiseError(OnErrorEventArgs eventArgs) return; } - OnError?.Invoke(this, eventArgs); + if (OnError != null) await OnError.Invoke(this, eventArgs); } /// /// Wont raise the given if .IsCancellationRequested /// - private void RaiseReconnected() + private async Task RaiseReconnected() { Logger?.TraceMethodCall(GetType()); if (Token.IsCancellationRequested) @@ -107,13 +108,13 @@ private void RaiseReconnected() return; } - OnReconnected?.Invoke(this, new OnConnectedEventArgs()); + if (OnReconnected != null) await OnReconnected.Invoke(this, new OnConnectedEventArgs()); } /// /// Wont raise the given if .IsCancellationRequested /// - internal void RaiseMessage(OnMessageEventArgs eventArgs) + internal async Task RaiseMessage(OnMessageEventArgs eventArgs) { Logger?.TraceMethodCall(GetType()); if (Token.IsCancellationRequested) @@ -121,13 +122,13 @@ internal void RaiseMessage(OnMessageEventArgs eventArgs) return; } - OnMessage?.Invoke(this, eventArgs); + if (OnMessage != null) await OnMessage.Invoke(this, eventArgs); } /// /// Wont raise the given if .IsCancellationRequested /// - internal void RaiseFatal(Exception? ex = null) + internal async Task RaiseFatal(Exception? ex = null) { Logger?.TraceMethodCall(GetType()); if (Token.IsCancellationRequested) @@ -139,19 +140,19 @@ internal void RaiseFatal(Exception? ex = null) ? new OnFatalErrorEventArgs(ex) : new OnFatalErrorEventArgs("Fatal network error."); - OnFatality?.Invoke(this, onFatalErrorEventArgs); + if (OnFatality != null) await OnFatality.Invoke(this, onFatalErrorEventArgs); } - private void RaiseDisconnected() + private async Task RaiseDisconnected() { Logger?.TraceMethodCall(GetType()); - OnDisconnected?.Invoke(this, new OnDisconnectedEventArgs()); + if (OnDisconnected != null) await OnDisconnected.Invoke(this, new OnDisconnectedEventArgs()); } - private void RaiseConnected() + private async Task RaiseConnected() { Logger?.TraceMethodCall(GetType()); - OnConnected?.Invoke(this, new OnConnectedEventArgs()); + if (OnConnected != null) await OnConnected.Invoke(this, new OnConnectedEventArgs()); } public async Task SendAsync(string message) @@ -166,7 +167,7 @@ public async Task SendAsync(string message) } catch (Exception e) { - RaiseSendFailed(new OnSendFailedEventArgs(e, message)); + await RaiseSendFailed(new OnSendFailedEventArgs(e, message)); return false; } finally @@ -253,7 +254,7 @@ private async Task OpenPrivateAsync(bool isReconnect) if (!IsConnected) { Logger?.TraceAction(GetType(), "Client couldn't establish a connection"); - RaiseFatal(); + await RaiseFatal(); return false; } @@ -262,7 +263,7 @@ private async Task OpenPrivateAsync(bool isReconnect) if (!isReconnect) { - RaiseConnected(); + await RaiseConnected(); } return true; @@ -270,8 +271,8 @@ private async Task OpenPrivateAsync(bool isReconnect) catch (Exception ex) { Logger?.LogExceptionAsError(GetType(), ex); - RaiseError(new OnErrorEventArgs(ex)); - RaiseFatal(); + await RaiseError(new OnErrorEventArgs(ex)); + await RaiseFatal(); return false; } } @@ -298,7 +299,7 @@ private async Task ClosePrivateAsync() $"{nameof(_cancellationTokenSource)}.{nameof(_cancellationTokenSource.Cancel)} is called"); CloseClient(); - RaiseDisconnected(); + await RaiseDisconnected(); _cancellationTokenSource = new CancellationTokenSource(); await Task.Delay(TimeSpan.FromMilliseconds(Options.DisconnectWait), CancellationToken.None); @@ -360,7 +361,7 @@ internal async Task ReconnectInternalAsync() var reconnected = await OpenPrivateAsync(true); if (reconnected) { - RaiseReconnected(); + await RaiseReconnected(); } return reconnected; diff --git a/src/TwitchLib.Communication/Clients/TcpClient.cs b/src/TwitchLib.Communication/Clients/TcpClient.cs index 5ffb9a2..4cbe12b 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -35,7 +35,7 @@ internal override async Task ListenTaskActionAsync() { var ex = new InvalidOperationException($"{nameof(_reader)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); - RaiseFatal(ex); + await RaiseFatal(ex); throw ex; } @@ -49,7 +49,7 @@ internal override async Task ListenTaskActionAsync() continue; } - RaiseMessage(new OnMessageEventArgs(input)); + await RaiseMessage(new OnMessageEventArgs(input)); } catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || ex.GetType() == typeof(OperationCanceledException)) @@ -60,7 +60,7 @@ internal override async Task ListenTaskActionAsync() catch (Exception ex) { Logger?.LogExceptionAsError(GetType(), ex); - RaiseError(new OnErrorEventArgs(ex)); + await RaiseError(new OnErrorEventArgs(ex)); break; } } @@ -78,7 +78,7 @@ protected override async Task ClientSendAsync(string message) { var ex = new InvalidOperationException($"{nameof(_writer)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); - RaiseFatal(ex); + await RaiseFatal(ex); throw ex; } diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index 4a7d4fd..dff9de9 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -45,7 +45,7 @@ internal override async Task ListenTaskActionAsync() { var ex = new InvalidOperationException($"{nameof(Client)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); - RaiseFatal(ex); + await RaiseFatal(ex); throw ex; } @@ -69,7 +69,7 @@ internal override async Task ListenTaskActionAsync() catch (Exception ex) { Logger?.LogExceptionAsError(GetType(), ex); - RaiseError(new OnErrorEventArgs(ex)); + await RaiseError(new OnErrorEventArgs(ex)); break; } @@ -83,14 +83,14 @@ internal override async Task ListenTaskActionAsync() { //optimization when we can read the whole message at once var message = Encoding.UTF8.GetString(bytes, 0, result.Count); - RaiseMessage(new OnMessageEventArgs(message)); + await RaiseMessage(new OnMessageEventArgs(message)); break; } memoryStream.Write(bytes, 0, result.Count); if (result.EndOfMessage) { var message = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Position); - RaiseMessage(new OnMessageEventArgs(message)); + await RaiseMessage(new OnMessageEventArgs(message)); memoryStream.Position = 0; } break; @@ -122,7 +122,7 @@ protected override async Task ClientSendAsync(string message) { var ex = new InvalidOperationException($"{nameof(Client)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); - RaiseFatal(ex); + await RaiseFatal(ex); throw ex; } @@ -140,7 +140,7 @@ protected override async Task ConnectClientAsync() { var ex = new InvalidOperationException($"{nameof(Client)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); - RaiseFatal(ex); + await RaiseFatal(ex); throw ex; } diff --git a/src/TwitchLib.Communication/Events/CoreEvents.cs b/src/TwitchLib.Communication/Events/CoreEvents.cs new file mode 100644 index 0000000..7bb2ede --- /dev/null +++ b/src/TwitchLib.Communication/Events/CoreEvents.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace TwitchLib.Communication.Events +{ + public class CoreEvents + { + /* + * Custom implementation of asynchronous event handler + * This is useful to properly and safely handle async Tasks + * Reference: https://medium.com/@a.lyskawa/the-hitchhiker-guide-to-asynchronous-events-in-c-e9840109fb53 + */ + public delegate Task AsyncEventHandler(object? sender, TEventArgs e); + } +} diff --git a/src/TwitchLib.Communication/Interfaces/IClient.cs b/src/TwitchLib.Communication/Interfaces/IClient.cs index 5c71b7c..ae3f858 100644 --- a/src/TwitchLib.Communication/Interfaces/IClient.cs +++ b/src/TwitchLib.Communication/Interfaces/IClient.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using TwitchLib.Communication.Events; +using static TwitchLib.Communication.Events.CoreEvents; namespace TwitchLib.Communication.Interfaces { @@ -19,37 +20,37 @@ public interface IClient : IDisposable /// /// Fires when the Client has connected /// - event EventHandler? OnConnected; + event AsyncEventHandler? OnConnected; /// /// Fires when the Client disconnects /// - event EventHandler? OnDisconnected; + event AsyncEventHandler? OnDisconnected; /// /// Fires when An Exception Occurs in the client /// - event EventHandler? OnError; + event AsyncEventHandler? OnError; /// /// Fires when a Fatal Error Occurs. /// - event EventHandler? OnFatality; + event AsyncEventHandler? OnFatality; /// /// Fires when a Message/ group of messages is received. /// - event EventHandler? OnMessage; + event AsyncEventHandler? OnMessage; /// /// Fires when a message Send event failed. /// - event EventHandler? OnSendFailed; + event AsyncEventHandler? OnSendFailed; /// /// Fires when the client reconnects automatically /// - event EventHandler? OnReconnected; + event AsyncEventHandler? OnReconnected; /// /// tries to connect to twitch according to ! From 860f7464d9255fedea64f3f000b15d7fe0890c09 Mon Sep 17 00:00:00 2001 From: Gimli_CZ Date: Sat, 6 May 2023 18:21:23 +0200 Subject: [PATCH 08/20] Removed delegate from class added some awaits to watchDog --- .../Clients/ClientTestsBase.cs | 1 - src/TwitchLib.Communication/Clients/ClientBase.cs | 1 - src/TwitchLib.Communication/Events/CoreEvents.cs | 9 ++------- src/TwitchLib.Communication/Interfaces/IClient.cs | 1 - .../Services/ConnectionWatchDog.cs | 4 ++-- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs index 656f097..14b10cd 100644 --- a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs +++ b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs @@ -8,7 +8,6 @@ using TwitchLib.Communication.Models; using TwitchLib.Communication.Tests.Helpers; using Xunit; -using static TwitchLib.Communication.Events.CoreEvents; using Xunit.Sdk; [assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index 05c7157..b737556 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -7,7 +7,6 @@ using TwitchLib.Communication.Interfaces; using TwitchLib.Communication.Models; using TwitchLib.Communication.Services; -using static TwitchLib.Communication.Events.CoreEvents; namespace TwitchLib.Communication.Clients { diff --git a/src/TwitchLib.Communication/Events/CoreEvents.cs b/src/TwitchLib.Communication/Events/CoreEvents.cs index 7bb2ede..1a356b1 100644 --- a/src/TwitchLib.Communication/Events/CoreEvents.cs +++ b/src/TwitchLib.Communication/Events/CoreEvents.cs @@ -1,17 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace TwitchLib.Communication.Events { - public class CoreEvents - { /* * Custom implementation of asynchronous event handler * This is useful to properly and safely handle async Tasks * Reference: https://medium.com/@a.lyskawa/the-hitchhiker-guide-to-asynchronous-events-in-c-e9840109fb53 */ public delegate Task AsyncEventHandler(object? sender, TEventArgs e); - } + } diff --git a/src/TwitchLib.Communication/Interfaces/IClient.cs b/src/TwitchLib.Communication/Interfaces/IClient.cs index ae3f858..b5578b3 100644 --- a/src/TwitchLib.Communication/Interfaces/IClient.cs +++ b/src/TwitchLib.Communication/Interfaces/IClient.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using TwitchLib.Communication.Events; -using static TwitchLib.Communication.Events.CoreEvents; namespace TwitchLib.Communication.Interfaces { diff --git a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs index 2bac625..48bd1a3 100644 --- a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs +++ b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs @@ -113,8 +113,8 @@ private async Task MonitorTaskActionAsync() catch (Exception ex) { _logger?.LogExceptionAsError(GetType(), ex); - _client.RaiseError(new OnErrorEventArgs(ex)); - _client.RaiseFatal(); + await _client.RaiseError(new OnErrorEventArgs(ex)); + await _client.RaiseFatal(); // To ensure CancellationTokenSource is set to null again call Stop(); await StopAsync(); From 0c4ab4e64a7391ea45e569831603903d745ad814 Mon Sep 17 00:00:00 2001 From: Bukk94 Date: Sun, 11 Jun 2023 01:13:29 +0200 Subject: [PATCH 09/20] Fixed double disconnection during reconnect, ignore all TaskCancelledExceptions, add proper IsRunning to ConnectionWatchDog --- .../Clients/ClientBase.cs | 14 ++++---------- .../Clients/WebsocketClient.cs | 9 ++++++--- .../Helpers/TaskHelper.cs | 18 ------------------ .../Services/ConnectionWatchDog.cs | 15 +++++++++++---- .../Services/NetworkServices.cs | 2 +- 5 files changed, 22 insertions(+), 36 deletions(-) delete mode 100644 src/TwitchLib.Communication/Helpers/TaskHelper.cs diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index d4b89e7..a9d9b0f 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -184,10 +184,7 @@ public Task OpenAsync() public async Task CloseAsync() { Logger?.TraceMethodCall(GetType()); - - // Network services has to be stopped first so that it wont reconnect - await _networkServices.StopAsync(); - + // ClosePrivate() also handles IClientOptions.DisconnectWait await ClosePrivateAsync(); } @@ -205,12 +202,6 @@ public void Dispose() public async Task ReconnectAsync() { Logger?.TraceMethodCall(GetType()); - - // Stops everything (including NetworkServices) - if (IsConnected) - { - await CloseAsync(); - } return await ReconnectInternalAsync(); } @@ -292,6 +283,8 @@ private async Task ClosePrivateAsync() { Logger?.TraceMethodCall(GetType()); + await _networkServices.StopAsync(); + // This cancellation traverse up to NetworkServices.ListenTask _cancellationTokenSource.Cancel(); Logger?.TraceAction(GetType(), @@ -356,6 +349,7 @@ private async Task ClosePrivateAsync() internal async Task ReconnectInternalAsync() { Logger?.TraceMethodCall(GetType()); + await ClosePrivateAsync(); var reconnected = await OpenPrivateAsync(true); if (reconnected) diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index 64884bd..6902736 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -59,10 +59,13 @@ internal override async Task ListenTaskActionAsync() { result = await Client.ReceiveAsync(buffer, Token); } - catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || - ex.GetType() == typeof(OperationCanceledException)) + catch (TaskCanceledException _) + { + // Swallow any cancellation exceptions + break; + } + catch (OperationCanceledException ex) { - // occurs if the Tasks are canceled by the CancellationTokenSource.Token Logger?.LogExceptionAsInformation(GetType(), ex); break; } diff --git a/src/TwitchLib.Communication/Helpers/TaskHelper.cs b/src/TwitchLib.Communication/Helpers/TaskHelper.cs deleted file mode 100644 index 790d5b0..0000000 --- a/src/TwitchLib.Communication/Helpers/TaskHelper.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; - -namespace TwitchLib.Communication.Helpers -{ - internal static class TaskHelper - { - internal static bool IsTaskRunning(this Task? task) - { - return task != null - && !task.IsFaulted - && !task.IsCompleted -#if NET - && !task.IsCompletedSuccessfully -#endif - && !task.IsCanceled; - } - } -} \ No newline at end of file diff --git a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs index 48bd1a3..5d4e28b 100644 --- a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs +++ b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs @@ -29,6 +29,8 @@ internal class ConnectionWatchDog where T : IDisposable private CancellationTokenSource? _cancellationTokenSource; private const int MonitorTaskDelayInMilliseconds = 200; + + public bool IsRunning { get; private set; } internal ConnectionWatchDog( ClientBase client, @@ -52,11 +54,13 @@ internal Task StartMonitorTaskAsync() // This should be the only place where a new instance of CancellationTokenSource is set _cancellationTokenSource = new CancellationTokenSource(); + IsRunning = true; return Task.Run(MonitorTaskActionAsync, _cancellationTokenSource.Token); } internal async Task StopAsync() { + IsRunning = false; _logger?.TraceMethodCall(GetType()); _cancellationTokenSource?.Cancel(); // give MonitorTaskAction a chance to catch cancellation @@ -72,7 +76,7 @@ private async Task MonitorTaskActionAsync() _logger?.TraceMethodCall(GetType()); try { - while (_cancellationTokenSource != null && + while (_cancellationTokenSource != null && !_cancellationTokenSource.Token.IsCancellationRequested) { // we expect the client is connected, @@ -85,7 +89,7 @@ private async Task MonitorTaskActionAsync() // ReconnectInternal() calls the correct Close-Method within the Client // ReconnectInternal() makes attempts to reconnect according to the ReconnectionPolicy within the IClientOptions _logger?.TraceAction(GetType(), "Try to reconnect"); - + var connected = await _client.ReconnectInternalAsync(); if (!connected) { @@ -104,8 +108,11 @@ private async Task MonitorTaskActionAsync() await Task.Delay(MonitorTaskDelayInMilliseconds); } } - catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || - ex.GetType() == typeof(OperationCanceledException)) + catch (TaskCanceledException _) + { + // Swallow any cancellation exceptions + } + catch (OperationCanceledException ex) { // Occurs if the Tasks are canceled by the CancellationTokenSource.Token _logger?.LogExceptionAsInformation(GetType(), ex); diff --git a/src/TwitchLib.Communication/Services/NetworkServices.cs b/src/TwitchLib.Communication/Services/NetworkServices.cs index 098a20b..c7c1dbb 100644 --- a/src/TwitchLib.Communication/Services/NetworkServices.cs +++ b/src/TwitchLib.Communication/Services/NetworkServices.cs @@ -33,7 +33,7 @@ internal NetworkServices( internal void Start() { _logger?.TraceMethodCall(GetType()); - if (_monitorTask == null || !_monitorTask.IsTaskRunning()) + if (_monitorTask == null || !_connectionWatchDog.IsRunning) { // this task is probably still running // may be in case of a network connection loss From 535f17ba53e8390cd462f1fd7b697a9722b2b15d Mon Sep 17 00:00:00 2001 From: Bukk94 Date: Sun, 11 Jun 2023 01:15:50 +0200 Subject: [PATCH 10/20] Removed unused using --- src/TwitchLib.Communication/Services/NetworkServices.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TwitchLib.Communication/Services/NetworkServices.cs b/src/TwitchLib.Communication/Services/NetworkServices.cs index c7c1dbb..917df0c 100644 --- a/src/TwitchLib.Communication/Services/NetworkServices.cs +++ b/src/TwitchLib.Communication/Services/NetworkServices.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using TwitchLib.Communication.Clients; using TwitchLib.Communication.Extensions; -using TwitchLib.Communication.Helpers; namespace TwitchLib.Communication.Services { From e3206a63b5735cb2f4a9521589d06fbd20663222 Mon Sep 17 00:00:00 2001 From: Bukk94 Date: Wed, 28 Jun 2023 14:48:01 +0200 Subject: [PATCH 11/20] Generate documentation file, updated copyright to year 2023 --- .../TwitchLib.Communication.csproj | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/TwitchLib.Communication/TwitchLib.Communication.csproj b/src/TwitchLib.Communication/TwitchLib.Communication.csproj index 047c530..76675cd 100644 --- a/src/TwitchLib.Communication/TwitchLib.Communication.csproj +++ b/src/TwitchLib.Communication/TwitchLib.Communication.csproj @@ -2,14 +2,14 @@ netstandard2.0 - enable - latest + enable + latest 2.0.0 $(VersionSuffix) swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin - Connection library used throughout TwitchLib to replace third party depedencies. - Copyright 2022 + Connection library used throughout TwitchLib to replace third party depedencies. + Copyright 2023 https://opensource.org/licenses/MIT https://github.com/TwitchLib/TwitchLib.Communication https://cdn.syzuna-programs.de/images/twitchlib.png @@ -21,11 +21,12 @@ 2.0.0 2.0.0 true + True - + - + From 80ae3aac15b55eca697857be0a4a89ec2297e149 Mon Sep 17 00:00:00 2001 From: Syzuna Date: Mon, 3 Jul 2023 22:03:51 +0200 Subject: [PATCH 12/20] Add Multitargetting for standard 2.0/2.1 and .net 6/7 --- src/TwitchLib.Communication/TwitchLib.Communication.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TwitchLib.Communication/TwitchLib.Communication.csproj b/src/TwitchLib.Communication/TwitchLib.Communication.csproj index 76675cd..984a9f5 100644 --- a/src/TwitchLib.Communication/TwitchLib.Communication.csproj +++ b/src/TwitchLib.Communication/TwitchLib.Communication.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.0;netstandard2.1;net6.0;net7.0 enable latest 2.0.0 From 911d2ccb9ae2635b00c7bbac9c375c1ea7dbfc7d Mon Sep 17 00:00:00 2001 From: Tobias 'Syzuna' Teske Date: Mon, 3 Jul 2023 22:07:31 +0200 Subject: [PATCH 13/20] bump used .NET version in GH action workflows --- .github/workflows/check-buildstatus.yml | 2 +- .github/workflows/preview-release.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/tests-linux.yml | 2 +- .github/workflows/tests-windows.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check-buildstatus.yml b/.github/workflows/check-buildstatus.yml index 3ce8c7d..85d0f2a 100644 --- a/.github/workflows/check-buildstatus.yml +++ b/.github/workflows/check-buildstatus.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: [ '6.0.x' ] + dotnet-version: [ '7.0.x' ] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 3b39c12..82c4047 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Restore dependencies run: dotnet restore - name: Build TwitchLib.Communication diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ae58ce..a3167f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Restore dependencies run: dotnet restore - name: Build TwitchLib.Communication diff --git a/.github/workflows/tests-linux.yml b/.github/workflows/tests-linux.yml index 9c3768b..0266b95 100644 --- a/.github/workflows/tests-linux.yml +++ b/.github/workflows/tests-linux.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: [ '6.0.x' ] + dotnet-version: [ '7.0.x' ] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/tests-windows.yml b/.github/workflows/tests-windows.yml index 022ce06..4aee907 100644 --- a/.github/workflows/tests-windows.yml +++ b/.github/workflows/tests-windows.yml @@ -9,7 +9,7 @@ jobs: runs-on: windows-latest strategy: matrix: - dotnet-version: [ '6.0.x' ] + dotnet-version: [ '7.0.x' ] steps: - uses: actions/checkout@v3 From b1032a82c1498ec8c3703d1bff24c53f63906b47 Mon Sep 17 00:00:00 2001 From: Tobias 'Syzuna' Teske Date: Mon, 3 Jul 2023 22:10:30 +0200 Subject: [PATCH 14/20] Fix tests and bump workflow action versions --- .github/workflows/check-buildstatus.yml | 4 ++-- .github/workflows/preview-release.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/tests-linux.yml | 2 +- .github/workflows/tests-windows.yml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check-buildstatus.yml b/.github/workflows/check-buildstatus.yml index 85d0f2a..c1cfaaf 100644 --- a/.github/workflows/check-buildstatus.yml +++ b/.github/workflows/check-buildstatus.yml @@ -13,9 +13,9 @@ jobs: dotnet-version: [ '7.0.x' ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: ${{ matrix.dotnet-version }} - name: Restore dependencies diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 82c4047..5e66190 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: 7.0.x - name: Restore dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3167f1..bbeead9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: 7.0.x - name: Restore dependencies diff --git a/.github/workflows/tests-linux.yml b/.github/workflows/tests-linux.yml index 0266b95..76c83c5 100644 --- a/.github/workflows/tests-linux.yml +++ b/.github/workflows/tests-linux.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: [ '7.0.x' ] + dotnet-version: [ '6.0.x', '7.0.x' ] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/tests-windows.yml b/.github/workflows/tests-windows.yml index 4aee907..68d48ac 100644 --- a/.github/workflows/tests-windows.yml +++ b/.github/workflows/tests-windows.yml @@ -9,7 +9,7 @@ jobs: runs-on: windows-latest strategy: matrix: - dotnet-version: [ '7.0.x' ] + dotnet-version: [ '6.0.x', '7.0.x' ] steps: - uses: actions/checkout@v3 From d9bdf98197f77d62ae5d8774a3fef0c97ba71849 Mon Sep 17 00:00:00 2001 From: Tobias 'Syzuna' Teske Date: Mon, 3 Jul 2023 22:12:58 +0200 Subject: [PATCH 15/20] fix test worklflow again --- .github/workflows/tests-linux.yml | 2 +- .github/workflows/tests-windows.yml | 2 +- .../TwitchLib.Communication.Tests.csproj | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests-linux.yml b/.github/workflows/tests-linux.yml index 76c83c5..0266b95 100644 --- a/.github/workflows/tests-linux.yml +++ b/.github/workflows/tests-linux.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: [ '6.0.x', '7.0.x' ] + dotnet-version: [ '7.0.x' ] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/tests-windows.yml b/.github/workflows/tests-windows.yml index 68d48ac..4aee907 100644 --- a/.github/workflows/tests-windows.yml +++ b/.github/workflows/tests-windows.yml @@ -9,7 +9,7 @@ jobs: runs-on: windows-latest strategy: matrix: - dotnet-version: [ '6.0.x', '7.0.x' ] + dotnet-version: [ '7.0.x' ] steps: - uses: actions/checkout@v3 diff --git a/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj b/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj index baed66a..b777674 100644 --- a/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj +++ b/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj @@ -1,10 +1,11 @@  - net6.0 + net7.0 false enable disable + latest From 1f56ecbfdbc3a228b542bb251b053948ba97f5a7 Mon Sep 17 00:00:00 2001 From: AoshiW Date: Mon, 3 Jul 2023 23:04:24 +0200 Subject: [PATCH 16/20] switch to File Scope Namespace, and more --- .../Clients/ClientTestsBase.cs | 1173 ++++++++--------- .../Clients/TcpClientTests.cs | 9 +- .../Clients/WebSocketClientTests.cs | 9 +- .../Helpers/TestLogHelper.cs | 109 +- .../Models/ReconnectionPolicyTests.cs | 61 +- .../TwitchLib.Communication.Tests.csproj | 2 +- .../Clients/ClientBase.cs | 627 ++++----- .../Clients/TcpClient.cs | 227 ++-- .../Clients/WebsocketClient.cs | 292 ++-- .../Enums/ClientType.cs | 13 +- .../Events/CoreEvents.cs | 18 +- .../Events/OnConnectedEventArgs.cs | 7 +- .../Events/OnDisconnectedEventArgs.cs | 7 +- .../Events/OnErrorEventArgs.cs | 15 +- .../Events/OnFatalErrorEventArgs.cs | 23 +- .../Events/OnMessageEventArgs.cs | 15 +- .../Events/OnSendFailedEventArgs.cs | 20 +- .../Extensions/LogExtensions.cs | 3 +- .../Interfaces/IClient.cs | 181 ++- .../Interfaces/IClientOptions.cs | 43 +- .../Models/ClientOptions.cs | 67 +- .../Models/NoReconnectionPolicy.cs | 23 +- .../Models/ReconnectionPolicy.cs | 351 +++-- .../Services/ConnectionWatchDog.cs | 204 ++- .../Services/NetworkServices.cs | 84 +- .../TwitchLib.Communication.csproj | 3 + 26 files changed, 1778 insertions(+), 1808 deletions(-) diff --git a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs index 14b10cd..7894e57 100644 --- a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs +++ b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs @@ -11,640 +11,640 @@ using Xunit.Sdk; [assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace TwitchLib.Communication.Tests.Clients +namespace TwitchLib.Communication.Tests.Clients; + +/// +/// bundles -Tests in one container +/// +/// +/// +/// +/// +/// +/// +/// +/// +public abstract class ClientTestsBase where T : IClient { - /// - /// bundles -Tests in one container - /// - /// - /// - /// - /// - /// - /// - /// - /// - public abstract class ClientTestsBase where T : IClient - { - private static TimeSpan WaitOneDuration => TimeSpan.FromSeconds(5); - private readonly IClientOptions? _options; - - protected ClientTestsBase(IClientOptions? options = null) + private static TimeSpan WaitOneDuration => TimeSpan.FromSeconds(5); + private readonly IClientOptions? _options; + + protected ClientTestsBase(IClientOptions? options = null) + { + _options = options; + } + + [Fact] + public async Task Client_Raises_OnConnected_EventArgs() + { + // create one logger per test-method! - cause one file per test-method is generated + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); + Assert.NotNull(client); + try + { + var pauseConnected = new ManualResetEvent(false); + + await Assert.RaisesAsync( + h => client.OnConnected += h, + h => client.OnConnected -= h, + async () => + { + client.OnConnected += async(sender, e) => + { + pauseConnected.Set(); + }; + + await client.OpenAsync(); + Assert.True(pauseConnected.WaitOne(WaitOneDuration)); + }); + } + catch (Exception e) { - _options = options; + logger.LogError(e.ToString()); + Assert.Fail(e.ToString()); } - - [Fact] - public async Task Client_Raises_OnConnected_EventArgs() + finally { - // create one logger per test-method! - cause one file per test-method is generated - var logger = TestLogHelper.GetLogger(); - var client = GetClient(logger, _options); - Assert.NotNull(client); - try - { - var pauseConnected = new ManualResetEvent(false); + client.Dispose(); + } + } - await Assert.RaisesAsync( - h => client.OnConnected += h, - h => client.OnConnected -= h, - async () => + [Fact] + public async Task Client_Raises_OnDisconnected_EventArgs() + { + // create one logger per test-method! - cause one file per test-method is generated + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); + Assert.NotNull(client); + try + { + var pauseDisconnected = new ManualResetEvent(false); + + await Assert.RaisesAsync( + h => client.OnDisconnected += h, + h => client.OnDisconnected -= h, + async () => + { + client.OnConnected += async (sender, e) => { - client.OnConnected += async(sender, e) => - { - pauseConnected.Set(); - }; - - await client.OpenAsync(); - Assert.True(pauseConnected.WaitOne(WaitOneDuration)); - }); - } - catch (Exception e) - { - logger.LogError(e.ToString()); - Assert.Fail(e.ToString()); - } - finally - { - client.Dispose(); - } - } + await client.CloseAsync(); + }; - [Fact] - public async Task Client_Raises_OnDisconnected_EventArgs() + client.OnDisconnected += async (sender, e) => + { + pauseDisconnected.Set(); + }; + await client.OpenAsync(); + + Assert.True(pauseDisconnected.WaitOne(WaitOneDuration)); + }); + } + catch (Exception e) { - // create one logger per test-method! - cause one file per test-method is generated - var logger = TestLogHelper.GetLogger(); - var client = GetClient(logger, _options); - Assert.NotNull(client); - try - { - var pauseDisconnected = new ManualResetEvent(false); + logger.LogError(e.ToString()); + Assert.Fail(e.ToString()); + } + finally + { + client.Dispose(); + } + } + + [Fact] + public async Task Client_Raises_OnReconnected_EventArgs() + { + // create one logger per test-method! - cause one file per test-method is generated + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); + Assert.NotNull(client); + try + { + var pauseReconnected = new ManualResetEvent(false); + + await Assert.RaisesAsync( + h => client.OnReconnected += h, + h => client.OnReconnected -= h, + async () => + { + client.OnConnected += async (s, e) => await client.ReconnectAsync(); - await Assert.RaisesAsync( - h => client.OnDisconnected += h, - h => client.OnDisconnected -= h, - async () => + client.OnReconnected += async (s, e) => { - client.OnConnected += async (sender, e) => - { - await client.CloseAsync(); - }; - - client.OnDisconnected += async (sender, e) => - { - pauseDisconnected.Set(); - }; - await client.OpenAsync(); - - Assert.True(pauseDisconnected.WaitOne(WaitOneDuration)); - }); - } - catch (Exception e) - { - logger.LogError(e.ToString()); - Assert.Fail(e.ToString()); - } - finally - { - client.Dispose(); - } + pauseReconnected.Set(); + }; + await client.OpenAsync(); + Assert.True(pauseReconnected.WaitOne(WaitOneDuration)); + }); } + catch (Exception e) + { + logger.LogError(e.ToString()); + Assert.Fail(e.ToString()); + } + finally + { + client.Dispose(); + } + } - [Fact] - public async Task Client_Raises_OnReconnected_EventArgs() + [Fact] + public void Dispose_Client_Before_Connecting_IsOK() + { + // create one logger per test-method! - cause one file per test-method is generated + var logger = TestLogHelper.GetLogger(); + IClient? client = null; + try { - // create one logger per test-method! - cause one file per test-method is generated - var logger = TestLogHelper.GetLogger(); - var client = GetClient(logger, _options); + client = GetClient(logger, _options); Assert.NotNull(client); - try - { - var pauseReconnected = new ManualResetEvent(false); - - await Assert.RaisesAsync( - h => client.OnReconnected += h, - h => client.OnReconnected -= h, - async () => - { - client.OnConnected += async (s, e) => await client.ReconnectAsync(); - - client.OnReconnected += async (s, e) => - { - pauseReconnected.Set(); - }; - await client.OpenAsync(); - Assert.True(pauseReconnected.WaitOne(WaitOneDuration)); - }); - } - catch (Exception e) - { - logger.LogError(e.ToString()); - Assert.Fail(e.ToString()); - } - finally - { - client.Dispose(); - } + client.Dispose(); } - - [Fact] - public void Dispose_Client_Before_Connecting_IsOK() + catch (Exception e) { - // create one logger per test-method! - cause one file per test-method is generated - var logger = TestLogHelper.GetLogger(); - IClient? client = null; - try - { - client = GetClient(logger, _options); - Assert.NotNull(client); - client.Dispose(); - } - catch (Exception e) - { - logger.LogError(e.ToString()); - Assert.Fail(e.ToString()); - } - finally - { - client?.Dispose(); - } + logger.LogError(e.ToString()); + Assert.Fail(e.ToString()); } - - private static TClient? GetClient(ILogger logger, IClientOptions? options = null) + finally { - var constructorParameterTypes = new Type[] - { - typeof(IClientOptions), - typeof(ILogger) - }; - - var constructor = typeof(TClient).GetConstructor(constructorParameterTypes); - var constructorParameters = new object[] - { - options ?? new ClientOptions(), - logger - }; - - return (TClient?)constructor?.Invoke(constructorParameters); + client?.Dispose(); } } + private static TClient? GetClient(ILogger logger, IClientOptions? options = null) + { + var constructorParameterTypes = new Type[] + { + typeof(IClientOptions), + typeof(ILogger) + }; + + var constructor = typeof(TClient).GetConstructor(constructorParameterTypes); + var constructorParameters = new object[] + { + options ?? new ClientOptions(), + logger + }; + + return (TClient?)constructor?.Invoke(constructorParameters); + } +} + - #region Modified Assert - //TL;DR: Extracted version of XUNIT with - //modification to accept new event Handler +#region Modified Assert +//TL;DR: Extracted version of XUNIT with +//modification to accept new event Handler - public partial class Assert - { +public partial class Assert +{ - /// - /// Verifies that a event with the exact event args (and not a derived type) is raised. - /// - /// The type of the event arguments to expect - /// Code to attach the event handler - /// Code to detach the event handler - /// A delegate to the code to be tested - /// The event sender and arguments wrapped in an object - /// Thrown when the expected event was not raised. - public static async Task> RaisesAsync(Action> attach, Action> detach, Func testCode) - { - var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + /// + /// Verifies that a event with the exact event args (and not a derived type) is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAsync(Action> attach, Action> detach, Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); - if (raisedEvent == null) - throw new RaisesException(typeof(T)); + if (raisedEvent == null) + throw new RaisesException(typeof(T)); - if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) - throw new RaisesException(typeof(T), raisedEvent.Arguments.GetType()); + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw new RaisesException(typeof(T), raisedEvent.Arguments.GetType()); - return raisedEvent; - } + return raisedEvent; + } - /// - /// Verifies that an event with the exact or a derived event args is raised. - /// - /// The type of the event arguments to expect - /// Code to attach the event handler - /// Code to detach the event handler - /// A delegate to the code to be tested - /// The event sender and arguments wrapped in an object - /// Thrown when the expected event was not raised. - public static async Task> RaisesAnyAsync(Action> attach, Action> detach, Func testCode) - { - var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + /// + /// Verifies that an event with the exact or a derived event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAnyAsync(Action> attach, Action> detach, Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); - if (raisedEvent == null) - throw new RaisesException(typeof(T)); + if (raisedEvent == null) + throw new RaisesException(typeof(T)); - return raisedEvent; - } + return raisedEvent; + } #if XUNIT_NULLABLE static async Task?> RaisesAsyncInternal(Action> attach, Action> detach, Func testCode) #else - static async Task> RaisesAsyncInternal(Action> attach, Action> detach, Func testCode) + static async Task> RaisesAsyncInternal(Action> attach, Action> detach, Func testCode) #endif - { - NotNull(attach); - NotNull(detach); - NotNull(testCode); + { + NotNull(attach); + NotNull(detach); + NotNull(testCode); #if XUNIT_NULLABLE RaisedEvent? raisedEvent = null; void handler(object? s, T args) => raisedEvent = new RaisedEvent(s, args); #else - RaisedEvent raisedEvent = null; - AsyncEventHandler value = (object s, T args) => - { - raisedEvent = new RaisedEvent(s, args); - return Task.CompletedTask; - }; - AsyncEventHandler handler = value; + RaisedEvent? raisedEvent = null; + AsyncEventHandler value = (object? s, T args) => + { + raisedEvent = new RaisedEvent(s, args); + return Task.CompletedTask; + }; + AsyncEventHandler handler = value; #endif - attach(handler); - await testCode(); - detach(handler); - return raisedEvent; - } + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + /// + /// Represents a raised event after the fact. + /// + /// The type of the event arguments. + public class RaisedEvent + { /// - /// Represents a raised event after the fact. + /// The sender of the event. /// - /// The type of the event arguments. - public class RaisedEvent - { - /// - /// The sender of the event. - /// #if XUNIT_NULLABLE public object? Sender { get; } #else - public object Sender { get; } + public object Sender { get; } #endif - /// - /// The event arguments. - /// - public T Arguments { get; } + /// + /// The event arguments. + /// + public T Arguments { get; } - /// - /// Creates a new instance of the class. - /// - /// The sender of the event. - /// The event arguments + /// + /// Creates a new instance of the class. + /// + /// The sender of the event. + /// The event arguments #if XUNIT_NULLABLE public RaisedEvent(object? sender, T args) #else - public RaisedEvent(object sender, T args) + public RaisedEvent(object sender, T args) #endif - { - Sender = sender; - Arguments = args; - } + { + Sender = sender; + Arguments = args; } + } #if XUNIT_NULLABLE public static void False([DoesNotReturnIf(parameterValue: true)] bool condition) #else - public static void False(bool condition) + public static void False(bool condition) #endif - { - False((bool?)condition, null); - } + { + False((bool?)condition, null); + } - /// - /// Verifies that the condition is false. - /// - /// The condition to be tested - /// Thrown if the condition is not false + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// Thrown if the condition is not false #if XUNIT_NULLABLE public static void False([DoesNotReturnIf(parameterValue: true)] bool? condition) #else - public static void False(bool? condition) + public static void False(bool? condition) #endif - { - False(condition, null); - } + { + False(condition, null); + } - /// - /// Verifies that the condition is false. - /// - /// The condition to be tested - /// The message to show when the condition is not false - /// Thrown if the condition is not false + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// The message to show when the condition is not false + /// Thrown if the condition is not false #if XUNIT_NULLABLE public static void False([DoesNotReturnIf(parameterValue: true)] bool condition, string? userMessage) #else - public static void False(bool condition, string userMessage) + public static void False(bool condition, string userMessage) #endif - { - False((bool?)condition, userMessage); - } + { + False((bool?)condition, userMessage); + } - /// - /// Verifies that the condition is false. - /// - /// The condition to be tested - /// The message to show when the condition is not false - /// Thrown if the condition is not false + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// The message to show when the condition is not false + /// Thrown if the condition is not false #if XUNIT_NULLABLE public static void False([DoesNotReturnIf(parameterValue: true)] bool? condition, string? userMessage) #else - public static void False(bool? condition, string userMessage) + public static void False(bool? condition, string userMessage) #endif - { - if (!condition.HasValue || condition.GetValueOrDefault()) - throw new FalseException(userMessage, condition); - } + { + if (!condition.HasValue || condition.GetValueOrDefault()) + throw new FalseException(userMessage, condition); + } - /// - /// Verifies that an expression is true. - /// - /// The condition to be inspected - /// Thrown when the condition is false + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// Thrown when the condition is false #if XUNIT_NULLABLE public static void True([DoesNotReturnIf(parameterValue: false)] bool condition) #else - public static void True(bool condition) + public static void True(bool condition) #endif - { - True((bool?)condition, null); - } + { + True((bool?)condition, null); + } - /// - /// Verifies that an expression is true. - /// - /// The condition to be inspected - /// Thrown when the condition is false + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// Thrown when the condition is false #if XUNIT_NULLABLE public static void True([DoesNotReturnIf(parameterValue: false)] bool? condition) #else - public static void True(bool? condition) + public static void True(bool? condition) #endif - { - True(condition, null); - } + { + True(condition, null); + } - /// - /// Verifies that an expression is true. - /// - /// The condition to be inspected - /// The message to be shown when the condition is false - /// Thrown when the condition is false + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// The message to be shown when the condition is false + /// Thrown when the condition is false #if XUNIT_NULLABLE public static void True([DoesNotReturnIf(parameterValue: false)] bool condition, string? userMessage) #else - public static void True(bool condition, string userMessage) + public static void True(bool condition, string userMessage) #endif - { - True((bool?)condition, userMessage); - } + { + True((bool?)condition, userMessage); + } - /// - /// Verifies that an expression is true. - /// - /// The condition to be inspected - /// The message to be shown when the condition is false - /// Thrown when the condition is false + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// The message to be shown when the condition is false + /// Thrown when the condition is false #if XUNIT_NULLABLE public static void True([DoesNotReturnIf(parameterValue: false)] bool? condition, string? userMessage) #else - public static void True(bool? condition, string userMessage) + public static void True(bool? condition, string userMessage) #endif - { - if (!condition.HasValue || !condition.GetValueOrDefault()) - throw new TrueException(userMessage, condition); - } + { + if (!condition.HasValue || !condition.GetValueOrDefault()) + throw new TrueException(userMessage, condition); + } - /// - /// Verifies that a string contains a given sub-string, using the current culture. - /// - /// The sub-string expected to be in the string - /// The string to be inspected - /// Thrown when the sub-string is not present inside the string + /// + /// Verifies that a string contains a given sub-string, using the current culture. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// Thrown when the sub-string is not present inside the string #if XUNIT_NULLABLE public static void Contains(string expectedSubstring, string? actualString) #else - public static void Contains(string expectedSubstring, string actualString) + public static void Contains(string expectedSubstring, string actualString) #endif - { - Contains(expectedSubstring, actualString, StringComparison.CurrentCulture); - } + { + Contains(expectedSubstring, actualString, StringComparison.CurrentCulture); + } - /// - /// Verifies that a string contains a given sub-string, using the given comparison type. - /// - /// The sub-string expected to be in the string - /// The string to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-string is not present inside the string + /// + /// Verifies that a string contains a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is not present inside the string #if XUNIT_NULLABLE public static void Contains(string expectedSubstring, string? actualString, StringComparison comparisonType) #else - public static void Contains(string expectedSubstring, string actualString, StringComparison comparisonType) + public static void Contains(string expectedSubstring, string actualString, StringComparison comparisonType) #endif - { - NotNull(expectedSubstring); + { + NotNull(expectedSubstring); - if (actualString == null || actualString.IndexOf(expectedSubstring, comparisonType) < 0) - throw new ContainsException(expectedSubstring, actualString); - } + if (actualString == null || actualString.IndexOf(expectedSubstring, comparisonType) < 0) + throw new ContainsException(expectedSubstring, actualString); + } - /// - /// Verifies that a string does not contain a given sub-string, using the current culture. - /// - /// The sub-string which is expected not to be in the string - /// The string to be inspected - /// Thrown when the sub-string is present inside the string + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string which is expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string #if XUNIT_NULLABLE public static void DoesNotContain(string expectedSubstring, string? actualString) #else - public static void DoesNotContain(string expectedSubstring, string actualString) + public static void DoesNotContain(string expectedSubstring, string actualString) #endif - { - DoesNotContain(expectedSubstring, actualString, StringComparison.CurrentCulture); - } + { + DoesNotContain(expectedSubstring, actualString, StringComparison.CurrentCulture); + } - /// - /// Verifies that a string does not contain a given sub-string, using the current culture. - /// - /// The sub-string which is expected not to be in the string - /// The string to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-string is present inside the given string + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string which is expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the given string #if XUNIT_NULLABLE public static void DoesNotContain(string expectedSubstring, string? actualString, StringComparison comparisonType) #else - public static void DoesNotContain(string expectedSubstring, string actualString, StringComparison comparisonType) + public static void DoesNotContain(string expectedSubstring, string actualString, StringComparison comparisonType) #endif - { - NotNull(expectedSubstring); + { + NotNull(expectedSubstring); - if (actualString != null && actualString.IndexOf(expectedSubstring, comparisonType) >= 0) - throw new DoesNotContainException(expectedSubstring, actualString); - } + if (actualString != null && actualString.IndexOf(expectedSubstring, comparisonType) >= 0) + throw new DoesNotContainException(expectedSubstring, actualString); + } - /// - /// Verifies that a string starts with a given string, using the current culture. - /// - /// The string expected to be at the start of the string - /// The string to be inspected - /// Thrown when the string does not start with the expected string + /// + /// Verifies that a string starts with a given string, using the current culture. + /// + /// The string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected string #if XUNIT_NULLABLE public static void StartsWith(string? expectedStartString, string? actualString) #else - public static void StartsWith(string expectedStartString, string actualString) + public static void StartsWith(string expectedStartString, string actualString) #endif - { - StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); - } + { + StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); + } - /// - /// Verifies that a string starts with a given string, using the given comparison type. - /// - /// The string expected to be at the start of the string - /// The string to be inspected - /// The type of string comparison to perform - /// Thrown when the string does not start with the expected string + /// + /// Verifies that a string starts with a given string, using the given comparison type. + /// + /// The string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected string #if XUNIT_NULLABLE public static void StartsWith(string? expectedStartString, string? actualString, StringComparison comparisonType) #else - public static void StartsWith(string expectedStartString, string actualString, StringComparison comparisonType) + public static void StartsWith(string expectedStartString, string actualString, StringComparison comparisonType) #endif - { - if (expectedStartString == null || actualString == null || !actualString.StartsWith(expectedStartString, comparisonType)) - throw new StartsWithException(expectedStartString, actualString); - } + { + if (expectedStartString == null || actualString == null || !actualString.StartsWith(expectedStartString, comparisonType)) + throw new StartsWithException(expectedStartString, actualString); + } - /// - /// Verifies that a string ends with a given string, using the current culture. - /// - /// The string expected to be at the end of the string - /// The string to be inspected - /// Thrown when the string does not end with the expected string + /// + /// Verifies that a string ends with a given string, using the current culture. + /// + /// The string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected string #if XUNIT_NULLABLE public static void EndsWith(string? expectedEndString, string? actualString) #else - public static void EndsWith(string expectedEndString, string actualString) + public static void EndsWith(string expectedEndString, string actualString) #endif - { - EndsWith(expectedEndString, actualString, StringComparison.CurrentCulture); - } + { + EndsWith(expectedEndString, actualString, StringComparison.CurrentCulture); + } - /// - /// Verifies that a string ends with a given string, using the given comparison type. - /// - /// The string expected to be at the end of the string - /// The string to be inspected - /// The type of string comparison to perform - /// Thrown when the string does not end with the expected string + /// + /// Verifies that a string ends with a given string, using the given comparison type. + /// + /// The string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected string #if XUNIT_NULLABLE public static void EndsWith(string? expectedEndString, string? actualString, StringComparison comparisonType) #else - public static void EndsWith(string expectedEndString, string actualString, StringComparison comparisonType) + public static void EndsWith(string expectedEndString, string actualString, StringComparison comparisonType) #endif - { - if (expectedEndString == null || actualString == null || !actualString.EndsWith(expectedEndString, comparisonType)) - throw new EndsWithException(expectedEndString, actualString); - } + { + if (expectedEndString == null || actualString == null || !actualString.EndsWith(expectedEndString, comparisonType)) + throw new EndsWithException(expectedEndString, actualString); + } - /// - /// Verifies that a string matches a regular expression. - /// - /// The regex pattern expected to match - /// The string to be inspected - /// Thrown when the string does not match the regex pattern + /// + /// Verifies that a string matches a regular expression. + /// + /// The regex pattern expected to match + /// The string to be inspected + /// Thrown when the string does not match the regex pattern #if XUNIT_NULLABLE public static void Matches(string expectedRegexPattern, string? actualString) #else - public static void Matches(string expectedRegexPattern, string actualString) + public static void Matches(string expectedRegexPattern, string actualString) #endif - { - NotNull(expectedRegexPattern); + { + NotNull(expectedRegexPattern); - if (actualString == null || !Regex.IsMatch(actualString, expectedRegexPattern)) - throw new MatchesException(expectedRegexPattern, actualString); - } + if (actualString == null || !Regex.IsMatch(actualString, expectedRegexPattern)) + throw new MatchesException(expectedRegexPattern, actualString); + } - /// - /// Verifies that a string matches a regular expression. - /// - /// The regex expected to match - /// The string to be inspected - /// Thrown when the string does not match the regex + /// + /// Verifies that a string matches a regular expression. + /// + /// The regex expected to match + /// The string to be inspected + /// Thrown when the string does not match the regex #if XUNIT_NULLABLE public static void Matches(Regex expectedRegex, string? actualString) #else - public static void Matches(Regex expectedRegex, string actualString) + public static void Matches(Regex expectedRegex, string actualString) #endif - { - NotNull(expectedRegex); + { + NotNull(expectedRegex); - if (actualString == null || !expectedRegex.IsMatch(actualString)) - throw new MatchesException(expectedRegex.ToString(), actualString); - } + if (actualString == null || !expectedRegex.IsMatch(actualString)) + throw new MatchesException(expectedRegex.ToString(), actualString); + } - /// - /// Verifies that a string does not match a regular expression. - /// - /// The regex pattern expected not to match - /// The string to be inspected - /// Thrown when the string matches the regex pattern + /// + /// Verifies that a string does not match a regular expression. + /// + /// The regex pattern expected not to match + /// The string to be inspected + /// Thrown when the string matches the regex pattern #if XUNIT_NULLABLE public static void DoesNotMatch(string expectedRegexPattern, string? actualString) #else - public static void DoesNotMatch(string expectedRegexPattern, string actualString) + public static void DoesNotMatch(string expectedRegexPattern, string actualString) #endif - { - NotNull(expectedRegexPattern); + { + NotNull(expectedRegexPattern); - if (actualString != null && Regex.IsMatch(actualString, expectedRegexPattern)) - throw new DoesNotMatchException(expectedRegexPattern, actualString); - } + if (actualString != null && Regex.IsMatch(actualString, expectedRegexPattern)) + throw new DoesNotMatchException(expectedRegexPattern, actualString); + } - /// - /// Verifies that a string does not match a regular expression. - /// - /// The regex expected not to match - /// The string to be inspected - /// Thrown when the string matches the regex + /// + /// Verifies that a string does not match a regular expression. + /// + /// The regex expected not to match + /// The string to be inspected + /// Thrown when the string matches the regex #if XUNIT_NULLABLE public static void DoesNotMatch(Regex expectedRegex, string? actualString) #else - public static void DoesNotMatch(Regex expectedRegex, string actualString) + public static void DoesNotMatch(Regex expectedRegex, string actualString) #endif - { - NotNull(expectedRegex); + { + NotNull(expectedRegex); - if (actualString != null && expectedRegex.IsMatch(actualString)) - throw new DoesNotMatchException(expectedRegex.ToString(), actualString); - } + if (actualString != null && expectedRegex.IsMatch(actualString)) + throw new DoesNotMatchException(expectedRegex.ToString(), actualString); + } - /// - /// Verifies that two strings are equivalent. - /// - /// The expected string value. - /// The actual string value. - /// Thrown when the strings are not equivalent. + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. #if XUNIT_NULLABLE public static void Equal(string? expected, string? actual) #else - public static void Equal(string expected, string actual) + public static void Equal(string expected, string actual) #endif - { - Equal(expected, actual, false, false, false); - } + { + Equal(expected, actual, false, false, false); + } - /// - /// Verifies that two strings are equivalent. - /// - /// The expected string value. - /// The actual string value. - /// If set to true, ignores cases differences. The invariant culture is used. - /// If set to true, treats \r\n, \r, and \n as equivalent. - /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. - /// Thrown when the strings are not equivalent. + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// Thrown when the strings are not equivalent. #if XUNIT_NULLABLE public static void Equal( string? expected, @@ -653,14 +653,14 @@ public static void Equal( bool ignoreLineEndingDifferences = false, bool ignoreWhiteSpaceDifferences = false) #else - public static void Equal( - string expected, - string actual, - bool ignoreCase = false, - bool ignoreLineEndingDifferences = false, - bool ignoreWhiteSpaceDifferences = false) + public static void Equal( + string expected, + string actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false) #endif - { + { #if XUNIT_SPAN if (expected == null && actual == null) return; @@ -669,162 +669,151 @@ public static void Equal( Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences); #else - // Start out assuming the one of the values is null - int expectedIndex = -1; - int actualIndex = -1; - int expectedLength = 0; - int actualLength = 0; + // Start out assuming the one of the values is null + int expectedIndex = -1; + int actualIndex = -1; + int expectedLength = 0; + int actualLength = 0; - if (expected == null) - { - if (actual == null) - return; - } - else if (actual != null) + if (expected == null) + { + if (actual == null) + return; + } + else if (actual != null) + { + // Walk the string, keeping separate indices since we can skip variable amounts of + // data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences. + expectedIndex = 0; + actualIndex = 0; + expectedLength = expected.Length; + actualLength = actual.Length; + + while (expectedIndex < expectedLength && actualIndex < actualLength) { - // Walk the string, keeping separate indices since we can skip variable amounts of - // data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences. - expectedIndex = 0; - actualIndex = 0; - expectedLength = expected.Length; - actualLength = actual.Length; - - while (expectedIndex < expectedLength && actualIndex < actualLength) - { - char expectedChar = expected[expectedIndex]; - char actualChar = actual[actualIndex]; + char expectedChar = expected[expectedIndex]; + char actualChar = actual[actualIndex]; - if (ignoreLineEndingDifferences && IsLineEnding(expectedChar) && IsLineEnding(actualChar)) - { - expectedIndex = SkipLineEnding(expected, expectedIndex); - actualIndex = SkipLineEnding(actual, actualIndex); - } - else if (ignoreWhiteSpaceDifferences && IsWhiteSpace(expectedChar) && IsWhiteSpace(actualChar)) + if (ignoreLineEndingDifferences && IsLineEnding(expectedChar) && IsLineEnding(actualChar)) + { + expectedIndex = SkipLineEnding(expected, expectedIndex); + actualIndex = SkipLineEnding(actual, actualIndex); + } + else if (ignoreWhiteSpaceDifferences && IsWhiteSpace(expectedChar) && IsWhiteSpace(actualChar)) + { + expectedIndex = SkipWhitespace(expected, expectedIndex); + actualIndex = SkipWhitespace(actual, actualIndex); + } + else + { + if (ignoreCase) { - expectedIndex = SkipWhitespace(expected, expectedIndex); - actualIndex = SkipWhitespace(actual, actualIndex); + expectedChar = Char.ToUpperInvariant(expectedChar); + actualChar = Char.ToUpperInvariant(actualChar); } - else + + if (expectedChar != actualChar) { - if (ignoreCase) - { - expectedChar = Char.ToUpperInvariant(expectedChar); - actualChar = Char.ToUpperInvariant(actualChar); - } - - if (expectedChar != actualChar) - { - break; - } - - expectedIndex++; - actualIndex++; + break; } - } - } - if (expectedIndex < expectedLength || actualIndex < actualLength) - { - throw new EqualException(expected, actual, expectedIndex, actualIndex); + expectedIndex++; + actualIndex++; + } } -#endif } - static bool IsLineEnding(char c) + + if (expectedIndex < expectedLength || actualIndex < actualLength) { - return c == '\r' || c == '\n'; + throw new EqualException(expected, actual, expectedIndex, actualIndex); } +#endif + } + static bool IsLineEnding(char c) + { + return c == '\r' || c == '\n'; + } + + static bool IsWhiteSpace(char c) + { + return c == ' ' || c == '\t'; + } - static bool IsWhiteSpace(char c) + static int SkipLineEnding(string value, int index) + { + if (value[index] == '\r') { - return c == ' ' || c == '\t'; + ++index; } - - static int SkipLineEnding(string value, int index) + if (index < value.Length && value[index] == '\n') { - if (value[index] == '\r') - { - ++index; - } - if (index < value.Length && value[index] == '\n') - { - ++index; - } - - return index; + ++index; } - static int SkipWhitespace(string value, int index) + return index; + } + + static int SkipWhitespace(string value, int index) + { + while (index < value.Length) { - while (index < value.Length) + switch (value[index]) { - switch (value[index]) - { - case ' ': - case '\t': - index++; - break; + case ' ': + case '\t': + index++; + break; - default: - return index; - } + default: + return index; } - - return index; } - /// - /// Verifies that an object reference is not null. - /// - /// The object to be validated - /// Thrown when the object reference is null + return index; + } + + /// + /// Verifies that an object reference is not null. + /// + /// The object to be validated + /// Thrown when the object reference is null #if XUNIT_NULLABLE public static void NotNull([NotNull] object? @object) #else - public static void NotNull(object @object) + public static void NotNull(object @object) #endif - { - if (@object == null) - throw new NotNullException(); - } + { + if (@object == null) + throw new NotNullException(); + } - /// - /// Verifies that an object reference is null. - /// - /// The object to be inspected - /// Thrown when the object reference is not null + /// + /// Verifies that an object reference is null. + /// + /// The object to be inspected + /// Thrown when the object reference is not null #if XUNIT_NULLABLE public static void Null([MaybeNull] object? @object) #else - public static void Null(object @object) + public static void Null(object @object) #endif - { - if (@object != null) - throw new NullException(@object); - } + { + if (@object != null) + throw new NullException(@object); + } - /// - /// Indicates that the test should immediately fail. - /// - /// The failure message + /// + /// Indicates that the test should immediately fail. + /// + /// The failure message #if XUNIT_NULLABLE [DoesNotReturn] #endif - public static void Fail(string message) - { - NotNull( message); - - throw new FailException(message); - } - - - + public static void Fail(string message) + { + NotNull( message); + throw new FailException(message); } - #endregion - - } - - - - +#endregion diff --git a/src/TwitchLib.Communication.Tests/Clients/TcpClientTests.cs b/src/TwitchLib.Communication.Tests/Clients/TcpClientTests.cs index 007f058..439e5d9 100644 --- a/src/TwitchLib.Communication.Tests/Clients/TcpClientTests.cs +++ b/src/TwitchLib.Communication.Tests/Clients/TcpClientTests.cs @@ -1,10 +1,9 @@ using TwitchLib.Communication.Clients; using TwitchLib.Communication.Models; -namespace TwitchLib.Communication.Tests.Clients +namespace TwitchLib.Communication.Tests.Clients; + +public class TcpClientTests : ClientTestsBase { - public class TcpClientTests : ClientTestsBase - { - public TcpClientTests() : base(new ClientOptions(useSsl: false)) { } - } + public TcpClientTests() : base(new ClientOptions(useSsl: false)) { } } \ No newline at end of file diff --git a/src/TwitchLib.Communication.Tests/Clients/WebSocketClientTests.cs b/src/TwitchLib.Communication.Tests/Clients/WebSocketClientTests.cs index fe9a507..0009ded 100644 --- a/src/TwitchLib.Communication.Tests/Clients/WebSocketClientTests.cs +++ b/src/TwitchLib.Communication.Tests/Clients/WebSocketClientTests.cs @@ -1,8 +1,7 @@ using TwitchLib.Communication.Clients; -namespace TwitchLib.Communication.Tests.Clients +namespace TwitchLib.Communication.Tests.Clients; + +public class WebSocketClientTests : ClientTestsBase { - public class WebSocketClientTests : ClientTestsBase - { - } -} \ No newline at end of file +} diff --git a/src/TwitchLib.Communication.Tests/Helpers/TestLogHelper.cs b/src/TwitchLib.Communication.Tests/Helpers/TestLogHelper.cs index ce5f44a..900e9a9 100644 --- a/src/TwitchLib.Communication.Tests/Helpers/TestLogHelper.cs +++ b/src/TwitchLib.Communication.Tests/Helpers/TestLogHelper.cs @@ -5,65 +5,64 @@ using Serilog.Events; using Serilog.Exceptions; -namespace TwitchLib.Communication.Tests.Helpers +namespace TwitchLib.Communication.Tests.Helpers; + +internal static class TestLogHelper { - internal static class TestLogHelper - { - private static readonly string OUTPUT_TEMPLATE = - "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u}] {Message:lj}{NewLine}{Exception}{NewLine}"; + private static readonly string OUTPUT_TEMPLATE = + "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u}] {Message:lj}{NewLine}{Exception}{NewLine}"; - private static readonly string NEW_TEST_RUN_INDICATOR; + private static readonly string NEW_TEST_RUN_INDICATOR; - static TestLogHelper() - { - StringBuilder builder = new StringBuilder(); - builder.AppendLine(); - builder.AppendLine(new string('-', 80)); - builder.Append(new string(' ', 34)); - builder.AppendLine("new Test-Run"); - builder.AppendLine(new string('-', 80)); - NEW_TEST_RUN_INDICATOR = builder.ToString(); - } + static TestLogHelper() + { + var builder = new StringBuilder(); + builder.AppendLine(); + builder.Append('-', 80).AppendLine(); + builder.Append(' ', 34); + builder.AppendLine("new Test-Run"); + builder.Append('-', 80).AppendLine(); + NEW_TEST_RUN_INDICATOR = builder.ToString(); + } - internal static Microsoft.Extensions.Logging.ILogger GetLogger( - LogEventLevel logEventLevel = LogEventLevel.Verbose, - [CallerMemberName] string callerMemberName = "TestMethod") - { - Serilog.ILogger logger = GetSerilogLogger(typeof(T).Name, - callerMemberName, - logEventLevel); - Microsoft.Extensions.Logging.ILoggerFactory loggerFactory = - new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); - return loggerFactory.CreateLogger(); - } + internal static Microsoft.Extensions.Logging.ILogger GetLogger( + LogEventLevel logEventLevel = LogEventLevel.Verbose, + [CallerMemberName] string callerMemberName = "TestMethod") + { + Serilog.ILogger logger = GetSerilogLogger(typeof(T).Name, + callerMemberName, + logEventLevel); + Microsoft.Extensions.Logging.ILoggerFactory loggerFactory = + new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); + return loggerFactory.CreateLogger(); + } - private static Serilog.ILogger GetSerilogLogger(string typeName, - string callerMemberName, - LogEventLevel logEventLevel) - { - Serilog.LoggerConfiguration loggerConfiguration = GetConfiguration(typeName, - callerMemberName, - logEventLevel); - Serilog.ILogger logger = loggerConfiguration.CreateLogger().ForContext(); - logger.Information(NEW_TEST_RUN_INDICATOR); - return logger; - } + private static Serilog.ILogger GetSerilogLogger(string typeName, + string callerMemberName, + LogEventLevel logEventLevel) + { + Serilog.LoggerConfiguration loggerConfiguration = GetConfiguration(typeName, + callerMemberName, + logEventLevel); + Serilog.ILogger logger = loggerConfiguration.CreateLogger().ForContext(); + logger.Information(NEW_TEST_RUN_INDICATOR); + return logger; + } - private static Serilog.LoggerConfiguration GetConfiguration(string typeName, - string callerMemberName, - LogEventLevel logEventLevel) - { - Serilog.LoggerConfiguration loggerConfiguration = new Serilog.LoggerConfiguration(); - loggerConfiguration.MinimumLevel.Verbose(); - string path = $"../../../Logs/{typeName}/{callerMemberName}.log"; - loggerConfiguration.WriteTo.File( - path: path, - restrictedToMinimumLevel: logEventLevel, - outputTemplate: OUTPUT_TEMPLATE - ); - loggerConfiguration.Enrich.WithExceptionDetails(); - loggerConfiguration.Enrich.FromLogContext(); - return loggerConfiguration; - } + private static Serilog.LoggerConfiguration GetConfiguration(string typeName, + string callerMemberName, + LogEventLevel logEventLevel) + { + var loggerConfiguration = new Serilog.LoggerConfiguration(); + loggerConfiguration.MinimumLevel.Verbose(); + string path = $"../../../Logs/{typeName}/{callerMemberName}.log"; + loggerConfiguration.WriteTo.File( + path: path, + restrictedToMinimumLevel: logEventLevel, + outputTemplate: OUTPUT_TEMPLATE + ); + loggerConfiguration.Enrich.WithExceptionDetails(); + loggerConfiguration.Enrich.FromLogContext(); + return loggerConfiguration; } -} \ No newline at end of file +} diff --git a/src/TwitchLib.Communication.Tests/Models/ReconnectionPolicyTests.cs b/src/TwitchLib.Communication.Tests/Models/ReconnectionPolicyTests.cs index e67a286..24d1186 100644 --- a/src/TwitchLib.Communication.Tests/Models/ReconnectionPolicyTests.cs +++ b/src/TwitchLib.Communication.Tests/Models/ReconnectionPolicyTests.cs @@ -2,39 +2,38 @@ using TwitchLib.Communication.Models; using Xunit; -namespace TwitchLib.Communication.Tests.Models +namespace TwitchLib.Communication.Tests.Models; + +public class ReconnectionPolicyTests { - public class ReconnectionPolicyTests + /// + /// Checks + ///

+ /// + ///

+ /// + ///
+ [Fact] + public void ReconnectionPolicy_OmitReconnect() { - /// - /// Checks - ///

- /// - ///

- /// - ///
- [Fact] - public void ReconnectionPolicy_OmitReconnect() + try + { + ReconnectionPolicy reconnectionPolicy = new NoReconnectionPolicy(); + Assert.False(reconnectionPolicy.AreAttemptsComplete()); + reconnectionPolicy.ProcessValues(); + Assert.True(reconnectionPolicy.AreAttemptsComplete()); + // in case of a normal connect, we expect the ReconnectionPolicy to be reset + reconnectionPolicy.Reset(false); + Assert.False(reconnectionPolicy.AreAttemptsComplete()); + reconnectionPolicy.ProcessValues(); + Assert.True(reconnectionPolicy.AreAttemptsComplete()); + // in case of a reconnect, we expect the ReconnectionPolicy not to be reset + reconnectionPolicy.Reset(true); + Assert.True(reconnectionPolicy.AreAttemptsComplete()); + } + catch (Exception e) { - try - { - ReconnectionPolicy reconnectionPolicy = new NoReconnectionPolicy(); - Assert.False(reconnectionPolicy.AreAttemptsComplete()); - reconnectionPolicy.ProcessValues(); - Assert.True(reconnectionPolicy.AreAttemptsComplete()); - // in case of a normal connect, we expect the ReconnectionPolicy to be reset - reconnectionPolicy.Reset(false); - Assert.False(reconnectionPolicy.AreAttemptsComplete()); - reconnectionPolicy.ProcessValues(); - Assert.True(reconnectionPolicy.AreAttemptsComplete()); - // in case of a reconnect, we expect the ReconnectionPolicy not to be reset - reconnectionPolicy.Reset(true); - Assert.True(reconnectionPolicy.AreAttemptsComplete()); - } - catch (Exception e) - { - Assert.Fail(e.ToString()); - } + Assert.Fail(e.ToString()); } } -} \ No newline at end of file +} diff --git a/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj b/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj index b777674..d6580d5 100644 --- a/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj +++ b/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj @@ -1,7 +1,7 @@  - net7.0 + net6.0;net7.0 false enable disable diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index a9d9b0f..0dfc91f 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -1,369 +1,384 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using TwitchLib.Communication.Events; using TwitchLib.Communication.Extensions; using TwitchLib.Communication.Interfaces; using TwitchLib.Communication.Models; using TwitchLib.Communication.Services; -namespace TwitchLib.Communication.Clients +namespace TwitchLib.Communication.Clients; + +/// +/// This bundles almost everything that and have in common +/// to be able to +/// +/// +/// pass instances of this to +/// +/// +/// and to access Methods of this instance within +/// +/// +/// +public abstract class ClientBase : IClient + where T : IDisposable { + private readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly NetworkServices _networkServices; + private CancellationTokenSource _cancellationTokenSource; + /// - /// This bundles almost everything that and have in common - /// to be able to - /// - /// - /// pass instances of this to and - /// - /// - /// and to access Methods of this instance within and - /// - /// + /// This is used for + /// whenever a call to is made + /// + internal CancellationToken Token => _cancellationTokenSource.Token; + + internal static TimeSpan TimeOutEstablishConnection => TimeSpan.FromSeconds(15); + + protected readonly ILogger>? Logger; + + protected abstract string Url { get; } + + /// + /// The underlying client. + /// + public T? Client { get; private set; } + + /// + public abstract bool IsConnected { get; } + + /// + public IClientOptions Options { get; } + + /// + public event AsyncEventHandler? OnConnected; + + /// + public event AsyncEventHandler? OnDisconnected; + + /// + public event AsyncEventHandler? OnError; + + /// + public event AsyncEventHandler? OnFatality; + + /// + public event AsyncEventHandler? OnMessage; + + /// + public event AsyncEventHandler? OnSendFailed; + + /// + public event AsyncEventHandler? OnReconnected; + + internal ClientBase( + IClientOptions? options, + ILogger>? logger) + { + Logger = logger; + _cancellationTokenSource = new CancellationTokenSource(); + Options = options ?? new ClientOptions(); + _networkServices = new NetworkServices(this, logger); + } + + /// + /// Wont raise the given if .IsCancellationRequested /// - public abstract class ClientBase : IClient - where T : IDisposable + private async Task RaiseSendFailed(OnSendFailedEventArgs eventArgs) { - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private readonly NetworkServices _networkServices; - private CancellationTokenSource _cancellationTokenSource; - - /// - /// This is used for - /// whenever a call to is made - /// - internal CancellationToken Token => _cancellationTokenSource.Token; - - internal static TimeSpan TimeOutEstablishConnection => TimeSpan.FromSeconds(15); - - protected readonly ILogger>? Logger; - - protected abstract string Url { get; } - - /// - /// The underlying client. - /// - public T? Client { get; private set; } - - public abstract bool IsConnected { get; } - - public IClientOptions Options { get; } - - public event AsyncEventHandler? OnConnected; - public event AsyncEventHandler? OnDisconnected; - public event AsyncEventHandler? OnError; - public event AsyncEventHandler? OnFatality; - public event AsyncEventHandler? OnMessage; - public event AsyncEventHandler? OnSendFailed; - public event AsyncEventHandler? OnReconnected; - - internal ClientBase( - IClientOptions? options, - ILogger>? logger) + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) { - Logger = logger; - _cancellationTokenSource = new CancellationTokenSource(); - Options = options ?? new ClientOptions(); - _networkServices = new NetworkServices(this, logger); + return; } - /// - /// Wont raise the given if .IsCancellationRequested - /// - private async Task RaiseSendFailed(OnSendFailedEventArgs eventArgs) - { - Logger?.TraceMethodCall(GetType()); - if (Token.IsCancellationRequested) - { - return; - } + if (OnSendFailed != null) await OnSendFailed.Invoke(this, eventArgs); + } - if(OnSendFailed != null) await OnSendFailed.Invoke(this, eventArgs); + /// + /// Wont raise the given if .IsCancellationRequested + /// + internal async Task RaiseError(OnErrorEventArgs eventArgs) + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; } - /// - /// Wont raise the given if .IsCancellationRequested - /// - internal async Task RaiseError(OnErrorEventArgs eventArgs) - { - Logger?.TraceMethodCall(GetType()); - if (Token.IsCancellationRequested) - { - return; - } + if (OnError != null) await OnError.Invoke(this, eventArgs); + } - if (OnError != null) await OnError.Invoke(this, eventArgs); + /// + /// Wont raise the given if .IsCancellationRequested + /// + private async Task RaiseReconnected() + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; } - /// - /// Wont raise the given if .IsCancellationRequested - /// - private async Task RaiseReconnected() - { - Logger?.TraceMethodCall(GetType()); - if (Token.IsCancellationRequested) - { - return; - } + if (OnReconnected != null) await OnReconnected.Invoke(this, new OnConnectedEventArgs()); + } - if (OnReconnected != null) await OnReconnected.Invoke(this, new OnConnectedEventArgs()); + /// + /// Wont raise the given if .IsCancellationRequested + /// + internal async Task RaiseMessage(OnMessageEventArgs eventArgs) + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; } - /// - /// Wont raise the given if .IsCancellationRequested - /// - internal async Task RaiseMessage(OnMessageEventArgs eventArgs) - { - Logger?.TraceMethodCall(GetType()); - if (Token.IsCancellationRequested) - { - return; - } + if (OnMessage != null) await OnMessage.Invoke(this, eventArgs); + } - if (OnMessage != null) await OnMessage.Invoke(this, eventArgs); + /// + /// Wont raise the given if .IsCancellationRequested + /// + internal async Task RaiseFatal(Exception? ex = null) + { + Logger?.TraceMethodCall(GetType()); + if (Token.IsCancellationRequested) + { + return; } - /// - /// Wont raise the given if .IsCancellationRequested - /// - internal async Task RaiseFatal(Exception? ex = null) - { - Logger?.TraceMethodCall(GetType()); - if (Token.IsCancellationRequested) - { - return; - } + var onFatalErrorEventArgs = ex != null + ? new OnFatalErrorEventArgs(ex) + : new OnFatalErrorEventArgs("Fatal network error."); - var onFatalErrorEventArgs = ex != null - ? new OnFatalErrorEventArgs(ex) - : new OnFatalErrorEventArgs("Fatal network error."); + if (OnFatality != null) await OnFatality.Invoke(this, onFatalErrorEventArgs); + } - if (OnFatality != null) await OnFatality.Invoke(this, onFatalErrorEventArgs); - } + private async Task RaiseDisconnected() + { + Logger?.TraceMethodCall(GetType()); + if (OnDisconnected != null) await OnDisconnected.Invoke(this, new OnDisconnectedEventArgs()); + } - private async Task RaiseDisconnected() - { - Logger?.TraceMethodCall(GetType()); - if (OnDisconnected != null) await OnDisconnected.Invoke(this, new OnDisconnectedEventArgs()); - } + private async Task RaiseConnected() + { + Logger?.TraceMethodCall(GetType()); + if (OnConnected != null) await OnConnected.Invoke(this, new OnConnectedEventArgs()); + } + + /// + public async Task SendAsync(string message) + { + Logger?.TraceMethodCall(GetType()); - private async Task RaiseConnected() + await _semaphore.WaitAsync(Token); + try { - Logger?.TraceMethodCall(GetType()); - if (OnConnected != null) await OnConnected.Invoke(this, new OnConnectedEventArgs()); + await ClientSendAsync(message); + return true; } - - public async Task SendAsync(string message) + catch (Exception e) { - Logger?.TraceMethodCall(GetType()); - - await _semaphore.WaitAsync(Token); - try - { - await ClientSendAsync(message); - return true; - } - catch (Exception e) - { - await RaiseSendFailed(new OnSendFailedEventArgs(e, message)); - return false; - } - finally - { - _semaphore.Release(); - } + await RaiseSendFailed(new OnSendFailedEventArgs(e, message)); + return false; } - - public Task OpenAsync() + finally { - Logger?.TraceMethodCall(GetType()); - return OpenPrivateAsync(false); + _semaphore.Release(); } + } - public async Task CloseAsync() - { - Logger?.TraceMethodCall(GetType()); + /// + public Task OpenAsync() + { + Logger?.TraceMethodCall(GetType()); + return OpenPrivateAsync(false); + } - // ClosePrivate() also handles IClientOptions.DisconnectWait - await ClosePrivateAsync(); - } + /// + public async Task CloseAsync() + { + Logger?.TraceMethodCall(GetType()); - /// - /// - /// - public void Dispose() - { - Logger?.TraceMethodCall(GetType()); - CloseAsync().GetAwaiter().GetResult(); - GC.SuppressFinalize(this); - } + // ClosePrivate() also handles IClientOptions.DisconnectWait + await ClosePrivateAsync(); + } - public async Task ReconnectAsync() - { - Logger?.TraceMethodCall(GetType()); + /// + /// + /// + public void Dispose() + { + Logger?.TraceMethodCall(GetType()); + CloseAsync().GetAwaiter().GetResult(); + GC.SuppressFinalize(this); + } - return await ReconnectInternalAsync(); - } + /// + public async Task ReconnectAsync() + { + Logger?.TraceMethodCall(GetType()); - private async Task OpenPrivateAsync(bool isReconnect) + return await ReconnectInternalAsync(); + } + + private async Task OpenPrivateAsync(bool isReconnect) + { + Logger?.TraceMethodCall(GetType()); + try { - Logger?.TraceMethodCall(GetType()); - try + if (Token.IsCancellationRequested) { - if (Token.IsCancellationRequested) - { - return false; - } + return false; + } - if (IsConnected) - { - return true; - } + if (IsConnected) + { + return true; + } - // Always create new client when opening new connection - Client = CreateClient(); - - var first = true; - Options.ReconnectionPolicy.Reset(isReconnect); - - while (!IsConnected && - !Options.ReconnectionPolicy.AreAttemptsComplete()) - { - Logger?.TraceAction(GetType(), "try to connect"); - if (!first) - { - await Task.Delay(Options.ReconnectionPolicy.GetReconnectInterval(), CancellationToken.None); - } - - await ConnectClientAsync(); - Options.ReconnectionPolicy.ProcessValues(); - first = false; - } + // Always create new client when opening new connection + Client = CreateClient(); - if (!IsConnected) - { - Logger?.TraceAction(GetType(), "Client couldn't establish a connection"); - await RaiseFatal(); - return false; - } + var first = true; + Options.ReconnectionPolicy.Reset(isReconnect); - Logger?.TraceAction(GetType(), "Client established a connection"); - _networkServices.Start(); - - if (!isReconnect) + while (!IsConnected && + !Options.ReconnectionPolicy.AreAttemptsComplete()) + { + Logger?.TraceAction(GetType(), "try to connect"); + if (!first) { - await RaiseConnected(); + await Task.Delay(Options.ReconnectionPolicy.GetReconnectInterval(), CancellationToken.None); } - return true; + await ConnectClientAsync(); + Options.ReconnectionPolicy.ProcessValues(); + first = false; } - catch (Exception ex) + + if (!IsConnected) { - Logger?.LogExceptionAsError(GetType(), ex); - await RaiseError(new OnErrorEventArgs(ex)); + Logger?.TraceAction(GetType(), "Client couldn't establish a connection"); await RaiseFatal(); return false; } - } - /// - /// Stops - /// by calling - ///

- /// and enforces the - ///

- /// afterwards it waits for the via given amount of milliseconds - ///

- ///

- /// will keep running, - /// because itself issued this call by calling - ///
- private async Task ClosePrivateAsync() - { - Logger?.TraceMethodCall(GetType()); - - await _networkServices.StopAsync(); - - // This cancellation traverse up to NetworkServices.ListenTask - _cancellationTokenSource.Cancel(); - Logger?.TraceAction(GetType(), - $"{nameof(_cancellationTokenSource)}.{nameof(_cancellationTokenSource.Cancel)} is called"); - - CloseClient(); - await RaiseDisconnected(); - _cancellationTokenSource = new CancellationTokenSource(); - - await Task.Delay(TimeSpan.FromMilliseconds(Options.DisconnectWait), CancellationToken.None); - } + Logger?.TraceAction(GetType(), "Client established a connection"); + _networkServices.Start(); - /// - /// Send method for the client. - /// - /// - /// Message to be send - /// - protected abstract Task ClientSendAsync(string message); - - /// - /// Instantiate the underlying client. - /// - protected abstract T CreateClient(); - - /// - /// one of the following specific methods - /// - /// - /// - /// - /// - /// - /// - /// - /// calls to one of the methods mentioned above, - /// also Dispose() the respective client, - /// so no additional Dispose() is needed - /// - protected abstract void CloseClient(); - - /// - /// Connect the client. - /// - protected abstract Task ConnectClientAsync(); - - /// - /// To issue a reconnect - ///

- /// especially for the - ///

- /// it stops all but ! - ///

- ///

- /// see also : - ///

- /// - ///
- /// - /// if a connection could be established, otherwise - /// - internal async Task ReconnectInternalAsync() - { - Logger?.TraceMethodCall(GetType()); - - await ClosePrivateAsync(); - var reconnected = await OpenPrivateAsync(true); - if (reconnected) + if (!isReconnect) { - await RaiseReconnected(); + await RaiseConnected(); } - return reconnected; + return true; } + catch (Exception ex) + { + Logger?.LogExceptionAsError(GetType(), ex); + await RaiseError(new OnErrorEventArgs(ex)); + await RaiseFatal(); + return false; + } + } + + /// + /// Stops + /// by calling + ///

+ /// and enforces the + ///

+ /// afterwards it waits for the via given amount of milliseconds + ///

+ ///

+ /// will keep running, + /// because itself issued this call by calling + ///
+ private async Task ClosePrivateAsync() + { + Logger?.TraceMethodCall(GetType()); + + await _networkServices.StopAsync(); + + // This cancellation traverse up to NetworkServices.ListenTask + _cancellationTokenSource.Cancel(); + Logger?.TraceAction(GetType(), + $"{nameof(_cancellationTokenSource)}.{nameof(_cancellationTokenSource.Cancel)} is called"); - /// - /// just the Action that listens for new Messages - /// the corresponding is held by - /// - internal abstract Task ListenTaskActionAsync(); + CloseClient(); + await RaiseDisconnected(); + _cancellationTokenSource = new CancellationTokenSource(); + + await Task.Delay(TimeSpan.FromMilliseconds(Options.DisconnectWait), CancellationToken.None); } -} \ No newline at end of file + + /// + /// Send method for the client. + /// + /// + /// Message to be send + /// + protected abstract Task ClientSendAsync(string message); + + /// + /// Instantiate the underlying client. + /// + protected abstract T CreateClient(); + + /// + /// one of the following specific methods + /// + /// + /// + /// + /// + /// + /// + /// + /// calls to one of the methods mentioned above, + /// also Dispose() the respective client, + /// so no additional Dispose() is needed + /// + protected abstract void CloseClient(); + + /// + /// Connect the client. + /// + protected abstract Task ConnectClientAsync(); + + /// + /// To issue a reconnect + ///

+ /// especially for the + ///

+ /// it stops all but ! + ///

+ ///

+ /// see also : + ///

+ /// + ///
+ /// + /// if a connection could be established, otherwise + /// + internal async Task ReconnectInternalAsync() + { + Logger?.TraceMethodCall(GetType()); + + await ClosePrivateAsync(); + var reconnected = await OpenPrivateAsync(true); + if (reconnected) + { + await RaiseReconnected(); + } + + return reconnected; + } + + /// + /// just the Action that listens for new Messages + /// the corresponding is held by + /// + internal abstract Task ListenTaskActionAsync(); +} diff --git a/src/TwitchLib.Communication/Clients/TcpClient.cs b/src/TwitchLib.Communication/Clients/TcpClient.cs index 6340ef1..7caee84 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -1,104 +1,104 @@ -using System; -using System.IO; -using System.Net.Security; -using System.Threading; -using System.Threading.Tasks; +using System.Net.Security; using Microsoft.Extensions.Logging; using TwitchLib.Communication.Events; using TwitchLib.Communication.Extensions; using TwitchLib.Communication.Interfaces; -namespace TwitchLib.Communication.Clients +namespace TwitchLib.Communication.Clients; + +public class TcpClient : ClientBase { - public class TcpClient : ClientBase - { - private StreamReader? _reader; - private StreamWriter? _writer; + private StreamReader? _reader; + private StreamWriter? _writer; + + /// + protected override string Url => "irc.chat.twitch.tv"; - protected override string Url => "irc.chat.twitch.tv"; + private int Port => Options.UseSsl ? 6697 : 6667; - private int Port => Options.UseSsl ? 6697 : 6667; + /// + public override bool IsConnected => Client?.Connected ?? false; - public override bool IsConnected => Client?.Connected ?? false; + public TcpClient( + IClientOptions? options = null, + ILogger? logger = null) + : base(options, logger) + { + } - public TcpClient( - IClientOptions? options = null, - ILogger? logger = null) - : base(options, logger) + internal override async Task ListenTaskActionAsync() + { + Logger?.TraceMethodCall(GetType()); + if (_reader == null) { + var ex = new InvalidOperationException($"{nameof(_reader)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + await RaiseFatal(ex); + throw ex; } - internal override async Task ListenTaskActionAsync() + while (IsConnected) { - Logger?.TraceMethodCall(GetType()); - if (_reader == null) - { - var ex = new InvalidOperationException($"{nameof(_reader)} was null!"); - Logger?.LogExceptionAsError(GetType(), ex); - await RaiseFatal(ex); - throw ex; - } - - while (IsConnected) + try { - try - { - var input = await _reader.ReadLineAsync(); - if (input is null) - { - continue; - } - - await RaiseMessage(new OnMessageEventArgs(input)); - } - catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || - ex.GetType() == typeof(OperationCanceledException)) - { - // occurs if the Tasks are canceled by the CancellationTokenSource.Token - Logger?.LogExceptionAsInformation(GetType(), ex); - } - catch (Exception ex) + var input = await _reader.ReadLineAsync(); + if (input is null) { - Logger?.LogExceptionAsError(GetType(), ex); - await RaiseError(new OnErrorEventArgs(ex)); - break; + continue; } - } - } - - protected override async Task ClientSendAsync(string message) - { - Logger?.TraceMethodCall(GetType()); - // this is not thread safe - // this method should only be called from 'ClientBase.Send()' - // where its call gets synchronized/locked - // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream?view=netstandard-2.0#remarks - if (_writer == null) + await RaiseMessage(new OnMessageEventArgs(input)); + } + catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || + ex.GetType() == typeof(OperationCanceledException)) + { + // occurs if the Tasks are canceled by the CancellationTokenSource.Token + Logger?.LogExceptionAsInformation(GetType(), ex); + } + catch (Exception ex) { - var ex = new InvalidOperationException($"{nameof(_writer)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); - await RaiseFatal(ex); - throw ex; + await RaiseError(new OnErrorEventArgs(ex)); + break; } + } + } - await _writer.WriteLineAsync(message); - await _writer.FlushAsync(); + /// + protected override async Task ClientSendAsync(string message) + { + Logger?.TraceMethodCall(GetType()); + + // this is not thread safe + // this method should only be called from 'ClientBase.Send()' + // where its call gets synchronized/locked + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream?view=netstandard-2.0#remarks + if (_writer == null) + { + var ex = new InvalidOperationException($"{nameof(_writer)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + await RaiseFatal(ex); + throw ex; } - protected override async Task ConnectClientAsync() + await _writer.WriteLineAsync(message); + await _writer.FlushAsync(); + } + + /// + protected override async Task ConnectClientAsync() + { + Logger?.TraceMethodCall(GetType()); + if (Client == null) { - Logger?.TraceMethodCall(GetType()); - if (Client == null) - { - Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); - Logger?.LogExceptionAsError(GetType(), ex); - throw ex; - } + Exception ex = new InvalidOperationException($"{nameof(Client)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + throw ex; + } - try - { - // https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios + try + { + // https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios #if NET6_0_OR_GREATER // within the following thread: // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout @@ -126,52 +126,53 @@ protected override async Task ConnectClientAsync() delayTaskCancellationTokenSource.Cancel(); } #endif - if (!Client.Connected) - { - Logger?.TraceAction(GetType(), "Client couldn't establish connection"); - return; - } - - Logger?.TraceAction(GetType(), "Client established connection successfully"); - Stream stream = Client.GetStream(); - if (Options.UseSsl) - { - var ssl = new SslStream(stream, false); - await ssl.AuthenticateAsClientAsync(Url); - stream = ssl; - } - _reader = new StreamReader(stream); - _writer = new StreamWriter(stream); - } - catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || - ex.GetType() == typeof(OperationCanceledException)) + if (!Client.Connected) { - // occurs if the Tasks are canceled by the CancellationTokenSource.Token - Logger?.LogExceptionAsInformation(GetType(), ex); + Logger?.TraceAction(GetType(), "Client couldn't establish connection"); + return; } - catch (Exception ex) + + Logger?.TraceAction(GetType(), "Client established connection successfully"); + Stream stream = Client.GetStream(); + if (Options.UseSsl) { - Logger?.LogExceptionAsError(GetType(), ex); + var ssl = new SslStream(stream, false); + await ssl.AuthenticateAsClientAsync(Url); + stream = ssl; } + _reader = new StreamReader(stream); + _writer = new StreamWriter(stream); } - - protected override System.Net.Sockets.TcpClient CreateClient() + catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || + ex.GetType() == typeof(OperationCanceledException)) { - Logger?.TraceMethodCall(GetType()); - - return new System.Net.Sockets.TcpClient - { - // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient.lingerstate?view=netstandard-2.0#remarks - LingerState = new System.Net.Sockets.LingerOption(true, 0) - }; + // occurs if the Tasks are canceled by the CancellationTokenSource.Token + Logger?.LogExceptionAsInformation(GetType(), ex); } - - protected override void CloseClient() + catch (Exception ex) { - Logger?.TraceMethodCall(GetType()); - _reader?.Dispose(); - _writer?.Dispose(); - Client?.Dispose(); + Logger?.LogExceptionAsError(GetType(), ex); } } + + /// + protected override System.Net.Sockets.TcpClient CreateClient() + { + Logger?.TraceMethodCall(GetType()); + + return new System.Net.Sockets.TcpClient + { + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient.lingerstate?view=netstandard-2.0#remarks + LingerState = new System.Net.Sockets.LingerOption(true, 0) + }; + } + + /// + protected override void CloseClient() + { + Logger?.TraceMethodCall(GetType()); + _reader?.Dispose(); + _writer?.Dispose(); + Client?.Dispose(); + } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index 6902736..125e971 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -1,155 +1,158 @@ -using System; -using System.IO; -using System.Net.WebSockets; +using System.Net.WebSockets; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using TwitchLib.Communication.Enums; using TwitchLib.Communication.Events; using TwitchLib.Communication.Extensions; using TwitchLib.Communication.Interfaces; -namespace TwitchLib.Communication.Clients +namespace TwitchLib.Communication.Clients; + +public class WebSocketClient : ClientBase { - public class WebSocketClient : ClientBase - { - protected override string Url { get; } + protected override string Url { get; } - public override bool IsConnected => Client?.State == WebSocketState.Open; + public override bool IsConnected => Client?.State == WebSocketState.Open; - public WebSocketClient( - IClientOptions? options = null, - ILogger? logger = null) - : base(options, logger) + public WebSocketClient( + IClientOptions? options = null, + ILogger? logger = null) + : base(options, logger) + { + switch (Options.ClientType) { - switch (Options.ClientType) - { - case ClientType.Chat: - Url = Options.UseSsl ? "wss://irc-ws.chat.twitch.tv:443" : "ws://irc-ws.chat.twitch.tv:80"; - break; - case ClientType.PubSub: - Url = Options.UseSsl ? "wss://pubsub-edge.twitch.tv:443" : "ws://pubsub-edge.twitch.tv:80"; - break; - default: - var ex = new ArgumentOutOfRangeException(nameof(Options.ClientType)); - Logger?.LogExceptionAsError(GetType(), ex); - throw ex; - } + case ClientType.Chat: + Url = Options.UseSsl ? "wss://irc-ws.chat.twitch.tv:443" : "ws://irc-ws.chat.twitch.tv:80"; + break; + case ClientType.PubSub: + Url = Options.UseSsl ? "wss://pubsub-edge.twitch.tv:443" : "ws://pubsub-edge.twitch.tv:80"; + break; + default: + var ex = new ArgumentOutOfRangeException(nameof(Options.ClientType)); + Logger?.LogExceptionAsError(GetType(), ex); + throw ex; + } + } + + internal override async Task ListenTaskActionAsync() + { + Logger?.TraceMethodCall(GetType()); + if (Client == null) + { + var ex = new InvalidOperationException($"{nameof(Client)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + await RaiseFatal(ex); + throw ex; } - internal override async Task ListenTaskActionAsync() + var memoryStream = new MemoryStream(); + var bytes = new byte[1024]; +#if NET + var buffer = new Memory(bytes); + ValueWebSocketReceiveResult result; +#else + var buffer = new ArraySegment(bytes); + WebSocketReceiveResult result; +#endif + while (IsConnected) { - Logger?.TraceMethodCall(GetType()); - if (Client == null) + try + { + result = await Client.ReceiveAsync(buffer, Token); + } + catch (TaskCanceledException) + { + // Swallow any cancellation exceptions + break; + } + catch (OperationCanceledException ex) + { + Logger?.LogExceptionAsInformation(GetType(), ex); + break; + } + catch (Exception ex) { - var ex = new InvalidOperationException($"{nameof(Client)} was null!"); Logger?.LogExceptionAsError(GetType(), ex); - await RaiseFatal(ex); - throw ex; + await RaiseError(new OnErrorEventArgs(ex)); + break; } - var memoryStream = new MemoryStream(); - var bytes = new byte[1024]; - var buffer = new ArraySegment(bytes); - WebSocketReceiveResult result; - while (IsConnected) + switch (result.MessageType) { - try - { - result = await Client.ReceiveAsync(buffer, Token); - } - catch (TaskCanceledException _) - { - // Swallow any cancellation exceptions + case WebSocketMessageType.Close: + await CloseAsync(); break; - } - catch (OperationCanceledException ex) - { - Logger?.LogExceptionAsInformation(GetType(), ex); + case WebSocketMessageType.Text: + if (result.EndOfMessage && memoryStream.Position == 0) + { + //optimization when we can read the whole message at once + var message = Encoding.UTF8.GetString(bytes, 0, result.Count); + await RaiseMessage(new OnMessageEventArgs(message)); + break; + } + memoryStream.Write(bytes, 0, result.Count); + if (result.EndOfMessage) + { + var message = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Position); + await RaiseMessage(new OnMessageEventArgs(message)); + memoryStream.Position = 0; + } break; - } - catch (Exception ex) - { - Logger?.LogExceptionAsError(GetType(), ex); - await RaiseError(new OnErrorEventArgs(ex)); + case WebSocketMessageType.Binary: + //todo break; - } - - switch (result.MessageType) - { - case WebSocketMessageType.Close: - await CloseAsync(); - break; - case WebSocketMessageType.Text: - if (result.EndOfMessage && memoryStream.Position == 0) - { - //optimization when we can read the whole message at once - var message = Encoding.UTF8.GetString(bytes, 0, result.Count); - await RaiseMessage(new OnMessageEventArgs(message)); - break; - } - memoryStream.Write(bytes, 0, result.Count); - if (result.EndOfMessage) - { - var message = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Position); - await RaiseMessage(new OnMessageEventArgs(message)); - memoryStream.Position = 0; - } - break; - case WebSocketMessageType.Binary: - //todo - break; - default: - Exception ex = new ArgumentOutOfRangeException(); - Logger?.LogExceptionAsError(GetType(), ex); - throw ex; - } + default: + Exception ex = new ArgumentOutOfRangeException(); + Logger?.LogExceptionAsError(GetType(), ex); + throw ex; } } + } - protected override async Task ClientSendAsync(string message) - { - Logger?.TraceMethodCall(GetType()); - - // this is not thread safe - // this method should only be called from 'ClientBase.Send()' - // where its call gets synchronized/locked - // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream?view=netstandard-2.0#remarks - - // https://stackoverflow.com/a/59619916 - // links from within this thread: - // the 4th point: https://www.codetinkerer.com/2018/06/05/aspnet-core-websockets.html - // https://github.com/dotnet/corefx/blob/d6b11250b5113664dd3701c25bdf9addfacae9cc/src/Common/src/System/Net/WebSockets/ManagedWebSocket.cs#L22-L28 - if (Client == null) - { - var ex = new InvalidOperationException($"{nameof(Client)} was null!"); - Logger?.LogExceptionAsError(GetType(), ex); - await RaiseFatal(ex); - throw ex; - } + /// + protected override async Task ClientSendAsync(string message) + { + Logger?.TraceMethodCall(GetType()); + + // this is not thread safe + // this method should only be called from 'ClientBase.Send()' + // where its call gets synchronized/locked + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.networkstream?view=netstandard-2.0#remarks - var bytes = Encoding.UTF8.GetBytes(message); - await Client.SendAsync(new ArraySegment(bytes), - WebSocketMessageType.Text, - true, - Token); + // https://stackoverflow.com/a/59619916 + // links from within this thread: + // the 4th point: https://www.codetinkerer.com/2018/06/05/aspnet-core-websockets.html + // https://github.com/dotnet/corefx/blob/d6b11250b5113664dd3701c25bdf9addfacae9cc/src/Common/src/System/Net/WebSockets/ManagedWebSocket.cs#L22-L28 + if (Client == null) + { + var ex = new InvalidOperationException($"{nameof(Client)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + await RaiseFatal(ex); + throw ex; } - protected override async Task ConnectClientAsync() + var bytes = Encoding.UTF8.GetBytes(message); + await Client.SendAsync(new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + Token); + } + + /// + protected override async Task ConnectClientAsync() + { + Logger?.TraceMethodCall(GetType()); + if (Client == null) { - Logger?.TraceMethodCall(GetType()); - if (Client == null) - { - var ex = new InvalidOperationException($"{nameof(Client)} was null!"); - Logger?.LogExceptionAsError(GetType(), ex); - await RaiseFatal(ex); - throw ex; - } + var ex = new InvalidOperationException($"{nameof(Client)} was null!"); + Logger?.LogExceptionAsError(GetType(), ex); + await RaiseFatal(ex); + throw ex; + } - try - { - // https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios + try + { + // https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios #if NET6_0_OR_GREATER // within the following thread: // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout @@ -176,34 +179,35 @@ protected override async Task ConnectClientAsync() delayTaskCancellationTokenSource.Cancel(); } #endif - if (!IsConnected) - { - Logger?.TraceAction(GetType(), "Client couldn't establish connection"); - } - } - catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || - ex.GetType() == typeof(OperationCanceledException)) + if (!IsConnected) { - // occurs if the Tasks are canceled by the CancellationTokenSource.Token - Logger?.LogExceptionAsInformation(GetType(), ex); - } - catch (Exception ex) - { - Logger?.LogExceptionAsError(GetType(), ex); + Logger?.TraceAction(GetType(), "Client couldn't establish connection"); } } - - protected override ClientWebSocket CreateClient() + catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || + ex.GetType() == typeof(OperationCanceledException)) { - Logger?.TraceMethodCall(GetType()); - return new ClientWebSocket(); + // occurs if the Tasks are canceled by the CancellationTokenSource.Token + Logger?.LogExceptionAsInformation(GetType(), ex); } - - protected override void CloseClient() + catch (Exception ex) { - Logger?.TraceMethodCall(GetType()); - Client?.Abort(); - Client?.Dispose(); + Logger?.LogExceptionAsError(GetType(), ex); } } + + /// + protected override ClientWebSocket CreateClient() + { + Logger?.TraceMethodCall(GetType()); + return new ClientWebSocket(); + } + + /// + protected override void CloseClient() + { + Logger?.TraceMethodCall(GetType()); + Client?.Abort(); + Client?.Dispose(); + } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Enums/ClientType.cs b/src/TwitchLib.Communication/Enums/ClientType.cs index dee4b24..7e8bea7 100644 --- a/src/TwitchLib.Communication/Enums/ClientType.cs +++ b/src/TwitchLib.Communication/Enums/ClientType.cs @@ -1,8 +1,7 @@ -namespace TwitchLib.Communication.Enums +namespace TwitchLib.Communication.Enums; + +public enum ClientType { - public enum ClientType - { - Chat, - PubSub - } -} \ No newline at end of file + Chat, + PubSub, +} diff --git a/src/TwitchLib.Communication/Events/CoreEvents.cs b/src/TwitchLib.Communication/Events/CoreEvents.cs index 1a356b1..3a31924 100644 --- a/src/TwitchLib.Communication/Events/CoreEvents.cs +++ b/src/TwitchLib.Communication/Events/CoreEvents.cs @@ -1,12 +1,8 @@ -using System.Threading.Tasks; +namespace TwitchLib.Communication.Events; -namespace TwitchLib.Communication.Events -{ - /* - * Custom implementation of asynchronous event handler - * This is useful to properly and safely handle async Tasks - * Reference: https://medium.com/@a.lyskawa/the-hitchhiker-guide-to-asynchronous-events-in-c-e9840109fb53 - */ - public delegate Task AsyncEventHandler(object? sender, TEventArgs e); - -} +/* +* Custom implementation of asynchronous event handler +* This is useful to properly and safely handle async Tasks +* Reference: https://medium.com/@a.lyskawa/the-hitchhiker-guide-to-asynchronous-events-in-c-e9840109fb53 +*/ +public delegate Task AsyncEventHandler(object? sender, TEventArgs e); diff --git a/src/TwitchLib.Communication/Events/OnConnectedEventArgs.cs b/src/TwitchLib.Communication/Events/OnConnectedEventArgs.cs index 7d96d52..50ddb21 100644 --- a/src/TwitchLib.Communication/Events/OnConnectedEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnConnectedEventArgs.cs @@ -1,6 +1,3 @@ -using System; +namespace TwitchLib.Communication.Events; -namespace TwitchLib.Communication.Events -{ - public class OnConnectedEventArgs : EventArgs { } -} +public class OnConnectedEventArgs : EventArgs { } diff --git a/src/TwitchLib.Communication/Events/OnDisconnectedEventArgs.cs b/src/TwitchLib.Communication/Events/OnDisconnectedEventArgs.cs index da3b830..bec279e 100644 --- a/src/TwitchLib.Communication/Events/OnDisconnectedEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnDisconnectedEventArgs.cs @@ -1,6 +1,3 @@ -using System; +namespace TwitchLib.Communication.Events; -namespace TwitchLib.Communication.Events -{ - public class OnDisconnectedEventArgs : EventArgs { } -} +public class OnDisconnectedEventArgs : EventArgs { } diff --git a/src/TwitchLib.Communication/Events/OnErrorEventArgs.cs b/src/TwitchLib.Communication/Events/OnErrorEventArgs.cs index 07752f0..49a3217 100644 --- a/src/TwitchLib.Communication/Events/OnErrorEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnErrorEventArgs.cs @@ -1,14 +1,11 @@ -using System; +namespace TwitchLib.Communication.Events; -namespace TwitchLib.Communication.Events +public class OnErrorEventArgs : EventArgs { - public class OnErrorEventArgs : EventArgs - { - public Exception Exception { get; } + public Exception Exception { get; } - public OnErrorEventArgs(Exception exception) - { - Exception = exception; - } + public OnErrorEventArgs(Exception exception) + { + Exception = exception; } } diff --git a/src/TwitchLib.Communication/Events/OnFatalErrorEventArgs.cs b/src/TwitchLib.Communication/Events/OnFatalErrorEventArgs.cs index 4f51fd8..47cf586 100644 --- a/src/TwitchLib.Communication/Events/OnFatalErrorEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnFatalErrorEventArgs.cs @@ -1,19 +1,16 @@ -using System; +namespace TwitchLib.Communication.Events; -namespace TwitchLib.Communication.Events +public class OnFatalErrorEventArgs : EventArgs { - public class OnFatalErrorEventArgs : EventArgs - { - public string Reason { get; } + public string Reason { get; } - public OnFatalErrorEventArgs(string reason) - { - Reason = reason; - } + public OnFatalErrorEventArgs(string reason) + { + Reason = reason; + } - public OnFatalErrorEventArgs(Exception e) - { - Reason = e.ToString(); - } + public OnFatalErrorEventArgs(Exception e) + { + Reason = e.ToString(); } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Events/OnMessageEventArgs.cs b/src/TwitchLib.Communication/Events/OnMessageEventArgs.cs index 9fe7282..934b896 100644 --- a/src/TwitchLib.Communication/Events/OnMessageEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnMessageEventArgs.cs @@ -1,14 +1,11 @@ -using System; +namespace TwitchLib.Communication.Events; -namespace TwitchLib.Communication.Events +public class OnMessageEventArgs : EventArgs { - public class OnMessageEventArgs : EventArgs - { - public string Message { get; } + public string Message { get; } - public OnMessageEventArgs(string message) - { - Message = message; - } + public OnMessageEventArgs(string message) + { + Message = message; } } diff --git a/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs b/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs index e1c1db1..51f41d2 100644 --- a/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs @@ -1,16 +1,14 @@ -using System; +namespace TwitchLib.Communication.Events; -namespace TwitchLib.Communication.Events +public class OnSendFailedEventArgs : EventArgs { - public class OnSendFailedEventArgs : EventArgs - { - public string Data { get; } - public Exception Exception { get; } + public string Data { get; } + + public Exception Exception { get; } - public OnSendFailedEventArgs(Exception exception, string data) - { - Exception = exception; - Data = data; - } + public OnSendFailedEventArgs(Exception exception, string data) + { + Exception = exception; + Data = data; } } diff --git a/src/TwitchLib.Communication/Extensions/LogExtensions.cs b/src/TwitchLib.Communication/Extensions/LogExtensions.cs index 04409ec..8be0a16 100644 --- a/src/TwitchLib.Communication/Extensions/LogExtensions.cs +++ b/src/TwitchLib.Communication/Extensions/LogExtensions.cs @@ -1,7 +1,6 @@ #pragma warning disable SYSLIB1006 // Multiple logging methods cannot use the same event id within a class -using Microsoft.Extensions.Logging; -using System; using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; namespace TwitchLib.Communication.Extensions { diff --git a/src/TwitchLib.Communication/Interfaces/IClient.cs b/src/TwitchLib.Communication/Interfaces/IClient.cs index b5578b3..1edd023 100644 --- a/src/TwitchLib.Communication/Interfaces/IClient.cs +++ b/src/TwitchLib.Communication/Interfaces/IClient.cs @@ -1,106 +1,103 @@ -using System; -using System.Threading.Tasks; -using TwitchLib.Communication.Events; +using TwitchLib.Communication.Events; -namespace TwitchLib.Communication.Interfaces +namespace TwitchLib.Communication.Interfaces; + +public interface IClient : IDisposable { - public interface IClient : IDisposable - { - /// - /// The current state of the connection. - /// - bool IsConnected { get; } + /// + /// The current state of the connection. + /// + bool IsConnected { get; } - /// - /// Client Configuration Options - /// - IClientOptions Options { get; } + /// + /// Client Configuration Options + /// + IClientOptions Options { get; } - /// - /// Fires when the Client has connected - /// - event AsyncEventHandler? OnConnected; + /// + /// Fires when the Client has connected + /// + event AsyncEventHandler? OnConnected; - /// - /// Fires when the Client disconnects - /// - event AsyncEventHandler? OnDisconnected; + /// + /// Fires when the Client disconnects + /// + event AsyncEventHandler? OnDisconnected; - /// - /// Fires when An Exception Occurs in the client - /// - event AsyncEventHandler? OnError; + /// + /// Fires when An Exception Occurs in the client + /// + event AsyncEventHandler? OnError; - /// - /// Fires when a Fatal Error Occurs. - /// - event AsyncEventHandler? OnFatality; + /// + /// Fires when a Fatal Error Occurs. + /// + event AsyncEventHandler? OnFatality; - /// - /// Fires when a Message/ group of messages is received. - /// - event AsyncEventHandler? OnMessage; + /// + /// Fires when a Message/ group of messages is received. + /// + event AsyncEventHandler? OnMessage; - /// - /// Fires when a message Send event failed. - /// - event AsyncEventHandler? OnSendFailed; + /// + /// Fires when a message Send event failed. + /// + event AsyncEventHandler? OnSendFailed; - /// - /// Fires when the client reconnects automatically - /// - event AsyncEventHandler? OnReconnected; + /// + /// Fires when the client reconnects automatically + /// + event AsyncEventHandler? OnReconnected; - /// - /// tries to connect to twitch according to ! - /// - /// - /// if a connection could be established, otherwise - /// - Task OpenAsync(); + /// + /// tries to connect to twitch according to ! + /// + /// + /// if a connection could be established, otherwise + /// + Task OpenAsync(); - /// - /// if the underlying Client is connected, - ///

- /// is invoked - ///

- /// before it makes a call to and - ///

- ///

- /// this Method is also used by 'TwitchLib.Client.TwitchClient' - ///

- /// whenever it receives a Reconnect-Message - ///

- ///

- /// so, if the twitch-servers want us to reconnect, - ///

- /// we have to close the connection and establish a new ones - ///

- ///

- /// can also be used for a manual reconnect - ///
- /// - /// , if the client reconnected; otherwise - /// - Task ReconnectAsync(); + /// + /// if the underlying Client is connected, + ///

+ /// is invoked + ///

+ /// before it makes a call to and + ///

+ ///

+ /// this Method is also used by 'TwitchLib.Client.TwitchClient' + ///

+ /// whenever it receives a Reconnect-Message + ///

+ ///

+ /// so, if the twitch-servers want us to reconnect, + ///

+ /// we have to close the connection and establish a new ones + ///

+ ///

+ /// can also be used for a manual reconnect + ///
+ /// + /// , if the client reconnected; otherwise + /// + Task ReconnectAsync(); - /// - /// stops everything - /// and waits for the via given amount of milliseconds - /// - Task CloseAsync(); + /// + /// stops everything + /// and waits for the via given amount of milliseconds + /// + Task CloseAsync(); - /// - /// Sends the given irc- - /// - /// - /// irc-message to send - /// - /// - /// , if the message was sent - ///

- /// otherwise - ///
- Task SendAsync(string message); - } -} \ No newline at end of file + /// + /// Sends the given irc- + /// + /// + /// irc-message to send + /// + /// + /// , if the message was sent + ///

+ /// otherwise + ///
+ Task SendAsync(string message); +} diff --git a/src/TwitchLib.Communication/Interfaces/IClientOptions.cs b/src/TwitchLib.Communication/Interfaces/IClientOptions.cs index fc8a049..e845484 100644 --- a/src/TwitchLib.Communication/Interfaces/IClientOptions.cs +++ b/src/TwitchLib.Communication/Interfaces/IClientOptions.cs @@ -1,29 +1,28 @@ using TwitchLib.Communication.Enums; using TwitchLib.Communication.Models; -namespace TwitchLib.Communication.Interfaces +namespace TwitchLib.Communication.Interfaces; + +public interface IClientOptions { - public interface IClientOptions - { - /// - /// Type of the Client to Create. Possible Types Chat or PubSub. - /// - ClientType ClientType { get; } + /// + /// Type of the Client to Create. Possible Types Chat or PubSub. + /// + ClientType ClientType { get; } - /// - /// How long to wait on a clean disconnect [in ms] (default 1_500ms). - /// - uint DisconnectWait { get; } + /// + /// How long to wait on a clean disconnect [in ms] (default 1_500ms). + /// + uint DisconnectWait { get; } - /// - /// Reconnection Policy Settings. Reconnect without Losing data etc. - /// The Default Policy applied is 10 reconnection attempts with 3 seconds between each attempt. - /// - ReconnectionPolicy ReconnectionPolicy { get; } + /// + /// Reconnection Policy Settings. Reconnect without Losing data etc. + /// The Default Policy applied is 10 reconnection attempts with 3 seconds between each attempt. + /// + ReconnectionPolicy ReconnectionPolicy { get; } - /// - /// Use Secure Connection [SSL] (default: true) - /// - bool UseSsl { get; } - } -} \ No newline at end of file + /// + /// Use Secure Connection [SSL] (default: true) + /// + bool UseSsl { get; } +} diff --git a/src/TwitchLib.Communication/Models/ClientOptions.cs b/src/TwitchLib.Communication/Models/ClientOptions.cs index c97dd89..f7cd21d 100644 --- a/src/TwitchLib.Communication/Models/ClientOptions.cs +++ b/src/TwitchLib.Communication/Models/ClientOptions.cs @@ -1,41 +1,40 @@ using TwitchLib.Communication.Enums; using TwitchLib.Communication.Interfaces; -namespace TwitchLib.Communication.Models +namespace TwitchLib.Communication.Models; + +public class ClientOptions : IClientOptions { - public class ClientOptions : IClientOptions - { - public ReconnectionPolicy ReconnectionPolicy { get; } - public bool UseSsl { get; } - public uint DisconnectWait { get; } - public ClientType ClientType { get; } + public ReconnectionPolicy ReconnectionPolicy { get; } + public bool UseSsl { get; } + public uint DisconnectWait { get; } + public ClientType ClientType { get; } - /// - /// - /// - /// your own - ///

- /// by leaving it , a , that makes every 3_000ms one attempt to connect for ten times, is going to be applied - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public ClientOptions( - ReconnectionPolicy? reconnectionPolicy = null, - bool useSsl = true, - uint disconnectWait = 1_500, - ClientType clientType = ClientType.Chat) - { - ReconnectionPolicy = reconnectionPolicy ?? new ReconnectionPolicy(3_000, maxAttempts: 10); - UseSsl = useSsl; - DisconnectWait = disconnectWait; - ClientType = clientType; - } + /// + /// + /// + /// your own + ///

+ /// by leaving it , a , that makes every 3_000ms one attempt to connect for ten times, is going to be applied + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public ClientOptions( + ReconnectionPolicy? reconnectionPolicy = null, + bool useSsl = true, + uint disconnectWait = 1_500, + ClientType clientType = ClientType.Chat) + { + ReconnectionPolicy = reconnectionPolicy ?? new ReconnectionPolicy(3_000, maxAttempts: 10); + UseSsl = useSsl; + DisconnectWait = disconnectWait; + ClientType = clientType; } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Models/NoReconnectionPolicy.cs b/src/TwitchLib.Communication/Models/NoReconnectionPolicy.cs index 63e3bfd..70b4df1 100644 --- a/src/TwitchLib.Communication/Models/NoReconnectionPolicy.cs +++ b/src/TwitchLib.Communication/Models/NoReconnectionPolicy.cs @@ -1,15 +1,14 @@ -namespace TwitchLib.Communication.Models +namespace TwitchLib.Communication.Models; + +/// +/// This policy should be used to omit reconnect-attempts. +/// +public class NoReconnectionPolicy : ReconnectionPolicy { - /// - /// This policy should be used to omit reconnect-attempts. - /// - public class NoReconnectionPolicy : ReconnectionPolicy + public NoReconnectionPolicy() + : base( + reconnectInterval: 0, + maxAttempts: 1) { - public NoReconnectionPolicy() - : base( - reconnectInterval: 0, - maxAttempts: 1) - { - } } -} \ No newline at end of file +} diff --git a/src/TwitchLib.Communication/Models/ReconnectionPolicy.cs b/src/TwitchLib.Communication/Models/ReconnectionPolicy.cs index b81a5cd..3bb1a2d 100644 --- a/src/TwitchLib.Communication/Models/ReconnectionPolicy.cs +++ b/src/TwitchLib.Communication/Models/ReconnectionPolicy.cs @@ -1,198 +1,197 @@ -namespace TwitchLib.Communication.Models +namespace TwitchLib.Communication.Models; + +/// +/// Connection/Reconnection-Policy +///

+///

+/// controls the attempts to make to connect and to reconnect to twitch +///

+///

+/// to omit reconnects and to only make one attempt to connect to twitch, please use +///
+public class ReconnectionPolicy { + private readonly int _reconnectStepInterval; + private readonly int? _initMaxAttempts; + private int _currentReconnectInterval; + private readonly int _maxReconnectInterval; + private int? _maxAttempts; + private int _attemptsMade; + /// - /// Connection/Reconnection-Policy + /// the or + /// infinitely + /// attempts to reconnect + ///

+ ///

+ /// with each attempt, the reconnect interval increases by 3_000 milliseconds + /// until it reaches 30_000 milliseconds ///

+ /// ///

- /// controls the attempts to make to connect and to reconnect to twitch ///

+ /// Example: ///

- /// to omit reconnects and to only make one attempt to connect to twitch, please use + /// try to connect -> couldn't connect -> wait 3_000 milliseconds -> try to connect -> couldn't connect -> wait 6_000 milliseconds -> and so on ///
- public class ReconnectionPolicy + public ReconnectionPolicy() { - private readonly int _reconnectStepInterval; - private readonly int? _initMaxAttempts; - private int _currentReconnectInterval; - private readonly int _maxReconnectInterval; - private int? _maxAttempts; - private int _attemptsMade; + _reconnectStepInterval = 3_000; + _currentReconnectInterval = _reconnectStepInterval; + _maxReconnectInterval = 30_000; + _maxAttempts = null; + _initMaxAttempts = null; + _attemptsMade = 0; + } - /// - /// the or - /// infinitely - /// attempts to reconnect - ///

- ///

- /// with each attempt, the reconnect interval increases by 3_000 milliseconds - /// until it reaches 30_000 milliseconds - ///

- /// - ///

- ///

- /// Example: - ///

- /// try to connect -> couldn't connect -> wait 3_000 milliseconds -> try to connect -> couldn't connect -> wait 6_000 milliseconds -> and so on - ///
- public ReconnectionPolicy() - { - _reconnectStepInterval = 3_000; - _currentReconnectInterval = _reconnectStepInterval; - _maxReconnectInterval = 30_000; - _maxAttempts = null; - _initMaxAttempts = null; - _attemptsMade = 0; - } + /// + /// the or + /// attempts to reconnect for times + ///

+ ///

+ /// with each attempt, the reconnect interval increases by the amount of + /// until it reaches + ///

+ ///

+ /// Example: + ///

+ /// = 3_000 + ///

+ /// = 30_000 + ///

+ /// try to connect -> couldnt connect -> wait 3_000 milliseconds -> try to connect -> couldnt connect -> wait 6_000 milliseconds -> and so on + ///
+ /// + /// minimum interval in milliseconds + /// + /// + /// maximum interval in milliseconds + /// + /// + /// means infinite; it never stops to try to reconnect + /// + public ReconnectionPolicy( + int minReconnectInterval, + int maxReconnectInterval, + int maxAttempts) + { + _reconnectStepInterval = minReconnectInterval; + _currentReconnectInterval = minReconnectInterval > maxReconnectInterval + ? maxReconnectInterval + : minReconnectInterval; + _maxReconnectInterval = maxReconnectInterval; + _maxAttempts = maxAttempts; + _initMaxAttempts = maxAttempts; + _attemptsMade = 0; + } - /// - /// the or - /// attempts to reconnect for times - ///

- ///

- /// with each attempt, the reconnect interval increases by the amount of - /// until it reaches - ///

- ///

- /// Example: - ///

- /// = 3_000 - ///

- /// = 30_000 - ///

- /// try to connect -> couldnt connect -> wait 3_000 milliseconds -> try to connect -> couldnt connect -> wait 6_000 milliseconds -> and so on - ///
- /// - /// minimum interval in milliseconds - /// - /// - /// maximum interval in milliseconds - /// - /// - /// means infinite; it never stops to try to reconnect - /// - public ReconnectionPolicy( - int minReconnectInterval, - int maxReconnectInterval, - int maxAttempts) - { - _reconnectStepInterval = minReconnectInterval; - _currentReconnectInterval = minReconnectInterval > maxReconnectInterval - ? maxReconnectInterval - : minReconnectInterval; - _maxReconnectInterval = maxReconnectInterval; - _maxAttempts = maxAttempts; - _initMaxAttempts = maxAttempts; - _attemptsMade = 0; - } + /// + /// the or + /// infinitely + /// attempts to reconnect + ///

+ ///

+ /// with each attempt, the reconnect interval increases by the amount of + /// until it reaches + ///

+ ///

+ /// Example: + ///

+ /// = 3_000 + ///

+ /// = 30_000 + ///

+ /// try to connect -> couldn't connect -> wait 3_000 milliseconds -> try to connect -> couldn't connect -> wait 6_000 milliseconds -> and so on + ///
+ /// + /// minimum interval in milliseconds + /// + /// + /// maximum interval in milliseconds + /// + public ReconnectionPolicy( + int minReconnectInterval, + int maxReconnectInterval) + { + _reconnectStepInterval = minReconnectInterval; + _currentReconnectInterval = minReconnectInterval > maxReconnectInterval + ? maxReconnectInterval + : minReconnectInterval; + _maxReconnectInterval = maxReconnectInterval; + _maxAttempts = null; + _initMaxAttempts = null; + _attemptsMade = 0; + } - /// - /// the or - /// infinitely - /// attempts to reconnect - ///

- ///

- /// with each attempt, the reconnect interval increases by the amount of - /// until it reaches - ///

- ///

- /// Example: - ///

- /// = 3_000 - ///

- /// = 30_000 - ///

- /// try to connect -> couldn't connect -> wait 3_000 milliseconds -> try to connect -> couldn't connect -> wait 6_000 milliseconds -> and so on - ///
- /// - /// minimum interval in milliseconds - /// - /// - /// maximum interval in milliseconds - /// - public ReconnectionPolicy( - int minReconnectInterval, - int maxReconnectInterval) - { - _reconnectStepInterval = minReconnectInterval; - _currentReconnectInterval = minReconnectInterval > maxReconnectInterval - ? maxReconnectInterval - : minReconnectInterval; - _maxReconnectInterval = maxReconnectInterval; - _maxAttempts = null; - _initMaxAttempts = null; - _attemptsMade = 0; - } + /// + /// the or + /// infinitely + /// attempts to reconnect every -milliseconds + /// + /// + /// Interval in milliseconds between trying to reconnect + /// + public ReconnectionPolicy(int reconnectInterval) + { + _reconnectStepInterval = reconnectInterval; + _currentReconnectInterval = reconnectInterval; + _maxReconnectInterval = reconnectInterval; + _maxAttempts = null; + _initMaxAttempts = null; + _attemptsMade = 0; + } - /// - /// the or - /// infinitely - /// attempts to reconnect every -milliseconds - /// - /// - /// Interval in milliseconds between trying to reconnect - /// - public ReconnectionPolicy(int reconnectInterval) - { - _reconnectStepInterval = reconnectInterval; - _currentReconnectInterval = reconnectInterval; - _maxReconnectInterval = reconnectInterval; - _maxAttempts = null; - _initMaxAttempts = null; - _attemptsMade = 0; - } + /// + /// the or + /// attempts to reconnect every -milliseconds for times + /// + /// + /// Interval in milliseconds between trying to reconnect + /// + /// + /// means infinite; it never stops to try to reconnect + /// + public ReconnectionPolicy( + int reconnectInterval, + int? maxAttempts) + { + _reconnectStepInterval = reconnectInterval; + _currentReconnectInterval = reconnectInterval; + _maxReconnectInterval = reconnectInterval; + _maxAttempts = maxAttempts; + _initMaxAttempts = maxAttempts; + _attemptsMade = 0; + } - /// - /// the or - /// attempts to reconnect every -milliseconds for times - /// - /// - /// Interval in milliseconds between trying to reconnect - /// - /// - /// means infinite; it never stops to try to reconnect - /// - public ReconnectionPolicy( - int reconnectInterval, - int? maxAttempts) - { - _reconnectStepInterval = reconnectInterval; - _currentReconnectInterval = reconnectInterval; - _maxReconnectInterval = reconnectInterval; - _maxAttempts = maxAttempts; - _initMaxAttempts = maxAttempts; - _attemptsMade = 0; - } + internal void Reset(bool isReconnect) + { + if (isReconnect) return; + _attemptsMade = 0; + _currentReconnectInterval = _reconnectStepInterval; + _maxAttempts = _initMaxAttempts; + } - internal void Reset(bool isReconnect) + internal void ProcessValues() + { + _attemptsMade++; + if (_currentReconnectInterval < _maxReconnectInterval) { - if (isReconnect) return; - _attemptsMade = 0; - _currentReconnectInterval = _reconnectStepInterval; - _maxAttempts = _initMaxAttempts; + _currentReconnectInterval += _reconnectStepInterval; } - internal void ProcessValues() + if (_currentReconnectInterval > _maxReconnectInterval) { - _attemptsMade++; - if (_currentReconnectInterval < _maxReconnectInterval) - { - _currentReconnectInterval += _reconnectStepInterval; - } - - if (_currentReconnectInterval > _maxReconnectInterval) - { - _currentReconnectInterval = _maxReconnectInterval; - } + _currentReconnectInterval = _maxReconnectInterval; } + } - public int GetReconnectInterval() - { - return _currentReconnectInterval; - } + public int GetReconnectInterval() + { + return _currentReconnectInterval; + } - public bool AreAttemptsComplete() - { - return _attemptsMade == _maxAttempts; - } + public bool AreAttemptsComplete() + { + return _attemptsMade == _maxAttempts; } -} \ No newline at end of file +} diff --git a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs index 5d4e28b..9776b29 100644 --- a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs +++ b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs @@ -1,131 +1,127 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using TwitchLib.Communication.Clients; using TwitchLib.Communication.Events; using TwitchLib.Communication.Extensions; -namespace TwitchLib.Communication.Services +namespace TwitchLib.Communication.Services; + +/// +/// Service that checks connection state. +/// +internal class ConnectionWatchDog where T : IDisposable { + private readonly ILogger? _logger; + private readonly ClientBase _client; + /// - /// Service that checks connection state. + /// + /// + /// should only be set to a new instance in + /// + /// + /// should only be set to in + /// + /// /// - internal class ConnectionWatchDog where T : IDisposable - { - private readonly ILogger? _logger; - private readonly ClientBase _client; + private CancellationTokenSource? _cancellationTokenSource; - /// - /// - /// - /// should only be set to a new instance in - /// - /// - /// should only be set to in - /// - /// - /// - private CancellationTokenSource? _cancellationTokenSource; + private const int MonitorTaskDelayInMilliseconds = 200; - private const int MonitorTaskDelayInMilliseconds = 200; - - public bool IsRunning { get; private set; } + public bool IsRunning { get; private set; } - internal ConnectionWatchDog( - ClientBase client, - ILogger? logger = null) - { - _logger = logger; - _client = client; - } + internal ConnectionWatchDog( + ClientBase client, + ILogger? logger = null) + { + _logger = logger; + _client = client; + } - internal Task StartMonitorTaskAsync() + internal Task StartMonitorTaskAsync() + { + _logger?.TraceMethodCall(GetType()); + // We dont want to start more than one WatchDog + if (_cancellationTokenSource != null) { - _logger?.TraceMethodCall(GetType()); - // We dont want to start more than one WatchDog - if (_cancellationTokenSource != null) - { - Exception ex = new InvalidOperationException("Monitor Task cant be started more than once!"); - _logger?.LogExceptionAsError(GetType(), ex); - throw ex; - } + Exception ex = new InvalidOperationException("Monitor Task cant be started more than once!"); + _logger?.LogExceptionAsError(GetType(), ex); + throw ex; + } - // This should be the only place where a new instance of CancellationTokenSource is set - _cancellationTokenSource = new CancellationTokenSource(); + // This should be the only place where a new instance of CancellationTokenSource is set + _cancellationTokenSource = new CancellationTokenSource(); - IsRunning = true; - return Task.Run(MonitorTaskActionAsync, _cancellationTokenSource.Token); - } + IsRunning = true; + return Task.Run(MonitorTaskActionAsync, _cancellationTokenSource.Token); + } - internal async Task StopAsync() - { - IsRunning = false; - _logger?.TraceMethodCall(GetType()); - _cancellationTokenSource?.Cancel(); - // give MonitorTaskAction a chance to catch cancellation - // otherwise it may result in an Exception - await Task.Delay(MonitorTaskDelayInMilliseconds * 2); - _cancellationTokenSource?.Dispose(); - // set it to null for the check within this.StartMonitorTask() - _cancellationTokenSource = null; - } + internal async Task StopAsync() + { + IsRunning = false; + _logger?.TraceMethodCall(GetType()); + _cancellationTokenSource?.Cancel(); + // give MonitorTaskAction a chance to catch cancellation + // otherwise it may result in an Exception + await Task.Delay(MonitorTaskDelayInMilliseconds * 2); + _cancellationTokenSource?.Dispose(); + // set it to null for the check within this.StartMonitorTask() + _cancellationTokenSource = null; + } - private async Task MonitorTaskActionAsync() + private async Task MonitorTaskActionAsync() + { + _logger?.TraceMethodCall(GetType()); + try { - _logger?.TraceMethodCall(GetType()); - try + while (_cancellationTokenSource != null && + !_cancellationTokenSource.Token.IsCancellationRequested) { - while (_cancellationTokenSource != null && - !_cancellationTokenSource.Token.IsCancellationRequested) + // we expect the client is connected, + // when this monitor task starts + // cause BaseClient.Open() starts NetworkServices after a connection could be established + if (!_client.IsConnected) { - // we expect the client is connected, - // when this monitor task starts - // cause BaseClient.Open() starts NetworkServices after a connection could be established - if (!_client.IsConnected) - { - _logger?.TraceAction(GetType(), "Client isn't connected anymore"); - // no call to close needed, - // ReconnectInternal() calls the correct Close-Method within the Client - // ReconnectInternal() makes attempts to reconnect according to the ReconnectionPolicy within the IClientOptions - _logger?.TraceAction(GetType(), "Try to reconnect"); - - var connected = await _client.ReconnectInternalAsync(); - if (!connected) - { - _logger?.TraceAction(GetType(), "Client couldn't reconnect"); - // if the ReconnectionPolicy is set up to be finite - // and no connection could be established - // a call to Client.Close() is made - // that public Close() also shuts down this ConnectionWatchDog - await _client.CloseAsync(); - break; - } + _logger?.TraceAction(GetType(), "Client isn't connected anymore"); + // no call to close needed, + // ReconnectInternal() calls the correct Close-Method within the Client + // ReconnectInternal() makes attempts to reconnect according to the ReconnectionPolicy within the IClientOptions + _logger?.TraceAction(GetType(), "Try to reconnect"); - _logger?.TraceAction(GetType(), "Client reconnected"); + var connected = await _client.ReconnectInternalAsync(); + if (!connected) + { + _logger?.TraceAction(GetType(), "Client couldn't reconnect"); + // if the ReconnectionPolicy is set up to be finite + // and no connection could be established + // a call to Client.Close() is made + // that public Close() also shuts down this ConnectionWatchDog + await _client.CloseAsync(); + break; } - await Task.Delay(MonitorTaskDelayInMilliseconds); + _logger?.TraceAction(GetType(), "Client reconnected"); } - } - catch (TaskCanceledException _) - { - // Swallow any cancellation exceptions - } - catch (OperationCanceledException ex) - { - // Occurs if the Tasks are canceled by the CancellationTokenSource.Token - _logger?.LogExceptionAsInformation(GetType(), ex); - } - catch (Exception ex) - { - _logger?.LogExceptionAsError(GetType(), ex); - await _client.RaiseError(new OnErrorEventArgs(ex)); - await _client.RaiseFatal(); - // To ensure CancellationTokenSource is set to null again call Stop(); - await StopAsync(); + await Task.Delay(MonitorTaskDelayInMilliseconds); } } + catch (TaskCanceledException) + { + // Swallow any cancellation exceptions + } + catch (OperationCanceledException ex) + { + // Occurs if the Tasks are canceled by the CancellationTokenSource.Token + _logger?.LogExceptionAsInformation(GetType(), ex); + } + catch (Exception ex) + { + _logger?.LogExceptionAsError(GetType(), ex); + await _client.RaiseError(new OnErrorEventArgs(ex)); + await _client.RaiseFatal(); + + // To ensure CancellationTokenSource is set to null again call Stop(); + await StopAsync(); + } } -} \ No newline at end of file +} diff --git a/src/TwitchLib.Communication/Services/NetworkServices.cs b/src/TwitchLib.Communication/Services/NetworkServices.cs index 917df0c..70869ae 100644 --- a/src/TwitchLib.Communication/Services/NetworkServices.cs +++ b/src/TwitchLib.Communication/Services/NetworkServices.cs @@ -1,55 +1,51 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using TwitchLib.Communication.Clients; using TwitchLib.Communication.Extensions; -namespace TwitchLib.Communication.Services +namespace TwitchLib.Communication.Services; + +/// +/// to bundle Network-Service-s +/// +internal class NetworkServices where T : IDisposable { - /// - /// to bundle Network-Service-s - /// - internal class NetworkServices where T : IDisposable - { - private Task? _listenTask; - private Task? _monitorTask; - private readonly ClientBase _client; - private readonly ILogger? _logger; - private readonly ConnectionWatchDog _connectionWatchDog; + private Task? _listenTask; + private Task? _monitorTask; + private readonly ClientBase _client; + private readonly ILogger? _logger; + private readonly ConnectionWatchDog _connectionWatchDog; - private CancellationToken Token => _client.Token; + private CancellationToken Token => _client.Token; - internal NetworkServices( - ClientBase client, - ILogger? logger = null) - { - _logger = logger; - _client = client; - _connectionWatchDog = new ConnectionWatchDog(_client, logger); - } + internal NetworkServices( + ClientBase client, + ILogger? logger = null) + { + _logger = logger; + _client = client; + _connectionWatchDog = new ConnectionWatchDog(_client, logger); + } - internal void Start() + internal void Start() + { + _logger?.TraceMethodCall(GetType()); + if (_monitorTask == null || !_connectionWatchDog.IsRunning) { - _logger?.TraceMethodCall(GetType()); - if (_monitorTask == null || !_connectionWatchDog.IsRunning) - { - // this task is probably still running - // may be in case of a network connection loss - // all other Tasks haven't been started or have been canceled! - // ConnectionWatchDog is the only one, that has a separate CancellationTokenSource! - - // Let those tasks run in the background, do not await them - _monitorTask = _connectionWatchDog.StartMonitorTaskAsync(); - } - - _listenTask = Task.Run(_client.ListenTaskActionAsync, Token); + // this task is probably still running + // may be in case of a network connection loss + // all other Tasks haven't been started or have been canceled! + // ConnectionWatchDog is the only one, that has a separate CancellationTokenSource! + + // Let those tasks run in the background, do not await them + _monitorTask = _connectionWatchDog.StartMonitorTaskAsync(); } - internal async Task StopAsync() - { - _logger?.TraceMethodCall(GetType()); - await _connectionWatchDog.StopAsync(); - } + _listenTask = Task.Run(_client.ListenTaskActionAsync, Token); + } + + internal async Task StopAsync() + { + _logger?.TraceMethodCall(GetType()); + await _connectionWatchDog.StopAsync(); } -} \ No newline at end of file +} diff --git a/src/TwitchLib.Communication/TwitchLib.Communication.csproj b/src/TwitchLib.Communication/TwitchLib.Communication.csproj index 984a9f5..8d0a550 100644 --- a/src/TwitchLib.Communication/TwitchLib.Communication.csproj +++ b/src/TwitchLib.Communication/TwitchLib.Communication.csproj @@ -3,6 +3,7 @@ netstandard2.0;netstandard2.1;net6.0;net7.0 enable + enable latest 2.0.0 $(VersionSuffix) @@ -22,6 +23,8 @@ 2.0.0 true True + strict + nullable From f92fff7d9c5b12f7106ec1bd34e1e3caf1734da4 Mon Sep 17 00:00:00 2001 From: AoshiW Date: Wed, 5 Jul 2023 09:54:11 +0200 Subject: [PATCH 17/20] remove .net6 from test, doc for Client Options --- .../TwitchLib.Communication.Tests.csproj | 2 +- src/TwitchLib.Communication/Models/ClientOptions.cs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj b/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj index d6580d5..c245c36 100644 --- a/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj +++ b/src/TwitchLib.Communication.Tests/TwitchLib.Communication.Tests.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0 + net7.0 false enable disable diff --git a/src/TwitchLib.Communication/Models/ClientOptions.cs b/src/TwitchLib.Communication/Models/ClientOptions.cs index f7cd21d..0741bf3 100644 --- a/src/TwitchLib.Communication/Models/ClientOptions.cs +++ b/src/TwitchLib.Communication/Models/ClientOptions.cs @@ -5,9 +5,16 @@ namespace TwitchLib.Communication.Models; public class ClientOptions : IClientOptions { + /// public ReconnectionPolicy ReconnectionPolicy { get; } + + /// public bool UseSsl { get; } + + /// public uint DisconnectWait { get; } + + /// public ClientType ClientType { get; } /// From 6649cad8c0c32bb6173afc58be2e6a3050eb6699 Mon Sep 17 00:00:00 2001 From: AoshiW <48525283+AoshiW@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:34:02 +0200 Subject: [PATCH 18/20] remove `strict` Co-authored-by: neon-sunset --- src/TwitchLib.Communication/TwitchLib.Communication.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TwitchLib.Communication/TwitchLib.Communication.csproj b/src/TwitchLib.Communication/TwitchLib.Communication.csproj index 8d0a550..b59d546 100644 --- a/src/TwitchLib.Communication/TwitchLib.Communication.csproj +++ b/src/TwitchLib.Communication/TwitchLib.Communication.csproj @@ -23,7 +23,6 @@ 2.0.0 true True - strict nullable From 96ec32e263c92a8d36ac98b677467bec064e4437 Mon Sep 17 00:00:00 2001 From: AoshiW Date: Sat, 5 Aug 2023 22:44:30 +0200 Subject: [PATCH 19/20] `catch when(...)` changes --- src/TwitchLib.Communication/Clients/TcpClient.cs | 6 ++---- src/TwitchLib.Communication/Clients/WebsocketClient.cs | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/TwitchLib.Communication/Clients/TcpClient.cs b/src/TwitchLib.Communication/Clients/TcpClient.cs index 7caee84..1bfc475 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -49,8 +49,7 @@ internal override async Task ListenTaskActionAsync() await RaiseMessage(new OnMessageEventArgs(input)); } - catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || - ex.GetType() == typeof(OperationCanceledException)) + catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) { // occurs if the Tasks are canceled by the CancellationTokenSource.Token Logger?.LogExceptionAsInformation(GetType(), ex); @@ -143,8 +142,7 @@ protected override async Task ConnectClientAsync() _reader = new StreamReader(stream); _writer = new StreamWriter(stream); } - catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || - ex.GetType() == typeof(OperationCanceledException)) + catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) { // occurs if the Tasks are canceled by the CancellationTokenSource.Token Logger?.LogExceptionAsInformation(GetType(), ex); diff --git a/src/TwitchLib.Communication/Clients/WebsocketClient.cs b/src/TwitchLib.Communication/Clients/WebsocketClient.cs index 125e971..516175e 100644 --- a/src/TwitchLib.Communication/Clients/WebsocketClient.cs +++ b/src/TwitchLib.Communication/Clients/WebsocketClient.cs @@ -184,8 +184,7 @@ protected override async Task ConnectClientAsync() Logger?.TraceAction(GetType(), "Client couldn't establish connection"); } } - catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || - ex.GetType() == typeof(OperationCanceledException)) + catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) { // occurs if the Tasks are canceled by the CancellationTokenSource.Token Logger?.LogExceptionAsInformation(GetType(), ex); From 0b90943b506c4623a1d102a94f370e18b0ef4317 Mon Sep 17 00:00:00 2001 From: Cole Date: Sat, 5 Aug 2023 14:16:42 -0700 Subject: [PATCH 20/20] OnSendFailedEventArgs: data -> message rename --- src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs b/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs index 51f41d2..4b0c4c9 100644 --- a/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs +++ b/src/TwitchLib.Communication/Events/OnSendFailedEventArgs.cs @@ -2,13 +2,13 @@ public class OnSendFailedEventArgs : EventArgs { - public string Data { get; } + public string Message { get; } public Exception Exception { get; } - public OnSendFailedEventArgs(Exception exception, string data) + public OnSendFailedEventArgs(Exception exception, string message) { Exception = exception; - Data = data; + Message = message; } }