diff --git a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs index 1fd81de..b0bacec 100644 --- a/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs +++ b/src/TwitchLib.Communication.Tests/Clients/ClientTestsBase.cs @@ -9,6 +9,7 @@ using TwitchLib.Communication.Tests.Helpers; using Xunit; +[assembly: CollectionBehavior(DisableTestParallelization = true)] namespace TwitchLib.Communication.Tests.Clients { /// @@ -24,33 +25,33 @@ namespace TwitchLib.Communication.Tests.Clients /// public abstract class ClientTestsBase where T : IClient { - private static uint WaitAfterDispose => 3; private static TimeSpan WaitOneDuration => TimeSpan.FromSeconds(5); - private static IClientOptions Options; + private readonly IClientOptions? _options; - public ClientTestsBase(IClientOptions options = null) + protected ClientTestsBase(IClientOptions? options = null) { - Options = options; + _options = options; } [Fact] - public void Client_Raises_OnConnected_EventArgs() + public async Task Client_Raises_OnConnected_EventArgs() { // create one logger per test-method! - cause one file per test-method is generated - ILogger logger = TestLogHelper.GetLogger(); - T? client = GetClient(logger, Options); + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); Assert.NotNull(client); try { - ManualResetEvent pauseConnected = new ManualResetEvent(false); + var pauseConnected = new ManualResetEvent(false); - Assert.Raises( + await Assert.RaisesAsync( h => client.OnConnected += h, h => client.OnConnected -= h, - () => + async () => { client.OnConnected += (sender, e) => pauseConnected.Set(); - client.Open(); + await client.OpenAsync(); + Assert.True(pauseConnected.WaitOne(WaitOneDuration)); }); } @@ -61,33 +62,34 @@ public void Client_Raises_OnConnected_EventArgs() } finally { - Cleanup(client); + client.Dispose(); } } [Fact] - public void Client_Raises_OnDisconnected_EventArgs() + public async Task Client_Raises_OnDisconnected_EventArgs() { // create one logger per test-method! - cause one file per test-method is generated - ILogger logger = TestLogHelper.GetLogger(); - T? client = GetClient(logger, Options); + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); Assert.NotNull(client); try { - ManualResetEvent pauseDisconnected = new ManualResetEvent(false); + var pauseDisconnected = new ManualResetEvent(false); - Assert.Raises( + await Assert.RaisesAsync( h => client.OnDisconnected += h, h => client.OnDisconnected -= h, - () => + async () => { - client.OnConnected += (sender, e) => + client.OnConnected += async (sender, e) => { - Task.Delay(WaitOneDuration).GetAwaiter().GetResult(); - client.Close(); + await client.CloseAsync(); }; + client.OnDisconnected += (sender, e) => pauseDisconnected.Set(); - client.Open(); + await client.OpenAsync(); + Assert.True(pauseDisconnected.WaitOne(WaitOneDuration)); }); } @@ -98,30 +100,30 @@ public void Client_Raises_OnDisconnected_EventArgs() } finally { - Cleanup(client); + client.Dispose(); } } [Fact] - public void Client_Raises_OnReconnected_EventArgs() + public async Task Client_Raises_OnReconnected_EventArgs() { // create one logger per test-method! - cause one file per test-method is generated - ILogger logger = TestLogHelper.GetLogger(); - T? client = GetClient(logger, Options); + var logger = TestLogHelper.GetLogger(); + var client = GetClient(logger, _options); Assert.NotNull(client); try { - ManualResetEvent pauseReconnected = new ManualResetEvent(false); + var pauseReconnected = new ManualResetEvent(false); - Assert.Raises( + await Assert.RaisesAsync( h => client.OnReconnected += h, h => client.OnReconnected -= h, - () => + async () => { - client.OnConnected += (s, e) => client.Reconnect(); + client.OnConnected += async (s, e) => await client.ReconnectAsync(); client.OnReconnected += (s, e) => pauseReconnected.Set(); - client.Open(); + await client.OpenAsync(); Assert.True(pauseReconnected.WaitOne(WaitOneDuration)); }); @@ -133,7 +135,7 @@ public void Client_Raises_OnReconnected_EventArgs() } finally { - Cleanup(client); + client.Dispose(); } } @@ -141,11 +143,11 @@ public void Client_Raises_OnReconnected_EventArgs() public void Dispose_Client_Before_Connecting_IsOK() { // create one logger per test-method! - cause one file per test-method is generated - ILogger logger = TestLogHelper.GetLogger(); + var logger = TestLogHelper.GetLogger(); IClient? client = null; try { - client = GetClient(logger, Options); + client = GetClient(logger, _options); Assert.NotNull(client); client.Dispose(); } @@ -156,29 +158,25 @@ public void Dispose_Client_Before_Connecting_IsOK() } finally { - Cleanup((T?)client); + client?.Dispose(); } } - private static void Cleanup(T? client) - { - client?.Dispose(); - Task.Delay(TimeSpan.FromSeconds(WaitAfterDispose)).GetAwaiter().GetResult(); - } - private static TClient? GetClient(ILogger logger, IClientOptions? options = null) { - Type[] constructorParameterTypes = new Type[] + var constructorParameterTypes = new Type[] { typeof(IClientOptions), typeof(ILogger) }; - ConstructorInfo? constructor = typeof(TClient).GetConstructor(constructorParameterTypes); - object[] constructorParameters = new object[] + + var constructor = typeof(TClient).GetConstructor(constructorParameterTypes); + var constructorParameters = new object[] { options ?? new ClientOptions(), logger }; + return (TClient?)constructor?.Invoke(constructorParameters); } } diff --git a/src/TwitchLib.Communication/Clients/ClientBase.cs b/src/TwitchLib.Communication/Clients/ClientBase.cs index 6b75074..578b1a1 100644 --- a/src/TwitchLib.Communication/Clients/ClientBase.cs +++ b/src/TwitchLib.Communication/Clients/ClientBase.cs @@ -22,9 +22,10 @@ namespace TwitchLib.Communication.Clients /// /// /// - public abstract class ClientBase : IClient where T : IDisposable + public abstract class ClientBase : IClient + where T : IDisposable { - private static readonly object Lock = new object(); + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); private readonly NetworkServices _networkServices; private CancellationTokenSource _cancellationTokenSource; @@ -126,7 +127,7 @@ internal void RaiseMessage(OnMessageEventArgs eventArgs) /// /// Wont raise the given if .IsCancellationRequested /// - internal void RaiseFatal(Exception e = null) + internal void RaiseFatal(Exception ex = null) { Logger?.TraceMethodCall(GetType()); if (Token.IsCancellationRequested) @@ -135,9 +136,9 @@ internal void RaiseFatal(Exception e = null) } var onFatalErrorEventArgs = new OnFatalErrorEventArgs("Fatal network error."); - if (e != null) + if (ex != null) { - onFatalErrorEventArgs = new OnFatalErrorEventArgs(e); + onFatalErrorEventArgs = new OnFatalErrorEventArgs(ex); } OnFatality?.Invoke(this, onFatalErrorEventArgs); @@ -155,67 +156,68 @@ private void RaiseConnected() OnConnected?.Invoke(this, new OnConnectedEventArgs()); } - public bool Send(string message) + public async Task SendAsync(string message) { Logger?.TraceMethodCall(GetType()); + + await _semaphore.WaitAsync(Token); try { - lock (Lock) - { - ClientSend(message); - return true; - } + await ClientSendAsync(message); + return true; } catch (Exception e) { - RaiseSendFailed(new OnSendFailedEventArgs() { Exception = e, Data = message }); + RaiseSendFailed(new OnSendFailedEventArgs { Exception = e, Data = message }); return false; } + finally + { + _semaphore.Release(); + } } - public bool Open() + public Task OpenAsync() { Logger?.TraceMethodCall(GetType()); - return OpenPrivate(false); + return OpenPrivateAsync(false); } - public void Close() + public async Task CloseAsync() { Logger?.TraceMethodCall(GetType()); // Network services has to be stopped first so that it wont reconnect - _networkServices.Stop(); + await _networkServices.StopAsync(); // ClosePrivate() also handles IClientOptions.DisconnectWait - ClosePrivate(); + await ClosePrivateAsync(); } /// - /// + /// /// public void Dispose() { Logger?.TraceMethodCall(GetType()); - Close(); + CloseAsync().GetAwaiter().GetResult(); GC.SuppressFinalize(this); } - public bool Reconnect() + public async Task ReconnectAsync() { Logger?.TraceMethodCall(GetType()); // Stops everything (including NetworkServices) if (IsConnected) { - Close(); + await CloseAsync(); } - // interface IClient doesnt declare a return value for Reconnect() - // so we can suppress IDE0058 of ReconnectInternal() - return ReconnectInternal(); + return await ReconnectInternalAsync(); } - private bool OpenPrivate(bool isReconnect) + private async Task OpenPrivateAsync(bool isReconnect) { Logger?.TraceMethodCall(GetType()); try @@ -235,17 +237,17 @@ private bool OpenPrivate(bool isReconnect) var first = true; Options.ReconnectionPolicy.Reset(isReconnect); - while (!IsConnected - && !Options.ReconnectionPolicy.AreAttemptsComplete()) + + while (!IsConnected && + !Options.ReconnectionPolicy.AreAttemptsComplete()) { Logger?.TraceAction(GetType(), "try to connect"); if (!first) { - Task.Delay(Options.ReconnectionPolicy.GetReconnectInterval(), CancellationToken.None) - .GetAwaiter().GetResult(); + await Task.Delay(Options.ReconnectionPolicy.GetReconnectInterval(), CancellationToken.None); } - ConnectClient(); + await ConnectClientAsync(); Options.ReconnectionPolicy.ProcessValues(); first = false; } @@ -259,6 +261,7 @@ private bool OpenPrivate(bool isReconnect) Logger?.TraceAction(GetType(), "Client established a connection"); _networkServices.Start(); + if (!isReconnect) { RaiseConnected(); @@ -276,7 +279,7 @@ private bool OpenPrivate(bool isReconnect) } /// - /// Stops + /// Stops /// by calling ///

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

///

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

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

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

///

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

/// otherwise ///
- bool Send(string message); + Task SendAsync(string message); } } \ No newline at end of file diff --git a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs index c2b8a9e..6bf4034 100644 --- a/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs +++ b/src/TwitchLib.Communication/Services/ConnectionWatchDog.cs @@ -22,7 +22,7 @@ internal class ConnectionWatchDog where T : IDisposable /// should only be set to a new instance in /// /// - /// should only be set to in + /// should only be set to in /// /// ///
@@ -38,7 +38,7 @@ internal ConnectionWatchDog( _client = client; } - internal Task StartMonitorTask() + internal Task StartMonitorTaskAsync() { _logger?.TraceMethodCall(GetType()); // We dont want to start more than one WatchDog @@ -52,27 +52,28 @@ internal Task StartMonitorTask() // This should be the only place where a new instance of CancellationTokenSource is set _cancellationTokenSource = new CancellationTokenSource(); - return Task.Run(MonitorTaskAction, _cancellationTokenSource.Token); + return Task.Run(MonitorTaskActionAsync, _cancellationTokenSource.Token); } - internal void Stop() + internal async Task StopAsync() { _logger?.TraceMethodCall(GetType()); _cancellationTokenSource?.Cancel(); // give MonitorTaskAction a chance to catch cancellation // otherwise it may result in an Exception - Task.Delay(MonitorTaskDelayInMilliseconds * 2).GetAwaiter().GetResult(); + await Task.Delay(MonitorTaskDelayInMilliseconds * 2); _cancellationTokenSource?.Dispose(); // set it to null for the check within this.StartMonitorTask() _cancellationTokenSource = null; } - private void MonitorTaskAction() + private async Task MonitorTaskActionAsync() { _logger?.TraceMethodCall(GetType()); try { - while (_cancellationTokenSource != null && !_cancellationTokenSource.Token.IsCancellationRequested) + while (_cancellationTokenSource != null && + !_cancellationTokenSource.Token.IsCancellationRequested) { // we expect the client is connected, // when this monitor task starts @@ -84,7 +85,8 @@ private void MonitorTaskAction() // ReconnectInternal() calls the correct Close-Method within the Client // ReconnectInternal() makes attempts to reconnect according to the ReconnectionPolicy within the IClientOptions _logger?.TraceAction(GetType(), "Try to reconnect"); - var connected = _client.ReconnectInternal(); + + var connected = await _client.ReconnectInternalAsync(); if (!connected) { _logger?.TraceAction(GetType(), "Client couldn't reconnect"); @@ -92,14 +94,14 @@ private void MonitorTaskAction() // and no connection could be established // a call to Client.Close() is made // that public Close() also shuts down this ConnectionWatchDog - _client.Close(); + await _client.CloseAsync(); break; } _logger?.TraceAction(GetType(), "Client reconnected"); } - Task.Delay(MonitorTaskDelayInMilliseconds).GetAwaiter().GetResult(); + await Task.Delay(MonitorTaskDelayInMilliseconds); } } catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException) || @@ -115,7 +117,7 @@ private void MonitorTaskAction() _client.RaiseFatal(); // To ensure CancellationTokenSource is set to null again call Stop(); - Stop(); + await StopAsync(); } } } diff --git a/src/TwitchLib.Communication/Services/NetworkServices.cs b/src/TwitchLib.Communication/Services/NetworkServices.cs index db27298..590e0ae 100644 --- a/src/TwitchLib.Communication/Services/NetworkServices.cs +++ b/src/TwitchLib.Communication/Services/NetworkServices.cs @@ -38,17 +38,19 @@ internal void Start() // this task is probably still running // may be in case of a network connection loss // all other Tasks haven't been started or have been canceled! - // ConnectionWatchDog is the only one, that has a seperate CancellationTokenSource! - _monitorTask = _connectionWatchDog.StartMonitorTask(); + // ConnectionWatchDog is the only one, that has a separate CancellationTokenSource! + + // Let those tasks run in the background, do not await them + _monitorTask = _connectionWatchDog.StartMonitorTaskAsync(); } - _listenTask = Task.Run(_client.ListenTaskAction, Token); + _listenTask = Task.Run(_client.ListenTaskActionAsync, Token); } - internal void Stop() + internal async Task StopAsync() { _logger?.TraceMethodCall(GetType()); - _connectionWatchDog.Stop(); + await _connectionWatchDog.StopAsync(); } } } \ No newline at end of file