diff --git a/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs b/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs index 5a59f3d..32142ff 100644 --- a/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs +++ b/AMWD.Protocols.Modbus.Common/Contracts/ModbusClientBase.cs @@ -10,19 +10,27 @@ namespace AMWD.Protocols.Modbus.Common.Contracts /// /// Base implementation of a Modbus client. /// - public abstract class ModbusClientBase : IDisposable + /// + /// Initializes a new instance of the class with a specific . + /// + /// The responsible for invoking the requests. + /// + /// if the connection should be disposed of by Dispose(), + /// otherwise if you inted to reuse the connection. + /// + public abstract class ModbusClientBase(IModbusConnection connection, bool disposeConnection) : IDisposable { private bool _isDisposed; /// /// Gets or sets a value indicating whether the connection should be disposed of by . /// - protected readonly bool disposeConnection; + protected readonly bool disposeConnection = disposeConnection; /// /// Gets or sets the responsible for invoking the requests. /// - protected readonly IModbusConnection connection; + protected readonly IModbusConnection connection = connection ?? throw new ArgumentNullException(nameof(connection)); /// /// Initializes a new instance of the class with a specific . @@ -32,20 +40,6 @@ namespace AMWD.Protocols.Modbus.Common.Contracts : this(connection, true) { } - /// - /// Initializes a new instance of the class with a specific . - /// - /// The responsible for invoking the requests. - /// - /// if the connection should be disposed of by Dispose(), - /// otherwise if you inted to reuse the connection. - /// - public ModbusClientBase(IModbusConnection connection, bool disposeConnection) - { - this.connection = connection ?? throw new ArgumentNullException(nameof(connection)); - this.disposeConnection = disposeConnection; - } - /// /// Gets or sets the protocol type to use. /// @@ -67,7 +61,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts Assertions(); var request = Protocol.SerializeReadCoils(unitId, startAddress, count); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); // The protocol processes complete bytes from the response. @@ -92,7 +86,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts Assertions(); var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); // The protocol processes complete bytes from the response. @@ -117,7 +111,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts Assertions(); var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList(); @@ -140,7 +134,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts Assertions(); var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList(); @@ -184,7 +178,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts do { var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); result = Protocol.DeserializeReadDeviceIdentification(response); @@ -247,7 +241,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts Assertions(); var request = Protocol.SerializeWriteSingleCoil(unitId, coil); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); var result = Protocol.DeserializeWriteSingleCoil(response); @@ -268,7 +262,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts Assertions(); var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); var result = Protocol.DeserializeWriteSingleHoldingRegister(response); @@ -289,7 +283,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts Assertions(); var request = Protocol.SerializeWriteMultipleCoils(unitId, coils); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response); @@ -309,7 +303,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts Assertions(); var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers); - var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken); + var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); Protocol.ValidateResponse(request, response); var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response); diff --git a/AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs b/AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs index 192cd23..434d303 100644 --- a/AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs +++ b/AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs @@ -192,17 +192,16 @@ namespace AMWD.Protocols.Modbus.Serial public Task StopAsync(CancellationToken cancellationToken = default) { Assertions(); - return StopAsyncInternal(cancellationToken); + StopAsyncInternal(); + return Task.CompletedTask; } - private Task StopAsyncInternal(CancellationToken cancellationToken) + private void StopAsyncInternal() { _stopCts?.Cancel(); _serialPort.Close(); _serialPort.DataReceived -= OnDataReceived; - - return Task.CompletedTask; } /// @@ -215,7 +214,7 @@ namespace AMWD.Protocols.Modbus.Serial _isDisposed = true; - StopAsyncInternal(CancellationToken.None).Wait(); + StopAsyncInternal(); _serialPort.Dispose(); _stopCts?.Dispose(); @@ -332,7 +331,7 @@ namespace AMWD.Protocols.Modbus.Serial responseBytes.AddRange(requestBytes.Take(2)); try { - var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken); + var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)]; for (int i = 0; i < coils.Count; i++) @@ -371,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Serial responseBytes.AddRange(requestBytes.Take(2)); try { - var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken); + var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)]; for (int i = 0; i < discreteInputs.Count; i++) @@ -410,7 +409,7 @@ namespace AMWD.Protocols.Modbus.Serial responseBytes.AddRange(requestBytes.Take(2)); try { - var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken); + var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] values = new byte[holdingRegisters.Count * 2]; for (int i = 0; i < holdingRegisters.Count; i++) @@ -444,7 +443,7 @@ namespace AMWD.Protocols.Modbus.Serial responseBytes.AddRange(requestBytes.Take(2)); try { - var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken); + var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] values = new byte[count * 2]; for (int i = 0; i < count; i++) @@ -492,7 +491,7 @@ namespace AMWD.Protocols.Modbus.Serial LowByte = requestBytes[5], }; - bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken); + bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (isSuccess) { // Response is an echo of the request @@ -531,7 +530,7 @@ namespace AMWD.Protocols.Modbus.Serial LowByte = requestBytes[5] }; - bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken); + bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (isSuccess) { // Response is an echo of the request @@ -591,7 +590,7 @@ namespace AMWD.Protocols.Modbus.Serial }); } - bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken); + bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (isSuccess) { // Response is an echo of the request @@ -648,7 +647,7 @@ namespace AMWD.Protocols.Modbus.Serial }); } - bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken); + bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (isSuccess) { // Response is an echo of the request @@ -705,7 +704,7 @@ namespace AMWD.Protocols.Modbus.Serial try { - var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken); + var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); var bodyBytes = new List(); @@ -855,5 +854,21 @@ namespace AMWD.Protocols.Modbus.Serial } #endregion Request Handling + + /// + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine($"RTU Proxy"); + sb.AppendLine($" {nameof(PortName)}: {PortName}"); + sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}"); + sb.AppendLine($" {nameof(DataBits)}: {DataBits}"); + sb.AppendLine($" {nameof(StopBits)}: {StopBits}"); + sb.AppendLine($" {nameof(Parity)}: {Parity}"); + sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}"); + + return sb.ToString(); + } } } diff --git a/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs b/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs index a091c39..d66800e 100644 --- a/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs +++ b/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs @@ -31,8 +31,7 @@ namespace AMWD.Protocols.Modbus.Serial private readonly Task _processingTask; private readonly AsyncQueue _requestQueue = new(); - // Only required to cover all logic branches on unit tests. - private bool _isUnitTest = false; + private readonly bool _isLinux; #endregion Fields @@ -41,6 +40,8 @@ namespace AMWD.Protocols.Modbus.Serial /// public ModbusSerialConnection(string portName) { + _isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + if (string.IsNullOrWhiteSpace(portName)) throw new ArgumentNullException(nameof(portName)); @@ -268,7 +269,7 @@ namespace AMWD.Protocols.Modbus.Serial try { // Get next request to process - var item = await _requestQueue.DequeueAsync(cancellationToken); + var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); // Remove registration => already removed from queue item.CancellationTokenRegistration.Dispose(); @@ -276,13 +277,13 @@ namespace AMWD.Protocols.Modbus.Serial // Build combined cancellation token using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token); // Wait for exclusive access - await _portLock.WaitAsync(linkedCts.Token); + await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); try { // Ensure connection is up await AssertConnection(linkedCts.Token); - await _serialPort.WriteAsync(item.Request, linkedCts.Token); + await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); linkedCts.Token.ThrowIfCancellationRequested(); @@ -291,7 +292,7 @@ namespace AMWD.Protocols.Modbus.Serial do { - int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token); + int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); if (readCount < 1) throw new EndOfStreamException(); @@ -322,7 +323,7 @@ namespace AMWD.Protocols.Modbus.Serial _portLock.Release(); _idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan); - await Task.Delay(InterRequestDelay, cancellationToken); + await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) @@ -353,7 +354,7 @@ namespace AMWD.Protocols.Modbus.Serial _serialPort.Close(); _serialPort.ResetRS485DriverStateFlags(); - if (DriverEnabledRS485 && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || _isUnitTest)) + if (DriverEnabledRS485 && _isLinux) { var flags = _serialPort.GetRS485DriverStateFlags(); flags |= RS485Flags.Enabled; @@ -361,7 +362,7 @@ namespace AMWD.Protocols.Modbus.Serial _serialPort.ChangeRS485DriverStateFlags(flags); } - using var connectTask = Task.Run(_serialPort.Open); + using var connectTask = Task.Run(_serialPort.Open, cancellationToken); if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask) { await connectTask; @@ -379,7 +380,7 @@ namespace AMWD.Protocols.Modbus.Serial try { - await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch { /* keep it quiet */ } diff --git a/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs b/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs index a9b6d94..278eb89 100644 --- a/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs +++ b/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs @@ -12,7 +12,7 @@ namespace System.IO int offset = 0; do { - int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken); + int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (count < 1) throw new EndOfStreamException(); @@ -30,7 +30,7 @@ namespace System.IO int offset = 0; do { - int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken); + int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (count < 1) throw new EndOfStreamException(); diff --git a/AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs b/AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs new file mode 100644 index 0000000..367b77b --- /dev/null +++ b/AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace AMWD.Protocols.Modbus.Tcp.Extensions +{ + internal static class TaskExtensions + { + public static async void Forget(this Task task) + { + try + { + await task; + } + catch + { /* keep it quiet */ } + } + } +} diff --git a/AMWD.Protocols.Modbus.Tcp/ModbusTcpConnection.cs b/AMWD.Protocols.Modbus.Tcp/ModbusTcpConnection.cs index ae8b7f0..7713aa5 100644 --- a/AMWD.Protocols.Modbus.Tcp/ModbusTcpConnection.cs +++ b/AMWD.Protocols.Modbus.Tcp/ModbusTcpConnection.cs @@ -208,7 +208,7 @@ namespace AMWD.Protocols.Modbus.Tcp try { // Get next request to process - var item = await _requestQueue.DequeueAsync(cancellationToken); + var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); // Remove registration => already removed from queue item.CancellationTokenRegistration.Dispose(); @@ -216,19 +216,19 @@ namespace AMWD.Protocols.Modbus.Tcp // Build combined cancellation token using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token); // Wait for exclusive access - await _clientLock.WaitAsync(linkedCts.Token); + await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); try { // Ensure connection is up - await AssertConnection(linkedCts.Token); + await AssertConnection(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); var stream = _tcpClient.GetStream(); await stream.FlushAsync(linkedCts.Token); #if NET6_0_OR_GREATER - await stream.WriteAsync(item.Request, linkedCts.Token); + await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); #else - await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token); + await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); #endif linkedCts.Token.ThrowIfCancellationRequested(); @@ -239,9 +239,9 @@ namespace AMWD.Protocols.Modbus.Tcp do { #if NET6_0_OR_GREATER - int readCount = await stream.ReadAsync(buffer, linkedCts.Token); + int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); #else - int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token); + int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false); #endif if (readCount < 1) throw new EndOfStreamException(); @@ -332,7 +332,7 @@ namespace AMWD.Protocols.Modbus.Tcp try { - await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch { /* keep it quiet */ } diff --git a/AMWD.Protocols.Modbus.Tcp/ModbusTcpProxy.cs b/AMWD.Protocols.Modbus.Tcp/ModbusTcpProxy.cs index 3e985c8..9460c4d 100644 --- a/AMWD.Protocols.Modbus.Tcp/ModbusTcpProxy.cs +++ b/AMWD.Protocols.Modbus.Tcp/ModbusTcpProxy.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using AMWD.Protocols.Modbus.Common; using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Protocols; +using AMWD.Protocols.Modbus.Tcp.Extensions; using AMWD.Protocols.Modbus.Tcp.Utils; namespace AMWD.Protocols.Modbus.Tcp @@ -17,7 +18,12 @@ namespace AMWD.Protocols.Modbus.Tcp /// /// Implements a Modbus TCP server proxying all requests to a Modbus client of choice. /// - public class ModbusTcpProxy : IModbusProxy + /// + /// Initializes a new instance of the class. + /// + /// The used to request the remote device, that should be proxied. + /// An to listen on. + public class ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress) : IModbusProxy { #region Fields @@ -25,30 +31,17 @@ namespace AMWD.Protocols.Modbus.Tcp private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100); - private TcpListenerWrapper _tcpListener; + private readonly TcpListenerWrapper _tcpListener = new(listenAddress, 502); private CancellationTokenSource _stopCts; private Task _clientConnectTask = Task.CompletedTask; private readonly SemaphoreSlim _clientListLock = new(1, 1); private readonly List _clients = []; - private readonly List _clientTasks = []; #endregion Fields #region Constructors - /// - /// Initializes a new instance of the class. - /// - /// The used to request the remote device, that should be proxied. - /// An to listen on. - public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress) - { - Client = client ?? throw new ArgumentNullException(nameof(client)); - - _tcpListener = new TcpListenerWrapper(listenAddress, 502); - } - #endregion Constructors #region Properties @@ -56,7 +49,7 @@ namespace AMWD.Protocols.Modbus.Tcp /// /// Gets the Modbus client used to request the remote device, that should be proxied. /// - public ModbusClientBase Client { get; } + public ModbusClientBase Client { get; } = client ?? throw new ArgumentNullException(nameof(client)); /// /// Gets the to listen on. @@ -140,16 +133,7 @@ namespace AMWD.Protocols.Modbus.Tcp try { - await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)); - } - catch (OperationCanceledException) - { - // Terminated - } - - try - { - await Task.WhenAny(Task.WhenAll(_clientTasks), Task.Delay(Timeout.Infinite, cancellationToken)); + await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(continueOnCapturedContext: false); } catch (OperationCanceledException) { @@ -174,6 +158,7 @@ namespace AMWD.Protocols.Modbus.Tcp _tcpListener.Dispose(); _stopCts?.Dispose(); + GC.SuppressFinalize(this); } private void Assertions() @@ -196,12 +181,13 @@ namespace AMWD.Protocols.Modbus.Tcp { try { - var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken); - await _clientListLock.WaitAsync(cancellationToken); + var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); + await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { _clients.Add(client); - _clientTasks.Add(HandleClientAsync(client, cancellationToken)); + // Can be ignored as it will terminate by itself on cancellation + HandleClientAsync(client, cancellationToken).Forget(); } finally { @@ -227,20 +213,20 @@ namespace AMWD.Protocols.Modbus.Tcp using (var cts = new CancellationTokenSource(ReadWriteTimeout)) using (cancellationToken.Register(cts.Cancel)) { - byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token); + byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token).ConfigureAwait(continueOnCapturedContext: false); requestBytes.AddRange(headerBytes); ushort length = headerBytes .Skip(4).Take(2).ToArray() .GetBigEndianUInt16(); - byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token); + byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token).ConfigureAwait(continueOnCapturedContext: false); requestBytes.AddRange(bodyBytes); } - byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken); + byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (responseBytes != null) - await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken); + await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } } catch @@ -249,7 +235,7 @@ namespace AMWD.Protocols.Modbus.Tcp } finally { - await _clientListLock.WaitAsync(cancellationToken); + await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { _clients.Remove(client); @@ -324,7 +310,7 @@ namespace AMWD.Protocols.Modbus.Tcp responseBytes.AddRange(requestBytes.Take(8)); try { - var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken); + var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)]; for (int i = 0; i < coils.Count; i++) @@ -363,7 +349,7 @@ namespace AMWD.Protocols.Modbus.Tcp responseBytes.AddRange(requestBytes.Take(8)); try { - var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken); + var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)]; for (int i = 0; i < discreteInputs.Count; i++) @@ -402,7 +388,7 @@ namespace AMWD.Protocols.Modbus.Tcp responseBytes.AddRange(requestBytes.Take(8)); try { - var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken); + var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] values = new byte[holdingRegisters.Count * 2]; for (int i = 0; i < holdingRegisters.Count; i++) @@ -436,7 +422,7 @@ namespace AMWD.Protocols.Modbus.Tcp responseBytes.AddRange(requestBytes.Take(8)); try { - var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken); + var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] values = new byte[count * 2]; for (int i = 0; i < count; i++) @@ -484,7 +470,7 @@ namespace AMWD.Protocols.Modbus.Tcp LowByte = requestBytes[11], }; - bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken); + bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (isSuccess) { // Response is an echo of the request @@ -524,7 +510,7 @@ namespace AMWD.Protocols.Modbus.Tcp LowByte = requestBytes[11] }; - bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken); + bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (isSuccess) { // Response is an echo of the request @@ -584,7 +570,7 @@ namespace AMWD.Protocols.Modbus.Tcp }); } - bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken); + bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (isSuccess) { // Response is an echo of the request @@ -641,7 +627,7 @@ namespace AMWD.Protocols.Modbus.Tcp }); } - bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken); + bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (isSuccess) { // Response is an echo of the request @@ -698,7 +684,7 @@ namespace AMWD.Protocols.Modbus.Tcp try { - var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken); + var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); var bodyBytes = new List(); @@ -761,7 +747,7 @@ namespace AMWD.Protocols.Modbus.Tcp } } - private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification) + private static byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification) { var result = new List { objectId }; switch ((ModbusDeviceIdentificationObject)objectId) @@ -851,5 +837,18 @@ namespace AMWD.Protocols.Modbus.Tcp } #endregion Request Handling + + /// + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine($"TCP Proxy"); + sb.AppendLine($" {nameof(ListenAddress)}: {ListenAddress}"); + sb.AppendLine($" {nameof(ListenPort)}: {ListenPort}"); + sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}"); + + return sb.ToString(); + } } } diff --git a/AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapperFactory.cs b/AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapperFactory.cs index 2e396dd..9f67cd2 100644 --- a/AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapperFactory.cs +++ b/AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapperFactory.cs @@ -3,6 +3,9 @@ using System.Net.Sockets; namespace AMWD.Protocols.Modbus.Tcp.Utils { + /// + /// Factory for creating instances. + /// [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal class TcpClientWrapperFactory {