aspnetcore/test/Microsoft.TestCommon/PortReserver.cs

136 lines
4.7 KiB
C#

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading;
namespace Microsoft.TestCommon
{
/// <summary>
/// This class allocates ports while ensuring that:
/// 1. Ports that are permanentaly taken (or taken for the duration of the test) are not being attempted to be used.
/// 2. Ports are not shared across different tests (but you can allocate two different ports in the same test).
///
/// Gotcha: If another application grabs a port during the test, we have a race condition.
/// </summary>
[DebuggerDisplay("Port: {PortNumber}, Port count for this app domain: {_appDomainOwnedPorts.Count}")]
public class PortReserver : IDisposable
{
private Mutex _portMutex;
// We use this list to hold on to all the ports used because the Mutex will be blown through on the same thread.
// Theoretically we can do a thread local hashset, but that makes dispose thread dependant, or requires more complicated concurrency checks.
// Since practically there is no perf issue or concern here, this keeps the code the simplest possible.
private static HashSet<int> _appDomainOwnedPorts = new HashSet<int>();
public int PortNumber
{
get;
private set;
}
public PortReserver(int basePort = 50231)
{
if (basePort <= 0)
{
throw new InvalidOperationException();
}
// Grab a cross appdomain/cross process/cross thread lock, to ensure only one port is reserved at a time.
using (Mutex mutex = GetGlobalMutex())
{
try
{
int port = basePort - 1;
while (true)
{
port++;
if (port > 65535)
{
throw new InvalidOperationException("Exceeded port range");
}
// AppDomainOwnedPorts check enables reserving two ports from the same thread in sequence.
// ListUsedTCPPort prevents port contention with other apps.
if (_appDomainOwnedPorts.Contains(port) ||
ListUsedTCPPort().Any(endPoint => endPoint.Port == port))
{
continue;
}
string mutexName = "WebStack-Port-" + port.ToString(CultureInfo.InvariantCulture); // Create a well known mutex
_portMutex = new Mutex(initiallyOwned: false, name: mutexName);
// If no one else is using this port grab it.
if (_portMutex.WaitOne(millisecondsTimeout: 0))
{
break;
}
// dispose this mutex since the port it represents is not available.
_portMutex.Dispose();
_portMutex = null;
}
PortNumber = port;
_appDomainOwnedPorts.Add(port);
}
finally
{
mutex.ReleaseMutex();
}
}
}
public string BaseUri
{
get
{
return String.Format(CultureInfo.InvariantCulture, "http://localhost:{0}/", PortNumber);
}
}
public void Dispose()
{
if (PortNumber == -1)
{
// Object already disposed
return;
}
using (Mutex mutex = GetGlobalMutex())
{
_portMutex.Dispose();
_appDomainOwnedPorts.Remove(PortNumber);
PortNumber = -1;
}
}
private static Mutex GetGlobalMutex()
{
Mutex mutex = new Mutex(initiallyOwned: false, name: "WebStack-RandomPortAcquisition");
if (!mutex.WaitOne(30000))
{
throw new InvalidOperationException();
}
return mutex;
}
private static IPEndPoint[] ListUsedTCPPort()
{
var usedPort = new HashSet<int>();
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
return ipGlobalProperties.GetActiveTcpListeners();
}
}
}