use anyhow::Result;
use rand::Rng;
use std::net::{SocketAddr, UdpSocket};
use tracing::{debug, info, warn};
const PORT_RANGE_START: u16 = 49152;
const PORT_RANGE_END: u16 = 65535;
const MAX_PORT_RETRIES: usize = 10;
#[derive(Debug, Clone)]
pub struct PortManager {
preferred_port: Option<u16>,
}
impl PortManager {
pub fn new() -> Self {
Self {
preferred_port: None,
}
}
pub fn with_preferred_port(port: u16) -> Self {
Self {
preferred_port: Some(port),
}
}
pub fn allocate_port(&mut self) -> Result<u16> {
if let Some(port) = self.preferred_port {
debug!("Trying preferred port: {}", port);
if self.is_port_available(port) {
info!("✅ Using preferred port: {}", port);
return Ok(port);
}
warn!("Preferred port {} not available", port);
}
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::from_entropy();
for attempt in 1..=MAX_PORT_RETRIES {
let port = rng.gen_range(PORT_RANGE_START..=PORT_RANGE_END);
debug!("Attempt {}: Trying random port {}", attempt, port);
if self.is_port_available(port) {
info!("✅ Allocated random port: {}", port);
self.preferred_port = Some(port); return Ok(port);
}
}
Err(anyhow::anyhow!(
"Failed to allocate port after {} attempts",
MAX_PORT_RETRIES
))
}
fn is_port_available(&self, port: u16) -> bool {
let ipv4_addr =
SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), port);
let ipv6_addr =
SocketAddr::new(std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), port);
match UdpSocket::bind(ipv4_addr) {
Ok(_socket) => {
debug!("Port {} available on IPv4", port);
true
}
Err(e) => {
debug!("Port {} unavailable on IPv4: {}", port, e);
match UdpSocket::bind(ipv6_addr) {
Ok(_socket) => {
debug!("Port {} available on IPv6", port);
true
}
Err(e2) => {
debug!("Port {} unavailable on IPv6: {}", port, e2);
false
}
}
}
}
}
pub fn get_preferred_port(&self) -> Option<u16> {
self.preferred_port
}
pub fn set_preferred_port(&mut self, port: u16) {
self.preferred_port = Some(port);
}
pub fn clear_preferred_port(&mut self) {
self.preferred_port = None;
}
}
impl Default for PortManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_port_manager_new() {
let pm = PortManager::new();
assert!(pm.get_preferred_port().is_none());
}
#[test]
fn test_port_manager_with_preferred() {
let pm = PortManager::with_preferred_port(50000);
assert_eq!(pm.get_preferred_port(), Some(50000));
}
#[test]
fn test_allocate_port_success() {
let mut pm = PortManager::new();
let port = pm.allocate_port().expect("should allocate port");
assert!((PORT_RANGE_START..=PORT_RANGE_END).contains(&port));
assert_eq!(pm.get_preferred_port(), Some(port));
}
#[test]
fn test_allocate_port_preferred() {
let mut pm = PortManager::with_preferred_port(63000);
let port = pm.allocate_port().expect("should allocate port");
assert!((PORT_RANGE_START..=PORT_RANGE_END).contains(&port));
}
#[test]
fn test_is_port_available_invalid() {
let pm = PortManager::new();
let _available = pm.is_port_available(80);
}
#[test]
fn test_set_clear_preferred() {
let mut pm = PortManager::new();
pm.set_preferred_port(55555);
assert_eq!(pm.get_preferred_port(), Some(55555));
pm.clear_preferred_port();
assert!(pm.get_preferred_port().is_none());
}
#[test]
fn test_default() {
let pm = PortManager::default();
assert!(pm.get_preferred_port().is_none());
}
#[test]
fn test_multiple_allocations() {
let mut pm1 = PortManager::new();
let mut pm2 = PortManager::new();
let port1 = pm1.allocate_port().expect("pm1 should allocate");
let port2 = pm2.allocate_port().expect("pm2 should allocate");
assert!((PORT_RANGE_START..=PORT_RANGE_END).contains(&port1));
assert!((PORT_RANGE_START..=PORT_RANGE_END).contains(&port2));
}
}