Files
urus/src/net.rs
T
Claude 5fe696992a feat(serve): supervised listener pool (v0.2 chunk 1)
OneForOne supervisor on the root actor, Restart::Permanent per listener.
ChildSpec factory owns its fd via Arc<OwnedFd> (OwnedFd gains Sync);
restarts reuse the fd — no re-dup, no fd-less window. wait_readable
failure now exits-and-restarts instead of being fatal. Conn actors stay
unsupervised bare spawns. Test hook INJECT_LISTENER_PANICS panics before
accept so a pending connection must survive into the restarted listener.

Roadmap: chunk 1 marked done; chunk 3 fork resolved to smarm-native
timed waits (RFC 008 landed at 393cdd0, reaper option removed); chunk 2
drain-then-stop decided; v0.4-3(b) unblocked.
2026-06-11 12:07:19 +00:00

173 lines
5.7 KiB
Rust

//! Thin TCP socket layer.
//!
//! We don't use `std::net::TcpListener` because we need non-blocking accept
//! integrated with smarm's epoll loop. So this drops down to libc: socket,
//! bind, listen, accept4. All fds carry `O_NONBLOCK | O_CLOEXEC` so smarm's
//! readiness primitives work as documented.
use std::io;
use std::net::SocketAddr;
use std::os::fd::RawFd;
// ---------------------------------------------------------------------------
// OwnedFd — a tiny RAII wrapper that closes on drop.
// ---------------------------------------------------------------------------
//
// Sent through smarm channels (which require `Send`) for zero-copy fd
// handoff between listener and connection actor. `Send` is safe: fd values
// are just integers, and ownership semantics — exactly-one closer — are
// enforced by the type itself.
#[derive(Debug)]
pub struct OwnedFd {
fd: RawFd,
}
impl OwnedFd {
/// Wrap a raw fd. The wrapper now owns the fd and will close it on drop.
pub fn from_raw(fd: RawFd) -> Self {
Self { fd }
}
pub fn as_raw(&self) -> RawFd { self.fd }
/// Release ownership without closing. The caller must close the fd.
pub fn into_raw(self) -> RawFd {
let fd = self.fd;
std::mem::forget(self);
fd
}
}
impl Drop for OwnedFd {
fn drop(&mut self) {
if self.fd >= 0 {
unsafe { libc::close(self.fd); }
}
}
}
// fd handoff: SAFETY: RawFd is an integer; sending one across threads
// transfers ownership in the same way moving an i32 would. The recipient
// becomes the unique closer.
unsafe impl Send for OwnedFd {}
// SAFETY: shared references only expose `as_raw(&self)` — reading an
// integer. No interior mutability; `into_raw` takes `self` by value and is
// unreachable through a shared reference. Needed so a supervisor
// `ChildSpec` factory (an `Fn` closure, hence shared across restarts) can
// own its listener fd via `Arc<OwnedFd>`.
unsafe impl Sync for OwnedFd {}
// ---------------------------------------------------------------------------
// bind_and_listen
// ---------------------------------------------------------------------------
const LISTEN_BACKLOG: i32 = 1024;
/// Create a non-blocking, SO_REUSEADDR TCP listener bound to `addr`. The
/// returned fd is ready for `accept4` calls; the caller registers it with
/// smarm via `wait_readable` between accepts.
pub fn bind_and_listen(addr: SocketAddr) -> io::Result<OwnedFd> {
let family = match addr {
SocketAddr::V4(_) => libc::AF_INET,
SocketAddr::V6(_) => libc::AF_INET6,
};
let fd = unsafe {
libc::socket(
family,
libc::SOCK_STREAM | libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
0,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
let owned = OwnedFd::from_raw(fd);
// SO_REUSEADDR — standard for servers; avoids TIME_WAIT bind failures
// on restart.
let opt: libc::c_int = 1;
let r = unsafe {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_REUSEADDR,
&opt as *const _ as *const libc::c_void,
std::mem::size_of_val(&opt) as libc::socklen_t,
)
};
if r < 0 {
return Err(io::Error::last_os_error());
}
// bind().
match addr {
SocketAddr::V4(a) => {
let sa = libc::sockaddr_in {
sin_family: libc::AF_INET as u16,
sin_port: a.port().to_be(),
sin_addr: libc::in_addr { s_addr: u32::from_ne_bytes(a.ip().octets()) },
sin_zero: [0; 8],
};
let r = unsafe {
libc::bind(
fd,
&sa as *const _ as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
)
};
if r < 0 { return Err(io::Error::last_os_error()); }
}
SocketAddr::V6(a) => {
let sa = libc::sockaddr_in6 {
sin6_family: libc::AF_INET6 as u16,
sin6_port: a.port().to_be(),
sin6_flowinfo: a.flowinfo(),
sin6_addr: libc::in6_addr { s6_addr: a.ip().octets() },
sin6_scope_id: a.scope_id(),
};
let r = unsafe {
libc::bind(
fd,
&sa as *const _ as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
)
};
if r < 0 { return Err(io::Error::last_os_error()); }
}
}
// listen().
let r = unsafe { libc::listen(fd, LISTEN_BACKLOG) };
if r < 0 { return Err(io::Error::last_os_error()); }
Ok(owned)
}
// ---------------------------------------------------------------------------
// accept_nonblocking
// ---------------------------------------------------------------------------
/// One non-blocking `accept4`. Returns the new fd on success,
/// `Err(WouldBlock)` if no connection is pending (caller should park on
/// `wait_readable(listener)` and retry), or other errors directly.
pub fn accept_nonblocking(listener: RawFd) -> io::Result<OwnedFd> {
let mut addr: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
let mut len: libc::socklen_t = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
let fd = unsafe {
libc::accept4(
listener,
&mut addr as *mut _ as *mut libc::sockaddr,
&mut len,
libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
Ok(OwnedFd::from_raw(fd))
}