use std::fs::File;
use std::io;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::os::fd::AsRawFd;
use std::os::unix::net::{UnixListener, UnixStream};
use std::time::Duration;
use super::{next, runtime, within};
use crate::alone::{alone, open_descriptors};
use crate::io::Handle;
const CLIENTS: usize = 4;
const RETRIES: usize = 3;
const CLOSED: usize = if cfg!(target_vendor = "apple") { 1 } else { 0 };
fn closed(clients: &[impl AsRawFd]) -> usize {
clients
.iter()
.filter(|client| {
let mut byte = 0u8;
let n = unsafe {
libc::recv(
client.as_raw_fd(),
(&raw mut byte).cast(),
1,
libc::MSG_PEEK | libc::MSG_DONTWAIT,
)
};
n == 0 || (n < 0 && io::Error::last_os_error().kind() != io::ErrorKind::WouldBlock)
})
.count()
}
struct Starved {
held: Vec<File>,
}
impl Starved {
fn new() -> Self {
let room = open_descriptors().into_iter().max().unwrap_or(0) as libc::rlim_t + 8;
let limit = libc::rlimit {
rlim_cur: room,
rlim_max: room,
};
assert_eq!(unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limit) }, 0);
let mut held = Vec::new();
while let Ok(null) = File::open("/dev/null") {
held.push(null);
}
Starved { held }
}
fn free(&mut self) {
self.held.clear();
}
}
fn cpu() -> Duration {
let mut ts: libc::timespec = unsafe { core::mem::zeroed() };
assert_eq!(
unsafe { libc::clock_gettime(libc::CLOCK_PROCESS_CPUTIME_ID, &mut ts) },
0
);
Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
}
fn is_out_of_descriptors(err: &io::Error) -> bool {
matches!(err.raw_os_error(), Some(libc::EMFILE | libc::ENFILE))
}
async fn idles() {
let (start, spent) = (cpu(), Duration::from_millis(300));
crate::time::sleep(spent).await;
let burned = cpu() - start;
assert!(burned < spent / 3, "{burned:?} of cpu while {spent:?} idle");
}
fn tcp_case(multi: bool) {
runtime().block_on(async {
let listener = Handle::<TcpListener>::bind("127.0.0.1:0").await.unwrap();
let addr: SocketAddr = listener.local_addr().unwrap();
let mut stream = within(listener.accept_multi()).await.unwrap();
let clients: Vec<_> = (0..CLIENTS)
.map(|_| TcpStream::connect(addr).unwrap())
.collect();
let mut starved = Starved::new();
let mut served = Vec::new();
if multi {
let first = crate::time::timeout(Duration::from_millis(300), next(&mut stream)).await;
assert!(
first.is_err(),
"no descriptor left, yet the stream moved: {:?}, {:?}",
first.as_ref().map(|c| c.is_some()),
stream.take_error()
);
starved.free();
for _ in 0..CLIENTS - CLOSED {
served.push(within(next(&mut stream)).await.expect("the stream ended"));
}
assert!(stream.take_error().is_none());
} else {
for _ in 0..RETRIES {
let err = within(listener.accept()).await.unwrap_err();
assert!(is_out_of_descriptors(&err), "{err}");
}
idles().await;
starved.free();
for _ in 0..CLIENTS - CLOSED {
served.push(within(listener.accept()).await.unwrap().0);
}
}
assert_eq!(closed(&clients), CLOSED);
});
}
fn unix_case() {
let path = std::env::temp_dir().join(format!("nuclei-emfile-{}.sock", std::process::id()));
let _ = std::fs::remove_file(&path);
runtime().block_on(async {
let listener = Handle::<UnixListener>::bind(&path).unwrap();
let clients: Vec<_> = (0..CLIENTS)
.map(|_| UnixStream::connect(&path).unwrap())
.collect();
let mut starved = Starved::new();
for _ in 0..RETRIES {
let err = within(listener.accept()).await.unwrap_err();
assert!(is_out_of_descriptors(&err), "{err}");
}
idles().await;
starved.free();
let mut served = Vec::new();
for _ in 0..CLIENTS - CLOSED {
served.push(within(listener.accept()).await.unwrap().0);
}
assert_eq!(closed(&clients), CLOSED);
});
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_tcp_accept_out_of_descriptors_fails_and_later_serves_the_backlog() {
if alone(
"net::tests::emfile::a_tcp_accept_out_of_descriptors_fails_and_later_serves_the_backlog",
) {
tcp_case(false);
}
}
#[test]
fn a_tcp_accept_multi_out_of_descriptors_waits_for_descriptors_and_serves_the_backlog() {
if alone(
"net::tests::emfile::a_tcp_accept_multi_out_of_descriptors_waits_for_descriptors_and_serves_the_backlog",
) {
tcp_case(true);
}
}
#[test]
fn a_unix_accept_out_of_descriptors_fails_and_later_serves_the_backlog() {
if alone(
"net::tests::emfile::a_unix_accept_out_of_descriptors_fails_and_later_serves_the_backlog",
) {
unix_case();
}
}