Files
irtt-rs/crates/irtt-app/src/cmd/server/run.rs
T
Nils Andreas Svee 48a50d7f3a
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 34s
Rust CI / cargo clippy (push) Successful in 35s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m14s
Rust CI / cargo doc (push) Successful in 23s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 29s
Supply chain / cargo-deny advisories (push) Successful in 9s
Supply chain / cargo-deny bans licenses sources (push) Successful in 5s
Supply chain / actionlint (push) Successful in 3s
Netem Interop / netem interop (push) Successful in 1m11s
tests: rework the actually useful tests to live in the integration crate
2026-10-03 13:59:38 +07:00

108 lines
4.4 KiB
Rust

use std::{
net::SocketAddr,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use irtt_server::{address_family_available, ServerSet, ServerSetError};
use super::args::ServerArgs;
/// How often the shutdown future samples the process-wide signal flag.
///
/// The flag a signal handler sets has no async wake mechanism, so the future
/// polls it on a timer rather than spinning. `ServerSet::run` owns everything
/// after that future becomes ready, including fanning it out to the listeners.
const SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(50);
/// Runs the configured listeners until the process is asked to shut down.
///
/// The applet owns exactly one current-thread Tokio runtime and one
/// [`ServerSet`], whatever number of addresses was requested. A single `--bind`
/// is an ordinary set of one and takes the same path as a set of several: one
/// construction, one shutdown future, one supervision rule. Listener
/// concurrency is async I/O concurrency and needs no worker threads.
pub fn run_server(
args: ServerArgs,
shutdown_requested: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(serve(args, shutdown_requested))
}
async fn serve(
args: ServerArgs,
shutdown_requested: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
let config = args.server_config();
let binds = args.resolve_binds();
let is_implicit_default = args.bind.is_empty();
let set = match ServerSet::bind(binds.clone(), config.clone()).await {
Ok(set) => set,
Err(ServerSetError::ListenerSetup { addr, source }) => {
let family_available = address_family_available(addr);
match implicit_default_fallback(is_implicit_default, family_available, &binds, addr) {
Some(remaining) => {
eprintln!(
"irtt-server: default listener {addr} unavailable ({source}); \
continuing with the remaining default listener"
);
// `remaining`'s lone survivor is bound exactly as it
// would be alone: the address family that just failed
// has no local support at all, so nothing on that family
// could be competing for the surviving listener's port.
ServerSet::bind(remaining, config).await?
}
None => return Err(ServerSetError::ListenerSetup { addr, source }.into()),
}
}
Err(err) => return Err(err.into()),
};
// Only once every listener is up. Announcing one address and then failing
// to bind the next would report a service that never started.
for addr in set.local_addrs() {
eprintln!("irtt-server: listening on {addr}");
}
set.run(shutdown_signal(shutdown_requested)).await?;
Ok(())
}
/// Whether a listener-setup failure for `failed_addr` should be retried with
/// a reduced bind set instead of failing the whole invocation, and if so,
/// what to retry with.
///
/// Only the implicit zero-argument default gets this treatment, and only
/// when `failed_addr`'s own address family has no local support on this host
/// at all (`family_available` is `false`) — not merely because its specific
/// port was unavailable for some other reason: already in use, permission
/// denied, no safe wildcard reply-source path, or anything else
/// `ServerSet::bind` can fail with. Those are real configuration or
/// environment problems this invocation should fail on, not silently work
/// around by dropping a listener the operator asked for. An explicit
/// `--bind` list keeps the library's ordinary all-or-nothing behavior
/// regardless.
fn implicit_default_fallback(
is_implicit_default: bool,
family_available: bool,
original_binds: &[SocketAddr],
failed_addr: SocketAddr,
) -> Option<Vec<SocketAddr>> {
if !is_implicit_default || family_available || !original_binds.contains(&failed_addr) {
return None;
}
let remaining: Vec<SocketAddr> = original_binds
.iter()
.copied()
.filter(|&addr| addr != failed_addr)
.collect();
(!remaining.is_empty()).then_some(remaining)
}
async fn shutdown_signal(shutdown_requested: &AtomicBool) {
while !shutdown_requested.load(Ordering::Relaxed) {
tokio::time::sleep(SHUTDOWN_POLL_INTERVAL).await;
}
}