use crate::daemon::{DaemonCommand, DaemonState};
use crate::server::core::{CoreOptions, start_daemon_core};
use choreo_transport::key::TransportSecretKey;
#[cfg(unix)]
use signal_hook::consts::{SIGINT, SIGTERM};
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, TcpStream};
#[cfg(unix)]
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use tracing::{error, info, warn};
#[cfg(windows)]
use uds_windows::UnixListener;
pub(crate) const CONNECTION_DRAIN_GRACE: Duration = Duration::from_secs(5);
const ACCEPT_PROBE_CONNECT_TIMEOUT: Duration = Duration::from_secs(1);
pub(crate) fn join_thread_bounded(handle: thread::JoinHandle<()>, deadline: Instant) -> bool {
while !handle.is_finished() {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
warn!("thread did not exit before shutdown deadline; abandoning join");
return false;
}
thread::sleep(remaining.min(Duration::from_millis(10)));
}
if let Err(e) = handle.join() {
error!("thread panicked during shutdown: {e:?}");
}
true
}
const CLIENT_THREAD_PRUNE_THRESHOLD: usize = 64;
pub(crate) const MAX_CONCURRENT_CONNECTIONS: usize = 256;
pub(crate) struct ConnectionSlot(Arc<AtomicUsize>);
impl Drop for ConnectionSlot {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::Relaxed);
}
}
pub(crate) fn try_take_connection_slot(count: &Arc<AtomicUsize>) -> Option<ConnectionSlot> {
if count.fetch_add(1, Ordering::Relaxed) >= MAX_CONCURRENT_CONNECTIONS {
count.fetch_sub(1, Ordering::Relaxed);
return None;
}
Some(ConnectionSlot(Arc::clone(count)))
}
fn push_client_thread(
client_threads: &mut Vec<thread::JoinHandle<()>>,
handle: thread::JoinHandle<()>,
) {
if client_threads.len() >= CLIENT_THREAD_PRUNE_THRESHOLD {
client_threads.retain(|h| !h.is_finished());
}
client_threads.push(handle);
}
fn drain_tcp_handles(
rx: &mpsc::Receiver<thread::JoinHandle<()>>,
client_threads: &mut Vec<thread::JoinHandle<()>>,
) {
while let Ok(handle) = rx.try_recv() {
push_client_thread(client_threads, handle);
}
}
#[cfg(feature = "metrics")]
fn start_metrics_server(addr_str: &str, shutdown: &Arc<AtomicBool>) -> io::Result<()> {
let addr: SocketAddr = addr_str.parse().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid --metrics-addr: {e}"),
)
})?;
let shutdown_flag = Arc::clone(shutdown);
thread::spawn(move || {
crate::metrics::serve_metrics(addr, shutdown_flag);
});
Ok(())
}
#[cfg(not(feature = "metrics"))]
fn start_metrics_server(addr_str: &str, _shutdown: &Arc<AtomicBool>) -> io::Result<()> {
Err(io::Error::other(format!(
"--metrics-addr {addr_str}: this build was compiled without the \
`metrics` feature; rebuild with `--features metrics` to serve /metrics"
)))
}
fn handle_accept_error(e: io::Error) {
match e.kind() {
io::ErrorKind::Interrupted | io::ErrorKind::ConnectionAborted => {}
_ => {
error!(error = %e, "accept error, retrying");
thread::sleep(Duration::from_millis(100));
}
}
}
pub(crate) fn remove_stale_socket(socket_path: &str) -> io::Result<()> {
if !Path::new(socket_path).exists() {
return Ok(());
}
if choreo_proto::socket_listening(socket_path) {
return Err(io::Error::other(format!(
"another daemon is already listening at {socket_path}; it must be \
stopped before starting a new one"
)));
}
std::fs::remove_file(socket_path).map_err(|e| {
io::Error::new(
e.kind(),
format!("removing the stale socket at {socket_path}: {e}"),
)
})
}
pub fn run_server(
socket_path: &str,
state: DaemonState,
metrics_addr: Option<String>,
tcp_addr: Option<String>,
transport_sk: TransportSecretKey,
acl: std::sync::Arc<crate::server::acl::SharedAcl>,
auto_exit: bool,
) -> io::Result<()> {
remove_stale_socket(socket_path)?;
let listener = UnixListener::bind(socket_path).map_err(|e| {
io::Error::new(
e.kind(),
format!("binding the Unix socket at {socket_path}: {e}"),
)
})?;
info!(%socket_path, "choreographr listening");
let core = start_daemon_core(
state,
CoreOptions {
acl: Some(Arc::clone(&acl)),
config_watchers: true,
auto_exit_wake_path: auto_exit.then(|| socket_path.to_string()),
},
)?;
let daemon_tx = core.daemon_tx.clone();
let shutdown = Arc::clone(&core.shutdown);
let global_lag = Arc::clone(&core.global_lag);
let conn_count = Arc::clone(&core.conn_count);
#[cfg(unix)]
{
let sig_shutdown = Arc::clone(&shutdown);
let sig_path = socket_path.to_string();
thread::spawn(move || {
let mut signals = match signal_hook::iterator::Signals::new([SIGINT, SIGTERM]) {
Ok(s) => s,
Err(e) => {
error!("failed to register signal handlers: {e}");
return;
}
};
for _ in signals.forever() {
sig_shutdown.store(true, Ordering::SeqCst);
let _ = choreo_proto::connect_unix(&sig_path);
}
});
}
#[cfg(windows)]
{
let sig_shutdown = Arc::clone(&shutdown);
let sig_path = socket_path.to_string();
thread::spawn(move || {
use signal_hook::consts::{SIGINT, SIGTERM};
let (sig_tx, sig_rx) = mpsc::channel::<()>();
let int_tx = sig_tx.clone();
let term_tx = sig_tx;
if let Err(e) = unsafe {
signal_hook::low_level::register(SIGINT, move || {
let _ = int_tx.send(());
})
}
.and_then(|_| unsafe {
signal_hook::low_level::register(SIGTERM, move || {
let _ = term_tx.send(());
})
}) {
error!("failed to register signal handlers: {e}");
return;
}
while sig_rx.recv().is_ok() {
sig_shutdown.store(true, Ordering::SeqCst);
let _ = choreo_proto::connect_unix(&sig_path);
}
});
}
crate::metrics::init().map_err(io::Error::other)?;
if let Some(ref addr_str) = metrics_addr {
start_metrics_server(addr_str, &shutdown)?;
}
let mut client_threads: Vec<thread::JoinHandle<()>> = Vec::new();
let (tcp_client_tx, tcp_client_rx) = mpsc::channel::<thread::JoinHandle<()>>();
let tcp_shutdown = Arc::clone(&shutdown);
let mut tcp_accept_handle: Option<thread::JoinHandle<()>> = None;
let mut tcp_accept_addr: Option<SocketAddr> = None;
if let Some(ref tcp_addr_str) = tcp_addr {
let addr: SocketAddr = tcp_addr_str.parse().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid --tcp-addr: {e}"),
)
})?;
tcp_accept_addr = Some(addr);
let listener = TcpListener::bind(addr)
.map_err(|e| io::Error::other(format!("failed to bind TCP listener on {addr}: {e}")))?;
info!("TCP (Noise IK) listening on {addr}");
let daemon_tx = daemon_tx.clone();
let acl = Arc::clone(&acl);
let tcp_client_tx = tcp_client_tx.clone();
let conn_count = Arc::clone(&conn_count);
let global_lag_tcp = Arc::clone(&global_lag);
tcp_accept_handle = Some(thread::spawn(move || {
loop {
if tcp_shutdown.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
match listener.accept() {
Ok((tcp, _)) => {
if tcp_shutdown.load(std::sync::atomic::Ordering::SeqCst) {
drop(tcp);
break;
}
let Some(slot) = try_take_connection_slot(&conn_count) else {
warn!(
"connection rejected: at the {MAX_CONCURRENT_CONNECTIONS} concurrent-connection cap"
);
continue;
};
crate::metrics::record_connection_accepted();
let tx = daemon_tx.clone();
let auto_exit_tx = if auto_exit { Some(tx.clone()) } else { None };
let sk_bytes = *transport_sk.as_bytes();
let acl = Arc::clone(&acl);
let global_lag = Arc::clone(&global_lag_tcp);
let (client_id, writer_tx, writer_rx) =
crate::server::connection::register_client_writer(&tx);
let handle = thread::spawn(move || {
let _slot = slot;
let result = crate::server::connection::tcp_handshake_and_client_thread(
tcp, sk_bytes, acl, tx, client_id, writer_tx, writer_rx, global_lag,
);
if let Err(e) = result {
error!(error = %e, "TCP client error");
}
drop(_slot);
if let Some(tx) = auto_exit_tx {
let _ = tx.send(DaemonCommand::LastClientDisconnected);
}
});
let _ = tcp_client_tx.send(handle);
}
Err(e) => {
handle_accept_error(e);
}
}
}
}));
}
loop {
if shutdown.load(Ordering::SeqCst) {
info!("accept loop: shutdown flag observed (pre-accept check)");
break;
}
drain_tcp_handles(&tcp_client_rx, &mut client_threads);
match listener.accept() {
Ok((stream, _)) => {
if shutdown.load(Ordering::SeqCst) {
info!("accept loop: woken by shutdown signal");
break;
}
let Some(slot) = try_take_connection_slot(&conn_count) else {
warn!(
"connection rejected: at the {MAX_CONCURRENT_CONNECTIONS} concurrent-connection cap"
);
continue; };
crate::metrics::record_connection_accepted();
let tx = daemon_tx.clone();
let auto_exit_tx = if auto_exit { Some(tx.clone()) } else { None };
let global_lag = Arc::clone(&global_lag);
let (client_id, writer_tx, writer_rx) =
crate::server::connection::register_client_writer(&daemon_tx);
push_client_thread(
&mut client_threads,
thread::spawn(move || {
let _slot = slot;
let result = crate::server::connection::client_thread(
stream, tx, client_id, writer_tx, writer_rx, global_lag,
);
if let Err(e) = result {
error!(error = %e, "client error");
}
drop(_slot);
if let Some(tx) = auto_exit_tx {
let _ = tx.send(DaemonCommand::LastClientDisconnected);
}
}),
);
}
Err(e) => {
handle_accept_error(e);
}
}
}
info!("shutting down");
if let Some(addr) = tcp_accept_addr {
let probe = match addr.ip() {
IpAddr::V4(ip) if ip.is_unspecified() => {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), addr.port())
}
IpAddr::V6(ip) if ip.is_unspecified() => {
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), addr.port())
}
_ => addr,
};
let _ = TcpStream::connect_timeout(&probe, ACCEPT_PROBE_CONNECT_TIMEOUT);
}
if let Some(handle) = tcp_accept_handle.take() {
let exited = join_thread_bounded(handle, Instant::now() + CONNECTION_DRAIN_GRACE);
info!(exited, "TCP accept thread drained");
}
drain_tcp_handles(&tcp_client_rx, &mut client_threads);
info!(
tracked_connection_threads = client_threads.len(),
"queueing shutdown broadcast + command-loop stop"
);
let _ = daemon_tx.send(DaemonCommand::BroadcastShuttingDown);
let _ = daemon_tx.send(DaemonCommand::Shutdown);
drop(daemon_tx);
core.cmd_handle.join().unwrap_or_else(|e| {
error!("command thread panicked: {e:?}");
});
info!("command loop thread joined");
drain_tcp_handles(&tcp_client_rx, &mut client_threads);
let drain_deadline = Instant::now() + CONNECTION_DRAIN_GRACE;
info!(
connection_threads = client_threads.len(),
"draining connection threads (bounded)"
);
for handle in client_threads {
join_thread_bounded(handle, drain_deadline);
}
info!("connection threads drained");
if Path::new(socket_path).exists() {
std::fs::remove_file(socket_path)?;
}
info!("shutdown complete");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "metrics")]
#[test]
fn metrics_addr_rejects_malformed_socket() {
let shutdown = Arc::new(AtomicBool::new(false));
let err = start_metrics_server("not-a-socket-address", &shutdown).unwrap_err();
assert!(
err.to_string().contains("invalid --metrics-addr"),
"unexpected error: {err}"
);
}
#[cfg(not(feature = "metrics"))]
#[test]
fn metrics_addr_refused_when_feature_off() {
let shutdown = Arc::new(AtomicBool::new(false));
let err = start_metrics_server("127.0.0.1:9464", &shutdown).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("--metrics-addr"), "unexpected error: {msg}");
assert!(
msg.contains("--features metrics"),
"error must point at the opt-in feature: {msg}"
);
}
#[test]
fn connection_slot_decrements_counter_on_drop() {
let count = Arc::new(AtomicUsize::new(0));
{
let _slot = try_take_connection_slot(&count).expect("under the cap");
assert_eq!(count.load(Ordering::Relaxed), 1);
}
assert_eq!(count.load(Ordering::Relaxed), 0);
}
#[test]
fn connection_cap_rejects_over_limit_and_releases_on_drop() {
let count = Arc::new(AtomicUsize::new(0));
let mut slots = Vec::new();
for _ in 0..MAX_CONCURRENT_CONNECTIONS {
let slot = try_take_connection_slot(&count).expect("under the cap");
slots.push(slot);
}
assert!(
try_take_connection_slot(&count).is_none(),
"at-cap connection must be rejected"
);
drop(slots);
assert_eq!(
count.load(Ordering::Relaxed),
0,
"every slot must be released when its connection exits"
);
}
#[test]
fn push_client_thread_prunes_finished_handles() {
let mut handles = Vec::new();
for _ in 0..=CLIENT_THREAD_PRUNE_THRESHOLD {
let h = thread::spawn(|| {});
while !h.is_finished() {
std::hint::spin_loop();
}
handles.push(h);
}
let (tx, rx) = mpsc::channel::<()>();
let live = thread::spawn(move || {
let _ = rx.recv();
});
push_client_thread(&mut handles, live);
assert_eq!(
handles.len(),
1,
"only the still-running handle must survive the prune"
);
tx.send(()).unwrap();
handles.pop().unwrap().join().unwrap();
}
#[test]
fn remove_stale_socket_removes_regular_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("sock");
std::fs::write(&path, b"not a socket").unwrap();
remove_stale_socket(path.to_str().unwrap()).unwrap();
assert!(!path.exists(), "stale leftover must be removed");
}
#[test]
fn remove_stale_socket_ignores_missing_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("absent");
remove_stale_socket(path.to_str().unwrap()).unwrap();
assert!(!path.exists());
}
}