use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use crate::error::{AsynError, AsynResult, AsynStatus};
use crate::interrupt::InterruptManager;
use crate::port::PortDriver;
use crate::port_actor::PortActor;
use crate::port_handle::PortHandle;
use crate::transport::InProcessClient;
use super::config::RuntimeConfig;
use super::event::RuntimeEvent;
#[derive(Clone)]
pub struct PortRuntimeHandle {
port_handle: PortHandle,
client: InProcessClient,
event_tx: broadcast::Sender<RuntimeEvent>,
shutdown_tx: Arc<std::sync::Mutex<Option<mpsc::Sender<()>>>>,
completion_rx: Arc<std::sync::Mutex<Option<std::sync::mpsc::Receiver<()>>>>,
port_name: String,
}
impl PortRuntimeHandle {
pub fn port_handle(&self) -> &PortHandle {
&self.port_handle
}
pub fn client(&self) -> &InProcessClient {
&self.client
}
pub fn subscribe_events(&self) -> broadcast::Receiver<RuntimeEvent> {
self.event_tx.subscribe()
}
pub fn shutdown(&self) {
let guard = self.shutdown_tx.lock().unwrap_or_else(|e| e.into_inner());
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(());
}
}
pub fn shutdown_and_wait(&self) {
self.shutdown();
let rx = self
.completion_rx
.lock()
.unwrap_or_else(|e| e.into_inner())
.take();
if let Some(rx) = rx {
let _ = rx.recv();
}
}
pub fn port_name(&self) -> &str {
&self.port_name
}
}
pub struct ConnectWaiter {
rx: std::sync::mpsc::Receiver<()>,
services: crate::services::PortServices,
callback: crate::exception::ExceptionCallbackId,
}
impl ConnectWaiter {
pub fn arm(services: &crate::services::PortServices, port_name: &str) -> Self {
let (tx, rx) = std::sync::mpsc::channel::<()>();
let waited_on = port_name.to_string();
let callback = services.exceptions().add_callback(move |event| {
if event.exception == crate::exception::AsynException::Connect
&& event.port_name == waited_on
{
let _ = tx.send(());
}
});
Self {
rx,
services: services.clone(),
callback,
}
}
pub fn wait(self, timeout: std::time::Duration) -> bool {
self.rx.recv_timeout(timeout).is_ok()
}
}
impl Drop for ConnectWaiter {
fn drop(&mut self) {
self.services.exceptions().remove_callback(self.callback);
}
}
pub fn create_port_runtime<D: PortDriver>(
driver: D,
config: RuntimeConfig,
) -> AsynResult<(PortRuntimeHandle, std::thread::JoinHandle<()>)> {
create_port_runtime_boxed(Box::new(driver), config)
}
pub fn create_port_runtime_boxed(
mut driver: Box<dyn PortDriver>,
config: RuntimeConfig,
) -> AsynResult<(PortRuntimeHandle, std::thread::JoinHandle<()>)> {
config.services.bind(driver.base_mut());
let connect_at_registration = driver.base().auto_connect && !driver.base().is_connected();
if connect_at_registration {
driver.base_mut().connect_retry_at = Some(std::time::Instant::now());
}
let port_name = driver.base().port_name.clone();
let can_block = driver.base().flags.can_block;
let multi_device = driver.base().flags.multi_device;
let max_addr = driver.base().max_addr as i32;
let interfaces = driver.capabilities();
let (event_tx, _) = broadcast::channel(256);
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(1);
let (completion_tx, completion_rx) = std::sync::mpsc::channel::<()>();
let shared_intr_state = driver.base().interrupts.shared_state();
let handle_interrupts = Arc::new(InterruptManager::from_shared_state(shared_intr_state));
let (tx, rx) = mpsc::channel(config.channel_capacity);
let actor = PortActor::new(driver, rx);
let actor_id = actor.id();
let event_tx_clone = event_tx.clone();
let name_clone = port_name.clone();
let connect_wait =
connect_at_registration.then(|| ConnectWaiter::arm(&config.services, &port_name));
let join_handle = match std::thread::Builder::new()
.name(format!("asyn-runtime-{port_name}"))
.spawn(move || {
let _ = event_tx_clone.send(RuntimeEvent::Started {
port_name: name_clone.clone(),
});
actor.run_with_shutdown(shutdown_rx);
let _ = event_tx_clone.send(RuntimeEvent::Stopped {
port_name: name_clone,
});
let _ = completion_tx.send(());
}) {
Ok(jh) => jh,
Err(e) => {
eprintln!("asyn: port '{port_name}' runtime thread could not be created: {e}");
return Err(port_thread_unavailable(&port_name, &e));
}
};
if let Some(waiter) = connect_wait {
let _ = waiter.wait(config.auto_connect_timeout);
}
let mut port_handle = PortHandle::new(tx, port_name.clone(), handle_interrupts, actor_id);
port_handle.set_can_block(can_block);
port_handle.set_capabilities(multi_device, max_addr);
port_handle.set_interfaces(interfaces);
let client = InProcessClient::new(port_handle.clone());
let handle = PortRuntimeHandle {
port_handle,
client,
event_tx,
shutdown_tx: Arc::new(std::sync::Mutex::new(Some(shutdown_tx))),
completion_rx: Arc::new(std::sync::Mutex::new(Some(completion_rx))),
port_name,
};
Ok((handle, join_handle))
}
fn port_thread_unavailable(port_name: &str, err: &std::io::Error) -> AsynError {
AsynError::Status {
status: AsynStatus::Error,
message: format!("port '{port_name}': runtime thread could not be created: {err}"),
}
}
pub fn port_runtime_unavailable(port_name: &str, err: &AsynError) -> ! {
eprintln!(
"FATAL: the IOC could not create the runtime for asyn port '{port_name}': {err}. \
Continuing would leave records bound to a port whose actor thread does not \
exist — every request queued to it would wait forever — so the process is \
aborting instead."
);
std::process::abort()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::param::ParamType;
use crate::port::{PortDriverBase, PortFlags};
struct TestPort {
base: PortDriverBase,
}
impl TestPort {
fn new(name: &str) -> Self {
let mut base = PortDriverBase::new(name, 1, PortFlags::default());
base.create_param("VAL", ParamType::Int32).unwrap();
base.create_param("F64", ParamType::Float64).unwrap();
Self { base }
}
}
impl PortDriver for TestPort {
fn base(&self) -> &PortDriverBase {
&self.base
}
fn base_mut(&mut self) -> &mut PortDriverBase {
&mut self.base
}
}
#[test]
fn port_runtime_int32_roundtrip() {
let (handle, _jh) = create_port_runtime(TestPort::new("rt_test"), RuntimeConfig::default())
.expect("the port runtime thread must start");
handle.port_handle().write_int32_blocking(0, 0, 42).unwrap();
assert_eq!(handle.port_handle().read_int32_blocking(0, 0).unwrap(), 42);
}
#[test]
fn port_runtime_client_roundtrip() {
use crate::protocol::command::PortCommand;
use crate::protocol::reply::ReplyPayload;
use crate::protocol::request::{PortRequest, ProtocolPriority, RequestMeta};
use crate::protocol::value::ParamValue;
use crate::transport::RuntimeClient;
let (handle, _jh) =
create_port_runtime(TestPort::new("rt_client"), RuntimeConfig::default())
.expect("the port runtime thread must start");
let client = handle.client();
let req = PortRequest {
meta: RequestMeta {
request_id: 1,
port_name: "rt_client".into(),
addr: 0,
reason: 0,
timeout_ms: 5000,
priority: ProtocolPriority::Medium,
block_token: None,
},
command: PortCommand::Int32Write { value: 77 },
};
let reply = client.request_blocking(req).unwrap();
assert_eq!(reply.payload, ReplyPayload::Ack);
let req = PortRequest {
meta: RequestMeta {
request_id: 2,
port_name: "rt_client".into(),
addr: 0,
reason: 0,
timeout_ms: 5000,
priority: ProtocolPriority::Medium,
block_token: None,
},
command: PortCommand::Int32Read,
};
let reply = client.request_blocking(req).unwrap();
match reply.payload {
ReplyPayload::Value(ParamValue::Int32(v)) => assert_eq!(v, 77),
_ => panic!("expected Int32 value"),
}
}
#[test]
fn port_runtime_shutdown() {
let (handle, jh) =
create_port_runtime(TestPort::new("rt_shutdown"), RuntimeConfig::default())
.expect("the port runtime thread must start");
drop(handle);
let result = jh.join();
assert!(result.is_ok());
}
#[test]
fn port_runtime_explicit_shutdown() {
let (handle, _jh) = create_port_runtime(
TestPort::new("rt_explicit_shutdown"),
RuntimeConfig::default(),
)
.expect("the port runtime thread must start");
handle.port_handle().write_int32_blocking(0, 0, 42).unwrap();
handle.shutdown_and_wait();
}
#[test]
fn port_runtime_shutdown_while_handles_exist() {
let (handle, _jh) = create_port_runtime(
TestPort::new("rt_shutdown_handles"),
RuntimeConfig::default(),
)
.expect("the port runtime thread must start");
let handle2 = handle.clone();
handle.shutdown_and_wait();
let result = handle2.port_handle().write_int32_blocking(0, 0, 99);
assert!(result.is_err());
}
#[test]
fn port_runtime_event_subscription() {
let (handle, _jh) =
create_port_runtime(TestPort::new("rt_events"), RuntimeConfig::default())
.expect("the port runtime thread must start");
let mut rx = handle.subscribe_events();
std::thread::sleep(std::time::Duration::from_millis(10));
match rx.try_recv() {
Ok(RuntimeEvent::Started { port_name }) => {
assert_eq!(port_name, "rt_events");
}
_ => {} }
}
#[test]
fn port_runtime_port_name() {
let (handle, _jh) =
create_port_runtime(TestPort::new("named_port"), RuntimeConfig::default())
.expect("the port runtime thread must start");
assert_eq!(handle.port_name(), "named_port");
}
#[cfg(target_os = "linux")]
#[test]
fn a_port_whose_thread_cannot_be_created_is_not_registered() {
const CHILD: &str = "EPICS_RS_PORT_THREAD_EAGAIN_CHILD";
const TEST: &str =
"runtime::port::tests::a_port_whose_thread_cannot_be_created_is_not_registered";
const PORT: &str = "eagain_port";
const DONE: &str = "no-port-was-registered";
if std::env::var_os(CHILD).is_some() {
let mut limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
assert_eq!(
unsafe { libc::getrlimit(libc::RLIMIT_NPROC, &mut limit) },
0,
"read the current process limit"
);
limit.rlim_cur = 1;
assert_eq!(
unsafe { libc::setrlimit(libc::RLIMIT_NPROC, &limit) },
0,
"lower the process limit"
);
let manager = crate::manager::PortManager::new();
let err = manager
.register_port(TestPort::new(PORT))
.err()
.expect("thread creation is refused, so registration must fail");
assert!(
err.to_string().contains(PORT),
"the error names the port that failed, got: {err}"
);
assert!(
manager.find_port_handle(PORT).is_err(),
"the manager must not hold a handle for a port that was never created"
);
assert!(
crate::asyn_record::get_port(PORT).is_none(),
"the process registry must not hold a port that was never created"
);
println!("{DONE}");
return;
}
if unsafe { libc::geteuid() } == 0 {
println!(
"skipped: running as root, which bypasses RLIMIT_NPROC \
(CAP_SYS_RESOURCE), so thread creation cannot be made to fail"
);
return;
}
let out =
std::process::Command::new(std::env::current_exe().expect("the test binary path"))
.args(["--exact", TEST, "--nocapture"])
.env(CHILD, "1")
.output()
.expect("re-exec the test binary");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success() && stdout.contains(DONE),
"child must reach the end of the boundary assertions.\n\
status: {:?}\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status
);
assert!(
stderr.contains(PORT),
"the diagnostic on stderr must name the port, got:\n{stderr}"
);
}
}