use crate::fs::{fail, stash, Slot, OK};
use crate::poll::{set_nonblocking, Interest, RawSocket};
use crate::task::{
build_future, stage_io_park, PollFn, POLL_PENDING, POLL_READY, STATE_MIN_SIZE,
STATE_SLOT_OUTPUT, STATE_SLOT_TAG, STATE_SLOT_TEMPS,
};
use crate::NovaStr;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
enum Sock {
Stream(TcpStream),
Listener(TcpListener),
}
thread_local! {
static SOCKETS: RefCell<HashMap<i64, Sock>> = RefCell::new(HashMap::new());
static NEXT_FD: Cell<i64> = const { Cell::new(1) };
}
fn with_stream<R>(fd: i64, f: impl FnOnce(&mut TcpStream) -> R) -> Option<R> {
SOCKETS.with(|sockets| {
let Ok(mut sockets) = sockets.try_borrow_mut() else {
crate::task::abort_with("nova_rt_net: handle table is already borrowed")
};
match sockets.get_mut(&fd) {
Some(Sock::Stream(s)) => Some(f(s)),
_ => None,
}
})
}
fn with_listener<R>(fd: i64, f: impl FnOnce(&mut TcpListener) -> R) -> Option<R> {
SOCKETS.with(|sockets| {
let Ok(mut sockets) = sockets.try_borrow_mut() else {
crate::task::abort_with("nova_rt_net: handle table is already borrowed")
};
match sockets.get_mut(&fd) {
Some(Sock::Listener(l)) => Some(f(l)),
_ => None,
}
})
}
fn register_new_socket(sock: Sock) -> i64 {
let fd = NEXT_FD.with(|next| {
let fd = next.get();
next.set(fd + 1);
fd
});
SOCKETS.with(|sockets| {
let Ok(mut sockets) = sockets.try_borrow_mut() else {
crate::task::abort_with("nova_rt_net: handle table is already borrowed")
};
sockets.insert(fd, sock);
});
fd
}
fn remove_socket(fd: i64) {
SOCKETS.with(|sockets| {
let Ok(mut sockets) = sockets.try_borrow_mut() else {
crate::task::abort_with("nova_rt_net: handle table is already borrowed")
};
sockets.remove(&fd);
});
}
fn stash_i64(n: i64) {
stash(Slot::Buffer, crate::bytes::gc_bytes(&n.to_le_bytes()));
}
fn closed_fd_error() -> i64 {
fail(&std::io::Error::other("socket is not open"))
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_net_close(fd: i64) -> i64 {
remove_socket(fd);
OK
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_net_listen(addr: *const NovaStr) -> i64 {
let addr = unsafe { crate::as_str(addr) };
let listener = match TcpListener::bind(addr) {
Ok(l) => l,
Err(e) => return fail(&e),
};
if let Err(e) = listener.set_nonblocking(true) {
return fail(&e);
}
let fd = register_new_socket(Sock::Listener(listener));
stash_i64(fd);
OK
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_net_local_port(fd: i64) -> i64 {
match with_listener(fd, |l| l.local_addr()) {
Some(Ok(addr)) => {
stash_i64(i64::from(addr.port()));
OK
}
Some(Err(e)) => fail(&e),
None => closed_fd_error(),
}
}
fn resolve_addr(addr: &str) -> std::io::Result<SocketAddr> {
addr.to_socket_addrs()?
.next()
.ok_or_else(|| std::io::Error::other("no addresses resolved for connect"))
}
#[cfg(unix)]
fn raw_socket_of(stream: &TcpStream) -> RawSocket {
use std::os::unix::io::AsRawFd;
RawSocket(i64::from(stream.as_raw_fd()))
}
#[cfg(windows)]
fn raw_socket_of(stream: &TcpStream) -> RawSocket {
use std::os::windows::io::AsRawSocket;
RawSocket(stream.as_raw_socket() as i64)
}
#[cfg(unix)]
fn raw_socket_of_listener(listener: &TcpListener) -> RawSocket {
use std::os::unix::io::AsRawFd;
RawSocket(i64::from(listener.as_raw_fd()))
}
#[cfg(windows)]
fn raw_socket_of_listener(listener: &TcpListener) -> RawSocket {
use std::os::windows::io::AsRawSocket;
RawSocket(listener.as_raw_socket() as i64)
}
#[cfg(unix)]
fn platform_connect(addr: SocketAddr) -> std::io::Result<TcpStream> {
use std::os::unix::io::FromRawFd;
let domain = match addr {
SocketAddr::V4(_) => libc::AF_INET,
SocketAddr::V6(_) => libc::AF_INET6,
};
let fd = unsafe { libc::socket(domain, libc::SOCK_STREAM, 0) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
let stream = unsafe { TcpStream::from_raw_fd(fd) };
set_nonblocking(raw_socket_of(&stream))?;
let rc = match addr {
SocketAddr::V4(a) => {
let mut sin: libc::sockaddr_in = unsafe { std::mem::zeroed() };
sin.sin_family = libc::AF_INET as libc::sa_family_t;
sin.sin_port = a.port().to_be();
sin.sin_addr = libc::in_addr {
s_addr: u32::from(*a.ip()).to_be(),
};
unsafe {
libc::connect(
fd,
std::ptr::addr_of!(sin).cast::<libc::sockaddr>(),
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
)
}
}
SocketAddr::V6(a) => {
let mut sin6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
sin6.sin6_family = libc::AF_INET6 as libc::sa_family_t;
sin6.sin6_port = a.port().to_be();
sin6.sin6_flowinfo = a.flowinfo();
sin6.sin6_addr = libc::in6_addr {
s6_addr: a.ip().octets(),
};
sin6.sin6_scope_id = a.scope_id();
unsafe {
libc::connect(
fd,
std::ptr::addr_of!(sin6).cast::<libc::sockaddr>(),
std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
)
}
}
};
if rc == 0 {
return Ok(stream);
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINPROGRESS) {
return Ok(stream);
}
Err(err)
}
#[cfg(windows)]
fn ensure_wsa_started() {
use std::sync::Once;
use windows_sys::Win32::Networking::WinSock::{WSAStartup, WSADATA};
static INIT: Once = Once::new();
INIT.call_once(|| {
let mut data = WSADATA::default();
let _ = unsafe { WSAStartup(0x0202, &mut data) };
});
}
#[cfg(windows)]
fn platform_connect(addr: SocketAddr) -> std::io::Result<TcpStream> {
use std::os::windows::io::FromRawSocket;
use windows_sys::Win32::Networking::WinSock::{
connect, socket, WSAGetLastError, AF_INET, AF_INET6, IN6_ADDR, IN6_ADDR_0, INVALID_SOCKET,
IN_ADDR, IN_ADDR_0, IPPROTO_TCP, SOCKADDR, SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_IN6_0,
SOCK_STREAM, WSAEWOULDBLOCK,
};
ensure_wsa_started();
let domain = match addr {
SocketAddr::V4(_) => i32::from(AF_INET),
SocketAddr::V6(_) => i32::from(AF_INET6),
};
let sock = unsafe { socket(domain, SOCK_STREAM, IPPROTO_TCP) };
if sock == INVALID_SOCKET {
return Err(std::io::Error::from_raw_os_error(unsafe {
WSAGetLastError()
}));
}
let stream = unsafe { TcpStream::from_raw_socket(sock as u64) };
set_nonblocking(raw_socket_of(&stream))?;
let rc = match addr {
SocketAddr::V4(a) => {
let sin = SOCKADDR_IN {
sin_family: AF_INET,
sin_port: a.port().to_be(),
sin_addr: IN_ADDR {
S_un: IN_ADDR_0 {
S_addr: u32::from(*a.ip()).to_be(),
},
},
sin_zero: [0; 8],
};
unsafe {
connect(
sock,
std::ptr::addr_of!(sin).cast::<SOCKADDR>(),
std::mem::size_of::<SOCKADDR_IN>() as i32,
)
}
}
SocketAddr::V6(a) => {
let sin6 = SOCKADDR_IN6 {
sin6_family: AF_INET6,
sin6_port: a.port().to_be(),
sin6_flowinfo: a.flowinfo(),
sin6_addr: IN6_ADDR {
u: IN6_ADDR_0 {
Byte: a.ip().octets(),
},
},
Anonymous: SOCKADDR_IN6_0 {
sin6_scope_id: a.scope_id(),
},
};
unsafe {
connect(
sock,
std::ptr::addr_of!(sin6).cast::<SOCKADDR>(),
std::mem::size_of::<SOCKADDR_IN6>() as i32,
)
}
}
};
if rc == 0 {
return Ok(stream);
}
let err = unsafe { WSAGetLastError() };
if err == WSAEWOULDBLOCK {
return Ok(stream);
}
Err(std::io::Error::from_raw_os_error(err))
}
const CONNECT_SLOT_SOCK: usize = STATE_SLOT_TEMPS;
const CONNECT_STATE_SIZE: usize = STATE_MIN_SIZE + 8;
const _: () = assert!(CONNECT_STATE_SIZE >= (CONNECT_SLOT_SOCK + 1) * 8);
enum Started {
WouldBlock(RawSocket),
Failed(i64),
}
fn start_connect(slots: *mut i64) -> Started {
let ptr = unsafe { slots.add(CONNECT_SLOT_SOCK).read() } as *const NovaStr;
let addr_str = unsafe { crate::as_str(ptr) };
let addr = match resolve_addr(addr_str) {
Ok(addr) => addr,
Err(e) => return Started::Failed(fail(&e)),
};
match platform_connect(addr) {
Ok(stream) => {
let raw = raw_socket_of(&stream);
let table_fd = register_new_socket(Sock::Stream(stream));
unsafe { slots.add(CONNECT_SLOT_SOCK).write(table_fd) };
Started::WouldBlock(raw)
}
Err(e) => Started::Failed(fail(&e)),
}
}
fn finish_connect(slots: *mut i64) -> i64 {
let table_fd = unsafe { slots.add(CONNECT_SLOT_SOCK).read() };
match with_stream(table_fd, |stream| stream.take_error()) {
Some(Ok(None)) => {
stash_i64(table_fd);
OK
}
Some(Ok(Some(e))) => {
remove_socket(table_fd);
fail(&e)
}
Some(Err(e)) => {
remove_socket(table_fd);
fail(&e)
}
None => closed_fd_error(),
}
}
unsafe extern "C-unwind" fn poll_connect(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
if tag == 0 {
unsafe { slots.add(STATE_SLOT_TAG).write(1) };
match start_connect(slots) {
Started::WouldBlock(sock) => {
stage_io_park(sock, Interest::Write, None);
return POLL_PENDING;
}
Started::Failed(status) => {
unsafe { slots.add(STATE_SLOT_OUTPUT).write(status) };
return POLL_READY;
}
}
}
let status = finish_connect(slots);
unsafe { slots.add(STATE_SLOT_OUTPUT).write(status) };
POLL_READY
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_net_connect_future(addr: *const NovaStr) -> *mut u8 {
let poll: PollFn = poll_connect;
build_future(poll, CONNECT_STATE_SIZE, |slots| {
unsafe { slots.add(CONNECT_SLOT_SOCK).write(addr as i64) };
})
}
enum ReadStep {
Done(i64),
WouldBlock(RawSocket),
}
fn try_read(fd: i64, max: i64) -> ReadStep {
let Ok(cap) = usize::try_from(max) else {
crate::task::abort_with("nova_rt_net: read: negative maximum")
};
let mut buf = vec![0u8; cap];
let outcome = with_stream(fd, |stream| match std::io::Read::read(stream, &mut buf) {
Ok(n) => {
buf.truncate(n);
stash(Slot::Buffer, crate::bytes::gc_bytes(&buf));
ReadStep::Done(OK)
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
ReadStep::WouldBlock(raw_socket_of(stream))
}
Err(e) => ReadStep::Done(fail(&e)),
});
match outcome {
Some(step) => step,
None => ReadStep::Done(closed_fd_error()),
}
}
enum WriteStep {
Done(i64),
WouldBlock(RawSocket),
}
fn try_write(fd: i64, bytes: &[u8]) -> WriteStep {
let outcome = with_stream(fd, |stream| match std::io::Write::write(stream, bytes) {
Ok(n) => {
stash_i64(n as i64);
WriteStep::Done(OK)
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
WriteStep::WouldBlock(raw_socket_of(stream))
}
Err(e) => WriteStep::Done(fail(&e)),
});
match outcome {
Some(step) => step,
None => WriteStep::Done(closed_fd_error()),
}
}
const READ_SLOT_FD: usize = STATE_SLOT_TEMPS;
const READ_SLOT_MAX: usize = STATE_SLOT_TEMPS + 1;
const READ_STATE_SIZE: usize = STATE_MIN_SIZE + 16;
const _: () = assert!(READ_STATE_SIZE >= (READ_SLOT_MAX + 1) * 8);
unsafe extern "C-unwind" fn poll_read(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let fd = unsafe { slots.add(READ_SLOT_FD).read() };
let max = unsafe { slots.add(READ_SLOT_MAX).read() };
match try_read(fd, max) {
ReadStep::Done(status) => {
unsafe { slots.add(STATE_SLOT_OUTPUT).write(status) };
POLL_READY
}
ReadStep::WouldBlock(socket) => {
stage_io_park(socket, Interest::Read, None);
POLL_PENDING
}
}
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_net_read_future(fd: i64, max: i64) -> *mut u8 {
let poll: PollFn = poll_read;
build_future(poll, READ_STATE_SIZE, |slots| {
unsafe {
slots.add(READ_SLOT_FD).write(fd);
slots.add(READ_SLOT_MAX).write(max);
}
})
}
const WRITE_SLOT_FD: usize = STATE_SLOT_TEMPS;
const WRITE_SLOT_BYTES: usize = STATE_SLOT_TEMPS + 1;
const WRITE_STATE_SIZE: usize = STATE_MIN_SIZE + 16;
const _: () = assert!(WRITE_STATE_SIZE >= (WRITE_SLOT_BYTES + 1) * 8);
unsafe extern "C-unwind" fn poll_write(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let fd = unsafe { slots.add(WRITE_SLOT_FD).read() };
let ptr = unsafe { slots.add(WRITE_SLOT_BYTES).read() } as *const NovaStr;
let bytes = unsafe { crate::bytes::as_bytes(ptr) };
match try_write(fd, bytes) {
WriteStep::Done(status) => {
unsafe { slots.add(STATE_SLOT_OUTPUT).write(status) };
POLL_READY
}
WriteStep::WouldBlock(socket) => {
stage_io_park(socket, Interest::Write, None);
POLL_PENDING
}
}
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_net_write_future(
fd: i64,
bytes: *const NovaStr,
) -> *mut u8 {
let poll: PollFn = poll_write;
build_future(poll, WRITE_STATE_SIZE, |slots| {
unsafe {
slots.add(WRITE_SLOT_FD).write(fd);
slots.add(WRITE_SLOT_BYTES).write(bytes as i64);
}
})
}
fn deadline_epoch() -> Instant {
static EPOCH: OnceLock<Instant> = OnceLock::new();
*EPOCH.get_or_init(Instant::now)
}
fn now_epoch_ms() -> i64 {
i64::try_from(deadline_epoch().elapsed().as_millis()).unwrap_or(i64::MAX)
}
fn instant_from_remaining_ms(remaining_ms: i64) -> Instant {
let ms = u64::try_from(remaining_ms).unwrap_or(0);
Instant::now() + Duration::from_millis(ms)
}
const RT_SLOT_FD: usize = STATE_SLOT_TEMPS;
const RT_SLOT_MAX: usize = STATE_SLOT_TEMPS + 1;
const RT_SLOT_DEADLINE: usize = STATE_SLOT_TEMPS + 2;
const RT_STATE_SIZE: usize = STATE_MIN_SIZE + 24;
const _: () = assert!(RT_STATE_SIZE >= (RT_SLOT_DEADLINE + 1) * 8);
unsafe extern "C-unwind" fn poll_read_timeout(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let fd = unsafe { slots.add(RT_SLOT_FD).read() };
let max = unsafe { slots.add(RT_SLOT_MAX).read() };
let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
let deadline_ms = if tag == 0 {
let ms = unsafe { slots.add(RT_SLOT_DEADLINE).read() };
let deadline = now_epoch_ms().saturating_add(ms.max(0));
unsafe {
slots.add(RT_SLOT_DEADLINE).write(deadline);
slots.add(STATE_SLOT_TAG).write(1);
}
deadline
} else {
unsafe { slots.add(RT_SLOT_DEADLINE).read() }
};
match try_read(fd, max) {
ReadStep::Done(status) => {
unsafe { slots.add(STATE_SLOT_OUTPUT).write(status) };
POLL_READY
}
ReadStep::WouldBlock(socket) => {
let remaining_ms = deadline_ms.saturating_sub(now_epoch_ms());
if remaining_ms <= 0 {
let status = fail(&std::io::Error::new(
std::io::ErrorKind::TimedOut,
"read_timeout: the deadline passed before any data was available",
));
unsafe { slots.add(STATE_SLOT_OUTPUT).write(status) };
return POLL_READY;
}
stage_io_park(
socket,
Interest::Read,
Some(instant_from_remaining_ms(remaining_ms)),
);
POLL_PENDING
}
}
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_net_read_timeout_future(
fd: i64,
max: i64,
ms: i64,
) -> *mut u8 {
let poll: PollFn = poll_read_timeout;
build_future(poll, RT_STATE_SIZE, |slots| {
unsafe {
slots.add(RT_SLOT_FD).write(fd);
slots.add(RT_SLOT_MAX).write(max);
slots.add(RT_SLOT_DEADLINE).write(ms);
}
})
}
enum AcceptStep {
Done(i64),
WouldBlock(RawSocket),
}
fn try_accept(fd: i64) -> AcceptStep {
let attempt = with_listener(fd, |listener| {
(raw_socket_of_listener(listener), listener.accept())
});
match attempt {
Some((_, Ok((stream, _peer)))) => {
if let Err(e) = stream.set_nonblocking(true) {
return AcceptStep::Done(fail(&e));
}
let accepted = register_new_socket(Sock::Stream(stream));
stash_i64(accepted);
AcceptStep::Done(OK)
}
Some((socket, Err(e))) if e.kind() == std::io::ErrorKind::WouldBlock => {
AcceptStep::WouldBlock(socket)
}
Some((_, Err(e))) => AcceptStep::Done(fail(&e)),
None => AcceptStep::Done(closed_fd_error()),
}
}
const ACCEPT_SLOT_FD: usize = STATE_SLOT_TEMPS;
const ACCEPT_STATE_SIZE: usize = STATE_MIN_SIZE + 8;
const _: () = assert!(ACCEPT_STATE_SIZE >= (ACCEPT_SLOT_FD + 1) * 8);
unsafe extern "C-unwind" fn poll_accept(state: *mut u8, _task_ctx: *mut u8) -> i64 {
let slots = state as *mut i64;
let fd = unsafe { slots.add(ACCEPT_SLOT_FD).read() };
match try_accept(fd) {
AcceptStep::Done(status) => {
unsafe { slots.add(STATE_SLOT_OUTPUT).write(status) };
POLL_READY
}
AcceptStep::WouldBlock(socket) => {
stage_io_park(socket, Interest::Read, None);
POLL_PENDING
}
}
}
#[no_mangle]
pub unsafe extern "C-unwind" fn nova_rt_net_accept_future(fd: i64) -> *mut u8 {
let poll: PollFn = poll_accept;
build_future(poll, ACCEPT_STATE_SIZE, |slots| {
unsafe { slots.add(ACCEPT_SLOT_FD).write(fd) };
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::{CONNECTION_REFUSED, OTHER, TIMED_OUT};
unsafe extern "C-unwind" fn poll_ready_immediately_for_test(
state: *mut u8,
_task_ctx: *mut u8,
) -> i64 {
unsafe { (state as *mut i64).add(STATE_SLOT_OUTPUT).write(0) };
POLL_READY
}
fn take_fd() -> i64 {
let ptr = crate::fs::take_for_test(Slot::Buffer) as *const NovaStr;
assert!(
!ptr.is_null(),
"nothing at all was pending in Slot::Buffer -- whatever was \
expected to stash a count did not"
);
let bytes = unsafe { crate::bytes::as_bytes(ptr) };
let Ok(arr) = <[u8; 8]>::try_from(bytes) else {
panic!(
"stash_i64 stashes an 8-byte payload; got {} bytes instead -- \
the encoding changed without updating this test helper to \
match (an empty slot is caught above, not here)",
bytes.len()
);
};
i64::from_le_bytes(arr)
}
fn with_test_current<R>(task_id: i64, f: impl FnOnce() -> R) -> R {
let previous = crate::task::current_task();
crate::task::set_current_for_test(Some(task_id));
let result = f();
crate::task::set_current_for_test(previous);
result
}
fn state_slot_of(fut: *mut u8, slot: usize) -> i64 {
let words = fut as *mut usize;
let state = unsafe { words.add(crate::task::FUTURE_SLOT_STATE).read() } as *mut i64;
unsafe { state.add(slot).read() }
}
fn connect_blocking_for_test(addr: &str) -> i64 {
let addr_ptr = crate::gc_str(addr);
let fut = unsafe { nova_rt_net_connect_future(addr_ptr) };
let status = unsafe { crate::task::nova_rt_task_block_on(fut) };
assert_eq!(status, OK, "test setup: connect must succeed");
state_slot_of(fut, CONNECT_SLOT_SOCK)
}
fn connect_status_for_test(addr: &str) -> i64 {
let addr_ptr = crate::gc_str(addr);
let fut = unsafe { nova_rt_net_connect_future(addr_ptr) };
unsafe { crate::task::nova_rt_task_block_on(fut) }
}
fn is_open_for_test(fd: i64) -> bool {
with_stream(fd, |_| ()).is_some()
}
fn is_listener_for_test(fd: i64) -> bool {
with_listener(fd, |_| ()).is_some()
}
#[cfg(unix)]
fn o_nonblock_is_set(socket: crate::poll::RawSocket) -> bool {
let fd = socket.0 as std::os::unix::io::RawFd;
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
assert!(
flags >= 0,
"fcntl(F_GETFL) failed for socket {}: {}",
socket.0,
std::io::Error::last_os_error()
);
flags & libc::O_NONBLOCK != 0
}
#[cfg(unix)]
fn fcntl_observes_std_nonblocking() -> bool {
static ANSWER: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ANSWER.get_or_init(|| {
let blocking = std::net::TcpListener::bind("127.0.0.1:0")
.expect("calibration: binding a loopback listener must succeed");
let nonblocking = std::net::TcpListener::bind("127.0.0.1:0")
.expect("calibration: binding a loopback listener must succeed");
nonblocking
.set_nonblocking(true)
.expect("calibration: set_nonblocking must succeed");
let reads_set = o_nonblock_is_set(raw_socket_of_listener(&nonblocking));
let reads_clear = !o_nonblock_is_set(raw_socket_of_listener(&blocking));
reads_set && reads_clear
})
}
#[cfg(unix)]
fn nonblocking_for_test(socket: crate::poll::RawSocket) -> Option<bool> {
if !fcntl_observes_std_nonblocking() {
return None;
}
Some(o_nonblock_is_set(socket))
}
#[cfg(not(unix))]
fn nonblocking_for_test(_socket: crate::poll::RawSocket) -> Option<bool> {
None
}
fn poll_fn_and_state(fut: *mut u8) -> (PollFn, *mut u8) {
let words = fut as *mut usize;
let poll_code = unsafe { words.add(crate::task::FUTURE_SLOT_POLL).read() };
let state = unsafe { words.add(crate::task::FUTURE_SLOT_STATE).read() } as *mut u8;
let poll: PollFn = unsafe { std::mem::transmute(poll_code) };
(poll, state)
}
fn drain_stray_pending_park_for_test() {
let pump: PollFn = poll_ready_immediately_for_test;
let pump_fut = build_future(pump, STATE_MIN_SIZE, |_| {});
assert_eq!(
unsafe { crate::task::nova_rt_task_block_on(pump_fut) },
0,
"test cleanup: the pump future must complete immediately"
);
}
fn await_peer_bytes_for_test(fd: i64, expected: usize) {
let socket = with_stream(fd, |stream| raw_socket_of(stream)).expect("fd must be open");
let deadline = Instant::now() + Duration::from_secs(10);
let mut buf = vec![0u8; expected];
loop {
let peeked = with_stream(fd, |stream| stream.peek(&mut buf)).expect("fd must be open");
match peeked {
Ok(n) if n >= expected => return,
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("test setup: peeking for the peer's bytes failed: {e}"),
}
assert!(
Instant::now() < deadline,
"test setup: the {expected} bytes the peer wrote never reached \
this socket's receive buffer"
);
crate::poll::wait(&[(socket, Interest::Read)], Some(deadline));
}
}
fn await_peer_addr_for_test(fd: i64) -> SocketAddr {
let socket = with_stream(fd, |stream| raw_socket_of(stream)).expect("fd must be open");
let deadline = Instant::now() + Duration::from_secs(10);
loop {
match with_stream(fd, |stream| stream.peer_addr()).expect("fd must be open") {
Ok(addr) => return addr,
Err(e) if e.kind() == std::io::ErrorKind::NotConnected => {}
Err(e) => panic!("test setup: reading the peer address failed: {e}"),
}
assert!(
Instant::now() < deadline,
"the connection this module reported established never became \
reportable by getpeername at all"
);
crate::poll::wait(&[(socket, Interest::Write)], Some(deadline));
}
}
fn take_bytes_for_test() -> Vec<u8> {
let ptr = crate::fs::take_for_test(Slot::Buffer) as *const NovaStr;
unsafe { crate::bytes::as_bytes(ptr) }.to_vec()
}
fn spawn_and_pump_for_test(fut: *mut u8) -> (i64, i64) {
let id = unsafe { crate::task::spawn_kept_for_test(fut) };
let pump: PollFn = poll_ready_immediately_for_test;
let pump_fut = build_future(pump, STATE_MIN_SIZE, |_| {});
assert_eq!(unsafe { crate::task::nova_rt_task_block_on(pump_fut) }, 0);
assert_ne!(
unsafe { crate::task::nova_rt_task_is_done(fut) },
0,
"test setup: the task must have been driven to completion as a \
side effect of draining this thread's whole queue"
);
(id, state_slot_of(fut, STATE_SLOT_OUTPUT))
}
fn fill_send_buffer_until_would_block_for_test(fd: i64, chunk: &[u8]) {
for _ in 0..256 {
match try_write(fd, chunk) {
WriteStep::Done(status) => {
assert_eq!(status, OK, "a write that is not WouldBlock must succeed")
}
WriteStep::WouldBlock(_) => return,
}
}
panic!(
"test setup: the send buffer never filled within 256 writes of \
{} bytes each -- nothing drained the peer, so it must eventually",
chunk.len()
);
}
fn poll_write_until_it_parks_for_test(
fd: i64,
chunk: &[u8],
fut: *mut u8,
socket: RawSocket,
context: &str,
) {
let (poll, state) = poll_fn_and_state(fut);
for _ in 0..64 {
fill_send_buffer_until_would_block_for_test(fd, chunk);
let status = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
if status == POLL_PENDING {
assert_eq!(
crate::task::staged_io_for_test(),
Some((socket, Interest::Write, None)),
"{context} must genuinely stage a park on Interest::Write \
for this exact socket, with no deadline -- not merely \
return POLL_PENDING while parking nothing"
);
return;
}
assert_eq!(
status, POLL_READY,
"{context} returned neither of the two statuses the ABI \
defines"
);
assert_eq!(
state_slot_of(fut, STATE_SLOT_OUTPUT),
OK,
"{context} did not park, so the only tolerable reason is that \
room reappeared and this write took it -- a failure status \
is the wrong status, not a drained buffer"
);
let written = with_test_current(0, take_fd);
assert!(
written > 0,
"{context} reported a successful write of {written} bytes, so \
no room can have reappeared -- a would-block reporting a \
zero-byte write instead of parking"
);
}
panic!(
"{context}: 64 attempts, each re-filling the send buffer with \
{}-byte writes until one reported WouldBlock, and not one of \
them ever parked -- nothing reads the peer, so the room a drain \
can free is bounded by its receive buffer and has to run out",
chunk.len()
);
}
fn dead_addr() -> String {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
drop(listener);
addr
}
#[test]
fn a_connected_socket_closes_once_and_then_reports_not_open() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
let read_status =
unsafe { crate::task::nova_rt_task_block_on(nova_rt_net_read_future(fd, 1)) };
assert_ne!(read_status, OK, "a read on a closed fd must fail");
}
#[test]
fn connecting_to_a_closed_port_is_connection_refused() {
let addr = dead_addr();
let status = connect_status_for_test(&addr);
if status != CONNECTION_REFUSED {
let rebind = match std::net::TcpListener::bind(&addr) {
Ok(listener) => {
drop(listener);
String::from("SUCCEEDED -- nothing holds it now, so this is NOT port theft")
}
Err(e) => format!("FAILED ({e}) -- something holds it, so theft IS confirmed here"),
};
let lines = [
format!(
"connect returned {status}, expected CONNECTION_REFUSED ({CONNECTION_REFUSED})"
),
format!(" address dialled: {addr}"),
format!(" re-bind of that port: {rebind}"),
String::from("This flake's cause is UNKNOWN; the bind-then-drop explanation is"),
String::from("refuted by measurement. Record this output; do not add a retry."),
];
panic!(
"{}",
lines.join(
"
"
)
);
}
}
#[test]
fn two_connected_sockets_do_not_collide() {
let listener_a = std::net::TcpListener::bind("127.0.0.1:0").expect("bind a");
let listener_b = std::net::TcpListener::bind("127.0.0.1:0").expect("bind b");
let addr_a = listener_a.local_addr().expect("addr a").to_string();
let addr_b = listener_b.local_addr().expect("addr b").to_string();
let fd_a = connect_blocking_for_test(&addr_a);
let fd_b = connect_blocking_for_test(&addr_b);
assert_ne!(
fd_a, fd_b,
"two live connections must not share a descriptor"
);
assert!(is_open_for_test(fd_a));
assert!(is_open_for_test(fd_b));
assert_eq!(unsafe { nova_rt_net_close(fd_a) }, OK);
assert!(
is_open_for_test(fd_b),
"closing one socket must not close the other"
);
assert_eq!(unsafe { nova_rt_net_close(fd_b) }, OK);
}
#[test]
fn close_is_idempotent_across_repeated_calls() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK, "the first close");
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK, "a second close");
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK, "a third close");
}
#[test]
fn closing_a_never_issued_fd_is_still_ok() {
assert_eq!(
unsafe { nova_rt_net_close(999_999) },
OK,
"closing an fd that was never open is still OK -- close is \
idempotent over absence, not only over a fd it previously held"
);
}
#[test]
fn net_listen_binds_an_ephemeral_port_and_registers_a_listener() {
let addr = crate::gc_str("127.0.0.1:0");
let status = unsafe { nova_rt_net_listen(addr) };
assert_eq!(status, OK, "binding loopback port 0 must succeed");
let fd = take_fd();
assert!(fd > 0, "fd must be a real handle, got {fd}");
assert!(
is_listener_for_test(fd),
"fd must be registered as a listener"
);
assert!(
!is_open_for_test(fd),
"and must NOT answer as a stream -- one table, two variants, and \
a listener is not readable"
);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn net_listen_reports_an_error_for_an_unbindable_address() {
let addr = crate::gc_str("192.0.2.1:1");
let status = unsafe { nova_rt_net_listen(addr) };
assert_ne!(status, OK, "binding an unroutable address must fail");
}
#[test]
fn net_local_port_reports_the_kernel_assigned_port() {
let addr = crate::gc_str("127.0.0.1:0");
assert_eq!(unsafe { nova_rt_net_listen(addr) }, OK);
let fd = take_fd();
let status = unsafe { nova_rt_net_local_port(fd) };
assert_eq!(status, OK, "a bound listener must report its port");
let port = take_fd();
assert!(
port > 0 && port < 65536,
"port must be a real, in-range port the kernel assigned, got {port}"
);
let actual = with_listener(fd, |l| l.local_addr())
.expect("the fd must still be registered as a listener")
.expect("a bound listener must have a local address");
assert_eq!(
port,
i64::from(actual.port()),
"the stashed port must be this listener's own port, unaltered -- \
the encode-and-stash step between `local_addr` and `Slot::Buffer` \
is what this pins"
);
let dialed = std::net::TcpStream::connect(("127.0.0.1", port as u16));
assert!(
dialed.is_ok(),
"nothing accepted a connection on port {port}. The assertion above \
has already established that this is the listener's own port, so \
the port number is not the problem -- this fd is registered and \
bound but not actually listening, which is what `net_listen` \
failing to reach its `listen` step would look like: {dialed:?}"
);
drop(dialed);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn net_local_port_on_a_stream_fd_is_an_error() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let status = unsafe { nova_rt_net_local_port(fd) };
assert_eq!(
status, OTHER,
"a stream is the wrong kind for local_port, and must report the \
same catch-all error an absent fd does -- not succeed with the \
stream's own local port, and not a distinct wrong-kind code"
);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn net_local_port_on_an_absent_fd_is_an_error() {
let status = unsafe { nova_rt_net_local_port(999_999) };
assert_eq!(
status, OTHER,
"an unregistered fd must not report a port, and reports the same \
catch-all error a wrong-kind fd does"
);
}
#[test]
fn reading_a_listener_fd_is_an_error_not_a_stream_read() {
let addr = crate::gc_str("127.0.0.1:0");
assert_eq!(unsafe { nova_rt_net_listen(addr) }, OK);
let fd = take_fd();
let status = unsafe { crate::task::nova_rt_task_block_on(nova_rt_net_read_future(fd, 16)) };
assert_eq!(
status, OTHER,
"a read on a listener must report the same catch-all error an \
absent fd does, not succeed and not a distinct wrong-kind code"
);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn connect_parks_on_its_first_poll_rather_than_completing_synchronously() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let addr_ptr = crate::gc_str(&addr);
let fut = unsafe { nova_rt_net_connect_future(addr_ptr) };
let words = fut as *mut usize;
let poll_code = unsafe { words.add(crate::task::FUTURE_SLOT_POLL).read() };
let state = unsafe { words.add(crate::task::FUTURE_SLOT_STATE).read() } as *mut u8;
let poll: PollFn = unsafe { std::mem::transmute(poll_code) };
let first = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
first, POLL_PENDING,
"a non-blocking connect must park on its first poll rather than \
complete synchronously"
);
assert_eq!(unsafe { crate::task::nova_rt_task_block_on(fut) }, OK);
let fd = state_slot_of(fut, CONNECT_SLOT_SOCK);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn an_ipv6_connect_parks_on_its_first_poll_and_then_establishes() {
let listener = std::net::TcpListener::bind("[::1]:0").expect(
"bind an IPv6 loopback listener -- see this test's own doc comment \
on why a runner without ::1 must fail here rather than skip",
);
let addr = listener.local_addr().expect("addr").to_string();
assert!(
matches!(resolve_addr(&addr), Ok(SocketAddr::V6(_))),
"test setup: {addr} must resolve to a V6 socket address, or \
nothing below reaches platform_connect's V6 arm at all"
);
let addr_ptr = crate::gc_str(&addr);
let fut = unsafe { nova_rt_net_connect_future(addr_ptr) };
let (poll, state) = poll_fn_and_state(fut);
let first = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
first, POLL_PENDING,
"a non-blocking V6 connect must park on its first poll rather \
than complete synchronously"
);
let fd = state_slot_of(fut, CONNECT_SLOT_SOCK);
let expected_socket = with_stream(fd, |stream| raw_socket_of(stream))
.expect("the first poll must have registered its connecting socket");
assert_eq!(
crate::task::staged_io_for_test(),
Some((expected_socket, Interest::Write, None)),
"a V6 connect must genuinely stage a park on Interest::Write for \
this exact socket, with no deadline -- not merely return \
POLL_PENDING while parking nothing"
);
assert_eq!(
unsafe { crate::task::nova_rt_task_block_on(fut) },
OK,
"a V6 connect to a live ::1 listener must establish"
);
assert_eq!(
await_peer_addr_for_test(fd),
listener.local_addr().expect("addr"),
"connect must have landed on this test's own ::1 listener, over \
IPv6 -- not on whatever other address a mis-built sockaddr_in6 \
happened to name"
);
let (_server, _) = listener.accept().expect("accept");
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_successful_connect_stashes_its_fd_via_slot_buffer() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let addr_ptr = crate::gc_str(&addr);
let fut = unsafe { nova_rt_net_connect_future(addr_ptr) };
let id = unsafe { crate::task::spawn_kept_for_test(fut) };
let pump: PollFn = poll_ready_immediately_for_test;
let pump_fut = build_future(pump, STATE_MIN_SIZE, |_| {});
assert_eq!(unsafe { crate::task::nova_rt_task_block_on(pump_fut) }, 0);
assert_ne!(
unsafe { crate::task::nova_rt_task_is_done(fut) },
0,
"test setup: the connect task must have been driven to completion \
as a side effect of draining this thread's whole queue"
);
assert_eq!(
state_slot_of(fut, STATE_SLOT_OUTPUT),
OK,
"test setup: connect must succeed"
);
let fd = with_test_current(id, take_fd);
assert!(fd > 0, "the stashed fd must be a real, positive descriptor");
assert!(is_open_for_test(fd));
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn the_connect_futures_layout_is_the_one_the_abi_declares() {
let addr_ptr = crate::gc_str("127.0.0.1:1");
let fut = unsafe { nova_rt_net_connect_future(addr_ptr) };
assert_eq!(
crate::gc::object_info(fut as usize),
Some((crate::task::FUTURE_SIZE, true)),
"the fat pointer must be exactly the two-word future, scanned"
);
let state = unsafe {
(fut as *mut usize)
.add(crate::task::FUTURE_SLOT_STATE)
.read()
};
assert_eq!(
crate::gc::object_info(state),
Some((CONNECT_STATE_SIZE, true)),
"the state object must be the ABI minimum plus the one temp slot \
holding the address pointer and later the socket, scanned"
);
let poll = unsafe {
(fut as *mut usize)
.add(crate::task::FUTURE_SLOT_POLL)
.read()
};
let expected: PollFn = poll_connect;
assert_eq!(
poll, expected as usize,
"word 0 must be the poll function's address, not the state's"
);
}
#[test]
fn a_read_with_no_data_parks_and_completes_when_data_arrives() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (mut server, _) = listener.accept().expect("accept");
let expected_socket =
with_stream(fd, |stream| raw_socket_of(stream)).expect("fd must be open");
let fut = unsafe { nova_rt_net_read_future(fd, 64) };
let (poll, state) = poll_fn_and_state(fut);
let first = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
first, POLL_PENDING,
"a read with no data waiting must park rather than complete \
synchronously"
);
assert_eq!(
crate::task::staged_io_for_test(),
Some((expected_socket, Interest::Read, None)),
"a read with no data waiting must genuinely stage a park on \
Interest::Read for this exact socket, with no deadline -- not \
merely return POLL_PENDING while parking nothing"
);
std::io::Write::write_all(&mut server, b"hello").expect("write");
await_peer_bytes_for_test(fd, b"hello".len());
let second = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(second, POLL_READY);
assert_eq!(
unsafe { (state as *mut i64).add(STATE_SLOT_OUTPUT).read() },
OK
);
let bytes = with_test_current(0, take_bytes_for_test);
assert_eq!(bytes, b"hello");
drain_stray_pending_park_for_test();
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_read_still_blocked_on_its_second_poll_stages_a_park_again() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let expected_socket =
with_stream(fd, |stream| raw_socket_of(stream)).expect("fd must be open");
let fut = unsafe { nova_rt_net_read_future(fd, 64) };
let (poll, state) = poll_fn_and_state(fut);
let first = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(first, POLL_PENDING, "test setup: the first poll must park");
assert_eq!(
crate::task::staged_io_for_test(),
Some((expected_socket, Interest::Read, None)),
"test setup: the first poll must stage a park"
);
drain_stray_pending_park_for_test();
let second = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
second, POLL_PENDING,
"still no data on the second poll -- must still park, not \
complete"
);
assert_eq!(
crate::task::staged_io_for_test(),
Some((expected_socket, Interest::Read, None)),
"a still-blocked read must stage a park again on its second \
poll, not only its first -- a future that stages once and \
never again leaves a spuriously-woken task parked forever with \
nothing left to wake it"
);
drain_stray_pending_park_for_test();
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_read_with_data_already_waiting_completes_on_the_first_poll_without_parking() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (mut server, _) = listener.accept().expect("accept");
std::io::Write::write_all(&mut server, b"hi").expect("write");
await_peer_bytes_for_test(fd, b"hi".len());
let fut = unsafe { nova_rt_net_read_future(fd, 64) };
let (poll, state) = poll_fn_and_state(fut);
let status = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
status, POLL_READY,
"data already waiting must complete on the very first poll, not \
park -- poll_join's own optimisation, applied here to a read"
);
assert_eq!(
unsafe { (state as *mut i64).add(STATE_SLOT_OUTPUT).read() },
OK
);
let bytes = with_test_current(0, take_bytes_for_test);
assert_eq!(bytes, b"hi");
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn an_eof_read_stashes_an_empty_payload_not_an_error() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (server, _) = listener.accept().expect("accept");
drop(server);
let fut = unsafe { nova_rt_net_read_timeout_future(fd, 64, 2_000) };
let (id, status) = spawn_and_pump_for_test(fut);
assert_eq!(status, OK, "EOF is a successful read, not a failure status");
let bytes = with_test_current(id, take_bytes_for_test);
assert_eq!(
bytes, b"" as &[u8],
"an empty read must stash a zero-length payload -- the EOF signal"
);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn try_write_reports_would_block_once_the_send_buffer_fills() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let chunk = vec![0xABu8; 1024 * 1024];
fill_send_buffer_until_would_block_for_test(fd, &chunk);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_write_against_a_full_send_buffer_parks_rather_than_spinning() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let expected_socket =
with_stream(fd, |stream| raw_socket_of(stream)).expect("fd must be open");
let chunk_bytes = vec![0xABu8; 1024 * 1024];
let chunk = crate::gc_bytes_for_test(&chunk_bytes);
let fut = unsafe { nova_rt_net_write_future(fd, chunk) };
poll_write_until_it_parks_for_test(
fd,
&chunk_bytes,
fut,
expected_socket,
"a write against a full send buffer",
);
drain_stray_pending_park_for_test();
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_write_still_blocked_on_its_second_poll_stages_a_park_again() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let expected_socket =
with_stream(fd, |stream| raw_socket_of(stream)).expect("fd must be open");
let chunk_bytes = vec![0xABu8; 1024 * 1024];
let chunk = crate::gc_bytes_for_test(&chunk_bytes);
let fut = unsafe { nova_rt_net_write_future(fd, chunk) };
poll_write_until_it_parks_for_test(
fd,
&chunk_bytes,
fut,
expected_socket,
"test setup: the first poll of a write against a full send buffer",
);
drain_stray_pending_park_for_test();
poll_write_until_it_parks_for_test(
fd,
&chunk_bytes,
fut,
expected_socket,
"a still-full write on a later poll -- a future that stages once \
and never again leaves a spuriously-woken task parked forever \
with nothing left to wake it",
);
drain_stray_pending_park_for_test();
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_ready_write_stashes_its_byte_count_via_slot_buffer() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let payload = crate::gc_bytes_for_test(b"hello");
let fut = unsafe { nova_rt_net_write_future(fd, payload) };
let (id, status) = spawn_and_pump_for_test(fut);
assert_eq!(status, OK);
let count = with_test_current(id, take_fd);
assert_eq!(
count, 5,
"a freshly connected socket's send buffer is empty (per \
`poll.rs`'s own `wait_reports_a_socket_ready_for_write`), so a \
small write must report its whole length, not a short count"
);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn using_a_closed_socket_is_an_error_not_a_panic_for_read_write_and_read_timeout() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
let read_status =
unsafe { crate::task::nova_rt_task_block_on(nova_rt_net_read_future(fd, 16)) };
assert_ne!(read_status, OK, "reading a closed socket must fail");
let payload = crate::gc_bytes_for_test(b"x");
let write_status =
unsafe { crate::task::nova_rt_task_block_on(nova_rt_net_write_future(fd, payload)) };
assert_ne!(write_status, OK, "writing a closed socket must fail");
let read_timeout_status = unsafe {
crate::task::nova_rt_task_block_on(nova_rt_net_read_timeout_future(fd, 16, 50))
};
assert_ne!(
read_timeout_status, OK,
"read_timeout on a closed socket must fail"
);
}
#[test]
fn a_read_timeout_against_a_silent_peer_reports_timed_out() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let fut = unsafe { nova_rt_net_read_timeout_future(fd, 64, 50) };
let status = unsafe { crate::task::nova_rt_task_block_on(fut) };
assert_eq!(status, TIMED_OUT);
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_read_timeout_with_no_data_stages_an_io_park_carrying_its_deadline() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let expected_socket =
with_stream(fd, |stream| raw_socket_of(stream)).expect("fd must be open");
let fut = unsafe { nova_rt_net_read_timeout_future(fd, 64, 5_000) };
let (poll, state) = poll_fn_and_state(fut);
let status = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
status, POLL_PENDING,
"read_timeout with no data waiting must park rather than \
complete synchronously"
);
match crate::task::staged_io_for_test() {
Some((socket, interest, deadline)) => {
assert_eq!(
socket, expected_socket,
"must park on this exact socket, not an unrelated one"
);
assert_eq!(interest, Interest::Read);
assert!(
deadline.is_some(),
"read_timeout's park must carry its own deadline -- \
without it, a passed deadline could never wake this \
task independently of the socket becoming ready"
);
}
None => panic!(
"read_timeout with no data waiting must genuinely stage a \
park, not merely return POLL_PENDING while parking nothing"
),
}
drain_stray_pending_park_for_test();
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_read_timeout_still_blocked_on_its_second_poll_stages_a_park_again() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let expected_socket =
with_stream(fd, |stream| raw_socket_of(stream)).expect("fd must be open");
let fut = unsafe { nova_rt_net_read_timeout_future(fd, 64, 5_000) };
let (poll, state) = poll_fn_and_state(fut);
let first = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(first, POLL_PENDING, "test setup: the first poll must park");
match crate::task::staged_io_for_test() {
Some((socket, interest, deadline)) => {
assert_eq!(socket, expected_socket, "test setup");
assert_eq!(interest, Interest::Read, "test setup");
assert!(deadline.is_some(), "test setup");
}
None => panic!("test setup: the first poll must stage a park"),
}
drain_stray_pending_park_for_test();
let second = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
second, POLL_PENDING,
"still no data and the deadline is nowhere near passing -- must \
still park, not complete"
);
match crate::task::staged_io_for_test() {
Some((socket, interest, deadline)) => {
assert_eq!(
socket, expected_socket,
"must park on this exact socket again"
);
assert_eq!(interest, Interest::Read);
assert!(
deadline.is_some(),
"a still-blocked read_timeout must stage a park again \
on its second poll, carrying its deadline again -- a \
future that stages once and never again leaves a \
spuriously-woken task parked forever with nothing left \
to wake it"
);
}
None => panic!(
"a still-blocked read_timeout must stage a park again on \
its second poll, not only its first"
),
}
drain_stray_pending_park_for_test();
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_read_timeout_still_blocked_on_its_second_poll_preserves_its_deadline() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (_server, _) = listener.accept().expect("accept");
let fut = unsafe { nova_rt_net_read_timeout_future(fd, 64, 5_000) };
let (poll, state) = poll_fn_and_state(fut);
let first = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(first, POLL_PENDING, "test setup: the first poll must park");
let first_deadline = match crate::task::staged_io_for_test() {
Some((_, _, Some(deadline))) => deadline,
other => panic!(
"test setup: the first poll must stage a park carrying a \
deadline, got {other:?}"
),
};
drain_stray_pending_park_for_test();
std::thread::sleep(std::time::Duration::from_millis(30));
let second = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
second, POLL_PENDING,
"still no data after the sleep -- must still park, not complete"
);
let second_deadline = match crate::task::staged_io_for_test() {
Some((_, _, Some(deadline))) => deadline,
other => panic!(
"the second poll must stage a park carrying a deadline, got \
{other:?}"
),
};
let drift = if second_deadline >= first_deadline {
second_deadline - first_deadline
} else {
first_deadline - second_deadline
};
assert!(
drift < std::time::Duration::from_millis(10),
"read_timeout's deadline must be preserved across polls, not \
recomputed fresh each time -- expected the second poll's \
staged deadline to stay within a few milliseconds of the first \
(ordinary rounding jitter), got a drift of {drift:?} after a \
deliberate 30ms gap between polls; a drift approaching the \
full gap means the deadline is sliding forward with the clock \
instead of staying fixed, which would let a read_timeout \
against a silent peer extend its own deadline forever and \
never actually fire"
);
drain_stray_pending_park_for_test();
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn a_read_timeout_completes_with_data_when_it_arrives_before_the_deadline() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr").to_string();
let fd = connect_blocking_for_test(&addr);
let (mut server, _) = listener.accept().expect("accept");
std::io::Write::write_all(&mut server, b"hi").expect("write");
let fut = unsafe { nova_rt_net_read_timeout_future(fd, 64, 5_000) };
let (id, status) = spawn_and_pump_for_test(fut);
assert_eq!(status, OK);
let bytes = with_test_current(id, take_bytes_for_test);
assert_eq!(bytes, b"hi");
assert_eq!(unsafe { nova_rt_net_close(fd) }, OK);
}
#[test]
fn accept_parks_until_a_client_connects_then_yields_a_stream_fd() {
let addr = crate::gc_str("127.0.0.1:0");
assert_eq!(unsafe { nova_rt_net_listen(addr) }, OK);
let listener_fd = take_fd();
assert_eq!(unsafe { nova_rt_net_local_port(listener_fd) }, OK);
let port = take_fd();
let expected_socket = with_listener(listener_fd, |l| raw_socket_of_listener(l))
.expect("test setup: the fd must be registered as a listener");
if let Some(nonblocking) = nonblocking_for_test(expected_socket) {
assert!(
nonblocking,
"nova_rt_net_listen must leave the listener non-blocking: the \
accept below would otherwise block this thread instead of \
parking the task, and surface as a CI timeout naming no \
line rather than as this message"
);
}
let fut = unsafe { nova_rt_net_accept_future(listener_fd) };
let (poll, state) = poll_fn_and_state(fut);
let first = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
assert_eq!(
first, POLL_PENDING,
"an accept with no client connected must park rather than settle"
);
assert_eq!(
crate::task::staged_io_for_test(),
Some((expected_socket, Interest::Read, None)),
"an accept with no client connected must genuinely stage a park on \
Interest::Read for this listener's own socket, with no deadline -- \
not merely return POLL_PENDING while parking nothing"
);
drain_stray_pending_park_for_test();
let client = std::net::TcpStream::connect(("127.0.0.1", port as u16))
.expect("test setup: a loopback connect to this listener must succeed");
let deadline = Instant::now() + Duration::from_secs(10);
let ready = loop {
let outcome = with_test_current(0, || unsafe { poll(state, std::ptr::null_mut()) });
if outcome == POLL_READY {
break outcome;
}
assert!(
Instant::now() < deadline,
"a client connected on port {port} and this accept never \
settled"
);
drain_stray_pending_park_for_test();
crate::poll::wait(&[(expected_socket, Interest::Read)], Some(deadline));
};
assert_eq!(ready, POLL_READY);
assert_eq!(
unsafe { (state as *mut i64).add(STATE_SLOT_OUTPUT).read() },
OK,
"a settled accept must report success, not a status"
);
let accepted_fd = with_test_current(0, take_fd);
assert!(
accepted_fd > 0 && accepted_fd != listener_fd,
"the accepted connection must get a fresh fd of its own, never the \
listener's: got {accepted_fd} against listener {listener_fd}"
);
assert!(
is_open_for_test(accepted_fd),
"the accepted fd must be registered as a stream, so every read, \
write and read_timeout works on it unchanged"
);
assert!(
!is_listener_for_test(accepted_fd),
"and must not answer as a listener -- one table, two variants"
);
assert!(
matches!(try_read(accepted_fd, 1), ReadStep::WouldBlock(_)),
"the accepted stream must be non-blocking: nothing has been \
written to it, so a read must report would-block rather than \
waiting for a byte"
);
let accepted_socket = with_stream(accepted_fd, |s| raw_socket_of(s))
.expect("the accepted fd must be a live stream entry");
if let Some(nonblocking) = nonblocking_for_test(accepted_socket) {
assert!(
nonblocking,
"try_accept must leave the accepted stream non-blocking: \
`accept` does not propagate the listener's mode on every \
platform, and a blocking connection would block this whole \
executor thread on its first read"
);
}
drop(client);
assert_eq!(unsafe { nova_rt_net_close(accepted_fd) }, OK);
assert_eq!(unsafe { nova_rt_net_close(listener_fd) }, OK);
}
#[test]
fn accepting_on_a_closed_or_wrong_kind_fd_is_an_error_not_a_park() {
let addr = crate::gc_str("127.0.0.1:0");
assert_eq!(unsafe { nova_rt_net_listen(addr) }, OK);
let closed_fd = take_fd();
assert_eq!(unsafe { nova_rt_net_close(closed_fd) }, OK);
let closed =
unsafe { crate::task::nova_rt_task_block_on(nova_rt_net_accept_future(closed_fd)) };
assert_eq!(
closed, OTHER,
"accepting on a closed listener must report the catch-all error, \
not succeed and not park"
);
let absent =
unsafe { crate::task::nova_rt_task_block_on(nova_rt_net_accept_future(999_999)) };
assert_eq!(
absent, OTHER,
"accepting on an fd this module never issued must report the same \
catch-all error a closed listener does"
);
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let stream_addr = listener.local_addr().expect("addr").to_string();
let stream_fd = connect_blocking_for_test(&stream_addr);
let (_server, _) = listener.accept().expect("accept");
let wrong_kind =
unsafe { crate::task::nova_rt_task_block_on(nova_rt_net_accept_future(stream_fd)) };
assert_eq!(
wrong_kind, OTHER,
"a connected stream is the wrong kind for accept, and must report \
the same catch-all error an absent fd does -- not accept on \
it, and not a distinct wrong-kind code"
);
assert_eq!(unsafe { nova_rt_net_close(stream_fd) }, OK);
}
#[test]
fn the_read_futures_layout_is_the_one_the_abi_declares() {
let fut = unsafe { nova_rt_net_read_future(999_999, 64) };
assert_eq!(
crate::gc::object_info(fut as usize),
Some((crate::task::FUTURE_SIZE, true)),
"the fat pointer must be exactly the two-word future, scanned"
);
let state = unsafe {
(fut as *mut usize)
.add(crate::task::FUTURE_SLOT_STATE)
.read()
};
assert_eq!(
crate::gc::object_info(state),
Some((READ_STATE_SIZE, true)),
"the state object must be the ABI minimum plus the two temp \
slots holding fd and max, scanned"
);
let poll = unsafe {
(fut as *mut usize)
.add(crate::task::FUTURE_SLOT_POLL)
.read()
};
let expected: PollFn = poll_read;
assert_eq!(
poll, expected as usize,
"word 0 must be the poll function's address, not the state's"
);
}
#[test]
fn the_write_futures_layout_is_the_one_the_abi_declares() {
let payload = crate::gc_bytes_for_test(b"unsent");
let fut = unsafe { nova_rt_net_write_future(999_999, payload) };
assert_eq!(
crate::gc::object_info(fut as usize),
Some((crate::task::FUTURE_SIZE, true)),
"the fat pointer must be exactly the two-word future, scanned"
);
let state = unsafe {
(fut as *mut usize)
.add(crate::task::FUTURE_SLOT_STATE)
.read()
};
assert_eq!(
crate::gc::object_info(state),
Some((WRITE_STATE_SIZE, true)),
"the state object must be the ABI minimum plus the two temp \
slots holding fd and the bytes pointer, scanned"
);
let poll = unsafe {
(fut as *mut usize)
.add(crate::task::FUTURE_SLOT_POLL)
.read()
};
let expected: PollFn = poll_write;
assert_eq!(
poll, expected as usize,
"word 0 must be the poll function's address, not the state's"
);
}
#[test]
fn the_read_timeout_futures_layout_is_the_one_the_abi_declares() {
let fut = unsafe { nova_rt_net_read_timeout_future(999_999, 64, 50) };
assert_eq!(
crate::gc::object_info(fut as usize),
Some((crate::task::FUTURE_SIZE, true)),
"the fat pointer must be exactly the two-word future, scanned"
);
let state = unsafe {
(fut as *mut usize)
.add(crate::task::FUTURE_SLOT_STATE)
.read()
};
assert_eq!(
crate::gc::object_info(state),
Some((RT_STATE_SIZE, true)),
"the state object must be the ABI minimum plus the three temp \
slots holding fd, max, and the reused ms-then-deadline slot, \
scanned"
);
let poll = unsafe {
(fut as *mut usize)
.add(crate::task::FUTURE_SLOT_POLL)
.read()
};
let expected: PollFn = poll_read_timeout;
assert_eq!(
poll, expected as usize,
"word 0 must be the poll function's address, not the state's"
);
}
#[test]
fn no_net_intrinsic_can_panic() {
let source = include_str!("net.rs");
let production = source.split("mod tests {").next().unwrap_or(source);
for needle in [
".borrow_mut()",
".borrow()",
"unwrap()",
".expect(",
"panic!",
"format!",
] {
assert!(
!production.contains(needle),
"a std/net intrinsic must not panic: `{needle}` found in this \
file's production code, which is reachable from a generated \
poll boundary with no landing pad to unwind through"
);
}
}
}