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; } }