use std::io::Write;
use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use anyhow::{Context, Result};
use crate::engine::snapshot::SnapshotStore;
use super::Hub;
pub(crate) const ACCEPT_POLL: Duration = Duration::from_millis(200);
pub(crate) fn accept_until(
name: &str,
listener: TcpListener,
stop: Option<Arc<AtomicBool>>,
mut on_accept: impl FnMut(TcpStream, SocketAddr),
) {
if stop.is_some()
&& let Err(e) = listener.set_nonblocking(true)
{
log::error!("{name}: set_nonblocking failed ({e}); port will not be shutdown-aware");
}
loop {
if let Some(s) = &stop
&& s.load(Ordering::Relaxed)
{
log::info!("{name} server shutting down (port released)");
return;
}
match listener.accept() {
Ok((stream, peer)) => {
if stop.is_some() {
let _ = stream.set_nonblocking(false);
}
on_accept(stream, peer);
}
Err(ref e) if stop.is_some() && e.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(ACCEPT_POLL);
}
Err(e) => {
log::error!("{name} accept failed: {e}");
return;
}
}
}
}
pub const CANBOAT_FORMAT_FAST_HEADER: &[u8] = b"# format=FAST\n";
pub fn spawn_snapshot(
bind: Ipv4Addr,
port: u16,
store: Arc<SnapshotStore>,
header: Option<&'static [u8]>,
stop: Option<Arc<AtomicBool>>,
) -> Result<JoinHandle<()>> {
let listener = TcpListener::bind(SocketAddrV4::new(bind, port))
.with_context(|| format!("binding snapshot TCP port {}:{}", bind, port))?;
log::info!("snapshot server listening on {}:{}", bind, port);
Ok(thread::Builder::new()
.name("snapshot-accept".into())
.spawn(move || snapshot_accept(listener, store, header, stop))
.expect("spawn snapshot accept"))
}
fn snapshot_accept(
listener: TcpListener,
store: Arc<SnapshotStore>,
header: Option<&'static [u8]>,
stop: Option<Arc<AtomicBool>>,
) {
accept_until("snapshot", listener, stop, |stream, peer| {
log::info!("snapshot client connected: {peer}");
let s = store.clone();
thread::Builder::new()
.name("snapshot-client".into())
.spawn(move || run_snapshot_client(stream, s, header))
.ok();
});
}
fn run_snapshot_client(
mut stream: TcpStream,
store: Arc<SnapshotStore>,
header: Option<&'static [u8]>,
) {
if let Err(e) = stream.shutdown(Shutdown::Read) {
log::debug!("snapshot: shutdown(read) failed: {e}");
}
if let Some(bytes) = header
&& stream.write_all(bytes).is_err()
{
return;
}
let doc = store.snapshot();
let _ = stream.write_all(doc.as_bytes());
let _ = stream.flush();
let _ = stream.shutdown(Shutdown::Both);
}
pub fn spawn_ais_snapshot(
bind: Ipv4Addr,
port: u16,
store: Arc<SnapshotStore>,
stop: Option<Arc<AtomicBool>>,
) -> Result<JoinHandle<()>> {
let listener = TcpListener::bind(SocketAddrV4::new(bind, port))
.with_context(|| format!("binding ais TCP port {}:{}", bind, port))?;
log::info!("ais server listening on {}:{}", bind, port);
Ok(thread::Builder::new()
.name("ais-accept".into())
.spawn(move || ais_snapshot_accept(listener, store, stop))
.expect("spawn ais accept"))
}
fn ais_snapshot_accept(
listener: TcpListener,
store: Arc<SnapshotStore>,
stop: Option<Arc<AtomicBool>>,
) {
accept_until("ais", listener, stop, |stream, peer| {
log::info!("ais client connected: {peer}");
let s = store.clone();
thread::Builder::new()
.name("ais-client".into())
.spawn(move || run_ais_snapshot_client(stream, s))
.ok();
});
}
fn run_ais_snapshot_client(mut stream: TcpStream, store: Arc<SnapshotStore>) {
if let Err(e) = stream.shutdown(Shutdown::Read) {
log::debug!("ais: shutdown(read) failed: {e}");
}
let doc = store.ais_snapshot();
let _ = stream.write_all(doc.as_bytes());
let _ = stream.flush();
let _ = stream.shutdown(Shutdown::Both);
}
pub fn spawn_stream_server(
name: &'static str,
bind: Ipv4Addr,
port: u16,
hub: Arc<Hub>,
header: Option<&'static [u8]>,
stop: Option<Arc<AtomicBool>>,
) -> Result<JoinHandle<()>> {
let listener = TcpListener::bind(SocketAddrV4::new(bind, port))
.with_context(|| format!("binding {name} TCP port {}:{}", bind, port))?;
log::info!("{name} server listening on {}:{} (RO)", bind, port);
Ok(thread::Builder::new()
.name(format!("{name}-accept"))
.spawn(move || stream_accept(name, listener, hub, header, stop))
.expect("spawn stream accept"))
}
fn stream_accept(
name: &'static str,
listener: TcpListener,
hub: Arc<Hub>,
header: Option<&'static [u8]>,
stop: Option<Arc<AtomicBool>>,
) {
accept_until(name, listener, stop, |stream, peer| {
log::info!("{name} client connected: {peer}");
let h = hub.clone();
thread::Builder::new()
.name(format!("{name}-client"))
.spawn(move || run_stream_client(name, stream, h, header))
.ok();
});
}
fn run_stream_client(
name: &'static str,
stream: TcpStream,
hub: Arc<Hub>,
header: Option<&'static [u8]>,
) {
if let Err(e) = stream.shutdown(Shutdown::Read) {
log::debug!("{name}: shutdown(read) failed: {e}");
}
let rx = hub.subscribe();
let mut stream = stream;
if let Some(bytes) = header
&& stream.write_all(bytes).is_err()
{
return;
}
const MAX_BATCH: usize = 64 * 1024;
let mut batch: Vec<u8> = Vec::with_capacity(4096);
while let Ok(line) = rx.recv() {
batch.clear();
batch.extend_from_slice(line.as_bytes());
while batch.len() < MAX_BATCH {
match rx.try_recv() {
Ok(line) => batch.extend_from_slice(line.as_bytes()),
Err(_) => break,
}
}
if stream.write_all(&batch).is_err() {
break;
}
}
drop(stream);
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::TcpStream;
use std::sync::atomic::AtomicBool;
fn spawn_on_a_free_port(stop: &Arc<AtomicBool>) -> (u16, JoinHandle<()>) {
for _ in 0..16 {
let port = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
.expect("reserve an ephemeral port")
.local_addr()
.expect("ephemeral listener has a local address")
.port();
match spawn_stream_server(
"test",
Ipv4Addr::LOCALHOST,
port,
Arc::new(Hub::new()),
None,
Some(stop.clone()),
) {
Ok(join) => return (port, join),
Err(e) if is_addr_in_use(&e) => continue,
Err(e) => panic!("bind test stream server: {e:#}"),
}
}
panic!("no ephemeral port stayed free across 16 attempts");
}
fn is_addr_in_use(e: &anyhow::Error) -> bool {
e.chain()
.filter_map(|c| c.downcast_ref::<std::io::Error>())
.any(|io| io.kind() == std::io::ErrorKind::AddrInUse)
}
#[test]
fn shutdown_aware_listener_releases_its_port() {
let stop = Arc::new(AtomicBool::new(false));
let (port, join) = spawn_on_a_free_port(&stop);
let mut connected = false;
for _ in 0..100 {
if TcpStream::connect((Ipv4Addr::LOCALHOST, port)).is_ok() {
connected = true;
break;
}
thread::sleep(Duration::from_millis(20));
}
assert!(connected, "server should accept connections while running");
stop.store(true, Ordering::Relaxed);
join.join().expect("accept thread joins after shutdown");
let rebind = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port));
assert!(
rebind.is_ok(),
"port must be re-bindable after shutdown: {:?}",
rebind.err()
);
}
}