using System.Net;
using System.Net.Sockets;
using Opc.Ua;
using Opc.Ua.Server;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
namespace ZB.MOM.WW.OtOpcUa;
///
/// Starts an on a freshly probed port, retrying on a bind race.
/// Probing a free port (bind 0, read, release) and handing it to the SDK later is a TOCTOU: under a
/// highly parallel test run another test can take the port in between, and the SDK then fails with
/// Failed to establish tcp listener sockets for Ipv4 and IPv6.. A
/// that failed to start is not safe to start again, so every attempt builds a NEW host and server on
/// a NEW port and the caller reads the port off the options it builds, never from a cached value.
/// This file is compiled into each test project that boots a real SDK server (linked, not a library).
///
internal static class OpcUaHostStart
{
private const int DefaultAttempts = 5;
/// Probes a currently free loopback TCP port. Racy by nature - pair with .
public static int AllocateFreePort()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
return ((IPEndPoint)listener.LocalEndpoint).Port;
}
///
/// Builds a host (via , given the port to bind) and a server (via
/// ) and starts them, retrying with a new port, host and server when
/// the bind fails. Any other failure, or the last bind failure, propagates.
///
/// The started host and server. The caller owns disposal of the host.
public static async Task<(OpcUaApplicationHost Host, TServer Server)> StartAsync(
Func createHost,
Func createServer,
CancellationToken ct,
int maxAttempts = DefaultAttempts)
where TServer : StandardServer
{
for (var attempt = 1; ; attempt++)
{
var host = createHost(AllocateFreePort());
var server = createServer();
try
{
await host.StartAsync(server, ct);
return (host, server);
}
catch (Exception ex) when (attempt < maxAttempts && IsBindFailure(ex))
{
try { await host.DisposeAsync(); }
catch { /* the half-started host is being discarded */ }
}
catch
{
try { await host.DisposeAsync(); }
catch { /* surface the original failure */ }
throw;
}
}
}
/// True when the exception chain says the OPC UA listener could not bind its port.
public static bool IsBindFailure(Exception? ex)
{
for (; ex is not null; ex = ex.InnerException)
{
if (ex is SocketException { SocketErrorCode: SocketError.AddressAlreadyInUse }) return true;
if (ex is ServiceResultException sre
&& (sre.StatusCode == StatusCodes.BadNoCommunication
|| sre.Message.Contains("Failed to establish tcp listener", StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
return false;
}
}