1use std::collections::HashMap;
18use std::future::Future;
19use std::net::IpAddr;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::time::Duration;
23
24use regex::Regex;
25use tracing::info;
26
27use spvirit_types::{
28 NdCodec, NdDimension, NtEnum, NtNdArray as NtNdArrayType, NtScalar, NtScalarArray,
29 NtTable as NtTableType, NtTableColumn, NtTimeStamp, PvValue, ScalarArrayValue, ScalarValue,
30};
31
32use crate::db::{load_db, parse_db};
33use crate::handler::PvListMode;
34use crate::monitor::MonitorRegistry;
35use crate::pv::scalar_family_record_type;
36use crate::pvstore::{Source, SourceRegistry, StoreSource};
37use crate::server::{PvaServerConfig, run_pva_server_with_registry};
38use crate::simple_store::{LinkDef, OnPutCallback, ScanCallback, SimplePvStore};
39use crate::types::{DbCommonState, OutputMode, RecordData, RecordInstance, RecordType};
40
41pub struct PvaServerBuilder {
54 records: HashMap<String, RecordInstance>,
55 on_put: HashMap<String, OnPutCallback>,
56 scans: Vec<(String, Duration, ScanCallback)>,
57 links: Vec<LinkDef>,
58 extra_sources: Vec<(String, i32, Arc<dyn Source>)>,
59 ioc: Option<(Vec<String>, Arc<dyn Source>)>,
63 tcp_port: u16,
64 udp_port: u16,
65 listen_ip: Option<IpAddr>,
66 advertise_ip: Option<IpAddr>,
67 compute_alarms: bool,
68 beacon_period_secs: u64,
69 conn_timeout: Duration,
70 pvlist_mode: PvListMode,
71 pvlist_max: usize,
72 pvlist_allow_pattern: Option<Regex>,
73 start_hooks: Vec<crate::events::StartHook>,
74 event_handlers: Vec<(String, crate::events::EventHandler)>,
75 event_sinks: Vec<Arc<dyn crate::events::EventSink>>,
76}
77
78impl PvaServerBuilder {
79 fn new() -> Self {
80 Self {
81 records: HashMap::new(),
82 on_put: HashMap::new(),
83 scans: Vec::new(),
84 links: Vec::new(),
85 extra_sources: Vec::new(),
86 ioc: None,
87 tcp_port: 5075,
88 udp_port: 5076,
89 listen_ip: None,
90 advertise_ip: None,
91 compute_alarms: false,
92 beacon_period_secs: 15,
93 conn_timeout: Duration::from_secs(64000),
94 pvlist_mode: PvListMode::List,
95 pvlist_max: 1024,
96 pvlist_allow_pattern: None,
97 start_hooks: Vec::new(),
98 event_handlers: Vec::new(),
99 event_sinks: Vec::new(),
100 }
101 }
102
103 pub fn ai(mut self, name: impl Into<String>, initial: f64) -> Self {
107 let name = name.into();
108 self.records.insert(
109 name.clone(),
110 make_scalar_record(&name, RecordType::Ai, ScalarValue::F64(initial)),
111 );
112 self
113 }
114
115 pub fn ao(mut self, name: impl Into<String>, initial: f64) -> Self {
117 let name = name.into();
118 self.records.insert(
119 name.clone(),
120 make_output_record(&name, RecordType::Ao, ScalarValue::F64(initial)),
121 );
122 self
123 }
124
125 pub fn bi(mut self, name: impl Into<String>, initial: bool) -> Self {
127 let name = name.into();
128 self.records.insert(
129 name.clone(),
130 make_scalar_record(&name, RecordType::Bi, ScalarValue::Bool(initial)),
131 );
132 self
133 }
134
135 pub fn bo(mut self, name: impl Into<String>, initial: bool) -> Self {
137 let name = name.into();
138 self.records.insert(
139 name.clone(),
140 make_output_record(&name, RecordType::Bo, ScalarValue::Bool(initial)),
141 );
142 self
143 }
144
145 pub fn string_in(mut self, name: impl Into<String>, initial: impl Into<String>) -> Self {
147 let name = name.into();
148 self.records.insert(
149 name.clone(),
150 make_scalar_record(
151 &name,
152 RecordType::StringIn,
153 ScalarValue::Str(initial.into()),
154 ),
155 );
156 self
157 }
158
159 pub fn string_out(mut self, name: impl Into<String>, initial: impl Into<String>) -> Self {
161 let name = name.into();
162 self.records.insert(
163 name.clone(),
164 make_output_record(
165 &name,
166 RecordType::StringOut,
167 ScalarValue::Str(initial.into()),
168 ),
169 );
170 self
171 }
172
173 pub fn waveform(mut self, name: impl Into<String>, data: ScalarArrayValue) -> Self {
175 let name = name.into();
176 self.records.insert(
177 name.clone(),
178 make_array_record(&name, RecordType::Waveform, data),
179 );
180 self
181 }
182
183 pub fn aai(mut self, name: impl Into<String>, data: ScalarArrayValue) -> Self {
185 let name = name.into();
186 self.records.insert(
187 name.clone(),
188 make_array_record(&name, RecordType::Aai, data),
189 );
190 self
191 }
192
193 pub fn aao(mut self, name: impl Into<String>, data: ScalarArrayValue) -> Self {
195 let name = name.into();
196 self.records.insert(
197 name.clone(),
198 make_array_record(&name, RecordType::Aao, data),
199 );
200 self
201 }
202
203 pub fn sub_array(
205 mut self,
206 name: impl Into<String>,
207 data: ScalarArrayValue,
208 indx: usize,
209 nelm: usize,
210 ) -> Self {
211 let name = name.into();
212 let ftvl = data.type_label().trim_end_matches("[]").to_string();
213 let malm = data.len();
214 let nord = nelm.min(malm.saturating_sub(indx));
215 self.records.insert(
216 name.clone(),
217 RecordInstance {
218 name: name.clone(),
219 record_type: RecordType::SubArray,
220 common: DbCommonState::default(),
221 data: RecordData::SubArray {
222 nt: NtScalarArray::from_value(data),
223 inp: None,
224 ftvl,
225 malm,
226 nelm,
227 nord,
228 indx,
229 },
230 raw_fields: HashMap::new(),
231 },
232 );
233 self
234 }
235
236 pub fn nt_table(
238 mut self,
239 name: impl Into<String>,
240 columns: Vec<(String, ScalarArrayValue)>,
241 ) -> Self {
242 let name = name.into();
243 let labels: Vec<String> = columns.iter().map(|(n, _)| n.clone()).collect();
244 let cols: Vec<NtTableColumn> = columns
245 .into_iter()
246 .map(|(n, v)| NtTableColumn { name: n, values: v })
247 .collect();
248 self.records.insert(
249 name.clone(),
250 RecordInstance {
251 name: name.clone(),
252 record_type: RecordType::NtTable,
253 common: DbCommonState::default(),
254 data: RecordData::NtTable {
255 nt: NtTableType {
256 labels,
257 columns: cols,
258 descriptor: None,
259 alarm: None,
260 time_stamp: None,
261 },
262 inp: None,
263 out: None,
264 omsl: OutputMode::Supervisory,
265 },
266 raw_fields: HashMap::new(),
267 },
268 );
269 self
270 }
271
272 pub fn nt_ndarray(
274 mut self,
275 name: impl Into<String>,
276 data: ScalarArrayValue,
277 dims: Vec<(i32, i32)>,
278 ) -> Self {
279 let name = name.into();
280 let dimension: Vec<NdDimension> = dims
281 .into_iter()
282 .map(|(size, offset)| NdDimension {
283 size,
284 offset,
285 full_size: size,
286 binning: 1,
287 reverse: false,
288 })
289 .collect();
290 let uncompressed_size = (data.len() * data.element_size_bytes().max(1)) as i64;
291 self.records.insert(
292 name.clone(),
293 RecordInstance {
294 name: name.clone(),
295 record_type: RecordType::NtNdArray,
296 common: DbCommonState::default(),
297 data: RecordData::NtNdArray {
298 nt: NtNdArrayType {
299 value: data,
300 codec: NdCodec {
301 name: String::new(),
302 parameters: Default::default(),
303 },
304 compressed_size: uncompressed_size,
305 uncompressed_size,
306 dimension,
307 unique_id: 0,
308 data_time_stamp: NtTimeStamp {
309 seconds_past_epoch: 0,
310 nanoseconds: 0,
311 user_tag: 0,
312 },
313 attribute: vec![],
314 descriptor: None,
315 alarm: None,
316 time_stamp: None,
317 display: None,
318 },
319 inp: None,
320 out: None,
321 omsl: OutputMode::Supervisory,
322 },
323 raw_fields: HashMap::new(),
324 },
325 );
326 self
327 }
328
329 pub fn mbbi(mut self, name: impl Into<String>, choices: Vec<String>, initial: i32) -> Self {
331 let name = name.into();
332 self.records.insert(
333 name.clone(),
334 RecordInstance {
335 name: name.clone(),
336 record_type: RecordType::Mbbi,
337 common: DbCommonState::default(),
338 data: RecordData::NtEnum {
339 nt: NtEnum::new(initial, choices),
340 inp: None,
341 out: None,
342 omsl: OutputMode::Supervisory,
343 },
344 raw_fields: HashMap::new(),
345 },
346 );
347 self
348 }
349
350 pub fn mbbo(mut self, name: impl Into<String>, choices: Vec<String>, initial: i32) -> Self {
352 let name = name.into();
353 self.records.insert(
354 name.clone(),
355 RecordInstance {
356 name: name.clone(),
357 record_type: RecordType::Mbbo,
358 common: DbCommonState::default(),
359 data: RecordData::NtEnum {
360 nt: NtEnum::new(initial, choices),
361 inp: None,
362 out: None,
363 omsl: OutputMode::Supervisory,
364 },
365 raw_fields: HashMap::new(),
366 },
367 );
368 self
369 }
370
371 pub fn generic(
373 mut self,
374 name: impl Into<String>,
375 struct_id: impl Into<String>,
376 fields: Vec<(String, PvValue)>,
377 ) -> Self {
378 let name = name.into();
379 self.records.insert(
380 name.clone(),
381 RecordInstance {
382 name: name.clone(),
383 record_type: RecordType::Generic,
384 common: DbCommonState::default(),
385 data: RecordData::Generic {
386 struct_id: struct_id.into(),
387 fields,
388 inp: None,
389 out: None,
390 omsl: OutputMode::Supervisory,
391 },
392 raw_fields: HashMap::new(),
393 },
394 );
395 self
396 }
397
398 pub fn db_file(mut self, path: impl AsRef<str>) -> Self {
412 match load_db(path.as_ref()) {
413 Ok(records) => {
414 self.records.extend(records);
415 }
416 Err(e) => {
417 panic!("failed to load db file '{}': {e}", path.as_ref());
418 }
419 }
420 self
421 }
422
423 pub fn db_string(mut self, content: &str) -> Self {
428 match parse_db(content) {
429 Ok(records) => {
430 self.records.extend(records);
431 }
432 Err(e) => {
433 panic!("failed to parse db string: {e}");
434 }
435 }
436 self
437 }
438
439 pub fn on_put<F>(mut self, name: impl Into<String>, callback: F) -> Self
443 where
444 F: Fn(&str, &spvirit_codec::spvd_decode::DecodedValue) + Send + Sync + 'static,
445 {
446 self.on_put.insert(name.into(), Arc::new(callback));
447 self
448 }
449
450 pub fn scan<F>(mut self, name: impl Into<String>, period: Duration, callback: F) -> Self
452 where
453 F: Fn(&str) -> ScalarValue + Send + Sync + 'static,
454 {
455 self.scans.push((name.into(), period, Arc::new(callback)));
456 self
457 }
458
459 pub fn on_start<F>(mut self, hook: F) -> Self
471 where
472 F: Fn(Arc<SimplePvStore>) -> Pin<Box<dyn Future<Output = ()> + Send>>
473 + Send
474 + Sync
475 + 'static,
476 {
477 self.start_hooks.push(Arc::new(hook));
478 self
479 }
480
481 pub fn on_event<F>(mut self, event: impl Into<String>, handler: F) -> Self
490 where
491 F: Fn(Arc<SimplePvStore>, String) -> Pin<Box<dyn Future<Output = ()> + Send>>
492 + Send
493 + Sync
494 + 'static,
495 {
496 self.event_handlers.push((event.into(), Arc::new(handler)));
497 self
498 }
499
500 pub fn event_sink(mut self, sink: Arc<dyn crate::events::EventSink>) -> Self {
509 self.event_sinks.push(sink);
510 self
511 }
512
513 pub fn link<F>(mut self, output: impl Into<String>, inputs: &[&str], compute: F) -> Self
528 where
529 F: Fn(&[ScalarValue]) -> ScalarValue + Send + Sync + 'static,
530 {
531 self.links.push(LinkDef {
532 output: output.into(),
533 inputs: inputs.iter().map(|s| s.to_string()).collect(),
534 compute: Arc::new(compute),
535 });
536 self
537 }
538
539 pub fn source(mut self, label: impl Into<String>, order: i32, source: Arc<dyn Source>) -> Self {
551 self.extra_sources.push((label.into(), order, source));
552 self
553 }
554
555 pub fn ioc<S: StoreSource + 'static>(mut self, ioc: Arc<S>) -> Self {
571 assert!(
572 self.ioc.is_none(),
573 "PvaServerBuilder::ioc may only be called once; \
574 register additional engines with .source()"
575 );
576 let names = ioc.record_names();
577 let source: Arc<dyn Source> = ioc;
578 self.ioc = Some((names, source));
579 self
580 }
581
582 pub fn port(mut self, port: u16) -> Self {
586 self.tcp_port = port;
587 self
588 }
589
590 pub fn udp_port(mut self, port: u16) -> Self {
592 self.udp_port = port;
593 self
594 }
595
596 pub fn listen_ip(mut self, ip: IpAddr) -> Self {
598 self.listen_ip = Some(ip);
599 self
600 }
601
602 pub fn advertise_ip(mut self, ip: IpAddr) -> Self {
604 self.advertise_ip = Some(ip);
605 self
606 }
607
608 pub fn compute_alarms(mut self, enabled: bool) -> Self {
610 self.compute_alarms = enabled;
611 self
612 }
613
614 pub fn beacon_period(mut self, secs: u64) -> Self {
616 self.beacon_period_secs = secs;
617 self
618 }
619
620 pub fn conn_timeout(mut self, timeout: Duration) -> Self {
622 self.conn_timeout = timeout;
623 self
624 }
625
626 pub fn pvlist_mode(mut self, mode: PvListMode) -> Self {
628 self.pvlist_mode = mode;
629 self
630 }
631
632 pub fn pvlist_max(mut self, max: usize) -> Self {
634 self.pvlist_max = max;
635 self
636 }
637
638 pub fn pvlist_allow_pattern(mut self, pattern: Regex) -> Self {
640 self.pvlist_allow_pattern = Some(pattern);
641 self
642 }
643
644 pub fn build(self) -> PvaServer {
646 if let Some((ioc_names, _)) = &self.ioc {
647 let engine: std::collections::HashSet<&str> =
648 ioc_names.iter().map(String::as_str).collect();
649
650 let mut overlap: Vec<&str> = self
654 .records
655 .keys()
656 .map(String::as_str)
657 .filter(|n| engine.contains(n))
658 .collect();
659 overlap.sort_unstable();
660 assert!(
661 overlap.is_empty(),
662 "the builtin store and the engine store both own {}: stores must be \
663 disjoint. Remove the builtin-store record (`.ai()`, `.db_file()` and \
664 friends), or rename the engine's record.",
665 overlap.join(", ")
666 );
667
668 let mut misdirected: Vec<String> = Vec::new();
669 for (name, _, _) in &self.scans {
670 if engine.contains(name.as_str()) {
671 misdirected.push(format!(".scan(\"{name}\")"));
672 }
673 }
674 for link in &self.links {
675 if engine.contains(link.output.as_str()) {
676 misdirected.push(format!(".link(\"{}\", …)", link.output));
677 }
678 for input in &link.inputs {
679 if engine.contains(input.as_str()) {
680 misdirected.push(format!(".link(…, input \"{input}\")"));
681 }
682 }
683 }
684 for name in self.on_put.keys() {
685 if engine.contains(name.as_str()) {
686 misdirected.push(format!(".on_put(\"{name}\")"));
687 }
688 }
689 misdirected.sort_unstable();
690 misdirected.dedup();
691 assert!(
692 misdirected.is_empty(),
693 "these builtin-store callbacks name a record the engine store owns, so \
694 they would never fire: {}. Express the behaviour in the engine's .db \
695 instead.",
696 misdirected.join(", ")
697 );
698 }
699
700 let store = Arc::new(SimplePvStore::new(
701 self.records,
702 self.on_put,
703 self.links,
704 self.compute_alarms,
705 ));
706
707 let mut config = PvaServerConfig::default();
708 config.tcp_port = self.tcp_port;
709 config.udp_port = self.udp_port;
710 config.compute_alarms = self.compute_alarms;
711 if let Some(ip) = self.listen_ip {
712 config.listen_ip = ip;
713 }
714 config.advertise_ip = self.advertise_ip;
715 config.beacon_period_secs = self.beacon_period_secs;
716 config.conn_timeout = self.conn_timeout;
717 config.pvlist_mode = self.pvlist_mode;
718 config.pvlist_max = self.pvlist_max;
719 config.pvlist_allow_pattern = self.pvlist_allow_pattern;
720
721 let events = Arc::new(crate::events::Events::new());
722 for (name, handler) in self.event_handlers {
723 events.add_handler(name, handler);
724 }
725 for sink in self.event_sinks {
726 events.add_sink(sink);
727 }
728
729 PvaServer {
730 store,
731 extra_sources: self.extra_sources,
732 ioc: self.ioc.map(|(_, source)| source),
733 config,
734 scans: self.scans,
735 monitor_registry: Arc::new(std::sync::OnceLock::new()),
736 events,
737 start_hooks: self.start_hooks,
738 }
739 }
740}
741
742fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
747 if let Some(s) = payload.downcast_ref::<String>() {
748 s.clone()
749 } else if let Some(s) = payload.downcast_ref::<&'static str>() {
750 (*s).to_string()
751 } else {
752 "panic payload was not a string".to_string()
753 }
754}
755
756pub struct PvaServer {
777 store: Arc<SimplePvStore>,
778 extra_sources: Vec<(String, i32, Arc<dyn Source>)>,
779 ioc: Option<Arc<dyn Source>>,
781 config: PvaServerConfig,
782 scans: Vec<(String, Duration, ScanCallback)>,
783 monitor_registry: Arc<std::sync::OnceLock<Arc<MonitorRegistry>>>,
793 events: Arc<crate::events::Events>,
794 start_hooks: Vec<crate::events::StartHook>,
795}
796
797impl PvaServer {
798 pub fn builder() -> PvaServerBuilder {
800 PvaServerBuilder::new()
801 }
802
803 pub fn store(&self) -> &Arc<SimplePvStore> {
805 &self.store
806 }
807
808 pub fn events(&self) -> &Arc<crate::events::Events> {
810 &self.events
811 }
812
813 pub async fn post_event(&self, event: &str) {
821 self.events.post(event).await;
822 }
823
824 pub async fn run_start_hooks(&self) -> Result<(), String> {
838 self.store.set_registry(self.resolved_monitor_registry()).await;
839 for (i, hook) in self.start_hooks.iter().enumerate() {
840 let fut = hook(self.store.clone());
841 let result =
842 futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(fut)).await;
843 if let Err(payload) = result {
844 return Err(format!(
852 "on_start hook #{i} panicked; aborting startup: {}",
853 panic_message(payload.as_ref())
854 ));
855 }
856 }
857 Ok(())
858 }
859
860 pub async fn pv<T: crate::pv::PvScalar>(
863 &self,
864 name: &str,
865 ) -> Result<crate::pv::Pv<T>, crate::pv::PvError> {
866 crate::pv::Pv::attach(&self.store, name).await
867 }
868
869 pub async fn array_pv(&self, name: &str) -> Result<crate::pv::PvArray, crate::pv::PvError> {
872 crate::pv::PvArray::attach(&self.store, name).await
873 }
874
875 pub fn add_source(&mut self, label: impl Into<String>, order: i32, source: Arc<dyn Source>) {
887 self.extra_sources.push((label.into(), order, source));
888 }
889
890 pub fn set_monitor_registry(&mut self, registry: Arc<MonitorRegistry>) {
902 let _ = self.monitor_registry.set(registry);
903 }
904
905 pub fn monitor_registry(&mut self) -> Arc<MonitorRegistry> {
909 self.resolved_monitor_registry()
910 }
911
912 fn resolved_monitor_registry(&self) -> Arc<MonitorRegistry> {
917 self.monitor_registry
918 .get_or_init(|| Arc::new(MonitorRegistry::new()))
919 .clone()
920 }
921
922 pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
926 self.run_start_hooks()
930 .await
931 .map_err(|e| -> Box<dyn std::error::Error> { Box::<dyn std::error::Error>::from(e) })?;
932
933 self.serve_after_start_hooks().await
934 }
935
936 pub async fn serve_after_start_hooks(self) -> Result<(), Box<dyn std::error::Error>> {
952 let registry = self.resolved_monitor_registry();
957 self.store.set_registry(registry.clone()).await;
958
959 let sources = Arc::new(SourceRegistry::new());
961 sources.add_store("builtin", 0, self.store.clone()).await;
962
963 if let Some(ioc) = &self.ioc {
964 sources.add_store("ioc", 5, ioc.clone()).await;
968 }
969
970 let field_provider: Arc<dyn crate::field_provider::RecordFieldProvider> =
973 self.store.clone();
974 sources
975 .add(
976 "record-fields",
977 10,
978 Arc::new(crate::record_fields::RecordFieldSource::new(field_provider)),
979 )
980 .await;
981
982 for (label, order, source) in &self.extra_sources {
984 sources.add(label.clone(), *order, source.clone()).await;
985 }
986
987 for (name, period, callback) in &self.scans {
989 let store = self.store.clone();
990 let name = name.clone();
991 let period = *period;
992 let callback = callback.clone();
993 tokio::spawn(async move {
994 let mut interval = tokio::time::interval(period);
995 loop {
996 interval.tick().await;
997 let new_val = callback(&name);
998 store.set_value(&name, new_val).await;
999 }
1000 });
1001 }
1002
1003 self.events.start_dispatcher(self.store.clone());
1005
1006 let pv_count = self.store.pv_names().await.len();
1007 info!(
1008 "PvaServer starting: {} PVs on port {}",
1009 pv_count, self.config.tcp_port
1010 );
1011
1012 run_pva_server_with_registry(sources, self.config, registry).await
1014 }
1015}
1016
1017impl PvaServer {
1020 pub fn serve(pvs: impl IntoIterator<Item = impl Into<crate::pv::AnyPv>>) -> ServeBuilder {
1023 ServeBuilder {
1024 inner: PvaServerBuilder::new(),
1025 handles: Vec::new(),
1026 }
1027 .pvs(pvs)
1028 }
1029}
1030
1031pub struct ServeBuilder {
1034 inner: PvaServerBuilder,
1035 handles: Vec<crate::pv::AnyPv>,
1036}
1037
1038impl ServeBuilder {
1039 pub fn pvs(mut self, pvs: impl IntoIterator<Item = impl Into<crate::pv::AnyPv>>) -> Self {
1040 self.handles.extend(pvs.into_iter().map(Into::into));
1041 self
1042 }
1043 pub fn db_file(mut self, path: impl AsRef<str>) -> Self {
1044 self.inner = self.inner.db_file(path);
1045 self
1046 }
1047 pub fn db_string(mut self, content: &str) -> Self {
1048 self.inner = self.inner.db_string(content);
1049 self
1050 }
1051 pub fn source(mut self, label: impl Into<String>, order: i32, source: Arc<dyn Source>) -> Self {
1052 self.inner = self.inner.source(label, order, source);
1053 self
1054 }
1055 pub fn port(mut self, port: u16) -> Self {
1056 self.inner = self.inner.port(port);
1057 self
1058 }
1059 pub fn udp_port(mut self, port: u16) -> Self {
1060 self.inner = self.inner.udp_port(port);
1061 self
1062 }
1063 pub fn listen_ip(mut self, ip: IpAddr) -> Self {
1064 self.inner = self.inner.listen_ip(ip);
1065 self
1066 }
1067 pub fn advertise_ip(mut self, ip: IpAddr) -> Self {
1068 self.inner = self.inner.advertise_ip(ip);
1069 self
1070 }
1071 pub fn compute_alarms(mut self, enabled: bool) -> Self {
1072 self.inner = self.inner.compute_alarms(enabled);
1073 self
1074 }
1075 pub fn beacon_period(mut self, secs: u64) -> Self {
1076 self.inner = self.inner.beacon_period(secs);
1077 self
1078 }
1079
1080 pub fn on_start<F>(mut self, hook: F) -> Self
1082 where
1083 F: Fn(Arc<SimplePvStore>) -> Pin<Box<dyn Future<Output = ()> + Send>>
1084 + Send
1085 + Sync
1086 + 'static,
1087 {
1088 self.inner = self.inner.on_start(hook);
1089 self
1090 }
1091
1092 pub fn on_event<F>(mut self, event: impl Into<String>, handler: F) -> Self
1094 where
1095 F: Fn(Arc<SimplePvStore>, String) -> Pin<Box<dyn Future<Output = ()> + Send>>
1096 + Send
1097 + Sync
1098 + 'static,
1099 {
1100 self.inner = self.inner.on_event(event, handler);
1101 self
1102 }
1103
1104 pub fn event_sink(mut self, sink: Arc<dyn crate::events::EventSink>) -> Self {
1106 self.inner = self.inner.event_sink(sink);
1107 self
1108 }
1109
1110 pub async fn build(mut self) -> PvaServer {
1118 let mut validators: Vec<(String, crate::simple_store::PutValidator)> = Vec::new();
1119 for h in &self.handles {
1120 let name = h.name().to_string();
1121 if let Some(rec) = h.take_record() {
1122 self.inner.records.insert(name.clone(), rec);
1123 }
1124 if let Some(v) = h.take_validator() {
1125 validators.push((name.clone(), v));
1126 }
1127 if let Some((period, cb)) = h.take_scan() {
1128 self.inner.scans.push((name.clone(), period, cb));
1129 }
1130 if let Some((inputs, compute)) = h.take_calc() {
1131 self.inner.links.push(LinkDef {
1132 output: name.clone(),
1133 inputs,
1134 compute,
1135 });
1136 }
1137 }
1138 let server = self.inner.build();
1139 let store = server.store().clone();
1140 for h in &self.handles {
1141 h.bind(&store);
1142 }
1143 for (name, v) in validators {
1144 store.set_validator(name, v).await;
1145 }
1146 server
1147 }
1148
1149 pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
1151 self.build().await.run().await
1152 }
1153
1154 pub async fn start(self) -> Result<RunningServer, String> {
1168 let server = self.build().await;
1169 let store = server.store().clone();
1170 let events = server.events().clone();
1171 server.run_start_hooks().await?;
1172 events.start_dispatcher(store.clone());
1178 let handle = tokio::spawn(async move {
1179 if let Err(e) = server.serve_after_start_hooks().await {
1180 tracing::error!("PvaServer exited with error: {e}");
1181 }
1182 });
1183 Ok(RunningServer {
1184 store,
1185 events,
1186 handle,
1187 })
1188 }
1189}
1190
1191pub struct RunningServer {
1193 store: Arc<SimplePvStore>,
1194 events: Arc<crate::events::Events>,
1199 handle: tokio::task::JoinHandle<()>,
1200}
1201
1202impl RunningServer {
1203 pub async fn pv<T: crate::pv::PvScalar>(
1205 &self,
1206 name: &str,
1207 ) -> Result<crate::pv::Pv<T>, crate::pv::PvError> {
1208 crate::pv::Pv::attach(&self.store, name).await
1209 }
1210
1211 pub async fn array_pv(&self, name: &str) -> Result<crate::pv::PvArray, crate::pv::PvError> {
1213 crate::pv::PvArray::attach(&self.store, name).await
1214 }
1215
1216 pub async fn add_scalar(
1222 &self,
1223 name: &str,
1224 value: ScalarValue,
1225 writable: bool,
1226 ) -> crate::pv::Pv<ScalarValue> {
1227 let rt = scalar_family_record_type(&value, writable);
1228 let record = if writable {
1229 make_output_record(name, rt, value)
1230 } else {
1231 make_scalar_record(name, rt, value)
1232 };
1233 self.store.insert(name.to_string(), record).await;
1234 crate::pv::Pv::attach(&self.store, name)
1235 .await
1236 .expect("record just inserted")
1237 }
1238
1239 pub async fn add_array(
1244 &self,
1245 name: &str,
1246 value: ScalarArrayValue,
1247 writable: bool,
1248 ) -> crate::pv::PvArray {
1249 let rt = if writable {
1250 RecordType::Aao
1251 } else {
1252 RecordType::Aai
1253 };
1254 let record = make_array_record(name, rt, value);
1255 self.store.insert(name.to_string(), record).await;
1256 crate::pv::PvArray::attach(&self.store, name)
1257 .await
1258 .expect("record just inserted")
1259 }
1260
1261 pub async fn add_enum(&self, name: &str, choices: Vec<String>, index: i32, writable: bool) {
1265 let record = make_enum_record(name, choices, index, writable);
1266 self.store.insert(name.to_string(), record).await;
1267 }
1268
1269 pub async fn add_table(&self, name: &str, columns: Vec<(String, ScalarArrayValue)>) {
1273 let record = make_table_record(name, columns);
1274 self.store.insert(name.to_string(), record).await;
1275 }
1276
1277 pub fn store(&self) -> &Arc<SimplePvStore> {
1278 &self.store
1279 }
1280
1281 pub fn events(&self) -> &Arc<crate::events::Events> {
1284 &self.events
1285 }
1286
1287 pub async fn post_event(&self, event: &str) {
1289 self.events.post(event).await;
1290 }
1291
1292 pub fn abort(&self) {
1293 self.handle.abort();
1294 }
1295}
1296
1297pub(crate) fn make_scalar_record(
1300 name: &str,
1301 record_type: RecordType,
1302 value: ScalarValue,
1303) -> RecordInstance {
1304 let nt = NtScalar::from_value(value);
1305 let data = match record_type {
1306 RecordType::Ai => RecordData::Ai {
1307 nt,
1308 inp: None,
1309 siml: None,
1310 siol: None,
1311 simm: false,
1312 },
1313 RecordType::Bi => RecordData::Bi {
1314 nt,
1315 inp: None,
1316 znam: "Off".to_string(),
1317 onam: "On".to_string(),
1318 siml: None,
1319 siol: None,
1320 simm: false,
1321 },
1322 RecordType::StringIn => RecordData::StringIn {
1323 nt,
1324 inp: None,
1325 siml: None,
1326 siol: None,
1327 simm: false,
1328 },
1329 RecordType::LongIn => RecordData::Ai {
1331 nt,
1332 inp: None,
1333 siml: None,
1334 siol: None,
1335 simm: false,
1336 },
1337 _ => panic!("make_scalar_record: unsupported type {record_type:?}"),
1338 };
1339 RecordInstance {
1340 name: name.to_string(),
1341 record_type,
1342 common: DbCommonState::default(),
1343 data,
1344 raw_fields: HashMap::new(),
1345 }
1346}
1347
1348pub(crate) fn make_output_record(
1349 name: &str,
1350 record_type: RecordType,
1351 value: ScalarValue,
1352) -> RecordInstance {
1353 let nt = NtScalar::from_value(value);
1354 let data = match record_type {
1355 RecordType::Ao => RecordData::Ao {
1356 nt,
1357 out: None,
1358 dol: None,
1359 omsl: OutputMode::Supervisory,
1360 drvl: None,
1361 drvh: None,
1362 oroc: None,
1363 siml: None,
1364 siol: None,
1365 simm: false,
1366 },
1367 RecordType::Bo => RecordData::Bo {
1368 nt,
1369 out: None,
1370 dol: None,
1371 omsl: OutputMode::Supervisory,
1372 znam: "Off".to_string(),
1373 onam: "On".to_string(),
1374 siml: None,
1375 siol: None,
1376 simm: false,
1377 },
1378 RecordType::StringOut => RecordData::StringOut {
1379 nt,
1380 out: None,
1381 dol: None,
1382 omsl: OutputMode::Supervisory,
1383 siml: None,
1384 siol: None,
1385 simm: false,
1386 },
1387 RecordType::LongOut => RecordData::Ao {
1389 nt,
1390 out: None,
1391 dol: None,
1392 omsl: OutputMode::Supervisory,
1393 drvl: None,
1394 drvh: None,
1395 oroc: None,
1396 siml: None,
1397 siol: None,
1398 simm: false,
1399 },
1400 _ => panic!("make_output_record: unsupported type {record_type:?}"),
1401 };
1402 RecordInstance {
1403 name: name.to_string(),
1404 record_type,
1405 common: DbCommonState::default(),
1406 data,
1407 raw_fields: HashMap::new(),
1408 }
1409}
1410
1411pub(crate) fn make_array_record(
1415 name: &str,
1416 record_type: RecordType,
1417 data: ScalarArrayValue,
1418) -> RecordInstance {
1419 let ftvl = data.type_label().trim_end_matches("[]").to_string();
1420 let nelm = data.len();
1421 let nt = NtScalarArray::from_value(data);
1422 let record_data = match record_type {
1423 RecordType::Waveform => RecordData::Waveform {
1424 nt,
1425 inp: None,
1426 ftvl,
1427 nelm,
1428 nord: nelm,
1429 },
1430 RecordType::Aai => RecordData::Aai {
1431 nt,
1432 inp: None,
1433 ftvl,
1434 nelm,
1435 nord: nelm,
1436 },
1437 RecordType::Aao => RecordData::Aao {
1438 nt,
1439 out: None,
1440 dol: None,
1441 omsl: OutputMode::Supervisory,
1442 ftvl,
1443 nelm,
1444 nord: nelm,
1445 },
1446 _ => panic!("make_array_record: unsupported type {record_type:?}"),
1447 };
1448 RecordInstance {
1449 name: name.to_string(),
1450 record_type,
1451 common: DbCommonState::default(),
1452 data: record_data,
1453 raw_fields: HashMap::new(),
1454 }
1455}
1456
1457pub(crate) fn make_enum_record(
1458 name: &str,
1459 choices: Vec<String>,
1460 index: i32,
1461 writable: bool,
1462) -> RecordInstance {
1463 RecordInstance {
1464 name: name.to_string(),
1465 record_type: if writable { RecordType::Mbbo } else { RecordType::Mbbi },
1466 common: DbCommonState::default(),
1467 data: RecordData::NtEnum {
1468 nt: NtEnum::new(index, choices),
1469 inp: None,
1470 out: None,
1471 omsl: OutputMode::Supervisory,
1472 },
1473 raw_fields: HashMap::new(),
1474 }
1475}
1476
1477pub(crate) fn make_table_record(
1478 name: &str,
1479 columns: Vec<(String, ScalarArrayValue)>,
1480) -> RecordInstance {
1481 let labels: Vec<String> = columns.iter().map(|(n, _)| n.clone()).collect();
1482 let cols: Vec<NtTableColumn> = columns
1483 .into_iter()
1484 .map(|(n, v)| NtTableColumn { name: n, values: v })
1485 .collect();
1486 RecordInstance {
1487 name: name.to_string(),
1488 record_type: RecordType::NtTable,
1489 common: DbCommonState::default(),
1490 data: RecordData::NtTable {
1491 nt: NtTableType { labels, columns: cols, descriptor: None, alarm: None, time_stamp: None },
1492 inp: None,
1493 out: None,
1494 omsl: OutputMode::Supervisory,
1495 },
1496 raw_fields: HashMap::new(),
1497 }
1498}
1499
1500#[cfg(test)]
1501mod tests {
1502 use super::*;
1503
1504 #[tokio::test]
1505 async fn on_start_hooks_are_stored_and_runnable_in_order() {
1506 use std::sync::Mutex;
1507 let log = std::sync::Arc::new(Mutex::new(Vec::new()));
1508
1509 let l1 = log.clone();
1510 let l2 = log.clone();
1511 let server = PvaServer::builder()
1512 .ai("T:A", 1.0)
1513 .on_start(move |_store| {
1514 let l = l1.clone();
1515 Box::pin(async move { l.lock().unwrap().push("first"); })
1516 })
1517 .on_start(move |_store| {
1518 let l = l2.clone();
1519 Box::pin(async move { l.lock().unwrap().push("second"); })
1520 })
1521 .build();
1522
1523 server.run_start_hooks().await.expect("hooks must succeed");
1524
1525 assert_eq!(log.lock().unwrap().as_slice(), &["first", "second"]);
1526 }
1527
1528 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1529 async fn serve_builder_start_surfaces_a_start_hook_abort() {
1530 const RUN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
1535 let started = tokio::time::timeout(
1536 RUN_TIMEOUT,
1537 PvaServer::serve(Vec::<crate::pv::AnyPv>::new())
1538 .port(0)
1539 .udp_port(0)
1540 .on_start(|_store| Box::pin(async { panic!("init failed: no DB") }))
1541 .start(),
1542 )
1543 .await
1544 .expect("ServeBuilder::start() did not return within RUN_TIMEOUT");
1545
1546 let err = started.err().expect("an aborting hook must fail start()");
1547 assert!(
1548 err.contains("init failed: no DB"),
1549 "start() must surface the hook's cause, got: {err}"
1550 );
1551 }
1552
1553 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1554 async fn serve_builder_start_returns_only_after_hooks_have_run() {
1555 const RUN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
1557 let server = tokio::time::timeout(
1558 RUN_TIMEOUT,
1559 PvaServer::serve(Vec::<crate::pv::AnyPv>::new())
1560 .port(0)
1561 .udp_port(0)
1562 .on_start(|store| {
1563 Box::pin(async move {
1564 store.insert(
1565 "T:HOOKED".to_string(),
1566 make_scalar_record(
1567 "T:HOOKED",
1568 RecordType::Ai,
1569 ScalarValue::F64(7.0),
1570 ),
1571 )
1572 .await;
1573 })
1574 })
1575 .start(),
1576 )
1577 .await
1578 .expect("ServeBuilder::start() did not return within RUN_TIMEOUT")
1579 .expect("hooks must succeed");
1580
1581 assert_eq!(
1582 server.store().get_value("T:HOOKED").await,
1583 Some(ScalarValue::F64(7.0)),
1584 "start() must not return before on_start hooks have finished"
1585 );
1586 server.abort();
1587 }
1588
1589 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1590 async fn running_server_can_post_to_a_serve_builder_handler() {
1591 const RUN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
1595 let server = tokio::time::timeout(
1596 RUN_TIMEOUT,
1597 PvaServer::serve(Vec::<crate::pv::AnyPv>::new())
1598 .port(0)
1599 .udp_port(0)
1600 .on_event("SHUTTER", |store, _event| {
1601 Box::pin(async move {
1602 store
1603 .insert(
1604 "T:FIRED".to_string(),
1605 make_scalar_record(
1606 "T:FIRED",
1607 RecordType::Ai,
1608 ScalarValue::F64(1.0),
1609 ),
1610 )
1611 .await;
1612 })
1613 })
1614 .start(),
1615 )
1616 .await
1617 .expect("ServeBuilder::start() did not return within RUN_TIMEOUT")
1618 .expect("hooks must succeed");
1619
1620 server.post_event("SHUTTER").await;
1621 server.events().drain().await;
1622
1623 assert_eq!(
1624 server.store().get_value("T:FIRED").await,
1625 Some(ScalarValue::F64(1.0)),
1626 "a ServeBuilder-registered handler must be reachable from the handle"
1627 );
1628 server.abort();
1629 }
1630
1631 #[tokio::test]
1632 async fn a_builder_registered_sink_receives_posted_events() {
1633 use std::sync::Mutex;
1634 struct RecordingSink(Mutex<Vec<String>>);
1635 impl crate::events::EventSink for RecordingSink {
1636 fn on_event(
1637 &self,
1638 event: &str,
1639 ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1640 let event = event.to_string();
1641 Box::pin(async move { self.0.lock().unwrap().push(event) })
1642 }
1643 }
1644
1645 let sink = Arc::new(RecordingSink(Mutex::new(Vec::new())));
1646 let server = PvaServer::builder()
1647 .ai("T:A", 1.0)
1648 .event_sink(sink.clone())
1649 .build();
1650
1651 server.post_event("SHUTTER").await;
1652
1653 assert_eq!(sink.0.lock().unwrap().as_slice(), &["SHUTTER".to_string()]);
1654 }
1655
1656 #[tokio::test]
1657 async fn a_panicking_on_start_hook_reports_its_cause() {
1658 let server = PvaServer::builder()
1663 .ai("T:A", 1.0)
1664 .on_start(|_store| {
1665 Box::pin(async {
1666 panic!("on_start hook for source 'db' raised: DB connection refused")
1667 })
1668 })
1669 .build();
1670
1671 let err = server
1672 .run_start_hooks()
1673 .await
1674 .expect_err("a panicking hook must abort startup");
1675
1676 assert!(
1677 err.contains("DB connection refused"),
1678 "error must carry the panic's cause, got: {err}"
1679 );
1680 assert!(
1681 err.contains("source 'db'"),
1682 "error must preserve the hook label the panic carried, got: {err}"
1683 );
1684 }
1685
1686 #[tokio::test]
1687 async fn on_start_hook_can_write_the_store() {
1688 let server = PvaServer::builder()
1689 .ao("T:SP", 0.0)
1690 .on_start(|store| {
1691 Box::pin(async move {
1692 store.set_value("T:SP", ScalarValue::F64(22.5)).await;
1693 })
1694 })
1695 .build();
1696
1697 server.run_start_hooks().await.expect("hooks must succeed");
1698
1699 assert_eq!(
1700 server.store().get_value("T:SP").await,
1701 Some(ScalarValue::F64(22.5))
1702 );
1703 }
1704
1705 #[tokio::test]
1706 async fn on_start_hook_write_reaches_a_subscribed_monitor() {
1707 use crate::state::MonitorSub;
1715
1716 let mut server = PvaServer::builder()
1717 .ao("T:SP", 0.0)
1718 .on_start(|store| {
1719 Box::pin(async move {
1720 store.set_value("T:SP", ScalarValue::F64(42.0)).await;
1721 })
1722 })
1723 .build();
1724
1725 let registry = server.monitor_registry();
1730 let (tx, mut rx) = tokio::sync::mpsc::channel(4);
1731 registry.conns.lock().await.insert(1, tx);
1732 registry.monitors.lock().await.insert(
1733 "T:SP".to_string(),
1734 vec![MonitorSub {
1735 conn_id: 1,
1736 ioid: 0,
1737 version: 2,
1738 is_be: true,
1739 running: true,
1740 pipeline_enabled: false,
1741 nfree: 0,
1742 filtered_desc: None,
1743 last_snapshot: None,
1744 }],
1745 );
1746
1747 server.run_start_hooks().await.expect("hooks must succeed");
1748
1749 let msg = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
1750 .await
1751 .expect("monitor update did not arrive within 5s -- on_start hook's write did not reach the registry")
1752 .expect("monitor channel closed unexpectedly");
1753 assert!(!msg.is_empty(), "monitor update message must not be empty");
1754 }
1755
1756 #[tokio::test]
1757 async fn post_event_reaches_a_builder_registered_handler() {
1758 use std::sync::Mutex;
1759 let seen = std::sync::Arc::new(Mutex::new(Vec::new()));
1760 let s = seen.clone();
1761
1762 let server = PvaServer::builder()
1763 .ai("T:A", 1.0)
1764 .on_event("SHUTTER", move |_store, event| {
1765 let s = s.clone();
1766 Box::pin(async move { s.lock().unwrap().push(event); })
1767 })
1768 .build();
1769
1770 server.events().start_dispatcher(server.store().clone());
1771 server.post_event("SHUTTER").await;
1772 server.events().drain().await;
1773
1774 assert_eq!(seen.lock().unwrap().as_slice(), &["SHUTTER".to_string()]);
1775 }
1776
1777 #[tokio::test]
1778 async fn scan_tasks_do_not_run_before_start_hooks_finish() {
1779 use std::sync::Mutex;
1780 use std::time::Duration;
1781 let log = std::sync::Arc::new(Mutex::new(Vec::new()));
1782
1783 let l = log.clone();
1784 let scan_log = log.clone();
1785 let server = PvaServer::builder()
1786 .ai("T:TICK", 0.0)
1787 .port(0)
1788 .on_start(move |_store| {
1789 let l = l.clone();
1790 Box::pin(async move {
1791 for _ in 0..50 {
1794 tokio::task::yield_now().await;
1795 }
1796 l.lock().unwrap().push("hook-done");
1797 })
1798 })
1799 .scan("T:TICK", Duration::from_millis(1), move |_name| {
1800 scan_log.lock().unwrap().push("scan");
1801 ScalarValue::F64(1.0)
1802 })
1803 .build();
1804
1805 let handle = tokio::spawn(async move {
1809 let _ = server.run().await;
1810 });
1811 tokio::time::sleep(Duration::from_millis(100)).await;
1812 handle.abort();
1813
1814 let entries = log.lock().unwrap().clone();
1815 assert_eq!(
1816 entries.first(),
1817 Some(&"hook-done"),
1818 "start hook must complete before the first scan tick; got {entries:?}"
1819 );
1820 }
1821
1822 #[tokio::test]
1823 async fn no_client_can_connect_before_start_hooks_finish() {
1824 use std::sync::atomic::{AtomicBool, Ordering};
1825 use std::time::Duration;
1826
1827 let gate = std::sync::Arc::new(tokio::sync::Notify::new());
1830 let hook_entered = std::sync::Arc::new(AtomicBool::new(false));
1831
1832 let g = gate.clone();
1833 let entered = hook_entered.clone();
1834 let server = PvaServer::builder()
1835 .ai("T:GATED", 0.0)
1836 .port(0)
1837 .on_start(move |store| {
1838 let g = g.clone();
1839 let entered = entered.clone();
1840 Box::pin(async move {
1841 entered.store(true, Ordering::SeqCst);
1842 g.notified().await;
1843 store.set_value("T:GATED", ScalarValue::F64(99.0)).await;
1844 })
1845 })
1846 .build();
1847
1848 let store = server.store().clone();
1849 let handle = tokio::spawn(async move {
1853 let _ = server.run().await;
1854 });
1855
1856 tokio::time::sleep(Duration::from_millis(50)).await;
1858 assert!(hook_entered.load(Ordering::SeqCst), "hook should have started");
1859 assert_eq!(
1860 store.get_value("T:GATED").await,
1861 Some(ScalarValue::F64(0.0)),
1862 "hook has not finished, so the initial value is still in place"
1863 );
1864
1865 gate.notify_waiters();
1866 tokio::time::sleep(Duration::from_millis(50)).await;
1867 assert_eq!(
1868 store.get_value("T:GATED").await,
1869 Some(ScalarValue::F64(99.0)),
1870 "hook must have completed once released"
1871 );
1872
1873 handle.abort();
1874 }
1875
1876 #[tokio::test]
1877 async fn run_aborts_when_a_start_hook_panics() {
1878 let server = PvaServer::builder()
1879 .ai("T:A", 1.0)
1880 .port(0)
1881 .on_start(|_store| Box::pin(async { panic!("init failed"); }))
1882 .build();
1883
1884 const RUN_TIMEOUT: Duration = Duration::from_secs(5);
1889 let result = tokio::time::timeout(RUN_TIMEOUT, server.run())
1890 .await
1891 .expect(
1892 "run() did not return within 5s — the panicking on_start hook's \
1893 error was likely swallowed, so run() fell through to bind the \
1894 listener and is now serving forever instead of aborting startup",
1895 );
1896
1897 assert!(result.is_err(), "run() must fail when a start hook panics");
1898 let msg = result.unwrap_err().to_string();
1899 assert!(
1900 msg.contains("on_start"),
1901 "error must name the failing hook, got: {msg}"
1902 );
1903 }
1904
1905 #[tokio::test]
1906 async fn serve_builder_forwards_on_start_and_on_event() {
1907 use std::sync::Mutex;
1908 let log = std::sync::Arc::new(Mutex::new(Vec::new()));
1909 let l1 = log.clone();
1910 let l2 = log.clone();
1911
1912 let temp = Pv::ai("T:TEMP", 20.0);
1913 let server = PvaServer::serve([temp])
1914 .on_start(move |_store| {
1915 let l = l1.clone();
1916 Box::pin(async move { l.lock().unwrap().push("started"); })
1917 })
1918 .on_event("GO", move |_store, _event| {
1919 let l = l2.clone();
1920 Box::pin(async move { l.lock().unwrap().push("evented"); })
1921 })
1922 .build()
1923 .await;
1924
1925 server.run_start_hooks().await.expect("hooks must succeed");
1926 server.events().start_dispatcher(server.store().clone());
1927 server.post_event("GO").await;
1928 server.events().drain().await;
1929
1930 assert_eq!(log.lock().unwrap().as_slice(), &["started", "evented"]);
1931 }
1932
1933 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1934 async fn running_server_add_scalar_and_array() {
1935 use spvirit_types::{ScalarArrayValue, ScalarValue};
1936
1937 let server = PvaServer::serve(Vec::<crate::pv::AnyPv>::new())
1938 .port(0)
1939 .udp_port(0)
1940 .start()
1941 .await
1942 .expect("server start hooks must succeed");
1943
1944 let h = server.add_scalar("RT:U32", ScalarValue::U32(7), true).await;
1946 assert_eq!(h.get().await.unwrap(), ScalarValue::U32(7));
1947 assert!(matches!(
1949 server.store().get_value("RT:U32").await,
1950 Some(ScalarValue::U32(7))
1951 ));
1952
1953 let _ = server.add_scalar("RT:I16", ScalarValue::I16(-3), false).await;
1955 assert!(matches!(
1956 server.store().get_value("RT:I16").await,
1957 Some(ScalarValue::I16(-3))
1958 ));
1959
1960 let a = server
1962 .add_array("RT:ARR", ScalarArrayValue::F64(vec![1.0, 2.0, 3.0]), true)
1963 .await;
1964 a.set(ScalarArrayValue::F64(vec![4.0, 5.0])).await.unwrap();
1965 assert!(matches!(
1966 server.store().get_nt("RT:ARR").await,
1967 Some(spvirit_types::NtPayload::ScalarArray(_))
1968 ));
1969
1970 server.abort();
1971 }
1972
1973 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1974 async fn running_server_add_enum_and_table() {
1975 use spvirit_types::{NtPayload, ScalarArrayValue};
1976
1977 let server = PvaServer::serve(Vec::<crate::pv::AnyPv>::new())
1978 .port(0)
1979 .udp_port(0)
1980 .start()
1981 .await
1982 .expect("server start hooks must succeed");
1983
1984 server
1986 .add_enum("RT:ENUM", vec!["OFF".into(), "ON".into(), "TRIP".into()], 1, true)
1987 .await;
1988 match server.store().get_nt("RT:ENUM").await {
1989 Some(NtPayload::Enum(e)) => {
1990 assert_eq!(e.index, 1);
1991 assert_eq!(e.choices, vec!["OFF", "ON", "TRIP"]);
1992 }
1993 other => panic!("expected enum, got {other:?}"),
1994 }
1995
1996 server.add_enum("RT:ENUM_RO", vec!["A".into(), "B".into()], 0, false).await;
1998 assert!(matches!(
1999 server.store().get_nt("RT:ENUM_RO").await,
2000 Some(NtPayload::Enum(_))
2001 ));
2002
2003 server
2005 .add_table(
2006 "RT:TBL",
2007 vec![
2008 ("id".into(), ScalarArrayValue::I32(vec![1, 2, 3])),
2009 ("x".into(), ScalarArrayValue::F64(vec![0.5, 1.5, 2.5])),
2010 ],
2011 )
2012 .await;
2013 match server.store().get_nt("RT:TBL").await {
2014 Some(NtPayload::Table(t)) => {
2015 assert_eq!(t.labels, vec!["id", "x"]);
2016 assert_eq!(t.columns.len(), 2);
2017 }
2018 other => panic!("expected table, got {other:?}"),
2019 }
2020
2021 server.abort();
2022 }
2023
2024 #[test]
2025 fn builder_creates_records() {
2026 let server = PvaServer::builder()
2027 .ai("T:AI", 1.0)
2028 .ao("T:AO", 2.0)
2029 .bi("T:BI", true)
2030 .bo("T:BO", false)
2031 .string_in("T:SI", "hello")
2032 .string_out("T:SO", "world")
2033 .build();
2034
2035 let rt = tokio::runtime::Builder::new_current_thread()
2036 .enable_all()
2037 .build()
2038 .unwrap();
2039 let names = rt.block_on(server.store.pv_names());
2040 assert_eq!(names.len(), 6);
2041 }
2042
2043 #[test]
2044 fn builder_defaults() {
2045 let server = PvaServer::builder().build();
2046 assert_eq!(server.config.tcp_port, 5075);
2047 assert_eq!(server.config.udp_port, 5076);
2048 assert!(!server.config.compute_alarms);
2049 }
2050
2051 #[test]
2052 fn builder_port_override() {
2053 let server = PvaServer::builder().port(9075).udp_port(9076).build();
2054 assert_eq!(server.config.tcp_port, 9075);
2055 assert_eq!(server.config.udp_port, 9076);
2056 }
2057
2058 #[test]
2059 fn builder_db_string() {
2060 let db = r#"
2061 record(ai, "TEST:VAL") {
2062 field(VAL, "3.14")
2063 }
2064 "#;
2065 let server = PvaServer::builder().db_string(db).build();
2066 let rt = tokio::runtime::Builder::new_current_thread()
2067 .enable_all()
2068 .build()
2069 .unwrap();
2070 assert!(rt.block_on(server.store.get_value("TEST:VAL")).is_some());
2071 }
2072
2073 #[test]
2074 #[should_panic(expected = "failed to parse db string")]
2075 fn a_malformed_db_string_aborts_the_builder_rather_than_serving_nothing() {
2076 let _ = PvaServer::builder().db_string("not a valid db line").build();
2080 }
2081
2082 #[test]
2083 fn a_malformed_db_file_aborts_the_builder_rather_than_serving_nothing() {
2084 let dir = std::env::temp_dir();
2085 let path = dir.join(format!(
2086 "spvirit-bad-{}-{}.db",
2087 std::process::id(),
2088 std::time::SystemTime::now()
2089 .duration_since(std::time::UNIX_EPOCH)
2090 .unwrap()
2091 .as_nanos()
2092 ));
2093 std::fs::write(&path, "not a valid db line").expect("write temp db file");
2094 let path_str = path.to_string_lossy().into_owned();
2095 let result = std::panic::catch_unwind(|| {
2096 let _ = PvaServer::builder().db_file(&path_str).build();
2097 });
2098 let _ = std::fs::remove_file(&path);
2099 let payload = result.expect_err("db_file must panic on a malformed file");
2100 let message = payload
2101 .downcast_ref::<String>()
2102 .cloned()
2103 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
2104 .unwrap_or_default();
2105 assert!(
2106 message.contains("failed to load db file"),
2107 "panic message must name the failure, got: {message}"
2108 );
2109 }
2110
2111 #[test]
2112 fn builder_waveform() {
2113 let data = ScalarArrayValue::F64(vec![1.0, 2.0, 3.0]);
2114 let server = PvaServer::builder().waveform("T:WF", data).build();
2115 let rt = tokio::runtime::Builder::new_current_thread()
2116 .enable_all()
2117 .build()
2118 .unwrap();
2119 let names = rt.block_on(server.store.pv_names());
2120 assert!(names.contains(&"T:WF".to_string()));
2121 }
2122
2123 #[test]
2124 fn builder_scan_callback() {
2125 let server = PvaServer::builder()
2126 .ai("SCAN:V", 0.0)
2127 .scan("SCAN:V", Duration::from_secs(1), |_name| {
2128 ScalarValue::F64(42.0)
2129 })
2130 .build();
2131 assert_eq!(server.scans.len(), 1);
2132 }
2133
2134 #[test]
2135 fn builder_on_put_callback() {
2136 let server = PvaServer::builder()
2137 .ao("PUT:V", 0.0)
2138 .on_put("PUT:V", |_name, _val| {})
2139 .build();
2140 let rt = tokio::runtime::Builder::new_current_thread()
2143 .enable_all()
2144 .build()
2145 .unwrap();
2146 assert!(rt.block_on(server.store.get_value("PUT:V")).is_some());
2147 }
2148
2149 #[test]
2150 fn store_runtime_get_set() {
2151 let server = PvaServer::builder().ao("RT:V", 0.0).build();
2152 let rt = tokio::runtime::Builder::new_current_thread()
2153 .enable_all()
2154 .build()
2155 .unwrap();
2156 let store = server.store().clone();
2157 rt.block_on(async {
2158 assert_eq!(store.get_value("RT:V").await, Some(ScalarValue::F64(0.0)));
2159 store.set_value("RT:V", ScalarValue::F64(99.0)).await;
2160 assert_eq!(store.get_value("RT:V").await, Some(ScalarValue::F64(99.0)));
2161 });
2162 }
2163
2164 #[test]
2165 fn link_propagates_on_set_value() {
2166 let server = PvaServer::builder()
2167 .ao("INPUT:A", 1.0)
2168 .ao("INPUT:B", 2.0)
2169 .ai("CALC:SUM", 0.0)
2170 .link("CALC:SUM", &["INPUT:A", "INPUT:B"], |values| {
2171 let a = match &values[0] {
2172 ScalarValue::F64(v) => *v,
2173 _ => 0.0,
2174 };
2175 let b = match &values[1] {
2176 ScalarValue::F64(v) => *v,
2177 _ => 0.0,
2178 };
2179 ScalarValue::F64(a + b)
2180 })
2181 .build();
2182
2183 let rt = tokio::runtime::Builder::new_current_thread()
2184 .enable_all()
2185 .build()
2186 .unwrap();
2187 let store = server.store().clone();
2188 rt.block_on(async {
2189 store.set_value("INPUT:A", ScalarValue::F64(10.0)).await;
2191 assert_eq!(
2192 store.get_value("CALC:SUM").await,
2193 Some(ScalarValue::F64(12.0))
2194 );
2195
2196 store.set_value("INPUT:B", ScalarValue::F64(5.0)).await;
2198 assert_eq!(
2199 store.get_value("CALC:SUM").await,
2200 Some(ScalarValue::F64(15.0))
2201 );
2202 });
2203 }
2204
2205 use crate::pv::{AnyPv, Pv};
2206
2207 #[tokio::test]
2208 async fn serve_builder_binds_handles_and_registers_everything() {
2209 let temp = Pv::ai("S:T", 22.5).mdel(0.1);
2210 let sp = Pv::ao("S:SP", 25.0).on_put(|_pv, _v: f64| Ok(()));
2211 let a = Pv::ai("S:A", 1.0);
2212 let b = Pv::ai("S:B", 2.0);
2213 let sum = Pv::calc("S:SUM", &[&a, &b], |vals| vals.iter().sum());
2214
2215 let server = PvaServer::serve([AnyPv::from(temp.clone()), AnyPv::from(sp)])
2219 .pvs([
2220 AnyPv::from(a.clone()),
2221 AnyPv::from(b),
2222 AnyPv::from(sum.clone()),
2223 ])
2224 .build()
2225 .await;
2226
2227 temp.set(23.0).await.unwrap();
2229 assert_eq!(temp.get().await, Ok(23.0));
2230
2231 a.set(10.0).await.unwrap();
2233 assert_eq!(sum.get().await, Ok(12.0));
2234
2235 let rec = server.store().get_record("S:T").await.unwrap();
2237 assert_eq!(rec.raw_fields.get("MDEL").map(String::as_str), Some("0.1"));
2238 }
2239
2240 #[tokio::test]
2241 async fn running_server_mints_handles_to_db_records() {
2242 let server = PvaServer::serve(Vec::<AnyPv>::new())
2249 .db_string("record(ao, \"DB:X\") {\n field(VAL, \"2.5\")\n}")
2250 .build()
2251 .await;
2252 let store = server.store().clone();
2253 let h: crate::pv::Pv<f64> = crate::pv::Pv::attach(&store, "DB:X").await.unwrap();
2254 assert_eq!(h.get().await, Ok(2.5));
2255 }
2256
2257 #[tokio::test]
2258 async fn homogeneous_iterator_feeds_serve_without_manual_erasure() {
2259 let bpms: Vec<Pv<f64>> = (0..100)
2260 .map(|i| Pv::ai(format!("BPM:{i:03}:X"), 0.0))
2261 .collect();
2262 let server = PvaServer::serve(bpms.iter().cloned()).build().await;
2263 assert_eq!(server.store().pv_names().await.len(), 100);
2264 bpms[42].set(1.23).await.unwrap();
2265 assert_eq!(bpms[42].get().await, Ok(1.23));
2266 }
2267
2268 #[tokio::test]
2269 async fn pva_server_mints_typed_handles_pre_run() {
2270 let server = PvaServer::serve([AnyPv::from(Pv::ai("PRE:X", 5.0))])
2271 .build()
2272 .await;
2273 let h: crate::pv::Pv<f64> = server.pv("PRE:X").await.unwrap();
2274 assert_eq!(h.get().await, Ok(5.0));
2275 assert!(matches!(
2276 server.pv::<bool>("PRE:X").await,
2277 Err(crate::pv::PvError::TypeMismatch { .. })
2278 ));
2279 }
2280}