use std::io::ErrorKind;
use std::net::TcpListener;
pub const MAX_PORT_ATTEMPTS: u16 = 100;
pub fn find_available_port(host: &str, start_port: u16) -> anyhow::Result<(TcpListener, u16)> {
let mut port = start_port;
let mut attempts = 0;
loop {
let addr = format!("{}:{}", host, port);
match TcpListener::bind(&addr) {
Ok(listener) => return Ok((listener, port)),
Err(e) => {
attempts += 1;
if attempts >= MAX_PORT_ATTEMPTS {
return Err(anyhow::anyhow!(
"Failed to find available port after {} attempts (tried ports {}-{}): {}",
MAX_PORT_ATTEMPTS,
start_port,
port,
e
));
}
if e.kind() == ErrorKind::AddrInUse {
if port == u16::MAX {
return Err(anyhow::anyhow!(
"Port number reached maximum value while searching for available port"
));
}
let next_port = port + 1;
log::info!("Port {} is in use, trying port {}", port, next_port);
port = next_port;
} else {
return Err(anyhow::anyhow!("Failed to bind to {}: {}", addr, e));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_available_port_succeeds() {
let result = find_available_port("127.0.0.1", 19000);
assert!(result.is_ok());
let (listener, port) = result.unwrap();
assert!(port >= 19000);
drop(listener);
}
#[test]
fn test_find_available_port_increments_on_collision() {
let first_listener = TcpListener::bind("127.0.0.1:19100").unwrap();
let first_port = first_listener.local_addr().unwrap().port();
let result = find_available_port("127.0.0.1", first_port);
assert!(result.is_ok());
let (_second_listener, second_port) = result.unwrap();
assert!(second_port > first_port);
drop(first_listener);
}
#[test]
fn test_find_available_port_max_port_error() {
if let Ok(_blocker) = TcpListener::bind("127.0.0.1:65535") {
let result = find_available_port("127.0.0.1", 65535);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("maximum value"),
"Expected 'maximum value' error, got: {}",
err_msg
);
}
}
#[test]
fn test_find_available_port_non_addr_in_use_error() {
let result = find_available_port("999.999.999.999", 8080);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Failed to bind"),
"Expected 'Failed to bind' error, got: {}",
err_msg
);
assert!(
!err_msg.contains("attempts"),
"Should not have retried on non-AddrInUse error, got: {}",
err_msg
);
}
}