use rand::Rng;
use std::collections::HashSet;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener};
use std::sync::{LazyLock, Mutex};
static ALLOCATED_PORTS: LazyLock<Mutex<HashSet<u16>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
#[derive(Debug)]
pub struct FreePort(u16);
impl FreePort {
pub fn new() -> Self {
let mut rng = rand::thread_rng();
for _ in 0..16 {
let port = rng.gen_range(8000..=65000);
{
let allocated = ALLOCATED_PORTS.lock().unwrap();
if allocated.contains(&port) {
continue;
}
}
if let Ok(listener) = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)) {
drop(listener);
{
let mut allocated = ALLOCATED_PORTS.lock().unwrap();
if allocated.insert(port) {
return FreePort(port);
}
}
}
}
panic!("Unable to find a free port after 16 attempts");
}
pub fn port(&self) -> u16 {
self.0
}
pub fn as_addr(&self) -> SocketAddr {
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, self.0))
}
}
impl Drop for FreePort {
fn drop(&mut self) {
let mut allocated = ALLOCATED_PORTS.lock().unwrap();
allocated.remove(&self.0);
}
}
impl Default for FreePort {
fn default() -> Self {
FreePort::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_free_port_allocation() {
let port = FreePort::new();
assert!(port.port() >= 8000 && port.port() <= 65000);
{
let allocated = ALLOCATED_PORTS.lock().unwrap();
assert!(allocated.contains(&port.port()));
}
}
#[test]
fn test_free_port_release() {
let port_num = {
let port = FreePort::new();
let port_num = port.port();
{
let allocated = ALLOCATED_PORTS.lock().unwrap();
assert!(allocated.contains(&port_num));
}
port_num
};
{
let allocated = ALLOCATED_PORTS.lock().unwrap();
assert!(!allocated.contains(&port_num));
}
}
#[test]
fn test_multiple_ports_no_conflict() {
let port1 = FreePort::new();
let port2 = FreePort::new();
let port3 = FreePort::new();
assert_ne!(port1.port(), port2.port());
assert_ne!(port1.port(), port3.port());
assert_ne!(port2.port(), port3.port());
{
let allocated = ALLOCATED_PORTS.lock().unwrap();
assert!(allocated.contains(&port1.port()));
assert!(allocated.contains(&port2.port()));
assert!(allocated.contains(&port3.port()));
}
}
#[test]
fn test_concurrent_allocation() {
let handles: Vec<_> = (0..10)
.map(|_| {
thread::spawn(|| {
let port = FreePort::new();
thread::sleep(std::time::Duration::from_millis(10));
port.port()
})
})
.collect();
let ports: Vec<u16> = handles.into_iter().map(|h| h.join().unwrap()).collect();
let mut unique_ports = HashSet::new();
for port in &ports {
assert!(
unique_ports.insert(*port),
"Port {} was allocated twice",
port
);
}
assert_eq!(ports.len(), 10);
}
#[test]
fn test_as_addr_format() {
let port = FreePort::new();
let expected = format!("127.0.0.1:{}", port.port());
assert_eq!(port.as_addr().to_string(), expected);
}
}