use std::io::{IsTerminal, Write};
use std::net::TcpListener;
const SEARCH_RANGE: u16 = 64;
pub fn listener(host: &str, port: u16) -> anyhow::Result<(TcpListener, u16)> {
let mut port = port;
loop {
match TcpListener::bind((host, port)) {
Ok(lst) => return Ok((lst, port)),
Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
port = ask(host, port)?;
}
Err(e) => {
return Err(anyhow::Error::new(e).context(format!("cannot bind {host}:{port}")))
}
}
}
}
fn ask(host: &str, port: u16) -> anyhow::Result<u16> {
let s = Style::detect();
let free = free_port(host, port);
eprintln!();
eprintln!(
" {r}{b}Port {port} is already in use.{x}",
r = s.red,
b = s.bold,
x = s.reset
);
eprintln!(
" {d}Something else is listening on {host}:{port}.{x}",
d = s.dim,
x = s.reset
);
eprintln!();
if !std::io::stdin().is_terminal() {
match free {
Some(free) => anyhow::bail!("port {port} is already in use — try --port {free}"),
None => anyhow::bail!("port {port} is already in use"),
}
}
let prompt = match free {
Some(free) => format!(" Start on port {free} instead? [Y/n/port] "),
None => " Start on another port? [n/port] ".to_string(),
};
loop {
eprint!("{prompt}");
let _ = std::io::stderr().flush();
let mut line = String::new();
if std::io::stdin().read_line(&mut line)? == 0 {
anyhow::bail!("port {port} is already in use");
}
let answer = line.trim();
match answer.to_ascii_lowercase().as_str() {
"" | "y" | "yes" => match free {
Some(free) => return Ok(free),
None => eprintln!(
" {d}Enter a port number, or n to give up.{x}",
d = s.dim,
x = s.reset
),
},
"n" | "no" => anyhow::bail!("port {port} is already in use"),
_ => match answer.parse::<u16>() {
Ok(0) => eprintln!(
" {d}0 would pick a random port; name one instead.{x}",
d = s.dim,
x = s.reset
),
Ok(p) => return Ok(p),
Err(_) => eprintln!(" {d}Not a port number.{x}", d = s.dim, x = s.reset),
},
}
}
}
fn free_port(host: &str, port: u16) -> Option<u16> {
(port.saturating_add(1)..=port.saturating_add(SEARCH_RANGE))
.find(|p| TcpListener::bind((host, *p)).is_ok())
}
struct Style {
bold: &'static str,
red: &'static str,
dim: &'static str,
reset: &'static str,
}
impl Style {
fn detect() -> Self {
if std::io::stderr().is_terminal() {
Style {
bold: "\x1b[1m",
red: "\x1b[31m",
dim: "\x1b[2m",
reset: "\x1b[0m",
}
} else {
Style {
bold: "",
red: "",
dim: "",
reset: "",
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_free_port_is_claimed_as_asked() {
let (lst, port) = listener("127.0.0.1", 0).unwrap();
assert_eq!(port, 0);
assert!(lst.local_addr().unwrap().port() != 0);
}
#[test]
fn the_offered_port_is_above_the_taken_one_and_actually_free() {
let taken = TcpListener::bind("127.0.0.1:0").unwrap();
let port = taken.local_addr().unwrap().port();
let free = free_port("127.0.0.1", port).expect("no free port nearby");
assert!(free > port);
assert!(TcpListener::bind(("127.0.0.1", free)).is_ok());
}
}