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..c245c36 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 + 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..1bfc475 100644 --- a/src/TwitchLib.Communication/Clients/TcpClient.cs +++ b/src/TwitchLib.Communication/Clients/TcpClient.cs @@ -1,104 +1,103 @@ -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 is TaskCanceledException or 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 +125,52 @@ 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 is TaskCanceledException or 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..516175e 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,34 @@ 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 is TaskCanceledException or 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..0741bf3 100644 --- a/src/TwitchLib.Communication/Models/ClientOptions.cs +++ b/src/TwitchLib.Communication/Models/ClientOptions.cs @@ -1,41 +1,47 @@ 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; } - /// - /// - /// - /// 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; - } + /// + 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 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..b59d546 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,7 @@ 2.0.0 true True + nullable