using System.Net;
using System.Net.Sockets;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
///
/// Loopback Modbus-TCP stub whose reply to each request is scripted by the test, and whose listener can
/// be stopped (connection refused) and restarted on the SAME port. Used by the transport self-heal tests
/// that need a device which disappears and comes back.
///
internal sealed class ScriptedModbusTcpServer : IAsyncDisposable
{
/// What the server does with one request.
public enum Action
{
/// Answer with an FC03-shaped response of zeroed registers.
Answer,
/// Read the request and never answer it (the client's per-op timeout fires).
Stall,
/// Close the socket without answering.
Drop,
}
private TcpListener? _listener;
private CancellationTokenSource _stop = new();
private int _requests;
private int _accepted;
/// Optional scripted reply. Receives the 0-based request index and the request PDU; returns
/// the raw bytes to write AFTER the MBAP TxId (i.e. the full ADU is built by the test), or null to fall
/// through to .
public Func? RawReply { get; set; }
/// Per-request action; defaults to .
public Func Script { get; set; } = _ => Action.Answer;
/// Gets the port the server listens on (stable across /).
public int Port { get; }
/// Gets the number of accepted TCP connections.
public int AcceptedConnections => Volatile.Read(ref _accepted);
/// Gets the number of requests received.
public int Requests => Volatile.Read(ref _requests);
public ScriptedModbusTcpServer()
{
var probe = new TcpListener(IPAddress.Loopback, 0);
probe.Start();
Port = ((IPEndPoint)probe.LocalEndpoint).Port;
probe.Stop();
Start();
}
/// (Re)starts listening on .
public void Start()
{
_stop = new CancellationTokenSource();
_listener = new TcpListener(IPAddress.Loopback, Port);
_listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_listener.Start();
var listener = _listener;
var token = _stop.Token;
_ = Task.Run(() => AcceptLoopAsync(listener, token));
}
/// Stops listening and closes every live connection — new connects are refused.
public void Stop()
{
try { _stop.Cancel(); } catch { }
try { _listener?.Stop(); } catch { }
_listener = null;
}
private async Task AcceptLoopAsync(TcpListener listener, CancellationToken token)
{
while (!token.IsCancellationRequested)
{
TcpClient client;
try { client = await listener.AcceptTcpClientAsync(token); }
catch { return; }
Interlocked.Increment(ref _accepted);
_ = Task.Run(() => ServeAsync(client, token));
}
}
private async Task ServeAsync(TcpClient client, CancellationToken token)
{
using var _ = client;
using var reg = token.Register(() => { try { client.Close(); } catch { } });
try
{
var stream = client.GetStream();
while (!token.IsCancellationRequested)
{
var header = new byte[7];
if (!await ReadExactly(stream, header, token)) return;
var len = (ushort)((header[4] << 8) | header[5]);
var pdu = new byte[len - 1];
if (!await ReadExactly(stream, pdu, token)) return;
var idx = Interlocked.Increment(ref _requests) - 1;
var raw = RawReply?.Invoke(idx, header, pdu);
if (raw is not null)
{
await stream.WriteAsync(raw, token);
await stream.FlushAsync(token);
continue;
}
switch (Script(idx))
{
case Action.Stall:
continue;
case Action.Drop:
client.Client.Shutdown(SocketShutdown.Both);
return;
}
var qty = (ushort)((pdu[3] << 8) | pdu[4]);
var respPdu = new byte[2 + qty * 2];
respPdu[0] = pdu[0];
respPdu[1] = (byte)(qty * 2);
await stream.WriteAsync(Adu(header, respPdu), token);
await stream.FlushAsync(token);
}
}
catch { /* connection torn down */ }
}
/// Builds a reply ADU echoing the request's TxId/unit around .
public static byte[] Adu(byte[] requestHeader, byte[] respPdu)
{
var respLen = (ushort)(1 + respPdu.Length);
var adu = new byte[7 + respPdu.Length];
adu[0] = requestHeader[0]; adu[1] = requestHeader[1];
adu[4] = (byte)(respLen >> 8); adu[5] = (byte)(respLen & 0xFF);
adu[6] = requestHeader[6];
Buffer.BlockCopy(respPdu, 0, adu, 7, respPdu.Length);
return adu;
}
private static async Task ReadExactly(Stream s, byte[] buf, CancellationToken token)
{
var read = 0;
while (read < buf.Length)
{
var n = await s.ReadAsync(buf.AsMemory(read), token);
if (n == 0) return false;
read += n;
}
return true;
}
public ValueTask DisposeAsync()
{
Stop();
return ValueTask.CompletedTask;
}
}