1#[cfg(not(target_arch = "wasm32"))]
20mod ca;
21mod formula;
22mod local;
23mod name;
24mod origin;
25#[cfg(not(target_arch = "wasm32"))]
26mod pva;
27#[cfg(target_arch = "wasm32")]
28mod pvws;
29#[cfg(any(target_arch = "wasm32", test))]
30mod pvws_msg;
31mod sim;
32mod value;
33
34use std::collections::HashMap;
35#[cfg(not(target_arch = "wasm32"))]
36use std::sync::atomic::AtomicUsize;
37use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
38use std::sync::{Arc, Weak};
39
40use parking_lot::{Mutex, RwLock};
41
42#[cfg(not(target_arch = "wasm32"))]
43use tokio::sync::mpsc;
44
45pub use formula::formula_inputs;
46pub use name::{PvName, Scheme};
47pub use origin::{same_origin, url_origin};
48#[cfg(target_arch = "wasm32")]
49pub use pvws::{set_pvws, set_write_token};
50pub use value::{
51 Image, ImageData, PvState, Severity, Value, long_string, long_string_bytes, split_array,
52 split_strings,
53};
54
55pub type Notifier = Arc<dyn Fn() + Send + Sync>;
57
58struct Tag(Notifier);
61
62pub type Listener = Arc<ListenFn>;
65pub type ListenFn = dyn Fn(&PvState) + Send + Sync;
66
67#[derive(Clone, Debug, PartialEq)]
69pub(crate) struct Put {
70 pub text: String,
71 pub long_string: bool,
74}
75
76pub(crate) enum Writer {
78 #[cfg(not(target_arch = "wasm32"))]
80 Channel(mpsc::Sender<Put>),
81 Local,
83 #[cfg(target_arch = "wasm32")]
85 Pvws(String),
86}
87
88#[derive(Default)]
90pub(crate) struct Control {
91 read_only: AtomicBool,
93 notice: Mutex<(u64, String)>,
96 repaint: Arc<RwLock<Option<Notifier>>>,
99 tags: Mutex<Vec<Weak<Tag>>>,
101}
102
103impl Control {
104 pub(crate) fn notify(&self, text: String) {
105 log::warn!("{text}");
106 {
107 let mut n = self.notice.lock();
108 *n = (n.0 + 1, text);
109 }
110 if let Some(n) = self.repaint.read().as_ref() {
111 n();
112 }
113 for tag in live(&self.tags) {
114 (tag.0)();
115 }
116 }
117}
118
119fn live(tags: &Mutex<Vec<Weak<Tag>>>) -> Vec<Arc<Tag>> {
121 let mut tags = tags.lock();
122 tags.retain(|t| t.strong_count() > 0);
123 tags.iter().filter_map(Weak::upgrade).collect()
124}
125
126pub(crate) const WRITE_QUEUE: usize = 16;
130
131#[cfg(not(target_arch = "wasm32"))]
133pub(crate) fn write_queue() -> (mpsc::Sender<Put>, mpsc::Receiver<Put>) {
134 mpsc::channel(WRITE_QUEUE)
135}
136
137enum After {
139 #[cfg(not(target_arch = "wasm32"))]
141 Stopped(Put),
142 #[cfg(not(target_arch = "wasm32"))]
144 Dropped(Put),
145 Full(Put),
147 Local(Put),
149 Refused,
151}
152
153thread_local! {
154 static CALLING: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
156 static DEFERRED: std::cell::RefCell<std::collections::VecDeque<Box<dyn FnOnce()>>> =
158 const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
159 static DRAINING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
162 static DRAINS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
163}
164
165fn calling_listeners<R>(f: impl FnOnce() -> R) -> R {
168 struct Calling;
169 impl Drop for Calling {
170 fn drop(&mut self) {
171 CALLING.with(|c| c.set(c.get() - 1));
172 }
173 }
174 CALLING.with(|c| c.set(c.get() + 1));
175 let _calling = Calling;
176 f()
177}
178
179fn after_listeners() {
183 if CALLING.with(|c| c.get()) != 0 || DRAINING.with(|d| d.get()) {
184 return;
185 }
186 struct Draining;
187 impl Drop for Draining {
188 fn drop(&mut self) {
189 DRAINING.with(|d| d.set(false));
190 }
191 }
192 DRAINING.with(|d| d.set(true));
193 DRAINS.with(|n| n.set(n.get() + 1));
194 let _draining = Draining;
195 while let Some(work) = DEFERRED.with(|d| d.borrow_mut().pop_front()) {
197 work();
198 }
199}
200
201pub(crate) fn drain_number() -> u64 {
204 DRAINS.with(|n| n.get())
205}
206
207pub(crate) fn outside_listeners(work: Box<dyn FnOnce()>) {
212 DEFERRED.with(|d| d.borrow_mut().push_back(work));
213 after_listeners();
214}
215
216pub(crate) struct Shared {
218 ctl: Arc<Control>,
219 name: String,
220 state: Mutex<PvState>,
221 version: AtomicU64,
223 writer: Mutex<Option<Writer>>,
224 pending: Mutex<Vec<Put>>,
226 backend_writes: bool,
229 #[cfg(not(target_arch = "wasm32"))]
232 in_flight: AtomicUsize,
233 notify: Arc<RwLock<Option<Notifier>>>,
234 tags: Mutex<Vec<Weak<Tag>>>,
237 listeners: Mutex<Vec<Weak<ListenFn>>>,
238 was_connected: AtomicBool,
240}
241
242impl Shared {
243 fn new(name: &PvName, notify: Arc<RwLock<Option<Notifier>>>, ctl: Arc<Control>) -> Arc<Self> {
244 Arc::new(Self {
245 ctl,
246 name: name.to_string(),
247 state: Mutex::new(PvState::default()),
248 version: AtomicU64::new(0),
249 writer: Mutex::new(None),
250 pending: Mutex::new(Vec::new()),
251 backend_writes: matches!(name.scheme, Scheme::Ca | Scheme::Pva),
252 #[cfg(not(target_arch = "wasm32"))]
253 in_flight: AtomicUsize::new(0),
254 notify,
255 tags: Mutex::new(Vec::new()),
256 listeners: Mutex::new(Vec::new()),
257 was_connected: AtomicBool::new(false),
258 })
259 }
260
261 pub(crate) fn update(&self, f: impl FnOnce(&mut PvState)) {
263 {
264 let mut state = self.state.lock();
265 f(&mut state);
266 self.version.fetch_add(1, Ordering::Relaxed);
267 if state.connected {
268 self.was_connected.store(true, Ordering::Relaxed);
269 }
270 let mut listeners = self.listeners.lock();
275 calling_listeners(|| {
276 listeners.retain(|l| match l.upgrade() {
277 Some(l) => {
278 let call = std::panic::AssertUnwindSafe(|| l(&state));
279 let ok = std::panic::catch_unwind(call).is_ok();
280 if !ok {
281 log::error!("{}: a listener panicked and was removed", self.name);
282 }
283 ok
284 }
285 None => false,
286 })
287 });
288 }
289 after_listeners();
290 if let Some(n) = self.notify.read().as_ref() {
291 n();
292 }
293 for tag in live(&self.tags) {
294 (tag.0)();
295 }
296 }
297
298 fn add_tag(&self, tag: &Arc<Tag>) {
300 let mut tags = self.tags.lock();
301 tags.retain(|t| t.strong_count() > 0);
302 if !tags
303 .iter()
304 .any(|t| std::ptr::eq(t.as_ptr(), Arc::as_ptr(tag)))
305 {
306 tags.push(Arc::downgrade(tag));
307 }
308 }
309
310 pub(crate) fn dropped(&self) -> bool {
313 let state = self.state.lock();
314 !state.connected && self.was_connected.load(Ordering::Relaxed)
315 }
316
317 pub(crate) fn snapshot(&self) -> PvState {
318 self.state.lock().clone()
319 }
320
321 #[cfg(not(target_arch = "wasm32"))]
323 pub(crate) fn write_result(&self, text: &str, error: Option<String>) {
324 self.answered();
325 if let Some(e) = &error {
326 self.ctl
327 .notify(format!("{}: write '{text}' failed: {e}", self.name));
328 }
329 self.update(|s| s.write_error = error);
330 }
331
332 #[cfg(not(target_arch = "wasm32"))]
334 fn answered(&self) {
335 let mut n = self.in_flight.load(Ordering::Acquire);
336 while n > 0 {
337 match self.in_flight.compare_exchange_weak(
338 n,
339 n - 1,
340 Ordering::AcqRel,
341 Ordering::Acquire,
342 ) {
343 Ok(_) => break,
344 Err(now) => n = now,
345 }
346 }
347 }
348
349 #[cfg(not(target_arch = "wasm32"))]
353 pub(crate) fn write_sent(&self, text: &str, error: Option<String>) {
354 if error.is_some() {
355 self.write_result(text, error);
356 } else {
357 self.answered();
358 }
359 }
360
361 #[cfg(not(target_arch = "wasm32"))]
365 pub(crate) fn can_write(&self, text: &str) -> bool {
366 let dropped = self.dropped();
367 if dropped {
368 self.write_result(text, Some("not connected".into()));
369 }
370 !dropped
371 }
372
373 #[cfg(not(target_arch = "wasm32"))]
375 pub(crate) fn connected(&self) -> bool {
376 self.state.lock().connected
377 }
378
379 pub(crate) fn set_writer(&self, writer: Writer) {
381 let mut slot = self.writer.lock();
382 *slot = Some(writer);
383 let pending = std::mem::take(&mut *self.pending.lock());
384 let mut failed: Vec<(Put, &str)> = Vec::new();
386 for put in pending {
387 match slot.as_ref() {
388 #[cfg(not(target_arch = "wasm32"))]
391 Some(Writer::Channel(tx)) => match tx.try_send(put) {
392 Ok(()) => {}
393 Err(mpsc::error::TrySendError::Full(put)) => {
394 failed.push((put, "too many writes waiting"))
395 }
396 Err(mpsc::error::TrySendError::Closed(put)) => {
397 failed.push((put, "the PV's connection has stopped"))
398 }
399 },
400 #[cfg(target_arch = "wasm32")]
401 Some(Writer::Pvws(name)) => {
402 pvws::write(name, &self.snapshot(), put.text, put.long_string)
403 }
404 _ => failed.push((put, "cannot be written")),
406 }
407 }
408 drop(slot);
409 for (put, why) in failed {
410 #[cfg(not(target_arch = "wasm32"))]
411 self.write_result(&put.text, Some(why.into()));
412 #[cfg(target_arch = "wasm32")]
413 self.ctl
414 .notify(format!("{}: write '{}' failed: {why}", self.name, put.text));
415 }
416 }
417
418 fn send(&self, writer: Option<&Writer>, put: Put) -> Option<After> {
424 match writer {
425 #[cfg(not(target_arch = "wasm32"))]
426 Some(Writer::Channel(tx)) => {
427 self.in_flight.fetch_add(1, Ordering::AcqRel);
431 match tx.try_send(put) {
433 Ok(()) => None,
434 Err(mpsc::error::TrySendError::Full(put)) => {
435 self.in_flight.fetch_sub(1, Ordering::AcqRel);
436 Some(After::Full(put))
437 }
438 Err(mpsc::error::TrySendError::Closed(put)) => Some(After::Stopped(put)),
439 }
440 }
441 Some(Writer::Local) => Some(After::Local(put)),
442 #[cfg(target_arch = "wasm32")]
443 Some(Writer::Pvws(name)) => {
444 pvws::write(name, &self.snapshot(), put.text, put.long_string);
445 None
446 }
447 None if self.backend_writes => {
448 let mut pending = self.pending.lock();
449 if pending.len() >= WRITE_QUEUE {
450 return Some(After::Full(put));
451 }
452 #[cfg(not(target_arch = "wasm32"))]
453 self.in_flight.fetch_add(1, Ordering::AcqRel);
454 pending.push(put);
455 None
456 }
457 None => Some(After::Refused),
458 }
459 }
460
461 fn finish(&self, after: After) {
462 match after {
463 #[cfg(not(target_arch = "wasm32"))]
464 After::Stopped(put) => {
465 self.write_result(&put.text, Some("the PV's connection has stopped".into()))
466 }
467 #[cfg(not(target_arch = "wasm32"))]
468 After::Dropped(put) => self.ctl.notify(format!(
469 "{}: not writing '{}', not connected",
470 self.name, put.text
471 )),
472 After::Full(put) => self.ctl.notify(format!(
473 "{}: not writing '{}', {WRITE_QUEUE} writes still waiting",
474 self.name, put.text
475 )),
476 After::Local(Put { text, long_string }) => self.update(|s| {
477 s.timestamp = Some(web_time::SystemTime::now());
479 s.value = if long_string && s.value.is_numeric_array() {
480 Value::LongArray(
481 long_string_bytes(&text, None)
482 .iter()
483 .map(|b| *b as i64)
484 .collect(),
485 )
486 } else {
487 local::apply(&s.value, &s.labels, &text)
488 }
489 }),
490 After::Refused => self.ctl.notify(format!("{} cannot be written", self.name)),
491 }
492 }
493}
494
495#[cfg(not(target_arch = "wasm32"))]
497type Task = tokio::task::JoinHandle<()>;
498#[cfg(target_arch = "wasm32")]
499type Task = pvws::Task;
500
501#[cfg(not(target_arch = "wasm32"))]
504const WRITE_GRACE: std::time::Duration = std::time::Duration::from_secs(10);
505
506struct Entry {
509 name: PvName,
510 shared: Arc<Shared>,
511 task: Option<Task>,
512 #[cfg(not(target_arch = "wasm32"))]
514 rt: tokio::runtime::Handle,
515 #[cfg_attr(not(test), allow(dead_code, reason = "only held"))]
517 formula: Option<Arc<formula::Formula>>,
518}
519
520impl Drop for Entry {
521 fn drop(&mut self) {
522 let Some(task) = self.task.take() else {
523 return;
524 };
525 #[cfg(not(target_arch = "wasm32"))]
528 if self.shared.in_flight.load(Ordering::Acquire) > 0 {
529 let shared = self.shared.clone();
530 self.rt.spawn(async move {
531 let start = tokio::time::Instant::now();
532 while shared.in_flight.load(Ordering::Acquire) > 0 && start.elapsed() < WRITE_GRACE
533 {
534 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
535 }
536 task.abort();
537 });
538 return;
539 }
540 task.abort();
541 }
542}
543
544#[derive(Clone)]
546pub struct Pv(Arc<Entry>);
547
548impl Pv {
549 pub fn name(&self) -> &PvName {
550 &self.0.name
551 }
552
553 pub fn dropped(&self) -> bool {
557 self.0.shared.dropped()
558 }
559
560 #[doc(hidden)]
563 pub fn set_connected_for_test(&self, connected: bool) {
564 self.0.shared.update(|s| s.connected = connected);
565 }
566
567 pub fn state(&self) -> PvState {
569 let mut state = self.0.shared.snapshot();
570 if self.refuses_writes() {
571 state.writable = false;
572 state.writable_known = true;
573 }
574 state
575 }
576
577 pub fn connected(&self) -> bool {
579 self.0.shared.state.lock().connected
580 }
581
582 pub fn version(&self) -> u64 {
585 self.0.shared.version.load(Ordering::Relaxed)
586 }
587
588 fn refuses_writes(&self) -> bool {
591 let shared = &self.0.shared;
592 shared.ctl.read_only.load(Ordering::Relaxed)
593 && !matches!(*shared.writer.lock(), Some(Writer::Local))
594 }
595
596 pub fn listen(&self, listener: &Listener) {
600 let shared = &self.0.shared;
601 {
602 let state = shared.state.lock();
603 calling_listeners(|| listener(&state));
604 let mut listeners = shared.listeners.lock();
605 listeners.retain(|l| l.strong_count() > 0);
607 listeners.push(Arc::downgrade(listener));
608 }
609 after_listeners();
610 }
611
612 pub fn write(&self, value: impl Into<String>) {
614 self.put(value.into(), false);
615 }
616
617 pub fn write_long_string(&self, value: impl Into<String>) {
628 self.put(value.into(), true);
629 }
630
631 fn put(&self, value: String, long_string: bool) {
632 let shared = &self.0.shared;
633 if self.refuses_writes() {
634 shared.ctl.notify(format!(
635 "{}: not writing '{value}', bobview is read-only",
636 shared.name
637 ));
638 return;
639 }
640 #[cfg(not(target_arch = "wasm32"))]
642 let dropped = shared.dropped();
643 let put = Put {
644 text: value,
645 long_string,
646 };
647 let writer = shared.writer.lock();
648 let after = match writer.as_ref() {
649 #[cfg(not(target_arch = "wasm32"))]
654 Some(Writer::Channel(_)) if dropped => Some(After::Dropped(put)),
655 w => shared.send(w, put),
656 };
657 drop(writer);
658 if let Some(after) = after {
659 shared.finish(after);
660 }
661 }
662}
663
664#[derive(Clone)]
666pub struct PvManager {
667 inner: Arc<Inner>,
668 tag: Option<Arc<Tag>>,
671}
672
673struct Inner {
674 #[cfg(not(target_arch = "wasm32"))]
676 rt: Option<tokio::runtime::Runtime>,
677 entries: Mutex<Entries>,
678 locals: Mutex<HashMap<String, Arc<Shared>>>,
679 #[cfg(not(target_arch = "wasm32"))]
682 ca: Arc<tokio::sync::OnceCell<epics_ca_rs::client::CaClient>>,
683 #[cfg(not(target_arch = "wasm32"))]
684 pva: Arc<std::sync::OnceLock<epics_pva_rs::client::PvaClient>>,
685 notify: Arc<RwLock<Option<Notifier>>>,
686 ctl: Arc<Control>,
687}
688
689#[cfg(not(target_arch = "wasm32"))]
690impl Drop for Inner {
691 fn drop(&mut self) {
692 if let Some(rt) = self.rt.take() {
693 rt.shutdown_background();
694 }
695 }
696}
697
698#[derive(Default)]
700struct Entries {
701 map: HashMap<String, Weak<Entry>>,
702 prune_at: usize,
705}
706
707impl Entries {
708 fn insert(&mut self, key: String, entry: Weak<Entry>) {
709 if self.map.len() >= self.prune_at {
710 self.map.retain(|_, w| w.strong_count() > 0);
711 self.prune_at = (2 * self.map.len()).max(64);
712 }
713 self.map.insert(key, entry);
714 }
715}
716
717impl Default for PvManager {
718 fn default() -> Self {
719 Self::new()
720 }
721}
722
723impl PvManager {
724 #[cfg(target_arch = "wasm32")]
725 pub fn new() -> Self {
726 let ctl = Arc::new(Control::default());
727 Self {
728 inner: Arc::new(Inner {
729 entries: Mutex::default(),
730 locals: Mutex::new(HashMap::new()),
731 notify: ctl.repaint.clone(),
732 ctl,
733 }),
734 tag: None,
735 }
736 }
737
738 #[cfg(not(target_arch = "wasm32"))]
739 pub fn new() -> Self {
740 let rt = tokio::runtime::Builder::new_multi_thread()
741 .worker_threads(2)
742 .thread_name("bobview-pv")
743 .enable_all()
744 .build()
745 .expect("failed to start the PV runtime");
746 let ctl = Arc::new(Control::default());
747 Self {
748 inner: Arc::new(Inner {
749 rt: Some(rt),
750 entries: Mutex::default(),
751 locals: Mutex::new(HashMap::new()),
752 ca: Arc::new(tokio::sync::OnceCell::new()),
753 pva: Arc::new(std::sync::OnceLock::new()),
754 notify: ctl.repaint.clone(),
755 ctl,
756 }),
757 tag: None,
758 }
759 }
760
761 pub fn set_notifier(&self, notifier: Notifier) {
763 *self.inner.notify.write() = Some(notifier);
764 }
765
766 pub fn tagged(&self, notifier: Notifier) -> Self {
772 let tag = Arc::new(Tag(notifier));
773 let mut tags = self.inner.ctl.tags.lock();
774 tags.retain(|t| t.strong_count() > 0);
775 tags.push(Arc::downgrade(&tag));
776 drop(tags);
777 Self {
778 inner: self.inner.clone(),
779 tag: Some(tag),
780 }
781 }
782
783 pub fn retagged(&self) -> Self {
788 match &self.tag {
789 Some(tag) => self.tagged(tag.0.clone()),
790 None => self.clone(),
791 }
792 }
793
794 pub fn set_read_only(&self, read_only: bool) {
797 self.inner.ctl.read_only.store(read_only, Ordering::Relaxed);
798 }
799
800 pub fn read_only(&self) -> bool {
801 self.inner.ctl.read_only.load(Ordering::Relaxed)
802 }
803
804 pub fn notice(&self) -> (u64, String) {
807 self.inner.ctl.notice.lock().clone()
808 }
809
810 #[cfg(not(target_arch = "wasm32"))]
812 pub fn runtime(&self) -> &tokio::runtime::Runtime {
813 self.inner.rt.as_ref().expect("runtime present until drop")
814 }
815
816 pub fn subscribe(&self, name: &str) -> Pv {
818 let name = PvName::parse(name);
819 let key = name.key();
820 let mut entries = self.inner.entries.lock();
821 if let Some(entry) = entries.map.get(&key).and_then(Weak::upgrade) {
822 drop(entries);
823 if let Some(tag) = &self.tag {
824 entry.shared.add_tag(tag);
825 }
826 return Pv(entry);
827 }
828 if name.pv.contains("$(") || name.pv.contains("${") {
831 log::warn!("{}: PV name has an undefined macro", name.pv);
832 }
833
834 let shared = || Shared::new(&name, self.inner.notify.clone(), self.inner.ctl.clone());
835 let (shared, task, formula) = match name.scheme {
836 Scheme::Loc => (self.local(&name), None, None),
837 Scheme::Formula => {
838 let shared = shared();
839 let f = formula::Formula::new(&name, shared.clone(), Arc::downgrade(&self.inner));
840 (shared, None, Some(f))
841 }
842 Scheme::Unsupported => {
843 let shared = shared();
844 log::warn!("{name}: PV type not supported");
845 shared.state.lock().error = Some("PV type not supported".into());
847 (shared, None, None)
848 }
849 _ => {
850 let shared = shared();
851 let task = self.spawn_backend(&name, shared.clone());
852 (shared, task, None)
853 }
854 };
855 let entry = Arc::new(Entry {
856 name,
857 shared,
858 task,
859 #[cfg(not(target_arch = "wasm32"))]
860 rt: self.runtime().handle().clone(),
861 formula: formula.clone(),
862 });
863 entries.insert(key, Arc::downgrade(&entry));
864 drop(entries);
866 if let Some(tag) = &self.tag {
867 entry.shared.add_tag(tag);
868 }
869 if let Some(f) = formula {
870 f.start();
871 }
872 Pv(entry)
873 }
874
875 fn local(&self, name: &PvName) -> Arc<Shared> {
876 let mut locals = self.inner.locals.lock();
877 locals
878 .entry(name.key())
879 .or_insert_with(|| {
880 local::create(
881 name,
882 Shared::new(name, self.inner.notify.clone(), self.inner.ctl.clone()),
883 )
884 })
885 .clone()
886 }
887
888 #[cfg(target_arch = "wasm32")]
889 fn spawn_backend(&self, name: &PvName, shared: Arc<Shared>) -> Option<Task> {
890 Some(match name.scheme {
891 Scheme::Sim => pvws::spawn(sim::run(name.clone(), shared)),
892 Scheme::Ca | Scheme::Pva => pvws::subscribe(name, shared),
893 Scheme::Loc | Scheme::Formula | Scheme::Unsupported => {
894 unreachable!("local, formula and unsupported PVs have no task")
895 }
896 })
897 }
898
899 #[cfg(not(target_arch = "wasm32"))]
900 fn spawn_backend(&self, name: &PvName, shared: Arc<Shared>) -> Option<Task> {
901 let rt = self.runtime().handle();
902 match name.scheme {
903 Scheme::Ca => {
904 let ca = self.inner.ca.clone();
905 let pv = name.pv.clone();
906 Some(rt.spawn(async move {
907 let client = ca.get_or_try_init(epics_ca_rs::client::CaClient::new).await;
908 match client {
909 Ok(client) => ca::run(client, &pv, shared).await,
910 Err(e) => {
911 log::error!("cannot start the CA client: {e}");
912 shared.update(|s| s.error = Some(format!("CA client: {e}")));
913 }
914 }
915 }))
916 }
917 Scheme::Sim => Some(rt.spawn(sim::run(name.clone(), shared))),
918 Scheme::Pva => {
919 let pva = self.inner.pva.clone();
920 let pv = name.pv.clone();
921 Some(rt.spawn(async move {
922 let client =
923 pva.get_or_init(|| epics_pva_rs::client::PvaClient::builder().build());
924 pva::run(client, &pv, shared).await
925 }))
926 }
927 Scheme::Loc | Scheme::Formula | Scheme::Unsupported => {
928 unreachable!("local, formula and unsupported PVs have no task")
929 }
930 }
931 }
932}
933
934#[cfg(test)]
935mod tests {
936 use super::*;
937
938 #[test]
939 fn listeners_see_each_change_until_dropped() {
940 let pvs = PvManager::new();
941 let pv = pvs.subscribe("loc://listen_test(1)");
942 let seen = Arc::new(Mutex::new(Vec::new()));
943 let s = seen.clone();
944 let listener: Listener = Arc::new(move |st: &PvState| {
945 s.lock().push(st.value.as_f64());
946 });
947 pv.listen(&listener);
948 pv.write("2");
949 pv.write("3");
950 drop(listener);
951 pv.write("4");
952 assert_eq!(*seen.lock(), [Some(1.0), Some(2.0), Some(3.0)]);
953 }
954
955 #[test]
956 fn local_writes_are_time_stamped() {
957 let pvs = PvManager::new();
958 let pv = pvs.subscribe("loc://stamp_test(1)");
959 assert_eq!(pv.state().timestamp, None);
960 pv.write("1");
961 assert!(pv.state().timestamp.is_some());
962 }
963
964 #[test]
965 fn read_only_refuses_writes_but_not_local_ones() {
966 let pvs = PvManager::new();
967 pvs.set_read_only(true);
968 let sim = pvs.subscribe("sim://sine");
969 let state = sim.state();
970 assert!(!state.writable && state.writable_known);
971 sim.write("1");
972 let (n, text) = pvs.notice();
973 assert_eq!(n, 1);
974 assert!(text.contains("read-only"), "{text}");
975
976 let loc = pvs.subscribe("loc://ro_test(1)");
977 assert!(loc.state().writable);
978 loc.write("5");
979 assert_eq!(loc.state().value.as_f64(), Some(5.0));
980 assert_eq!(pvs.notice().0, 1);
981
982 pvs.set_read_only(false);
983 assert!(!pvs.read_only());
984 }
985
986 #[test]
987 fn a_failed_put_is_remembered_until_one_works() {
988 let pvs = PvManager::new();
989 let pv = pvs.subscribe("sim://sine");
990 let shared = &pv.0.shared;
991 shared.write_result("3", Some("no write access".into()));
992 assert_eq!(pv.state().write_error.as_deref(), Some("no write access"));
993 let (n, text) = pvs.notice();
994 assert_eq!(n, 1);
995 assert!(text.contains("write '3' failed: no write access"), "{text}");
996 shared.write_result("4", None);
997 assert_eq!(pv.state().write_error, None);
998 }
999
1000 #[test]
1001 fn a_sent_put_leaves_an_earlier_failure_shown() {
1002 let pvs = PvManager::new();
1003 let pv = pvs.subscribe("sim://sine");
1004 let shared = &pv.0.shared;
1005 shared.write_sent("3", Some("not connected".into()));
1006 assert_eq!(pv.state().write_error.as_deref(), Some("not connected"));
1007 assert_eq!(pvs.notice().0, 1);
1008 shared.write_sent("4", None);
1010 assert_eq!(pv.state().write_error.as_deref(), Some("not connected"));
1011 assert_eq!(pvs.notice().0, 1);
1012 }
1013
1014 #[test]
1015 fn a_new_notice_asks_for_a_repaint() {
1016 let pvs = PvManager::new();
1017 let (pv, _rx) = queued(&pvs, true);
1019 let calls = Arc::new(AtomicU64::new(0));
1020 let c = calls.clone();
1021 pvs.set_notifier(Arc::new(move || {
1022 c.fetch_add(1, Ordering::Relaxed);
1023 }));
1024 pvs.set_read_only(true);
1025 pv.write("1");
1026 assert_eq!(pvs.notice().0, 1);
1027 assert_eq!(calls.load(Ordering::Relaxed), 1);
1028 }
1029
1030 #[test]
1031 fn tagged_pvs_call_only_the_notifiers_of_handles_showing_them() {
1032 let pvs = PvManager::new();
1033 let counter = || {
1034 let calls = Arc::new(AtomicU64::new(0));
1035 let c = calls.clone();
1036 let notifier: Notifier = Arc::new(move || {
1037 c.fetch_add(1, Ordering::Relaxed);
1038 });
1039 (calls, notifier)
1040 };
1041 let ((a_calls, a), (b_calls, b)) = (counter(), counter());
1042 let calls = || {
1043 (
1044 a_calls.load(Ordering::Relaxed),
1045 b_calls.load(Ordering::Relaxed),
1046 )
1047 };
1048 let (a, b) = (pvs.tagged(a), pvs.tagged(b));
1049 let x = a.subscribe("loc://tag_x(0)");
1050 let y = b.subscribe("loc://tag_y(0)");
1051 let both = a.subscribe("loc://tag_both(0)");
1053 let _also = b.subscribe("loc://tag_both(0)");
1054 x.write("1");
1055 assert_eq!(calls(), (1, 0));
1056 y.write("1");
1057 assert_eq!(calls(), (1, 1));
1058 both.write("1");
1059 assert_eq!(calls(), (2, 2));
1060 let old = b;
1062 let b = old.retagged();
1063 drop(old);
1064 both.write("2");
1065 y.write("2");
1066 assert_eq!(calls(), (3, 2));
1067 let _z = b.subscribe("loc://tag_both(0)");
1068 both.write("3");
1069 assert_eq!(calls(), (4, 3));
1070 pvs.subscribe("sys://time").write("1");
1072 assert_eq!(calls(), (5, 4));
1073 let plain = pvs.subscribe("loc://tag_plain(0)");
1075 plain.write("1");
1076 assert_eq!(calls(), (5, 4));
1077 }
1078
1079 fn queued(pvs: &PvManager, connected: bool) -> (Pv, mpsc::Receiver<Put>) {
1081 let name = PvName::parse("ca://QUEUED");
1082 let shared = Shared::new(&name, pvs.inner.notify.clone(), pvs.inner.ctl.clone());
1083 let (tx, rx) = write_queue();
1084 shared.set_writer(Writer::Channel(tx));
1085 shared.update(|s| s.connected = connected);
1086 let entry = Entry {
1087 name,
1088 shared,
1089 task: None,
1090 rt: pvs.runtime().handle().clone(),
1091 formula: None,
1092 };
1093 (Pv(Arc::new(entry)), rx)
1094 }
1095
1096 #[test]
1097 fn writes_to_a_dropped_pv_fail_at_once() {
1098 let pvs = PvManager::new();
1099 let (pv, mut rx) = queued(&pvs, true);
1100 pv.0.shared.update(|s| s.connected = false);
1101 assert!(pv.dropped());
1102 pv.write("1");
1103 assert!(rx.try_recv().is_err());
1104 let (n, text) = pvs.notice();
1105 assert_eq!(n, 1);
1106 assert!(text.contains("not connected"), "{text}");
1107 assert_eq!(pv.0.shared.in_flight.load(Ordering::Acquire), 0);
1110 assert!(!pv.0.shared.can_write("1"));
1112 assert_eq!(pv.state().write_error.as_deref(), Some("not connected"));
1113 }
1114
1115 #[test]
1116 fn writes_to_a_pv_yet_to_connect_are_queued() {
1117 let pvs = PvManager::new();
1118 let (pv, mut rx) = queued(&pvs, false);
1119 assert!(!pv.dropped());
1120 pv.write("1");
1121 pv.write_long_string("a");
1122 assert_eq!(pvs.notice().0, 0);
1123 let put = rx.try_recv().unwrap();
1124 assert_eq!((put.text.as_str(), put.long_string), ("1", false));
1125 let put = rx.try_recv().unwrap();
1126 assert_eq!((put.text.as_str(), put.long_string), ("a", true));
1127 assert!(pv.0.shared.can_write("1"));
1129 assert_eq!(pv.state().write_error, None);
1130 for i in 0..WRITE_QUEUE + 1 {
1132 pv.write(i.to_string());
1133 }
1134 assert_eq!(pvs.notice().0, 1);
1135 }
1136
1137 #[test]
1138 fn the_write_queue_is_bounded() {
1139 let pvs = PvManager::new();
1140 let (pv, mut rx) = queued(&pvs, true);
1141 for i in 0..WRITE_QUEUE + 3 {
1142 pv.write(i.to_string());
1143 }
1144 let (n, text) = pvs.notice();
1145 assert_eq!(n, 3);
1146 assert!(text.contains("writes still waiting"), "{text}");
1147 let mut got = Vec::new();
1148 while let Ok(v) = rx.try_recv() {
1149 got.push(v.text);
1150 }
1151 assert_eq!(got.len(), WRITE_QUEUE);
1152 assert_eq!(got[0], "0");
1153 assert_eq!(pv.0.shared.in_flight.load(Ordering::Acquire), WRITE_QUEUE);
1155 drop(rx);
1156 pv.write("x");
1157 assert!(pvs.notice().1.contains("stopped"));
1158 }
1159
1160 #[test]
1161 fn long_strings_are_written_into_number_arrays_only() {
1162 let pvs = PvManager::new();
1163 let pv = pvs.subscribe("loc://long_string_test<VLongArray>(0, 0)");
1164 pv.write_long_string("/tmp/a");
1165 let state = pv.state();
1166 assert_eq!(state.value.as_long_string().as_deref(), Some("/tmp/a"));
1167 let Value::LongArray(a) = &state.value else {
1168 panic!("{:?}", state.value)
1169 };
1170 assert_eq!(a.last(), Some(&0));
1171 pv.write("[1, 2]");
1173 assert_eq!(pv.state().value, Value::LongArray(Arc::from(vec![1, 2])));
1174 let s = pvs.subscribe("loc://long_string_str(\"x\")");
1175 s.write_long_string("y");
1176 assert_eq!(s.state().value, Value::Str("y".into()));
1177 }
1178
1179 #[test]
1180 fn a_write_with_no_task_to_send_it_is_reported() {
1181 let pvs = PvManager::new();
1182 let pv = pvs.subscribe("sim://sine");
1183 let (tx, rx) = write_queue();
1184 drop(rx);
1185 pv.0.shared.set_writer(Writer::Channel(tx));
1186 pv.0.shared.update(|s| s.connected = true);
1187 pv.write("7");
1188 let (n, text) = pvs.notice();
1189 assert_eq!(n, 1);
1190 assert!(text.contains("write '7' failed"), "{text}");
1191 assert!(pv.state().write_error.is_some());
1192 }
1193
1194 #[test]
1195 fn a_panicking_listener_is_dropped_and_the_pv_goes_on() {
1196 let pvs = PvManager::new();
1197 let pv = pvs.subscribe("loc://listen_panic(1)");
1198 let listener: Listener = Arc::new(|st: &PvState| {
1199 if st.value.as_f64() == Some(2.0) {
1200 panic!("listener bug");
1201 }
1202 });
1203 pv.listen(&listener);
1204 pv.write("2");
1205 pv.write("3");
1206 assert_eq!(pv.state().value.as_f64(), Some(3.0));
1207 assert!(pv.0.shared.listeners.lock().is_empty());
1208 }
1209
1210 #[test]
1211 fn listen_drops_dead_listeners() {
1212 let pvs = PvManager::new();
1213 let pv = pvs.subscribe("loc://listen_prune(1)");
1214 for _ in 0..3 {
1215 let listener: Listener = Arc::new(|_: &PvState| {});
1216 pv.listen(&listener);
1217 }
1218 assert_eq!(pv.0.shared.listeners.lock().len(), 1);
1219 }
1220
1221 #[test]
1222 fn a_write_made_before_the_writer_is_sent_and_counted_once() {
1223 let shared = Shared::new(
1224 &PvName::parse("ca://early"),
1225 Arc::new(RwLock::new(None)),
1226 Arc::default(),
1227 );
1228 let put = |text: &str| Put {
1229 text: text.into(),
1230 long_string: false,
1231 };
1232 assert!(shared.send(None, put("1")).is_none());
1233 assert_eq!(shared.in_flight.load(Ordering::Acquire), 1);
1234 let (tx, mut rx) = write_queue();
1235 shared.set_writer(Writer::Channel(tx));
1236 assert_eq!(rx.try_recv().ok(), Some(put("1")));
1237 assert_eq!(shared.in_flight.load(Ordering::Acquire), 1);
1238 shared.write_result("1", None);
1239 assert_eq!(shared.in_flight.load(Ordering::Acquire), 0);
1240 }
1241
1242 #[test]
1243 fn a_sent_ca_put_is_no_longer_in_flight() {
1244 let pvs = PvManager::new();
1245 let (pv, mut rx) = queued(&pvs, true);
1246 let shared = &pv.0.shared;
1247 shared.write_result("0", Some("refused".into()));
1248 pv.write("1");
1249 assert_eq!(shared.in_flight.load(Ordering::Acquire), 1);
1250 let put = rx.try_recv().unwrap();
1251 shared.write_sent(&put.text, None);
1253 assert_eq!(shared.in_flight.load(Ordering::Acquire), 0);
1254 assert_eq!(pv.state().write_error.as_deref(), Some("refused"));
1256 }
1257
1258 #[test]
1259 fn writes_made_before_the_writer_is_sent_are_bounded() {
1260 let pvs = PvManager::new();
1261 let shared = Shared::new(
1262 &PvName::parse("ca://early_cap"),
1263 pvs.inner.notify.clone(),
1264 pvs.inner.ctl.clone(),
1265 );
1266 for i in 0..WRITE_QUEUE + 2 {
1267 let put = Put {
1268 text: i.to_string(),
1269 long_string: false,
1270 };
1271 if let Some(after) = shared.send(None, put) {
1272 shared.finish(after);
1273 }
1274 }
1275 let (n, text) = pvs.notice();
1276 assert_eq!(n, 2);
1277 assert!(text.contains("writes still waiting"), "{text}");
1278 assert_eq!(shared.in_flight.load(Ordering::Acquire), WRITE_QUEUE);
1279 let (tx, mut rx) = write_queue();
1280 shared.set_writer(Writer::Channel(tx));
1281 let mut got = 0;
1282 while rx.try_recv().is_ok() {
1283 got += 1;
1284 }
1285 assert_eq!(got, WRITE_QUEUE);
1286 }
1287
1288 #[test]
1289 fn unsupported_pvs_never_connect_and_refuse_writes() {
1290 let pvs = PvManager::new();
1291 let pv = pvs.subscribe("sys://time");
1292 let s = pv.state();
1293 assert!(!s.connected);
1294 assert!(s.error.is_some());
1295 pv.write("1");
1296 assert!(pvs.notice().1.contains("cannot be written"));
1297 }
1298}