use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::error::{CaError, CaResult};
use crate::runtime::net::cas_server_port;
use crate::server::record::{self, Record, SubroutineFn};
use crate::server::database::PvDatabase;
use crate::server::device_support::DeviceSupport;
use crate::server::iocsh::{self, registry::CommandDef};
use crate::server::{DeviceSupportFactory, access_security, autosave};
use autosave::startup::AutosaveStartupConfig;
pub mod init_hooks {
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InitHookState {
AtIocBuild,
AtBeginning,
AfterCallbackInit,
AfterCaLinkInit,
AfterInitDrvSup,
AfterInitRecSup,
AfterInitDevSup,
AfterInitDatabase,
AfterFinishDevSup,
AfterScanInit,
AfterInitialProcess,
AfterCaServerInit,
AfterIocBuilt,
AtIocRun,
AfterDatabaseRunning,
AfterCaServerRunning,
AfterIocRunning,
}
impl InitHookState {
pub fn name(&self) -> &'static str {
match self {
InitHookState::AtIocBuild => "initHookAtIocBuild",
InitHookState::AtBeginning => "initHookAtBeginning",
InitHookState::AfterCallbackInit => "initHookAfterCallbackInit",
InitHookState::AfterCaLinkInit => "initHookAfterCaLinkInit",
InitHookState::AfterInitDrvSup => "initHookAfterInitDrvSup",
InitHookState::AfterInitRecSup => "initHookAfterInitRecSup",
InitHookState::AfterInitDevSup => "initHookAfterInitDevSup",
InitHookState::AfterInitDatabase => "initHookAfterInitDatabase",
InitHookState::AfterFinishDevSup => "initHookAfterFinishDevSup",
InitHookState::AfterScanInit => "initHookAfterScanInit",
InitHookState::AfterInitialProcess => "initHookAfterInitialProcess",
InitHookState::AfterCaServerInit => "initHookAfterCaServerInit",
InitHookState::AfterIocBuilt => "initHookAfterIocBuilt",
InitHookState::AtIocRun => "initHookAtIocRun",
InitHookState::AfterDatabaseRunning => "initHookAfterDatabaseRunning",
InitHookState::AfterCaServerRunning => "initHookAfterCaServerRunning",
InitHookState::AfterIocRunning => "initHookAfterIocRunning",
}
}
}
pub type InitHookFunction = Arc<dyn Fn(InitHookState) + Send + Sync>;
static HOOKS: Mutex<Vec<InitHookFunction>> = Mutex::new(Vec::new());
pub fn init_hook_register(func: InitHookFunction) {
HOOKS.lock().unwrap().push(func);
}
pub fn init_hook_announce(state: InitHookState) {
let snapshot: Vec<InitHookFunction> = HOOKS.lock().unwrap().clone();
for cb in snapshot {
cb(state);
}
}
#[cfg(test)]
pub fn init_hook_free() {
HOOKS.lock().unwrap().clear();
}
}
pub use init_hooks::{InitHookFunction, InitHookState, init_hook_announce, init_hook_register};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GroupLoadRequest {
pub filename: String,
pub macros: String,
}
static GROUP_LOAD_REQUESTS: std::sync::LazyLock<Mutex<Vec<GroupLoadRequest>>> =
std::sync::LazyLock::new(|| Mutex::new(Vec::new()));
pub fn take_group_load_requests() -> Vec<GroupLoadRequest> {
std::mem::take(&mut *GROUP_LOAD_REQUESTS.lock().unwrap())
}
pub fn db_load_group_startup_command() -> CommandDef {
use crate::server::iocsh::registry::{
ArgDesc, ArgType, ArgValue, CommandContext, CommandOutcome,
};
CommandDef::new(
"dbLoadGroup",
vec![
ArgDesc {
name: "filename",
arg_type: ArgType::String,
optional: false,
},
ArgDesc {
name: "macros",
arg_type: ArgType::String,
optional: true,
},
],
"dbLoadGroup <jsonFilename> [<macros>]",
move |args: &[ArgValue], ctx: &CommandContext| {
let filename = match args.first() {
Some(ArgValue::String(s)) => s.clone(),
_ => return Err("dbLoadGroup: missing filename".into()),
};
let macros = match args.get(1) {
Some(ArgValue::String(s)) => s.clone(),
_ => String::new(),
};
let mut queue = GROUP_LOAD_REQUESTS.lock().unwrap();
if let Some(rest) = filename.strip_prefix('-') {
if rest == "*" {
let n = queue.len();
queue.clear();
ctx.println(&format!(
"dbLoadGroup: cleared all queued group files ({n} removed)"
));
} else {
let before = queue.len();
queue.retain(|r| !(r.filename == rest && r.macros == macros));
let dropped = before - queue.len();
ctx.println(&format!(
"dbLoadGroup: removed '{rest}' ({dropped} queued entr{} dropped)",
if dropped == 1 { "y" } else { "ies" }
));
}
return Ok(CommandOutcome::Continue);
}
if let Err(e) = std::fs::metadata(&filename) {
return Err(format!("dbLoadGroup: error opening \"{filename}\": {e}"));
}
queue.retain(|r| !(r.filename == filename && r.macros == macros));
queue.push(GroupLoadRequest {
filename: filename.clone(),
macros,
});
ctx.println(&format!(
"dbLoadGroup: queued '{filename}' ({} group file(s) queued)",
queue.len()
));
Ok(CommandOutcome::Continue)
},
)
}
pub struct DeviceSupportContext<'a> {
pub dtyp: &'a str,
pub inp: &'a str,
pub out: &'a str,
}
pub type DynamicDeviceSupportFactory =
Box<dyn Fn(&DeviceSupportContext) -> Option<Box<dyn DeviceSupport>> + Send + Sync>;
pub type LinkSetInstaller = Box<
dyn FnOnce(
Arc<PvDatabase>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Vec<CommandDef>> + Send + 'static>,
> + Send
+ 'static,
>;
pub struct IocRunConfig {
pub db: Arc<PvDatabase>,
pub port: u16,
pub tcp_port: Option<u16>,
pub acf: Option<access_security::AccessSecurityConfig>,
pub autosave_config: Option<autosave::SaveSetConfig>,
pub autosave_manager: Option<Arc<autosave::AutosaveManager>>,
pub shell_commands: Vec<CommandDef>,
pub after_init_hooks: Vec<Box<dyn FnOnce() + Send>>,
}
pub struct IocApplication {
port: u16,
tcp_port: Option<u16>,
device_factories: HashMap<String, DeviceSupportFactory>,
dynamic_device_factory: Option<DynamicDeviceSupportFactory>,
record_factories: HashMap<String, super::RecordFactory>,
subroutine_registry: HashMap<String, Arc<SubroutineFn>>,
acf: Option<access_security::AccessSecurityConfig>,
autosave_config: Option<autosave::SaveSetConfig>,
autosave_startup: Option<Arc<Mutex<AutosaveStartupConfig>>>,
startup_commands: Vec<CommandDef>,
shell_commands: Vec<CommandDef>,
startup_script: Option<String>,
inline_records: Vec<(String, Box<dyn Record>)>,
after_init_hooks: Vec<Box<dyn FnOnce() + Send>>,
link_set_installers: Vec<LinkSetInstaller>,
}
impl IocApplication {
pub fn new() -> Self {
let mut device_factories: HashMap<String, DeviceSupportFactory> = HashMap::new();
device_factories.insert(
"getenv".to_string(),
Box::new(|| -> Box<dyn DeviceSupport> {
Box::new(crate::server::builtin_devices::GetenvDeviceSupport::new())
}),
);
Self {
port: cas_server_port(),
tcp_port: None,
device_factories,
dynamic_device_factory: None,
record_factories: HashMap::new(),
subroutine_registry: HashMap::new(),
acf: None,
autosave_config: None,
autosave_startup: None,
startup_commands: Vec::new(),
shell_commands: Vec::new(),
startup_script: None,
inline_records: Vec::new(),
after_init_hooks: Vec::new(),
link_set_installers: Vec::new(),
}
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn tcp_port(mut self, port: u16) -> Self {
self.tcp_port = Some(port);
self
}
pub fn register_device_support<F>(mut self, dtyp: &str, factory: F) -> Self
where
F: Fn() -> Box<dyn DeviceSupport> + Send + Sync + 'static,
{
self.device_factories
.insert(dtyp.to_string(), Box::new(factory));
self
}
pub fn register_dynamic_device_support<F>(mut self, factory: F) -> Self
where
F: Fn(&DeviceSupportContext) -> Option<Box<dyn DeviceSupport>> + Send + Sync + 'static,
{
if let Some(existing) = self.dynamic_device_factory.take() {
self.dynamic_device_factory = Some(Box::new(move |ctx: &DeviceSupportContext| {
factory(ctx).or_else(|| existing(ctx))
}));
} else {
self.dynamic_device_factory = Some(Box::new(factory));
}
self
}
pub fn register_startup_command(mut self, cmd: CommandDef) -> Self {
self.startup_commands.push(cmd);
self
}
pub fn register_shell_command(mut self, cmd: CommandDef) -> Self {
self.shell_commands.push(cmd);
self
}
pub fn register_after_init(mut self, hook: impl FnOnce() + Send + 'static) -> Self {
self.after_init_hooks.push(Box::new(hook));
self
}
pub fn register_link_set_installer<F, Fut>(mut self, installer: F) -> Self
where
F: FnOnce(Arc<PvDatabase>) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Vec<CommandDef>> + Send + 'static,
{
self.link_set_installers
.push(Box::new(move |db| Box::pin(installer(db))));
self
}
pub fn startup_script(mut self, path: &str) -> Self {
self.startup_script = Some(path.to_string());
self
}
pub fn register_record_type<F>(mut self, type_name: &str, factory: F) -> Self
where
F: Fn() -> Box<dyn Record> + Send + Sync + 'static,
{
self.record_factories
.insert(type_name.to_string(), Box::new(factory));
self
}
pub fn register_subroutine<F>(mut self, name: &str, func: F) -> Self
where
F: Fn(&mut dyn Record) -> CaResult<()> + Send + Sync + 'static,
{
self.subroutine_registry
.insert(name.to_string(), Arc::new(Box::new(func)));
self
}
pub fn autosave(mut self, config: autosave::SaveSetConfig) -> Self {
self.autosave_config = Some(config);
self
}
pub fn autosave_startup(mut self, config: Arc<Mutex<AutosaveStartupConfig>>) -> Self {
self.autosave_startup = Some(config);
self
}
pub fn acf(mut self, config: access_security::AccessSecurityConfig) -> Self {
self.acf = Some(config);
self
}
pub fn record(mut self, name: &str, record: impl Record) -> Self {
self.inline_records
.push((name.to_string(), Box::new(record)));
self
}
pub fn record_boxed(mut self, name: &str, record: Box<dyn Record>) -> Self {
self.inline_records.push((name.to_string(), record));
self
}
pub async fn run<F, Fut>(self, protocol_runner: F) -> CaResult<()>
where
F: FnOnce(IocRunConfig) -> Fut + Send + 'static,
Fut: std::future::Future<Output = CaResult<()>> + Send,
{
let db = Arc::new(PvDatabase::new());
let handle = tokio::runtime::Handle::current();
let Self {
port,
tcp_port,
device_factories,
dynamic_device_factory,
record_factories,
subroutine_registry,
acf,
autosave_config,
autosave_startup,
mut startup_commands,
mut shell_commands,
startup_script,
inline_records,
after_init_hooks,
link_set_installers,
} = self;
for (name, factory) in record_factories {
super::db_loader::register_record_type(&name, factory);
}
if let Some(ref config) = autosave_startup {
let cmds = AutosaveStartupConfig::register_startup_commands(config.clone());
startup_commands.extend(cmds);
}
startup_commands.push(db_load_group_startup_command());
for (name, record) in inline_records {
db.add_record(&name, record).await?;
}
if let Some(script) = startup_script {
let db1 = db.clone();
let h1 = handle.clone();
let (tx, rx) = crate::runtime::sync::oneshot::channel();
std::thread::Builder::new()
.name("iocsh-startup".into())
.spawn(move || {
let shell = iocsh::IocShell::new(db1, h1);
for cmd in startup_commands {
shell.register(cmd);
}
let result = shell.execute_script(&script);
let _ = tx.send(result);
})
.expect("failed to spawn startup thread");
let result = rx
.await
.map_err(|_| CaError::InvalidValue("startup thread dropped".into()))?;
result.map_err(|e| CaError::InvalidValue(e))?;
}
let (pass0_files, pass1_files, builder_opt) = if let Some(ref config) = autosave_startup {
let cfg = config.lock().unwrap();
let pass0: Vec<std::path::PathBuf> = cfg
.pass0_restores
.iter()
.map(|r| cfg.resolve_save_file(&r.filename))
.collect();
let pass1: Vec<std::path::PathBuf> = cfg
.pass1_restores
.iter()
.map(|r| cfg.resolve_save_file(&r.filename))
.collect();
let builder = if !cfg.monitor_sets.is_empty() || !cfg.triggered_sets.is_empty() {
Some(cfg.into_builder())
} else {
None
};
(pass0, pass1, builder)
} else {
(Vec::new(), Vec::new(), None)
};
type AsyncHook = Box<
dyn FnOnce()
-> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>
+ Send
+ 'static,
>;
let mut lifecycle_hooks: Vec<(InitHookState, AsyncHook)> = Vec::new();
{
let db_p0 = db.clone();
let files = pass0_files.clone();
lifecycle_hooks.push((
InitHookState::AfterInitDevSup,
Box::new(move || {
Box::pin(async move {
for sav_path in &files {
match autosave::restore_from_file(&db_p0, sav_path).await {
Ok(count) if count > 0 => {
eprintln!(
"pass0 restore: {count} PVs from {}",
sav_path.display()
);
}
Err(e) => {
eprintln!(
"pass0 restore warning: {} - {e}",
sav_path.display()
);
}
_ => {}
}
}
})
}),
));
}
{
let db_p1 = db.clone();
let files = pass1_files.clone();
let cfg_path = autosave_config.as_ref().map(|c| c.save_path.clone());
lifecycle_hooks.push((
InitHookState::AfterInitDatabase,
Box::new(move || {
Box::pin(async move {
for sav_path in &files {
match autosave::restore_from_file(&db_p1, sav_path).await {
Ok(count) if count > 0 => {
eprintln!(
"pass1 restore: {count} PVs from {}",
sav_path.display()
);
}
Err(e) => {
eprintln!(
"pass1 restore warning: {} - {e}",
sav_path.display()
);
}
_ => {}
}
}
if let Some(path) = cfg_path {
match autosave::restore_from_file(&db_p1, &path).await {
Ok(count) if count > 0 => {
eprintln!("autosave: restored {count} PVs");
}
Err(e) => {
eprintln!("autosave restore warning: {} - {e}", path.display());
}
_ => {}
}
}
})
}),
));
}
macro_rules! announce {
($state:expr) => {{
let state = $state;
init_hook_announce(state);
let mut i = 0;
while i < lifecycle_hooks.len() {
if lifecycle_hooks[i].0 == state {
let (_, hook) = lifecycle_hooks.remove(i);
hook().await;
} else {
i += 1;
}
}
}};
}
announce!(InitHookState::AtIocBuild);
announce!(InitHookState::AtBeginning);
announce!(InitHookState::AfterCallbackInit);
announce!(InitHookState::AfterCaLinkInit);
for installer in link_set_installers {
shell_commands.extend(installer(db.clone()).await);
}
announce!(InitHookState::AfterInitDrvSup);
announce!(InitHookState::AfterInitRecSup);
let record_count =
wire_device_support(&db, &device_factories, &dynamic_device_factory).await?;
announce!(InitHookState::AfterInitDevSup);
wire_subroutines(&db, &subroutine_registry).await;
let io_intr_count = setup_io_intr(db.clone()).await;
setup_property_posts(db.clone()).await;
db.setup_cp_links().await;
let link_wait_secs = crate::runtime::env::get("EPICS_RS_INIT_LINK_TIMEOUT")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(10.0)
.max(0.0);
if link_wait_secs > 0.0 {
let (connected, total) = db
.wait_for_external_links(std::time::Duration::from_secs_f64(link_wait_secs))
.await;
if total > 0 {
if connected == total {
eprintln!("iocInit: {connected}/{total} external links connected");
} else {
let unconnected = db.unconnected_external_links().await;
eprintln!(
"iocInit: {connected}/{total} external links connected after \
{link_wait_secs}s — proceeding without: {}",
unconnected.join(", ")
);
}
}
}
announce!(InitHookState::AfterInitDatabase);
announce!(InitHookState::AfterFinishDevSup);
announce!(InitHookState::AfterScanInit);
{
let pini_records = db.pini_records().await;
for name in &pini_records {
let mut visited = std::collections::HashSet::new();
let _ = db.process_record_with_links(name, &mut visited, 0).await;
}
db.mark_pini_done();
}
announce!(InitHookState::AfterInitialProcess);
let autosave_manager = if let Some(builder) = builder_opt {
match builder.build().await {
Ok(mgr) => {
eprintln!("autosave: {} save set(s) configured", mgr.set_names().len());
Some(Arc::new(mgr))
}
Err(e) => {
eprintln!("autosave: failed to build manager: {e}");
None
}
}
} else {
None
};
let total_records = db.all_record_names().await.len();
eprintln!(
"iocInit: {total_records} records, {record_count} with device support, {io_intr_count} I/O Intr"
);
announce!(InitHookState::AfterCaServerInit);
announce!(InitHookState::AfterIocBuilt);
announce!(InitHookState::AtIocRun);
announce!(InitHookState::AfterDatabaseRunning);
announce!(InitHookState::AfterCaServerRunning);
for hook in after_init_hooks {
hook();
}
announce!(InitHookState::AfterIocRunning);
let pending = db.take_after_ioc_running();
if !pending.is_empty() {
let db1 = db.clone();
let h1 = handle.clone();
let shell_cmds_clone = shell_commands.clone();
let (tx, rx) = crate::runtime::sync::oneshot::channel();
std::thread::Builder::new()
.name("iocsh-after-ioc-running".into())
.spawn(move || {
let shell = iocsh::IocShell::new(db1, h1);
for cmd in shell_cmds_clone {
shell.register(cmd);
}
let mut errs: Vec<String> = Vec::new();
for line in pending {
if let Err(e) = shell.execute_line(&line) {
errs.push(format!("{line}: {e}"));
}
}
let _ = tx.send(errs);
})
.expect("failed to spawn afterIocRunning thread");
if let Ok(errs) = rx.await {
for e in errs {
eprintln!("afterIocRunning: {e}");
}
}
}
let config = IocRunConfig {
db,
port,
tcp_port,
acf,
autosave_config,
autosave_manager,
shell_commands,
after_init_hooks: Vec::new(),
};
let runner_fut = protocol_runner(config);
tokio::pin!(runner_fut);
let ctrl_c = async {
let _ = tokio::signal::ctrl_c().await;
};
#[cfg(unix)]
let sigterm = async {
if let Ok(mut sig) =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
{
let _ = sig.recv().await;
} else {
std::future::pending::<()>().await;
}
};
#[cfg(not(unix))]
let sigterm = std::future::pending::<()>();
tokio::select! {
biased;
res = &mut runner_fut => res,
_ = ctrl_c => {
tracing::info!(target: "epics_base_rs::ioc_app", "SIGINT received, shutting down IOC");
Ok(())
}
_ = sigterm => {
tracing::info!(target: "epics_base_rs::ioc_app", "SIGTERM received, shutting down IOC");
Ok(())
}
}
}
}
pub(crate) async fn wire_device_support(
db: &PvDatabase,
factories: &HashMap<String, DeviceSupportFactory>,
dynamic_factory: &Option<DynamicDeviceSupportFactory>,
) -> CaResult<usize> {
let names = db.all_record_names().await;
let mut count = 0;
for name in names {
if let Some(rec_arc) = db.get_record(&name).await {
let mut instance = rec_arc.write().await;
let dtyp = instance.common.dtyp.clone();
if !crate::server::device_support::is_soft_dtyp(&dtyp) {
let ctx = DeviceSupportContext {
dtyp: &dtyp,
inp: &instance.common.inp,
out: &instance.common.out,
};
let dev_opt = if let Some(factory) = factories.get(&dtyp) {
Some(factory())
} else if let Some(dyn_factory) = dynamic_factory {
dyn_factory(&ctx)
} else {
None
};
if let Some(dev) = dev_opt {
crate::server::device_support::wire_device_to_record(&mut instance, dev);
count += 1;
} else {
eprintln!(
"warning: no device support registered for DTYP '{dtyp}' (record: {name})"
);
}
}
}
}
Ok(count)
}
async fn wire_subroutines(db: &PvDatabase, registry: &HashMap<String, Arc<SubroutineFn>>) {
if registry.is_empty() {
return;
}
let names = db.all_record_names().await;
for name in names {
if let Some(rec_arc) = db.get_record(&name).await {
let mut instance = rec_arc.write().await;
if instance.record.record_type() == "sub" {
if let Some(crate::types::EpicsValue::String(snam)) =
instance.record.get_field("SNAM")
{
if let Some(sub_fn) = registry.get(snam.as_str_lossy().as_ref()) {
instance.subroutine = Some(sub_fn.clone());
}
}
}
}
}
}
async fn setup_io_intr(db: Arc<PvDatabase>) -> usize {
let all_names = db.all_record_names().await;
let io_intr_recs: Vec<(
String,
Arc<crate::runtime::sync::RwLock<record::RecordInstance>>,
)> = {
let mut recs = Vec::new();
for name in &all_names {
if let Some(arc) = db.get_record(name).await {
recs.push((name.clone(), arc));
}
}
recs
};
let mut count = 0;
for (name, rec_arc) in io_intr_recs {
let mut inst = rec_arc.write().await;
let independent = inst
.device
.as_ref()
.is_some_and(|d| d.io_intr_scan_independent());
if inst.common.scan == record::ScanType::IoIntr || independent {
if let Some(mut dev) = inst.device.take() {
if let Some(mut intr_rx) = dev.io_intr_receiver() {
let db_clone = db.clone();
let rec_name = name.clone();
let rec_arc_clone = rec_arc.clone();
crate::runtime::task::spawn(async move {
while intr_rx.recv().await.is_some() {
let process = independent || {
let inst = rec_arc_clone.read().await;
inst.common.scan == record::ScanType::IoIntr
};
if !process {
continue;
}
let mut visited = std::collections::HashSet::new();
let _ = db_clone
.process_record_with_links(&rec_name, &mut visited, 0)
.await;
}
});
count += 1;
}
inst.device = Some(dev);
}
}
}
count
}
pub(crate) async fn setup_property_posts(db: Arc<PvDatabase>) -> usize {
let names = db.all_record_names().await;
let mut count = 0;
for name in names {
if let Some(rec_arc) = db.get_record(&name).await {
let mut inst = rec_arc.write().await;
if let Some(mut dev) = inst.device.take() {
if let Some(mut rx) = dev.property_post_receiver() {
let db_clone = db.clone();
let rec_name = name.clone();
crate::runtime::task::spawn(async move {
while let Some(fields) = rx.recv().await {
let _ = db_clone.post_property_fields(&rec_name, fields).await;
}
});
count += 1;
}
inst.device = Some(dev);
}
}
}
count
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex as StdMutex;
use std::sync::atomic::{AtomicUsize, Ordering};
static INIT_HOOK_TEST_LOCK: StdMutex<()> = StdMutex::new(());
#[test]
fn dbloadgroup_startup_command_queues_and_removes() {
let _ = take_group_load_requests();
let rt = tokio::runtime::Runtime::new().unwrap();
let db = Arc::new(PvDatabase::new());
let shell = iocsh::IocShell::new(db, rt.handle().clone());
shell.register(db_load_group_startup_command());
let a = std::env::temp_dir().join("qsrv_q_a.json");
let b = std::env::temp_dir().join("qsrv_q_b.json");
std::fs::write(&a, "{}").unwrap();
std::fs::write(&b, "{}").unwrap();
shell
.execute_line(&format!("dbLoadGroup(\"{}\")", a.display()))
.unwrap();
shell
.execute_line(&format!("dbLoadGroup(\"{}\",\"M=1\")", b.display()))
.unwrap();
shell
.execute_line(&format!("dbLoadGroup(\"{}\")", a.display()))
.unwrap();
assert!(
shell
.execute_line("dbLoadGroup(\"/no/such/group.json\")")
.is_err(),
"a missing group file must error at command time"
);
shell
.execute_line(&format!("dbLoadGroup(\"-{}\")", a.display()))
.unwrap();
let reqs = take_group_load_requests();
assert_eq!(reqs.len(), 1, "only the (b, M=1) entry must remain");
assert_eq!(reqs[0].filename, b.to_string_lossy());
assert_eq!(reqs[0].macros, "M=1");
shell
.execute_line(&format!("dbLoadGroup(\"{}\")", b.display()))
.unwrap();
shell.execute_line("dbLoadGroup(\"-*\")").unwrap();
assert!(
take_group_load_requests().is_empty(),
"dbLoadGroup(\"-*\") must clear the queue"
);
let _ = std::fs::remove_file(&a);
let _ = std::fs::remove_file(&b);
}
#[test]
fn init_hook_register_and_announce_in_order() {
let _guard = INIT_HOOK_TEST_LOCK.lock().unwrap();
init_hooks::init_hook_free();
let seen: Arc<StdMutex<Vec<InitHookState>>> = Arc::new(StdMutex::new(Vec::new()));
let seen_cb = seen.clone();
init_hook_register(Arc::new(move |state| {
seen_cb.lock().unwrap().push(state);
}));
let order = [
InitHookState::AtIocBuild,
InitHookState::AfterInitDevSup,
InitHookState::AfterInitDatabase,
InitHookState::AfterInitialProcess,
InitHookState::AfterIocRunning,
];
for &s in &order {
init_hook_announce(s);
}
let got = seen.lock().unwrap().clone();
assert_eq!(got, order, "hooks must fire in announce order");
init_hooks::init_hook_free();
}
#[test]
fn init_hook_reentrant_register_does_not_deadlock() {
let _guard = INIT_HOOK_TEST_LOCK.lock().unwrap();
init_hooks::init_hook_free();
let inner_calls = Arc::new(AtomicUsize::new(0));
let inner_for_outer = inner_calls.clone();
init_hook_register(Arc::new(move |_state| {
let inner = inner_for_outer.clone();
init_hook_register(Arc::new(move |_s| {
inner.fetch_add(1, Ordering::SeqCst);
}));
}));
init_hook_announce(InitHookState::AtIocBuild);
assert_eq!(inner_calls.load(Ordering::SeqCst), 0);
init_hook_announce(InitHookState::AfterIocRunning);
assert!(inner_calls.load(Ordering::SeqCst) >= 1);
init_hooks::init_hook_free();
}
#[test]
fn init_hook_state_names_match_c() {
assert_eq!(InitHookState::AtIocBuild.name(), "initHookAtIocBuild");
assert_eq!(
InitHookState::AfterInitDevSup.name(),
"initHookAfterInitDevSup"
);
assert_eq!(
InitHookState::AfterInitDatabase.name(),
"initHookAfterInitDatabase"
);
assert_eq!(
InitHookState::AfterIocRunning.name(),
"initHookAfterIocRunning"
);
}
#[tokio::test]
async fn test_ioc_application_empty() {
let db = Arc::new(PvDatabase::new());
let factories = HashMap::new();
let count = wire_device_support(&db, &factories, &None).await.unwrap();
assert_eq!(count, 0);
}
#[tokio::test]
async fn test_wire_device_support_no_dtyp() {
use crate::server::records::ai::AiRecord;
let db = Arc::new(PvDatabase::new());
db.add_record("TEST", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
let factories = HashMap::new();
let count = wire_device_support(&db, &factories, &None).await.unwrap();
assert_eq!(count, 0); }
#[tokio::test]
async fn wire_device_support_forwards_info_tags_to_driver() {
use crate::server::device_support::{DeviceReadOutcome, DeviceSupport};
use crate::server::record::ScanType;
use crate::server::records::ai::AiRecord;
use std::sync::{Arc as StdArc, Mutex as StdMutex};
struct RecordingDev {
seen: StdArc<StdMutex<HashMap<String, String>>>,
}
impl DeviceSupport for RecordingDev {
fn write(&mut self, _record: &mut dyn crate::server::record::Record) -> CaResult<()> {
Ok(())
}
fn dtyp(&self) -> &str {
"TestRecording"
}
fn read(
&mut self,
_record: &mut dyn crate::server::record::Record,
) -> CaResult<DeviceReadOutcome> {
Ok(DeviceReadOutcome::ok())
}
fn apply_record_info(&mut self, info: &HashMap<String, String>) {
let mut g = self.seen.lock().unwrap();
*g = info.clone();
}
fn set_record_info(&mut self, _name: &str, _scan: ScanType) {}
}
let seen = StdArc::new(StdMutex::new(HashMap::<String, String>::new()));
let seen_factory = seen.clone();
let mut factories: HashMap<String, DeviceSupportFactory> = HashMap::new();
factories.insert(
"TestRecording".to_string(),
Box::new(move || {
Box::new(RecordingDev {
seen: seen_factory.clone(),
})
}),
);
let db = Arc::new(PvDatabase::new());
db.add_record("AI:WITH:INFO", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
let rec = db.get_record("AI:WITH:INFO").await.unwrap();
{
let mut inst = rec.write().await;
inst.common.dtyp = "TestRecording".to_string();
inst.set_info("asyn:READBACK", "1");
inst.set_info("Q:group", "demo");
}
let count = wire_device_support(&db, &factories, &None).await.unwrap();
assert_eq!(count, 1, "device support must have attached");
let observed = seen.lock().unwrap().clone();
assert_eq!(observed.get("asyn:READBACK").map(String::as_str), Some("1"));
assert_eq!(observed.get("Q:group").map(String::as_str), Some("demo"));
}
}