f02067d295
The tests probe a free port (bind 0, release) and start the SDK server later; under a highly parallel run another test can take the port in between and the SDK fails with 'Failed to establish tcp listener sockets for Ipv4 and IPv6.'. OpcUaHostStart.StartAsync builds a fresh host + server on a fresh port per attempt (up to 5) when the failure is a bind failure; all 45 start sites in OpcUaServer.Tests, OpcUaServer.IntegrationTests and Runtime.Tests use it. Per-attempt GUID ApplicationUris are hoisted out of the host factory because a changed URI against a reused PKI store fails with 'Cannot access private key'. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
82 lines
3.4 KiB
C#
82 lines
3.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Starts an <see cref="OpcUaApplicationHost"/> 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
|
|
/// <c>Failed to establish tcp listener sockets for Ipv4 and IPv6.</c>. A <see cref="StandardServer"/>
|
|
/// 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).
|
|
/// </summary>
|
|
internal static class OpcUaHostStart
|
|
{
|
|
private const int DefaultAttempts = 5;
|
|
|
|
/// <summary>Probes a currently free loopback TCP port. Racy by nature - pair with <see cref="StartAsync{TServer}"/>.</summary>
|
|
public static int AllocateFreePort()
|
|
{
|
|
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
return ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a host (via <paramref name="createHost"/>, given the port to bind) and a server (via
|
|
/// <paramref name="createServer"/>) and starts them, retrying with a new port, host and server when
|
|
/// the bind fails. Any other failure, or the last bind failure, propagates.
|
|
/// </summary>
|
|
/// <returns>The started host and server. The caller owns disposal of the host.</returns>
|
|
public static async Task<(OpcUaApplicationHost Host, TServer Server)> StartAsync<TServer>(
|
|
Func<int, OpcUaApplicationHost> createHost,
|
|
Func<TServer> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>True when the exception chain says the OPC UA listener could not bind its port.</summary>
|
|
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;
|
|
}
|
|
}
|