use std::net::{Ipv4Addr, SocketAddr, TcpListener};
use crate::config::FrameSection;
use crate::error::HostError;
pub const PREFERRED_PAGE_PORT: u16 = 4190;
#[derive(Debug)]
pub struct PageServer {
listener: TcpListener,
addr: SocketAddr,
}
impl PageServer {
pub fn resolve(frame: &FrameSection) -> Result<Self, HostError> {
match frame.bind {
Some(addr) => Self::bind_exact(addr),
None => Self::walk_forward(),
}
}
fn bind_exact(addr: SocketAddr) -> Result<Self, HostError> {
match TcpListener::bind(addr) {
Ok(listener) => Self::from_listener(listener),
Err(source) => Err(HostError::PageServerUnavailable { addr, source }),
}
}
fn walk_forward() -> Result<Self, HostError> {
for port in PREFERRED_PAGE_PORT..=u16::MAX {
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
match TcpListener::bind(addr) {
Ok(listener) => return Self::from_listener(listener),
Err(source) if source.kind() == std::io::ErrorKind::AddrInUse => {}
Err(source) => return Err(HostError::PageServerUnavailable { addr, source }),
}
}
Err(HostError::NoFreePagePort {
from: PREFERRED_PAGE_PORT,
})
}
fn from_listener(listener: TcpListener) -> Result<Self, HostError> {
let addr = listener.local_addr().map_err(|source| HostError::Bind {
addr: SocketAddr::from((Ipv4Addr::LOCALHOST, 0)),
source,
})?;
Ok(Self { listener, addr })
}
#[must_use]
pub const fn local_addr(&self) -> SocketAddr {
self.addr
}
#[must_use]
pub fn into_listener(self) -> TcpListener {
self.listener
}
}
#[cfg(test)]
mod tests {
use super::{PREFERRED_PAGE_PORT, PageServer};
use crate::config::FrameSection;
use crate::error::HostError;
use std::net::{Ipv4Addr, SocketAddr, TcpListener};
use std::path::PathBuf;
fn frame_section(bind: Option<SocketAddr>) -> FrameSection {
FrameSection {
bind,
assets: PathBuf::from("page/dist"),
auth_token: String::new(),
channel: Some("demo.events".to_owned()),
}
}
#[test]
fn walk_forward_prefers_4190_when_free() -> Result<(), Box<dyn std::error::Error>> {
let page = PageServer::resolve(&frame_section(None))?;
assert!(
page.local_addr().port() >= PREFERRED_PAGE_PORT,
"walk-forward must land at or after the preferred port"
);
Ok(())
}
#[test]
fn walk_forward_skips_a_squatted_preferred_port() -> Result<(), Box<dyn std::error::Error>> {
let squatter =
TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, PREFERRED_PAGE_PORT)));
let Ok(squatter) = squatter else {
return Ok(());
};
let page = PageServer::resolve(&frame_section(None))?;
assert!(
page.local_addr().port() > PREFERRED_PAGE_PORT,
"the walk must skip the squatted preferred port"
);
drop(squatter);
Ok(())
}
#[test]
fn explicit_bind_that_is_taken_fails_loudly() -> Result<(), Box<dyn std::error::Error>> {
let held = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))?;
let taken = held.local_addr()?;
let result = PageServer::resolve(&frame_section(Some(taken)));
let Err(HostError::PageServerUnavailable { addr, .. }) = result else {
return Err(format!("expected PageServerUnavailable, got {result:?}").into());
};
assert_eq!(addr, taken, "the refusal must name the taken address");
Ok(())
}
#[test]
fn explicit_bind_that_is_free_binds_exactly() -> Result<(), Box<dyn std::error::Error>> {
let scratch = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))?;
let free = scratch.local_addr()?;
drop(scratch);
let page = PageServer::resolve(&frame_section(Some(free)))?;
assert_eq!(
page.local_addr(),
free,
"an explicit free bind must bind exactly, never move"
);
Ok(())
}
}