Refactoring connection to use an idle timeout and automatically close the underlying data channel
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System.Collections.Generic
|
||||
{
|
||||
// ============================================================================================================================= //
|
||||
// Source: https://git.am-wd.de/am-wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Utilities/AsyncQueue.cs //
|
||||
// ============================================================================================================================= //
|
||||
[Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class AsyncQueue<T>
|
||||
{
|
||||
private readonly Queue<T> _queue = new();
|
||||
|
||||
private TaskCompletionSource<bool> _dequeueTcs = new();
|
||||
private readonly TaskCompletionSource<bool> _availableTcs = new();
|
||||
|
||||
public T Dequeue()
|
||||
{
|
||||
lock (_queue)
|
||||
{
|
||||
return _queue.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
public void Enqueue(T item)
|
||||
{
|
||||
lock (_queue)
|
||||
{
|
||||
_queue.Enqueue(item);
|
||||
SetToken(_dequeueTcs);
|
||||
SetToken(_availableTcs);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> DequeueAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
TaskCompletionSource<bool> internalDequeueTcs;
|
||||
lock (_queue)
|
||||
{
|
||||
if (_queue.Count > 0)
|
||||
return _queue.Dequeue();
|
||||
|
||||
internalDequeueTcs = ResetToken(ref _dequeueTcs);
|
||||
}
|
||||
|
||||
await WaitAsync(internalDequeueTcs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryDequeue(out T result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Dequeue();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
lock (_queue)
|
||||
{
|
||||
var copy = new Queue<T>(_queue);
|
||||
_queue.Clear();
|
||||
|
||||
bool found = false;
|
||||
int count = copy.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var element = copy.Dequeue();
|
||||
if (found)
|
||||
{
|
||||
_queue.Enqueue(element);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((element == null && item == null) || element?.Equals(item) == true)
|
||||
{
|
||||
found = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
_queue.Enqueue(element);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetToken(TaskCompletionSource<bool> tcs)
|
||||
{
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
private static TaskCompletionSource<bool> ResetToken(ref TaskCompletionSource<bool> tcs)
|
||||
{
|
||||
if (tcs.Task.IsCompleted)
|
||||
{
|
||||
tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
return tcs;
|
||||
}
|
||||
|
||||
private static async Task WaitAsync(TaskCompletionSource<bool> tcs, CancellationToken cancellationToken)
|
||||
{
|
||||
if (await Task.WhenAny(tcs.Task, Task.Delay(-1, cancellationToken)) == tcs.Task)
|
||||
{
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NetworkStream.Dispose" />
|
||||
public virtual void Dispose()
|
||||
=> _stream.Dispose();
|
||||
|
||||
|
||||
21
AMWD.Protocols.Modbus.Tcp/Utils/RequestQueueItem.cs
Normal file
21
AMWD.Protocols.Modbus.Tcp/Utils/RequestQueueItem.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
internal class RequestQueueItem
|
||||
{
|
||||
public byte[] Request { get; set; }
|
||||
|
||||
public Func<IReadOnlyList<byte>, bool> ValidateResponseComplete { get; set; }
|
||||
|
||||
public TaskCompletionSource<IReadOnlyList<byte>> TaskCompletionSource { get; set; }
|
||||
|
||||
public CancellationTokenSource CancellationTokenSource { get; set; }
|
||||
|
||||
public CancellationTokenRegistration CancellationTokenRegistration { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
/// <inheritdoc cref="Socket"/>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class SocketWrapper : IDisposable
|
||||
{
|
||||
[Obsolete("Constructor only for mocking on UnitTests!", error: true)]
|
||||
public SocketWrapper()
|
||||
{ }
|
||||
|
||||
public SocketWrapper(Socket socket)
|
||||
{
|
||||
Client = socket;
|
||||
}
|
||||
|
||||
public virtual Socket Client { get; }
|
||||
|
||||
/// <inheritdoc cref="Socket.Dispose()"/>
|
||||
public virtual void Dispose()
|
||||
=> Client.Dispose();
|
||||
|
||||
/// <inheritdoc cref="Socket.IOControl(IOControlCode, byte[], byte[])"/>
|
||||
public virtual int IOControl(IOControlCode ioControlCode, byte[] optionInValue, byte[] optionOutValue)
|
||||
=> Client.IOControl(ioControlCode, optionInValue, optionOutValue);
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <inheritdoc cref="Socket.SetSocketOption(SocketOptionLevel, SocketOptionName, bool)"/>
|
||||
public virtual void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, bool optionValue)
|
||||
=> Client.SetSocketOption(optionLevel, optionName, optionValue);
|
||||
|
||||
/// <inheritdoc cref="Socket.SetSocketOption(SocketOptionLevel, SocketOptionName, int)"/>
|
||||
public virtual void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue)
|
||||
=> Client.SetSocketOption(optionLevel, optionName, optionValue);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,17 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class TcpClientWrapper : IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly TcpClient _client = new();
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Connected" />
|
||||
public virtual bool Connected => _client.Connected;
|
||||
|
||||
/// <inheritdoc cref="TcpClient.ReceiveTimeout" />
|
||||
public virtual int ReceiveTimeout
|
||||
{
|
||||
@@ -26,15 +35,9 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
set => _client.SendTimeout = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Connected" />
|
||||
public virtual bool Connected => _client.Connected;
|
||||
#endregion Properties
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Client" />
|
||||
public virtual SocketWrapper Client
|
||||
{
|
||||
get => new(_client.Client);
|
||||
set => _client.Client = value.Client;
|
||||
}
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Close" />
|
||||
public virtual void Close()
|
||||
@@ -52,12 +55,18 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
|
||||
#endif
|
||||
|
||||
/// <inheritdoc cref="TcpClient.GetStream" />
|
||||
public virtual NetworkStreamWrapper GetStream()
|
||||
=> new(_client.GetStream());
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Dispose()" />
|
||||
public virtual void Dispose()
|
||||
=> _client.Dispose();
|
||||
|
||||
/// <inheritdoc cref="TcpClient.GetStream" />
|
||||
public virtual NetworkStreamWrapper GetStream()
|
||||
=> new(_client.GetStream());
|
||||
#endregion IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user