File: src\Servers\Kestrel\shared\test\ServerRetryHelper.cs
Web Access
Project: src\src\Servers\Kestrel\Transport.Quic\test\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj (Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net;
using System.Net.NetworkInformation;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.InternalTesting;
 
public static class ServerRetryHelper
{
    private const int RetryCount = 20;
 
    /// <summary>
    /// Retry a func. Useful when a test needs an explicit port and you want to avoid port conflicts.
    /// </summary>
    public static async Task BindPortsWithRetry(Func<int, Task> retryFunc, ILogger logger)
    {
        var retryCount = 0;
 
        // Add a random number to starting port to reduce chance of conflicts because of multiple tests using this retry.
        var nextPortAttempt = 30000 + Random.Shared.Next(10000);
 
        while (true)
        {
            // Find a port that's available for TCP and UDP. Start with the given port search upwards from there.
            var port = GetAvailablePort(nextPortAttempt, logger);
 
            try
            {
                await retryFunc(port);
                break;
            }
            catch (Exception ex)
            {
                retryCount++;
                nextPortAttempt = port + Random.Shared.Next(100);
 
                if (retryCount >= RetryCount)
                {
                    throw;
                }
                else
                {
                    logger.LogError(ex, "Error running test {retryCount}. Retrying.", retryCount);
                }
            }
        }
    }
 
    private static int GetAvailablePort(int startingPort, ILogger logger)
    {
        logger.LogInformation("Searching for free port starting at {startingPort}.", startingPort);
 
        var unavailableEndpoints = new List<IPEndPoint>();
 
        var properties = IPGlobalProperties.GetIPGlobalProperties();
 
        // Ignore active connections
        AddEndpoints(startingPort, unavailableEndpoints, properties.GetActiveTcpConnections().Select(c => c.LocalEndPoint));
 
        // Ignore active tcp listners
        AddEndpoints(startingPort, unavailableEndpoints, properties.GetActiveTcpListeners());
 
        // Ignore active UDP listeners
        AddEndpoints(startingPort, unavailableEndpoints, properties.GetActiveUdpListeners());
 
        logger.LogInformation("Found {count} unavailable endpoints.", unavailableEndpoints.Count);
 
        for (var i = startingPort; i < ushort.MaxValue; i++)
        {
            var match = unavailableEndpoints.FirstOrDefault(ep => ep.Port == i);
            if (match == null)
            {
                logger.LogInformation("Port {i} free.", i);
                return i;
            }
            else
            {
                logger.LogInformation("Port {i} in use. End point: {match}", i, match);
            }
        }
 
        throw new Exception($"Couldn't find a free port after {startingPort}.");
 
        static void AddEndpoints(int startingPort, List<IPEndPoint> endpoints, IEnumerable<IPEndPoint> activeEndpoints)
        {
            foreach (IPEndPoint endpoint in activeEndpoints)
            {
                if (endpoint.Port >= startingPort)
                {
                    endpoints.Add(endpoint);
                }
            }
        }
    }
}