use std::io::{Read, Write};
use std::net::{IpAddr, SocketAddr, TcpListener, TcpStream, UdpSocket};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use std::time::{Duration, SystemTime};
use parking_lot::Mutex;
use crate::asyn_trace;
use crate::drivers::ip_port::{
DrvAsynIPPort, is_nonfatal_read_timeout, maxchars_zero_error, socket_poll_timeout,
};
use crate::drivers::option_parse::parse_yn_option;
use crate::error::{AsynError, AsynResult, AsynStatus};
use crate::exception::AsynException;
use crate::interfaces::InterfaceType;
use crate::interpose::{EomReason, OctetReadResult};
use crate::interrupt::{InterruptManager, InterruptValue, OctetFanOut};
use crate::param::ParamValue;
use crate::port::{ExceptionAnnouncer, PortDriver, PortDriverBase, PortFlags};
use crate::trace::{TraceManager, TraceMask};
use crate::user::AsynUser;
pub const DEFAULT_MAX_CLIENTS: usize = 64;
pub const UDP_MAX_DATAGRAM: usize = 65507;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IpServerProtocol {
#[default]
Tcp,
Udp,
}
#[derive(Debug, Clone)]
pub struct IpServerConfig {
pub bind_host: String,
pub bind_port: u16,
pub protocol: IpServerProtocol,
pub max_clients: usize,
pub no_process_eos: bool,
pub read_timeout: Option<Duration>,
}
impl IpServerConfig {
pub fn parse(spec: &str) -> AsynResult<Self> {
let trimmed = spec.trim();
let mut tokens: Vec<&str> = trimmed.split_whitespace().collect();
if tokens.is_empty() {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: "empty IP server port spec".into(),
});
}
let mut protocol = IpServerProtocol::Tcp;
if tokens.len() == 2 {
let last = tokens.last().unwrap().to_ascii_uppercase();
match last.as_str() {
"TCP" => {
protocol = IpServerProtocol::Tcp;
tokens.pop();
}
"UDP" => {
protocol = IpServerProtocol::Udp;
tokens.pop();
}
_ => {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!(
"unknown protocol token '{}' in '{spec}' (expected tcp or udp)",
tokens.last().unwrap()
),
});
}
}
}
if tokens.len() != 1 {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!("unexpected tokens after host:port in '{spec}'"),
});
}
let addr_part = tokens[0];
let (host, port) = if let Some(rest) = addr_part.strip_prefix('[') {
let end = rest.find(']').ok_or_else(|| AsynError::Status {
status: AsynStatus::Error,
message: format!("missing closing bracket in IPv6 address: '{spec}'"),
})?;
let host = rest[..end].to_string();
let port_part = rest[end + 1..]
.strip_prefix(':')
.ok_or_else(|| AsynError::Status {
status: AsynStatus::Error,
message: format!("missing port after bracketed IPv6 address: '{spec}'"),
})?;
let port: u16 = port_part.parse().map_err(|_| AsynError::Status {
status: AsynStatus::Error,
message: format!("invalid port in '{spec}'"),
})?;
(host, port)
} else {
let (host, port_part) =
addr_part
.rsplit_once(':')
.ok_or_else(|| AsynError::Status {
status: AsynStatus::Error,
message: format!("missing port in '{spec}' (expected host:port)"),
})?;
let port: u16 = port_part.parse().map_err(|_| AsynError::Status {
status: AsynStatus::Error,
message: format!("invalid port in '{spec}'"),
})?;
(host.to_string(), port)
};
Ok(Self {
bind_host: host,
bind_port: port,
protocol,
max_clients: DEFAULT_MAX_CLIENTS,
no_process_eos: false,
read_timeout: None,
})
}
}
struct SlotFailure {
closed: bool,
error: AsynError,
}
impl SlotFailure {
fn kept(error: AsynError) -> Self {
Self {
closed: false,
error,
}
}
fn closed(error: AsynError) -> Self {
Self {
closed: true,
error,
}
}
}
pub struct ClientSlot {
stream: Mutex<Option<TcpStream>>,
peer: Mutex<Option<SocketAddr>>,
occupied: Arc<AtomicBool>,
disconnect_on_read_timeout: AtomicBool,
}
impl ClientSlot {
fn new_empty() -> Self {
Self {
stream: Mutex::new(None),
peer: Mutex::new(None),
occupied: Arc::new(AtomicBool::new(false)),
disconnect_on_read_timeout: AtomicBool::new(false),
}
}
fn set_disconnect_on_read_timeout(&self, yes: bool) {
self.disconnect_on_read_timeout
.store(yes, Ordering::Release);
}
fn disconnect_on_read_timeout(&self) -> bool {
self.disconnect_on_read_timeout.load(Ordering::Acquire)
}
fn is_occupied(&self) -> bool {
self.occupied.load(Ordering::Acquire)
}
fn connection_cell(&self) -> Arc<AtomicBool> {
Arc::clone(&self.occupied)
}
fn assign(&self, stream: TcpStream, peer: SocketAddr) {
*self.stream.lock() = Some(stream);
*self.peer.lock() = Some(peer);
self.occupied.store(true, Ordering::Release);
}
fn clear(&self) {
*self.stream.lock() = None;
*self.peer.lock() = None;
self.occupied.store(false, Ordering::Release);
}
fn peer_addr(&self) -> Option<SocketAddr> {
*self.peer.lock()
}
fn read_or_close(
&self,
buf: &mut [u8],
timeout: Duration,
device: &str,
) -> Result<usize, SlotFailure> {
if buf.is_empty() {
return Err(SlotFailure::kept(maxchars_zero_error()));
}
let mut guard = self.stream.lock();
let Some(stream) = guard.as_mut() else {
return Err(SlotFailure::kept(AsynError::Status {
status: AsynStatus::Disconnected,
message: format!("{device} has no client"),
}));
};
let _ = stream.set_read_timeout(Some(socket_poll_timeout(timeout)));
let res = stream.read(buf);
drop(guard);
match res {
Ok(0) => {
self.clear();
Err(SlotFailure::closed(AsynError::Status {
status: AsynStatus::Disconnected,
message: format!("{device} peer closed"),
}))
}
Ok(n) => Ok(n),
Err(e) => Err(self.classify_read_error(e, device, timeout)),
}
}
fn write_or_close(&self, data: &[u8], device: &str) -> Result<(), SlotFailure> {
let mut guard = self.stream.lock();
let Some(stream) = guard.as_mut() else {
return Err(SlotFailure::kept(AsynError::Status {
status: AsynStatus::Disconnected,
message: format!("{device} has no client"),
}));
};
let res = stream.write_all(data).and_then(|()| stream.flush());
drop(guard);
match res {
Ok(()) => Ok(()),
Err(e) => Err(self.classify_io_error(e, device, "write")),
}
}
fn classify_read_error(
&self,
e: std::io::Error,
device: &str,
timeout: Duration,
) -> SlotFailure {
if is_nonfatal_read_timeout(e.kind())
&& !(self.disconnect_on_read_timeout() && timeout > Duration::ZERO)
{
return SlotFailure::kept(AsynError::Status {
status: AsynStatus::Timeout,
message: "read timeout".to_string(),
});
}
self.clear();
SlotFailure::closed(AsynError::Status {
status: AsynStatus::Error,
message: format!("{device} read error: {e}"),
})
}
fn classify_io_error(&self, e: std::io::Error, device: &str, what: &str) -> SlotFailure {
if is_nonfatal_read_timeout(e.kind()) {
return SlotFailure::kept(AsynError::Status {
status: AsynStatus::Timeout,
message: format!("{what} timeout"),
});
}
self.clear();
SlotFailure::closed(AsynError::Status {
status: AsynStatus::Error,
message: format!("{device} {what} error: {e}"),
})
}
fn drain_input(&self) {
let mut g = self.stream.lock();
let Some(stream) = g.as_mut() else { return };
if stream.set_nonblocking(true).is_err() {
return;
}
let mut buf = [0u8; 512];
loop {
match stream.read(&mut buf) {
Ok(0) => break,
Ok(_) => continue,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(_) => break,
}
}
let _ = stream.set_nonblocking(false);
}
}
struct Acceptor {
listener: TcpListener,
slots: Vec<Arc<ClientSlot>>,
read_timeout: Option<Duration>,
max_clients: usize,
port_name: String,
announcer: ExceptionAnnouncer,
interrupts: InterruptManager,
trace: Option<Arc<TraceManager>>,
shutdown: Arc<AtomicBool>,
}
impl Acceptor {
fn seed_child_trace(&self, child: &str) {
let Some(trace) = &self.trace else { return };
trace.set_trace_mask(Some(child), trace.get_trace_mask(Some(&self.port_name)));
trace.set_trace_io_mask(Some(child), trace.get_trace_io_mask(Some(&self.port_name)));
}
fn accept_one(&self) -> AsynResult<Option<usize>> {
let (stream, peer) = match self.listener.accept() {
Ok(v) => v,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(None),
Err(e) => {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!("accept failed: {e}"),
});
}
};
stream
.set_nonblocking(false)
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("set_nonblocking(false) on accepted client failed: {e}"),
})?;
if let Some(t) = self.read_timeout {
stream
.set_read_timeout(Some(t))
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("set_read_timeout failed: {e}"),
})?;
}
for (i, slot) in self.slots.iter().enumerate() {
if !slot.is_occupied() {
slot.assign(stream, peer);
self.announcer.announce(AsynException::Connect, i as i32);
let child = format!("{}:{}", self.port_name, i);
self.seed_child_trace(&child);
asyn_trace!(
Some(self.trace),
&self.port_name,
TraceMask::FLOW,
"new connection from {peer} on {child}"
);
self.interrupts.notify_octet(
OctetFanOut::EveryUser,
InterruptValue {
reason: 0,
addr: i as i32,
value: ParamValue::Octet(child),
timestamp: SystemTime::now(),
iface: Some(InterfaceType::Octet),
..Default::default()
},
);
return Ok(Some(i));
}
}
drop(stream);
Err(AsynError::Status {
status: AsynStatus::Error,
message: format!(
"no free client slot (max_clients={}); dropped connection from {peer}",
self.max_clients
),
})
}
fn run(&self) {
asyn_trace!(
Some(self.trace),
&self.port_name,
TraceMask::FLOW,
"started listening for connections on {}",
self.port_name
);
while !self.shutdown.load(Ordering::SeqCst) {
match self.accept_one() {
Ok(Some(_)) => {}
Ok(None) => std::thread::sleep(ACCEPT_POLL_INTERVAL),
Err(e) => asyn_trace!(
Some(self.trace),
&self.port_name,
TraceMask::ERROR,
"accept error on {}: {}",
self.port_name,
e.message()
),
}
}
asyn_trace!(
Some(self.trace),
&self.port_name,
TraceMask::FLOW,
"terminating connection thread for {}",
self.port_name
);
}
}
pub struct DrvAsynIPServerPort {
base: PortDriverBase,
config: IpServerConfig,
listener: Mutex<Option<TcpListener>>,
slots: Vec<Arc<ClientSlot>>,
udp_socket: Mutex<Option<Arc<UdpSocket>>>,
udp_cache: Arc<Mutex<UdpCache>>,
udp_shutdown: Arc<AtomicBool>,
udp_thread: Mutex<Option<JoinHandle<()>>>,
accept_shutdown: Arc<AtomicBool>,
accept_thread: Mutex<Option<JoinHandle<()>>>,
}
const ACCEPT_POLL_INTERVAL: Duration = Duration::from_millis(10);
struct UdpCache {
data: Vec<u8>,
pos: usize,
}
impl UdpCache {
fn new() -> Self {
Self {
data: Vec::new(),
pos: 0,
}
}
fn is_empty(&self) -> bool {
self.pos >= self.data.len()
}
fn clear(&mut self) {
self.data.clear();
self.pos = 0;
}
}
impl DrvAsynIPServerPort {
pub fn new(port_name: &str, spec: &str) -> AsynResult<Self> {
let config = IpServerConfig::parse(spec)?;
Self::with_config(port_name, config)
}
pub fn with_config(port_name: &str, config: IpServerConfig) -> AsynResult<Self> {
if config.max_clients == 0 {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: "maxClients must be > 0 (C drvAsynIPServerPort: \"No clients.\")".into(),
});
}
let max = config.max_clients;
let mut base = PortDriverBase::new(
port_name,
max,
PortFlags {
multi_device: true,
can_block: true,
destructible: true,
},
);
base.init_connected(false);
base.auto_connect = true;
let mut slots = Vec::with_capacity(max);
for _ in 0..max {
slots.push(Arc::new(ClientSlot::new_empty()));
}
Ok(Self {
base,
config,
listener: Mutex::new(None),
slots,
udp_socket: Mutex::new(None),
udp_cache: Arc::new(Mutex::new(UdpCache::new())),
udp_shutdown: Arc::new(AtomicBool::new(false)),
udp_thread: Mutex::new(None),
accept_shutdown: Arc::new(AtomicBool::new(false)),
accept_thread: Mutex::new(None),
})
}
fn open_listener(&mut self) -> AsynResult<()> {
if self.config.protocol == IpServerProtocol::Udp {
return self.open_udp_listener();
}
let addr = self.resolve_bind_addr()?;
let listener = self.bind_with_options(addr)?;
listener
.set_nonblocking(false)
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("set_nonblocking failed: {e}"),
})?;
self.start_accept_loop(&listener)?;
*self.listener.lock() = Some(listener);
self.base.set_connected(true);
Ok(())
}
fn start_accept_loop(&mut self, listener: &TcpListener) -> AsynResult<()> {
let accept_listener = listener.try_clone().map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("listener try_clone failed: {e}"),
})?;
accept_listener
.set_nonblocking(true)
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("set_nonblocking failed: {e}"),
})?;
self.accept_shutdown.store(false, Ordering::SeqCst);
let acceptor = Acceptor {
listener: accept_listener,
slots: self.slots.clone(),
read_timeout: self.config.read_timeout,
max_clients: self.config.max_clients,
port_name: self.base.port_name.clone(),
announcer: self.base.exception_announcer(),
interrupts: InterruptManager::from_shared_state(self.base.interrupts.shared_state()),
trace: self.base.trace.clone(),
shutdown: Arc::clone(&self.accept_shutdown),
};
let port_name = self.base.port_name.clone();
let handle = std::thread::Builder::new()
.name(format!("ipserver-accept-{port_name}"))
.spawn(move || acceptor.run())
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("accept thread spawn failed: {e}"),
})?;
*self.accept_thread.lock() = Some(handle);
Ok(())
}
fn stop_accept_loop(&mut self) {
self.accept_shutdown.store(true, Ordering::SeqCst);
if let Some(handle) = self.accept_thread.lock().take() {
let _ = handle.join();
}
}
fn open_udp_listener(&mut self) -> AsynResult<()> {
let addr = self.resolve_bind_addr()?;
let domain = if addr.is_ipv4() {
socket2::Domain::IPV4
} else {
socket2::Domain::IPV6
};
let sock = socket2::Socket::new(domain, socket2::Type::DGRAM, Some(socket2::Protocol::UDP))
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("UDP socket() failed: {e}"),
})?;
#[cfg(unix)]
sock.set_reuse_port(true).map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("UDP SO_REUSEPORT failed: {e}"),
})?;
sock.set_reuse_address(true)
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("UDP SO_REUSEADDR failed: {e}"),
})?;
sock.bind(&addr.into()).map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("UDP bind '{addr}' failed: {e}"),
})?;
let socket = UdpSocket::from(sock);
socket
.set_read_timeout(Some(Duration::from_millis(200)))
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("UDP set_read_timeout failed: {e}"),
})?;
let socket = Arc::new(socket);
self.udp_shutdown.store(false, Ordering::SeqCst);
let socket_t = Arc::clone(&socket);
let cache_t = Arc::clone(&self.udp_cache);
let shutdown_t = Arc::clone(&self.udp_shutdown);
let port_name = self.base.port_name.clone();
let interrupts_t = InterruptManager::from_shared_state(self.base.interrupts.shared_state());
let handle = std::thread::Builder::new()
.name(format!("udp-server-{port_name}"))
.spawn(move || udp_recv_loop(socket_t, cache_t, shutdown_t, port_name, interrupts_t))
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("UDP recv thread spawn failed: {e}"),
})?;
*self.udp_socket.lock() = Some(socket);
*self.udp_thread.lock() = Some(handle);
self.base.set_connected(true);
Ok(())
}
fn resolve_bind_addr(&self) -> AsynResult<SocketAddr> {
let host = self.config.bind_host.trim();
let port = self.config.bind_port;
if host.is_empty() || host.eq_ignore_ascii_case("localhost") {
return Ok(SocketAddr::from(([0, 0, 0, 0], port)));
}
if let Ok(ip) = host.parse::<IpAddr>() {
return Ok(SocketAddr::new(ip, port));
}
use std::net::ToSocketAddrs;
(host, port)
.to_socket_addrs()
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("cannot resolve bind host '{host}': {e}"),
})?
.next()
.ok_or_else(|| AsynError::Status {
status: AsynStatus::Error,
message: format!("cannot resolve bind host '{host}': no addresses"),
})
}
fn bind_with_options(&self, addr: SocketAddr) -> AsynResult<TcpListener> {
let domain = if addr.is_ipv4() {
socket2::Domain::IPV4
} else {
socket2::Domain::IPV6
};
let socket =
socket2::Socket::new(domain, socket2::Type::STREAM, Some(socket2::Protocol::TCP))
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("socket() failed: {e}"),
})?;
socket
.set_reuse_address(true)
.map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("SO_REUSEADDR failed: {e}"),
})?;
socket.bind(&addr.into()).map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("bind '{addr}' failed: {e}"),
})?;
socket.listen(128).map_err(|e| AsynError::Status {
status: AsynStatus::Error,
message: format!("listen failed: {e}"),
})?;
Ok(TcpListener::from(socket))
}
pub fn local_port(&self) -> u16 {
self.listener
.lock()
.as_ref()
.and_then(|l| l.local_addr().ok())
.map(|a| a.port())
.unwrap_or(0)
}
pub fn drop_client(&self, addr: i32) -> AsynResult<()> {
let idx = self.slot_index(addr)?;
let slot = &self.slots[idx];
if slot.is_occupied() {
slot.clear();
self.base.announce_exception(AsynException::Connect, addr);
}
Ok(())
}
fn slot_index(&self, addr: i32) -> AsynResult<usize> {
if addr < 0 || (addr as usize) >= self.slots.len() {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!("addr {addr} out of range (max {})", self.slots.len()),
});
}
Ok(addr as usize)
}
fn slot_arc(&self, addr: i32) -> AsynResult<Arc<ClientSlot>> {
let idx = self.slot_index(addr)?;
let slot = self.slots[idx].clone();
if slot.is_occupied() {
Ok(slot)
} else {
Err(AsynError::Status {
status: AsynStatus::Error,
message: format!("slot {addr} has no connected client"),
})
}
}
pub fn peer(&self, addr: i32) -> Option<SocketAddr> {
let idx = self.slot_index(addr).ok()?;
self.slots[idx].peer_addr()
}
pub fn child_port_name(&self, idx: usize) -> String {
format!("{}:{}", self.base.port_name, idx)
}
pub fn child_port_names(&self) -> Vec<String> {
(0..self.slots.len())
.map(|i| self.child_port_name(i))
.collect()
}
pub fn make_subport(&self, idx: usize) -> AsynResult<DrvAsynIPSubport> {
if idx >= self.slots.len() {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!(
"subport idx {idx} out of range (max_clients={})",
self.slots.len()
),
});
}
let name = self.child_port_name(idx);
Ok(DrvAsynIPSubport::new(
name,
Arc::clone(&self.slots[idx]),
self.config.no_process_eos,
))
}
}
impl PortDriver for DrvAsynIPServerPort {
fn base(&self) -> &PortDriverBase {
&self.base
}
fn base_mut(&mut self) -> &mut PortDriverBase {
&mut self.base
}
fn capabilities(&self) -> Vec<crate::interfaces::Capability> {
use crate::interfaces::Capability::*;
let mut caps = vec![OctetRead, OctetWrite, Flush, Connect];
if self.config.protocol == IpServerProtocol::Tcp {
caps.push(Int32Read);
caps.push(Int32Write);
}
caps
}
fn connect(&mut self, _user: &AsynUser) -> AsynResult<()> {
let already_up = self.base.is_connected()
&& (self.listener.lock().is_some() || self.udp_socket.lock().is_some());
if already_up {
return Ok(());
}
self.open_listener()
}
fn disconnect(&mut self, _user: &AsynUser) -> AsynResult<()> {
for (i, slot) in self.slots.iter().enumerate() {
if slot.is_occupied() {
slot.clear();
self.base
.announce_exception(AsynException::Connect, i as i32);
}
}
self.stop_accept_loop();
self.stop_udp_worker();
*self.udp_socket.lock() = None;
self.udp_cache.lock().clear();
*self.listener.lock() = None;
self.base.set_connected(false);
Ok(())
}
fn shutdown(&mut self) -> AsynResult<()> {
self.stop_accept_loop();
self.stop_udp_worker();
*self.udp_socket.lock() = None;
self.udp_cache.lock().clear();
*self.listener.lock() = None;
Ok(())
}
fn read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
if self.config.protocol == IpServerProtocol::Udp {
if buf.is_empty() {
return Err(maxchars_zero_error());
}
return Ok(self.udp_drain_into(buf));
}
let res = self.base_read_octet(user, buf)?;
Ok(res.nbytes_transferred)
}
fn io_read_octet_eom(
&mut self,
user: &AsynUser,
buf: &mut [u8],
) -> AsynResult<(usize, EomReason)> {
if self.config.protocol == IpServerProtocol::Udp {
if buf.is_empty() {
return Err(maxchars_zero_error());
}
return Ok(self.udp_drain_into_eom(buf));
}
let res = self.base_read_octet(user, buf)?;
Ok((res.nbytes_transferred, res.eom_reason))
}
fn write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
if self.config.protocol == IpServerProtocol::Udp {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: "UDP server-mode port is read-only (C asyn writeIt returns asynError)"
.into(),
});
}
if user.addr < 0 {
for (i, slot) in self.slots.iter().enumerate() {
if !slot.is_occupied() {
continue;
}
if let Err(f) = self.write_to_slot(slot, i as i32, data) {
tracing::debug!(
target: "asyn_rs::ip_server_port",
addr = i,
error = %f.error,
"broadcast write to slot failed"
);
if f.closed {
self.base
.announce_exception(AsynException::Connect, i as i32);
}
}
}
return Ok(data.len());
}
let arc = self.slot_arc(user.addr)?;
match self.write_to_slot(&arc, user.addr, data) {
Ok(()) => Ok(data.len()),
Err(f) => Err(self.finish_slot_failure(user.addr, f)),
}
}
fn io_flush(&mut self, user: &mut AsynUser) -> AsynResult<()> {
if self.config.protocol == IpServerProtocol::Udp {
self.udp_cache.lock().clear();
return Ok(());
}
if user.addr < 0 {
for slot in &self.slots {
slot.drain_input();
}
} else if let Ok(idx) = self.slot_index(user.addr) {
self.slots[idx].drain_input();
}
Ok(())
}
}
impl DrvAsynIPServerPort {
fn base_read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
let arc = self.slot_arc(user.addr)?;
let device = self.slot_device_name(user.addr);
match arc.read_or_close(buf, user.timeout, &device) {
Ok(n) => Ok(OctetReadResult {
nbytes_transferred: n,
eom_reason: if n >= buf.len() {
EomReason::CNT
} else {
EomReason::empty()
},
}),
Err(f) => Err(self.finish_slot_failure(user.addr, f)),
}
}
fn finish_slot_failure(&mut self, addr: i32, failure: SlotFailure) -> AsynError {
if failure.closed {
self.base.announce_exception(AsynException::Connect, addr);
}
failure.error
}
fn slot_device_name(&self, addr: i32) -> String {
format!("{}:{}", self.base.port_name, addr)
}
fn write_to_slot(&self, slot: &ClientSlot, addr: i32, data: &[u8]) -> Result<(), SlotFailure> {
slot.write_or_close(data, &self.slot_device_name(addr))
}
fn udp_drain_into(&self, buf: &mut [u8]) -> usize {
self.udp_drain_into_eom(buf).0
}
fn udp_drain_into_eom(&self, buf: &mut [u8]) -> (usize, EomReason) {
let mut cache = self.udp_cache.lock();
if cache.is_empty() {
return (0, EomReason::empty());
}
let avail = cache.data.len() - cache.pos;
let n = avail.min(buf.len());
buf[..n].copy_from_slice(&cache.data[cache.pos..cache.pos + n]);
cache.pos += n;
let mut eom = EomReason::empty();
if cache.is_empty() {
cache.clear();
eom |= EomReason::END;
}
if n == buf.len() && !buf.is_empty() {
eom |= EomReason::CNT;
}
(n, eom)
}
fn stop_udp_worker(&mut self) {
self.udp_shutdown.store(true, Ordering::SeqCst);
if let Some(handle) = self.udp_thread.lock().take() {
let _ = handle.join();
}
}
pub fn udp_cache_pending(&self) -> usize {
let c = self.udp_cache.lock();
c.data.len().saturating_sub(c.pos)
}
}
fn udp_recv_loop(
socket: Arc<UdpSocket>,
cache: Arc<Mutex<UdpCache>>,
shutdown: Arc<AtomicBool>,
port_name: String,
interrupts: InterruptManager,
) {
let mut buf = vec![0u8; UDP_MAX_DATAGRAM];
loop {
if shutdown.load(Ordering::SeqCst) {
break;
}
let cache_empty = cache.lock().is_empty();
if !cache_empty {
std::thread::sleep(Duration::from_millis(1));
continue;
}
match socket.recv(&mut buf) {
Ok(n) => {
{
let mut c = cache.lock();
c.data.clear();
c.data.extend_from_slice(&buf[..n]);
c.pos = 0;
}
interrupts.notify_octet(
OctetFanOut::EveryUser,
InterruptValue {
reason: 0,
addr: 0,
value: ParamValue::Octet(String::from_utf8_lossy(&buf[..n]).into_owned()),
timestamp: SystemTime::now(),
iface: Some(InterfaceType::Octet),
..Default::default()
},
);
}
Err(e) if is_nonfatal_read_timeout(e.kind()) => {
continue;
}
Err(e) => {
tracing::warn!(
target: "asyn_rs::ip_server_port",
port = %port_name,
error = %e,
"UDP recv error — exiting recv loop"
);
break;
}
}
}
}
pub struct DrvAsynIPSubport {
base: PortDriverBase,
slot: Arc<ClientSlot>,
}
impl DrvAsynIPSubport {
fn new(port_name: String, slot: Arc<ClientSlot>, no_process_eos: bool) -> Self {
let mut base = PortDriverBase::new(
&port_name,
1,
PortFlags {
multi_device: false,
can_block: true,
destructible: true,
},
);
base.share_connection(slot.connection_cell());
DrvAsynIPPort::apply_ip_port_configure(
&mut base,
true,
no_process_eos,
);
Self { base, slot }
}
pub fn peer(&self) -> Option<SocketAddr> {
self.slot.peer_addr()
}
fn finish_slot_failure(&mut self, failure: SlotFailure) -> AsynError {
if failure.closed {
self.base.announce_exception(AsynException::Connect, 0);
self.base.set_connected(false);
}
failure.error
}
}
impl PortDriver for DrvAsynIPSubport {
fn base(&self) -> &PortDriverBase {
&self.base
}
fn base_mut(&mut self) -> &mut PortDriverBase {
&mut self.base
}
fn capabilities(&self) -> Vec<crate::interfaces::Capability> {
crate::interfaces::octet_transport_capabilities()
}
fn connect(&mut self, _user: &AsynUser) -> AsynResult<()> {
self.base.sync_connection_edge();
if !self.base.is_connected() {
return Err(AsynError::Status {
status: AsynStatus::Disconnected,
message: "no client assigned to this subport slot yet".into(),
});
}
Ok(())
}
fn set_option(&mut self, _user: &mut AsynUser, key: &str, value: &str) -> AsynResult<()> {
if key.eq_ignore_ascii_case("disconnectOnReadTimeout") {
let yes = parse_yn_option("disconnectOnReadTimeout", value)?;
self.slot.set_disconnect_on_read_timeout(yes);
return Ok(());
}
Err(AsynError::OptionNotFound(key.to_string()))
}
fn get_option(&self, key: &str) -> AsynResult<String> {
if key.eq_ignore_ascii_case("disconnectOnReadTimeout") {
return Ok(if self.slot.disconnect_on_read_timeout() {
"Y".to_string()
} else {
"N".to_string()
});
}
Err(AsynError::OptionNotFound(key.to_string()))
}
fn disconnect(&mut self, _user: &AsynUser) -> AsynResult<()> {
if self.slot.is_occupied() {
self.slot.clear();
self.base.announce_exception(AsynException::Connect, 0);
}
self.base.set_connected(false);
Ok(())
}
fn read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
let device = self.base.port_name.clone();
match self.slot.read_or_close(buf, user.timeout, &device) {
Ok(n) => Ok(n),
Err(f) => Err(self.finish_slot_failure(f)),
}
}
fn write_octet(&mut self, _user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
let device = self.base.port_name.clone();
match self.slot.write_or_close(data, &device) {
Ok(()) => Ok(data.len()),
Err(f) => Err(self.finish_slot_failure(f)),
}
}
fn io_flush(&mut self, _user: &mut AsynUser) -> AsynResult<()> {
self.slot.drain_input();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn wait_for_slot(srv: &DrvAsynIPServerPort, idx: usize) -> usize {
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while srv.peer(idx as i32).is_none() {
assert!(
std::time::Instant::now() < deadline,
"slot {idx} never filled — the accept loop is not running"
);
std::thread::sleep(Duration::from_millis(2));
}
idx
}
fn read_until_slot_torn_down(mut step: impl FnMut() -> (AsynResult<usize>, bool)) -> AsynError {
let deadline = std::time::Instant::now() + Duration::from_secs(5);
loop {
let (res, torn_down) = step();
match res {
Ok(n) => panic!("a reset peer cannot deliver {n} bytes"),
Err(e) if e.status() == AsynStatus::Timeout => {}
Err(e) => {
assert!(
torn_down,
"the read returned teardown status {e:?} but the slot is \
still occupied — read_or_close must clear() before returning"
);
return e;
}
}
assert!(
std::time::Instant::now() < deadline,
"the peer's abortive close never surfaced to the server read"
);
std::thread::sleep(Duration::from_millis(5));
}
}
#[test]
fn parse_basic_ipv4() {
let cfg = IpServerConfig::parse("0.0.0.0:8080").unwrap();
assert_eq!(cfg.bind_host, "0.0.0.0");
assert_eq!(cfg.bind_port, 8080);
assert_eq!(cfg.max_clients, DEFAULT_MAX_CLIENTS);
}
#[test]
fn parse_with_tcp_token() {
let cfg = IpServerConfig::parse("127.0.0.1:5000 TCP").unwrap();
assert_eq!(cfg.bind_host, "127.0.0.1");
assert_eq!(cfg.bind_port, 5000);
}
#[test]
fn parse_rejects_so_reuseport_token() {
assert!(IpServerConfig::parse("0.0.0.0:9000 SO_REUSEPORT").is_err());
assert!(IpServerConfig::parse("0.0.0.0:9000 TCP SO_REUSEPORT").is_err());
}
#[test]
fn parse_ipv6_bracket_form() {
let cfg = IpServerConfig::parse("[::1]:7000").unwrap();
assert_eq!(cfg.bind_host, "::1");
assert_eq!(cfg.bind_port, 7000);
}
#[test]
fn parse_udp_protocol_token() {
let cfg = IpServerConfig::parse("0.0.0.0:7000 UDP").unwrap();
assert_eq!(cfg.protocol, IpServerProtocol::Udp);
let cfg2 = IpServerConfig::parse("0.0.0.0:7000").unwrap();
assert_eq!(cfg2.protocol, IpServerProtocol::Tcp, "default is TCP");
}
#[test]
fn with_config_rejects_zero_max_clients() {
let cfg = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 0,
no_process_eos: false,
read_timeout: None,
};
match DrvAsynIPServerPort::with_config("zero_clients", cfg) {
Err(AsynError::Status { message, .. }) => {
assert!(
message.contains("maxClients"),
"expected maxClients rejection, got: {message}"
);
}
Ok(_) => panic!("expected maxClients==0 to be rejected"),
Err(other) => panic!("wrong error variant: {other:?}"),
}
let ok = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 1,
no_process_eos: false,
read_timeout: None,
};
assert!(DrvAsynIPServerPort::with_config("one_client", ok).is_ok());
}
#[test]
fn resolve_bind_addr_maps_localhost_and_empty_to_inaddr_any() {
let any = IpAddr::from([0, 0, 0, 0]);
let empty = DrvAsynIPServerPort::new("rb_empty", ":0").unwrap();
assert_eq!(
empty.resolve_bind_addr().unwrap().ip(),
any,
"empty host => INADDR_ANY"
);
let lh = DrvAsynIPServerPort::new("rb_localhost", "localhost:0").unwrap();
assert_eq!(
lh.resolve_bind_addr().unwrap().ip(),
any,
"localhost => INADDR_ANY (C does NOT map it to loopback)"
);
let lh_upper = DrvAsynIPServerPort::new("rb_localhost_upper", "LocalHost:0").unwrap();
assert_eq!(
lh_upper.resolve_bind_addr().unwrap().ip(),
any,
"localhost match is case-insensitive (C epicsStrCaseCmp)"
);
let explicit = DrvAsynIPServerPort::new("rb_explicit", "127.0.0.1:0").unwrap();
assert_eq!(
explicit.resolve_bind_addr().unwrap().ip(),
IpAddr::from([127, 0, 0, 1]),
"explicit IP literal binds verbatim"
);
}
#[test]
fn connect_localhost_named_host_binds() {
let mut srv = DrvAsynIPServerPort::new("rb_connect_lh", "localhost:0").unwrap();
srv.connect(&AsynUser::default()).unwrap();
assert!(srv.local_port() > 0, "listener bound to an ephemeral port");
srv.disconnect(&AsynUser::default()).unwrap();
}
#[cfg(unix)]
#[test]
fn reuse_port_set_for_udp_only_not_tcp() {
use socket2::SockRef;
let mut udp = DrvAsynIPServerPort::new("rp_udp", "127.0.0.1:0 UDP").unwrap();
udp.connect(&AsynUser::default()).unwrap();
{
let g = udp.udp_socket.lock();
let s = g.as_ref().expect("udp socket bound");
assert!(
SockRef::from(&**s).reuse_port().unwrap(),
"UDP server must enable SO_REUSEPORT (C datagram-fanout helper)"
);
}
udp.disconnect(&AsynUser::default()).unwrap();
let mut tcp = DrvAsynIPServerPort::new("rp_tcp", "127.0.0.1:0").unwrap();
tcp.connect(&AsynUser::default()).unwrap();
{
let g = tcp.listener.lock();
let s = g.as_ref().expect("tcp listener bound");
assert!(
!SockRef::from(s).reuse_port().unwrap(),
"TCP listener must NOT set SO_REUSEPORT (fanout is UDP-only in C)"
);
}
tcp.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn udp_server_flush_discards_cached_datagram() {
let mut srv = DrvAsynIPServerPort::new("udp_flush", "127.0.0.1:0 UDP").unwrap();
{
let mut cache = srv.udp_cache.lock();
cache.data = b"stale".to_vec();
cache.pos = 0;
}
assert_eq!(srv.udp_cache_pending(), 5);
let mut user = AsynUser::default().with_addr(0);
srv.io_flush(&mut user).unwrap();
assert_eq!(
srv.udp_cache_pending(),
0,
"flush must discard the cached datagram"
);
let mut buf = [0u8; 16];
let n = srv
.read_octet(&AsynUser::default().with_addr(0), &mut buf)
.unwrap();
assert_eq!(
n, 0,
"flush-then-read must not re-return the stale datagram"
);
}
#[test]
fn tcp_server_flush_drains_staged_socket_input() {
use std::io::Write as _;
use std::net::TcpStream as ClientStream;
let mut srv = DrvAsynIPServerPort::new("tcp_flush_drain", "127.0.0.1:0").unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let mut client = ClientStream::connect(("127.0.0.1", port)).unwrap();
let idx = wait_for_slot(&srv, 0);
client.write_all(b"stale-bytes").unwrap();
client.flush().unwrap();
std::thread::sleep(Duration::from_millis(50));
let mut user = AsynUser::default().with_addr(idx as i32);
srv.io_flush(&mut user).unwrap();
let read_user = AsynUser::default()
.with_addr(idx as i32)
.with_timeout(Duration::from_millis(100));
let mut buf = [0u8; 64];
match srv.read_octet(&read_user, &mut buf) {
Err(AsynError::Status {
status: AsynStatus::Timeout,
..
}) => {}
Ok(0) => {}
other => panic!("expected drained (timeout / 0 bytes), got {other:?}"),
}
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn a_fatal_read_error_frees_the_client_slot() {
use socket2::{Domain, Socket, Type};
let mut config = IpServerConfig::parse("127.0.0.1:0 TCP").unwrap();
config.max_clients = 1;
let mut srv = DrvAsynIPServerPort::with_config("rst_frees_slot", config).unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
let client = Socket::new(Domain::IPV4, Type::STREAM, None).unwrap();
client.connect(&addr.into()).unwrap();
let idx = wait_for_slot(&srv, 0);
assert_eq!(idx, 0);
assert!(srv.slots[0].is_occupied(), "the client owns the only slot");
client.set_linger(Some(Duration::ZERO)).unwrap();
drop(client); std::thread::sleep(Duration::from_millis(50));
let read_user = AsynUser::default()
.with_addr(0)
.with_timeout(Duration::from_millis(200));
let mut buf = [0u8; 64];
let err = read_until_slot_torn_down(|| {
let res = srv.read_octet(&read_user, &mut buf);
(res, !srv.slots[0].is_occupied())
});
assert!(
matches!(err.status(), AsynStatus::Error | AsynStatus::Disconnected),
"an abortive close is C's fatal-errno asynError or a recv==0 EOF, got {err:?}"
);
assert!(
!srv.slots[0].is_occupied(),
"C's readIt closeConnection frees the slot on a fatal errno, not just on EOF"
);
let _client2 = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
wait_for_slot(&srv, 0);
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn the_child_port_honours_disconnect_on_read_timeout() {
let mut config = IpServerConfig::parse("127.0.0.1:0 TCP").unwrap();
config.max_clients = 1;
let mut srv = DrvAsynIPServerPort::with_config("srv_drto", config).unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let mut child = srv.make_subport(0).unwrap();
let _client = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
wait_for_slot(&srv, 0);
let user = AsynUser::default().with_timeout(Duration::from_millis(50));
let mut buf = [0u8; 64];
let err = child.read_octet(&user, &mut buf).unwrap_err();
assert_eq!(err.status(), AsynStatus::Timeout, "got {err:?}");
assert!(
srv.slots[0].is_occupied(),
"with the option off a read timeout leaves the socket intact (C :810-811)"
);
let mut opt_user = AsynUser::default();
child
.set_option(&mut opt_user, "disconnectOnReadTimeout", "Y")
.unwrap();
assert_eq!(child.get_option("disconnectOnReadTimeout").unwrap(), "Y");
let poll_user = AsynUser::default().with_timeout(Duration::ZERO);
let err = child.read_octet(&poll_user, &mut buf).unwrap_err();
assert_eq!(err.status(), AsynStatus::Timeout, "got {err:?}");
assert!(
srv.slots[0].is_occupied(),
"a zero-timeout poll is not a read timeout C tears down on (:797)"
);
let err = child.read_octet(&user, &mut buf).unwrap_err();
assert_eq!(err.status(), AsynStatus::Error, "got {err:?}");
assert!(
!srv.slots[0].is_occupied(),
"disconnectOnReadTimeout=Y frees the slot on a read timeout \
(drvAsynIPPort.c:797-799, first disjunct)"
);
assert!(!child.base().is_connected());
let _client2 = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
wait_for_slot(&srv, 0);
assert!(child.base().is_connected());
assert_eq!(child.get_option("disconnectOnReadTimeout").unwrap(), "Y");
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn a_reused_client_slot_revives_the_child_subport() {
use socket2::{Domain, Socket, Type};
let mut config = IpServerConfig::parse("127.0.0.1:0 TCP").unwrap();
config.max_clients = 1;
let mut srv = DrvAsynIPServerPort::with_config("slot_revive", config).unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let mut child = srv.make_subport(0).unwrap();
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
let victim = Socket::new(Domain::IPV4, Type::STREAM, None).unwrap();
victim.connect(&addr.into()).unwrap();
wait_for_slot(&srv, 0);
child.connect(&AsynUser::default()).unwrap();
assert!(
child.base().is_connected(),
"the child serves the new client"
);
victim.set_linger(Some(Duration::ZERO)).unwrap();
drop(victim); std::thread::sleep(Duration::from_millis(50));
let read_user = AsynUser::default().with_timeout(Duration::from_millis(200));
let mut buf = [0u8; 64];
let err = read_until_slot_torn_down(|| {
let res = child.read_octet(&read_user, &mut buf);
(res, !child.base().is_connected())
});
assert!(
matches!(err.status(), AsynStatus::Error | AsynStatus::Disconnected),
"an abortive close is C's fatal-errno asynError or a recv==0 EOF, got {err:?}"
);
assert!(
!child.base().is_connected(),
"the child's own read tore the slot down (C closeConnection + exceptionDisconnect)"
);
assert!(!srv.slots[0].is_occupied());
let mut client2 = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
wait_for_slot(&srv, 0);
assert!(
child.base().is_connected(),
"a reused slot revives the child: C calls connectDevice on it \
(drvAsynIPServerPort.c:357-367)"
);
let mut write_user = AsynUser::default();
child
.write_octet(&mut write_user, b"hello\n")
.expect("the revived child must write to the new client's socket");
let mut got = [0u8; 6];
client2
.set_read_timeout(Some(Duration::from_secs(1)))
.unwrap();
client2.read_exact(&mut got).unwrap();
assert_eq!(&got, b"hello\n");
client2.write_all(b"world\n").unwrap();
let n = child
.read_octet(&read_user, &mut buf)
.expect("the revived child must read from the new client's socket");
assert_eq!(&buf[..n], b"world\n");
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn tcp_server_flush_no_connection_is_harmless() {
let mut srv = DrvAsynIPServerPort::new("tcp_flush_empty", "127.0.0.1:0").unwrap();
let mut user = AsynUser::default().with_addr(0);
srv.io_flush(&mut user).unwrap();
let mut bcast = AsynUser::default().with_addr(-1);
srv.io_flush(&mut bcast).unwrap();
}
#[test]
fn a_child_port_terminates_a_read_on_the_terminator_and_fans_it_out() {
use crate::interrupt::{InterruptFilter, InterruptValue};
use std::io::Write as _;
use std::net::TcpStream as ClientStream;
use std::sync::Mutex as StdMutex;
let mut srv = DrvAsynIPServerPort::new("srv_eos", "127.0.0.1:0").unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let mut client = ClientStream::connect(("127.0.0.1", port)).unwrap();
let idx = wait_for_slot(&srv, 0);
let mut sub = srv.make_subport(idx).unwrap();
sub.connect(&AsynUser::default()).unwrap();
let seen: Arc<StdMutex<Vec<String>>> = Arc::new(StdMutex::new(Vec::new()));
let seen_cb = seen.clone();
let _sub_cb = sub.base().interrupts.register_sync_callback(
InterruptFilter::default(),
move |iv: &InterruptValue| {
if let ParamValue::Octet(s) = &iv.value {
seen_cb.lock().unwrap().push(s.clone());
}
},
);
sub.set_input_eos(&AsynUser::default(), b"\n").unwrap();
client.write_all(b"line1\nline2\n").unwrap();
client.flush().unwrap();
std::thread::sleep(Duration::from_millis(50));
let user = AsynUser::default().with_timeout(Duration::from_millis(500));
let mut buf = [0u8; 64];
let (n, eom) = crate::port::octet_read_chain(&mut sub, &user, &mut buf).unwrap();
assert_eq!(&buf[..n], b"line1");
assert!(eom.contains(EomReason::EOS), "eom = {eom:?}");
let (n, eom) = crate::port::octet_read_chain(&mut sub, &user, &mut buf).unwrap();
assert_eq!(&buf[..n], b"line2");
assert!(eom.contains(EomReason::EOS), "eom = {eom:?}");
let got = seen.lock().unwrap().concat();
assert_eq!(got, "line1\nline2\n", "raw chunks fanned out, got {got:?}");
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn every_octet_user_hears_a_new_connection_whatever_slot_it_lands_in() {
use crate::interrupt::{InterruptFilter, InterruptValue};
use std::net::TcpStream as ClientStream;
use std::sync::Mutex as StdMutex;
let mut srv = DrvAsynIPServerPort::with_config(
"srv_announce",
IpServerConfig {
max_clients: 2,
..IpServerConfig::parse("127.0.0.1:0").unwrap()
},
)
.unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let seen: Arc<StdMutex<Vec<String>>> = Arc::new(StdMutex::new(Vec::new()));
let seen_cb = seen.clone();
let _cb = srv.base().interrupts.register_sync_callback(
InterruptFilter {
addr: Some(0),
..InterruptFilter::default()
},
move |iv: &InterruptValue| {
if let ParamValue::Octet(s) = &iv.value {
seen_cb.lock().unwrap().push(s.clone());
}
},
);
let _c0 = ClientStream::connect(("127.0.0.1", port)).unwrap();
wait_for_slot(&srv, 0);
let _c1 = ClientStream::connect(("127.0.0.1", port)).unwrap();
wait_for_slot(&srv, 1);
let got = seen.lock().unwrap().clone();
assert_eq!(
got,
vec!["srv_announce:0".to_string(), "srv_announce:1".to_string()],
"the addr-0 user must hear about the slot-1 client too"
);
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn a_child_port_inherits_the_parents_trace_masks_at_the_accept() {
use crate::services::PortServices;
use crate::trace::TraceIoMask;
use std::net::TcpStream as ClientStream;
let trace = Arc::new(TraceManager::new());
let services = PortServices::new(trace.clone());
let mut srv = DrvAsynIPServerPort::new("srv_trace", "127.0.0.1:0").unwrap();
services.bind(srv.base_mut());
let parent_mask = TraceMask::ERROR | TraceMask::FLOW | TraceMask::IO_DRIVER;
let parent_io = TraceIoMask::ASCII | TraceIoMask::HEX;
trace.set_trace_mask(Some("srv_trace"), parent_mask);
trace.set_trace_io_mask(Some("srv_trace"), parent_io);
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let _client = ClientStream::connect(("127.0.0.1", port)).unwrap();
wait_for_slot(&srv, 0);
assert_eq!(trace.get_trace_mask(Some("srv_trace:0")), parent_mask);
assert_eq!(trace.get_trace_io_mask(Some("srv_trace:0")), parent_io);
trace.set_trace_mask(Some("srv_trace"), TraceMask::ERROR);
assert_eq!(trace.get_trace_mask(Some("srv_trace:0")), parent_mask);
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn subport_flush_drains_staged_socket_input() {
use std::io::Write as _;
use std::net::TcpStream as ClientStream;
let mut srv = DrvAsynIPServerPort::new("sub_flush_drain", "127.0.0.1:0").unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let mut client = ClientStream::connect(("127.0.0.1", port)).unwrap();
let idx = wait_for_slot(&srv, 0);
let mut sub = srv.make_subport(idx).unwrap();
sub.connect(&AsynUser::default()).unwrap();
client.write_all(b"stale").unwrap();
client.flush().unwrap();
std::thread::sleep(Duration::from_millis(50));
sub.io_flush(&mut AsynUser::default()).unwrap();
let read_user = AsynUser::default().with_timeout(Duration::from_millis(100));
let mut buf = [0u8; 64];
match sub.read_octet(&read_user, &mut buf) {
Err(AsynError::Status {
status: AsynStatus::Timeout,
..
}) => {}
Ok(0) => {}
other => panic!("expected drained (timeout / 0 bytes), got {other:?}"),
}
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn a_udp_datagram_fans_out_to_every_octet_user() {
use crate::interrupt::{InterruptFilter, InterruptValue};
use std::net::UdpSocket as ClientSock;
use std::sync::Mutex as StdMutex;
let mut srv = DrvAsynIPServerPort::new("udp_intr", "127.0.0.1:0 UDP").unwrap();
srv.connect(&AsynUser::default()).unwrap();
let server_addr = srv
.udp_socket
.lock()
.as_ref()
.unwrap()
.local_addr()
.unwrap();
let seen: Arc<StdMutex<Vec<String>>> = Arc::new(StdMutex::new(Vec::new()));
let seen_cb = seen.clone();
let _cb = srv.base().interrupts.register_sync_callback(
InterruptFilter {
addr: Some(3),
..InterruptFilter::default()
},
move |iv: &InterruptValue| {
if let ParamValue::Octet(s) = &iv.value {
seen_cb.lock().unwrap().push(s.clone());
}
},
);
let client = ClientSock::bind("127.0.0.1:0").unwrap();
client.send_to(b"telemetry", server_addr).unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(3);
loop {
if !seen.lock().unwrap().is_empty() {
break;
}
assert!(
std::time::Instant::now() < deadline,
"no octet interrupt for the datagram (C: drvAsynIPServerPort.c:312-321)"
);
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(seen.lock().unwrap().as_slice(), ["telemetry".to_string()]);
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn udp_server_receives_datagrams_from_any_peer() {
use std::net::UdpSocket as ClientSock;
let mut srv = DrvAsynIPServerPort::new("udp_srv", "127.0.0.1:0 UDP").unwrap();
srv.connect(&AsynUser::default()).unwrap();
let server_addr = srv
.udp_socket
.lock()
.as_ref()
.unwrap()
.local_addr()
.unwrap();
let c1 = ClientSock::bind("127.0.0.1:0").unwrap();
let c2 = ClientSock::bind("127.0.0.1:0").unwrap();
c1.send_to(b"alpha", server_addr).unwrap();
c2.send_to(b"bravo", server_addr).unwrap();
let user = AsynUser::default()
.with_addr(0)
.with_timeout(Duration::from_secs(2));
let mut got: Vec<String> = Vec::new();
let deadline = std::time::Instant::now() + Duration::from_secs(3);
let mut buf = [0u8; 64];
while got.len() < 2 && std::time::Instant::now() < deadline {
let n = srv.read_octet(&user, &mut buf).unwrap();
if n == 0 {
std::thread::sleep(Duration::from_millis(10));
continue;
}
got.push(String::from_utf8_lossy(&buf[..n]).to_string());
}
got.sort();
assert_eq!(got, vec!["alpha".to_string(), "bravo".to_string()]);
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn udp_server_write_octet_errors() {
let mut srv = DrvAsynIPServerPort::new("udp_srv2", "127.0.0.1:0 UDP").unwrap();
srv.connect(&AsynUser::default()).unwrap();
let mut user = AsynUser::default().with_addr(0);
let err = srv.write_octet(&mut user, b"x").unwrap_err();
match err {
AsynError::Status { message, .. } => {
assert!(
message.contains("read-only"),
"expected read-only error, got: {message}"
);
}
_ => panic!("wrong error variant"),
}
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn udp_server_read_returns_zero_when_empty() {
let mut srv = DrvAsynIPServerPort::new("udp_srv3", "127.0.0.1:0 UDP").unwrap();
srv.connect(&AsynUser::default()).unwrap();
let user = AsynUser::default()
.with_addr(0)
.with_timeout(Duration::from_millis(50));
let mut buf = [0u8; 64];
let n = srv.read_octet(&user, &mut buf).unwrap();
assert_eq!(n, 0, "empty UDP cache must return 0 bytes, not error");
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn udp_server_read_eom_reports_end_at_datagram_boundary() {
let mut srv = DrvAsynIPServerPort::new("udp_srv_eom", "127.0.0.1:0 UDP").unwrap();
{
let mut cache = srv.udp_cache.lock();
cache.data = b"hello".to_vec();
cache.pos = 0;
}
let user = AsynUser::default().with_addr(0);
let mut small = [0u8; 3];
let (n, eom) = srv.io_read_octet_eom(&user, &mut small).unwrap();
assert_eq!(n, 3);
assert_eq!(&small[..3], b"hel");
assert!(eom.contains(EomReason::CNT), "partial drain must flag CNT");
assert!(
!eom.contains(EomReason::END),
"partial drain must NOT flag END"
);
let mut rest = [0u8; 16];
let (n, eom) = srv.io_read_octet_eom(&user, &mut rest).unwrap();
assert_eq!(n, 2);
assert_eq!(&rest[..2], b"lo");
assert!(
eom.contains(EomReason::END),
"datagram boundary must flag END"
);
assert!(
!eom.contains(EomReason::CNT),
"full drain must NOT flag CNT"
);
let mut buf = [0u8; 16];
let (n, eom) = srv.io_read_octet_eom(&user, &mut buf).unwrap();
assert_eq!(n, 0);
assert!(eom.is_empty(), "empty cache poll reports no EOM");
{
let mut cache = srv.udp_cache.lock();
cache.data = b"abcd".to_vec();
cache.pos = 0;
}
let mut exact = [0u8; 4];
let (n, eom) = srv.io_read_octet_eom(&user, &mut exact).unwrap();
assert_eq!(n, 4);
assert_eq!(&exact, b"abcd");
assert!(eom.contains(EomReason::END), "exact fit must flag END");
assert!(eom.contains(EomReason::CNT), "exact fit must also flag CNT");
let mut after = [0u8; 4];
let (n, eom) = srv.io_read_octet_eom(&user, &mut after).unwrap();
assert_eq!(n, 0);
assert!(eom.is_empty());
}
#[test]
fn udp_server_zero_length_read_rejected() {
let mut srv = DrvAsynIPServerPort::new("udp_srv_maxchars", "127.0.0.1:0 UDP").unwrap();
{
let mut cache = srv.udp_cache.lock();
cache.data = b"keepme".to_vec();
cache.pos = 0;
}
let user = AsynUser::default().with_addr(0);
let mut empty: [u8; 0] = [];
assert!(
matches!(
srv.read_octet(&user, &mut empty),
Err(AsynError::Status {
status: AsynStatus::Error,
..
})
),
"UDP read_octet with maxchars==0 must return asynError"
);
let mut empty_eom: [u8; 0] = [];
assert!(
matches!(
srv.io_read_octet_eom(&user, &mut empty_eom),
Err(AsynError::Status {
status: AsynStatus::Error,
..
})
),
"UDP io_read_octet_eom with maxchars==0 must return asynError"
);
let mut buf = [0u8; 16];
let n = srv.read_octet(&user, &mut buf).unwrap();
assert_eq!(
&buf[..n],
b"keepme",
"rejected reads must not drain the cache"
);
}
#[test]
fn udp_server_disconnect_stops_worker_cleanly() {
let mut srv = DrvAsynIPServerPort::new("udp_srv4", "127.0.0.1:0 UDP").unwrap();
srv.connect(&AsynUser::default()).unwrap();
srv.disconnect(&AsynUser::default()).unwrap();
srv.connect(&AsynUser::default()).unwrap();
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn udp_server_shutdown_joins_recv_worker() {
let mut srv = DrvAsynIPServerPort::new("udp_srv_sd", "127.0.0.1:0 UDP").unwrap();
srv.connect(&AsynUser::default()).unwrap();
assert!(srv.udp_thread.lock().is_some());
let start = std::time::Instant::now();
srv.shutdown().unwrap();
let elapsed = start.elapsed();
assert!(
srv.udp_thread.lock().is_none(),
"shutdown must join and clear the recv worker handle"
);
assert!(
srv.udp_socket.lock().is_none(),
"shutdown must drop the UDP socket"
);
assert!(
elapsed < Duration::from_secs(3),
"shutdown join took too long ({elapsed:?}) — worker did not exit"
);
srv.connect(&AsynUser::default()).unwrap();
srv.disconnect(&AsynUser::default()).unwrap();
}
#[test]
fn tcp_server_shutdown_releases_listener() {
let mut srv = DrvAsynIPServerPort::new("tcp_srv_sd", "127.0.0.1:0").unwrap();
srv.connect(&AsynUser::default()).unwrap();
assert!(srv.listener.lock().is_some());
srv.shutdown().unwrap();
assert!(
srv.listener.lock().is_none(),
"shutdown must drop the TCP listener"
);
}
#[test]
fn parse_rejects_missing_port() {
assert!(IpServerConfig::parse("0.0.0.0").is_err());
}
#[test]
fn parse_rejects_unknown_protocol_token() {
let err = IpServerConfig::parse("0.0.0.0:8080 BOGUS").unwrap_err();
match err {
AsynError::Status { message, .. } => {
assert!(
message.contains("unknown protocol token") || message.contains("BOGUS"),
"msg={message}"
);
}
_ => panic!("expected Status err"),
}
}
#[test]
fn server_accepts_and_round_trips() {
let mut srv = DrvAsynIPServerPort::new("srv1", "127.0.0.1:0").unwrap();
let user = AsynUser::default();
srv.connect(&user).unwrap();
let port = srv.local_port();
assert!(port > 0);
let client_handle = std::thread::spawn(move || {
let mut s = std::net::TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
s.write_all(b"hello-server").unwrap();
let mut buf = [0u8; 32];
let n = s.read(&mut buf).unwrap();
buf[..n].to_vec()
});
wait_for_slot(&srv, 0);
let mut user = AsynUser::new(0).with_timeout(Duration::from_secs(2));
let mut buf = [0u8; 32];
let n = srv.read_octet(&user, &mut buf).unwrap();
assert_eq!(&buf[..n], b"hello-server");
srv.write_octet(&mut user, b"hello-client").unwrap();
let reply = client_handle.join().unwrap();
assert_eq!(reply, b"hello-client");
}
#[test]
fn slot_table_caps_concurrent_clients() {
let cfg = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 2,
no_process_eos: false,
read_timeout: None,
};
let mut srv = DrvAsynIPServerPort::with_config("srv2", cfg).unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let _c1 = std::net::TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
wait_for_slot(&srv, 0);
let _c2 = std::net::TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
wait_for_slot(&srv, 1);
let mut c3 = std::net::TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
c3.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut buf = [0u8; 1];
assert_eq!(
c3.read(&mut buf).unwrap(),
0,
"a client over max_clients must see its connection closed (C: \"too many \
clients\" + epicsSocketDestroy), not sit in the backlog"
);
}
#[test]
fn drop_client_releases_slot() {
let cfg = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 1,
no_process_eos: false,
read_timeout: None,
};
let mut srv = DrvAsynIPServerPort::with_config("srv3", cfg).unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let _c1 = std::net::TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
wait_for_slot(&srv, 0);
assert!(srv.peer(0).is_some());
srv.drop_client(0).unwrap();
assert!(srv.peer(0).is_none());
let _c2 = std::net::TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
wait_for_slot(&srv, 0);
assert!(srv.peer(0).is_some());
}
#[test]
fn child_port_names_match_c_convention() {
let cfg = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 3,
no_process_eos: false,
read_timeout: None,
};
let srv = DrvAsynIPServerPort::with_config("parent", cfg).unwrap();
assert_eq!(srv.child_port_name(0), "parent:0");
assert_eq!(srv.child_port_name(1), "parent:1");
assert_eq!(srv.child_port_name(2), "parent:2");
assert_eq!(
srv.child_port_names(),
vec![
"parent:0".to_string(),
"parent:1".to_string(),
"parent:2".to_string()
]
);
}
#[test]
fn a_child_port_has_the_shape_drv_asyn_ip_port_configure_gives_it() {
let build = |name: &str, no_process_eos: bool| {
let cfg = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 2,
no_process_eos,
read_timeout: None,
};
DrvAsynIPServerPort::with_config(name, cfg).unwrap()
};
let srv = build("child_eos_on", false);
let child = srv.make_subport(1).unwrap();
assert_eq!(child.base().port_name, "child_eos_on:1");
assert!(
child.base().octet_interrupt_process,
"a child fans its reads out to interrupt users, like every drvAsynIPPort"
);
assert_eq!(
child.base().interpose_octet.len(),
1,
"a child gets the default EOS interpose"
);
assert!(!child.base().auto_connect, "C passes noAutoConnect=1");
let srv = build("child_eos_off", true);
let child = srv.make_subport(0).unwrap();
assert!(child.base().octet_interrupt_process);
assert_eq!(
child.base().interpose_octet.len(),
0,
"noProcessEos must suppress the EOS interpose"
);
assert!(!child.base().auto_connect);
}
#[test]
fn the_server_port_itself_gets_no_eos_interpose() {
let cfg = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 1,
no_process_eos: false,
read_timeout: None,
};
let srv = DrvAsynIPServerPort::with_config("srv_no_eos", cfg).unwrap();
assert_eq!(srv.base().interpose_octet.len(), 0);
}
#[test]
fn make_subport_rejects_out_of_range_idx() {
let cfg = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 2,
no_process_eos: false,
read_timeout: None,
};
let srv = DrvAsynIPServerPort::with_config("p2", cfg).unwrap();
assert!(srv.make_subport(0).is_ok());
assert!(srv.make_subport(1).is_ok());
match srv.make_subport(2) {
Err(AsynError::Status { message, .. }) => {
assert!(message.contains("out of range"), "msg={message}");
}
Ok(_) => panic!("expected out-of-range error"),
Err(other) => panic!("expected Status error, got {other:?}"),
}
}
#[test]
fn subport_shares_slot_with_parent_after_accept() {
let cfg = IpServerConfig {
bind_host: "127.0.0.1".into(),
bind_port: 0,
protocol: IpServerProtocol::Tcp,
max_clients: 1,
no_process_eos: false,
read_timeout: None,
};
let mut srv = DrvAsynIPServerPort::with_config("psh", cfg).unwrap();
srv.connect(&AsynUser::default()).unwrap();
let port = srv.local_port();
let mut sub = srv.make_subport(0).unwrap();
assert!(sub.connect(&AsynUser::default()).is_err());
let client_handle = std::thread::spawn(move || {
let mut c = std::net::TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
let mut buf = [0u8; 5];
let _ = c.read(&mut buf).unwrap();
buf
});
wait_for_slot(&srv, 0);
sub.connect(&AsynUser::default()).unwrap();
assert!(sub.peer().is_some());
let mut user = AsynUser::default();
sub.write_octet(&mut user, b"hello").unwrap();
let buf = client_handle.join().unwrap();
assert_eq!(&buf, b"hello");
}
}