Skip to main content

pv/
lib.rs

1//! PV access for bobview.
2//!
3//! Widgets ask a [`PvManager`] for a [`Pv`] by name and read its latest
4//! [`PvState`] each frame. The manager owns a tokio runtime on which one task
5//! per PV talks to the backend chosen by the name's prefix:
6//!
7//! | prefix    | backend                                 |
8//! |-----------|-----------------------------------------|
9//! | `ca://`   | Channel Access (the default)            |
10//! | `pva://`  | PV Access                               |
11//! | `loc://`  | local PVs held in this process          |
12//! | `sim://`  | simulated signals (sine, ramp, noise …) |
13//! | `=`, `eq://` | formulas over other PVs              |
14//!
15//! In a browser (wasm32) there is no runtime: `sim://` runs on the page's
16//! event loop, and `ca://` and `pva://` PVs come over a PV Web Socket
17//! ([`set_pvws`]).
18
19#[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
55/// Called whenever any PV changes, so a UI can schedule a repaint.
56pub type Notifier = Arc<dyn Fn() + Send + Sync>;
57
58/// A notifier that PVs are tagged with as they are subscribed through a
59/// [`PvManager::tagged`] handle, called only when one of them changes.
60struct Tag(Notifier);
61
62/// Called with a PV's new state on each change, from the thread making it,
63/// for as long as the caller keeps the `Arc` (see [`Pv::listen`]).
64pub type Listener = Arc<ListenFn>;
65pub type ListenFn = dyn Fn(&PvState) + Send + Sync;
66
67/// A write of text to a PV.
68#[derive(Clone, Debug, PartialEq)]
69pub(crate) struct Put {
70    pub text: String,
71    /// Write the text into an array of numbers as a long string (see
72    /// [`Pv::write_long_string`]).
73    pub long_string: bool,
74}
75
76/// Where writes to a PV go.
77pub(crate) enum Writer {
78    /// To the backend task, which converts and sends them.
79    #[cfg(not(target_arch = "wasm32"))]
80    Channel(mpsc::Sender<Put>),
81    /// Straight into the local value.
82    Local,
83    /// Over the PV Web Socket, which converts them.
84    #[cfg(target_arch = "wasm32")]
85    Pvws(String),
86}
87
88/// Settings and messages shared by every PV of one [`PvManager`].
89#[derive(Default)]
90pub(crate) struct Control {
91    /// Refuse every write except to `loc://` PVs (which only live here).
92    read_only: AtomicBool,
93    /// The latest message for the user (a refused or failed write), with a
94    /// number that changes with each one.
95    notice: Mutex<(u64, String)>,
96    /// The UI's repaint callback (see [`PvManager::set_notifier`]), so a
97    /// new notice is shown without waiting for some PV to change.
98    repaint: Arc<RwLock<Option<Notifier>>>,
99    /// Every tag made, called on a new notice too: each window shows it.
100    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
119/// The tags in `tags` still held, the others dropped from it.
120fn 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
126/// Writes to one PV that may wait for the backend at once; more are
127/// refused, rather than queued to be applied late. Writes made before the
128/// backend task starts count toward it too.
129pub(crate) const WRITE_QUEUE: usize = 16;
130
131/// The queue of a backend's writes, for [`Writer::Channel`].
132#[cfg(not(target_arch = "wasm32"))]
133pub(crate) fn write_queue() -> (mpsc::Sender<Put>, mpsc::Receiver<Put>) {
134    mpsc::channel(WRITE_QUEUE)
135}
136
137/// A write's last step, taken once the writer lock is released.
138enum After {
139    /// The backend task has stopped: the write is lost.
140    #[cfg(not(target_arch = "wasm32"))]
141    Stopped(Put),
142    /// The PV connected once and has since dropped: refused at once.
143    #[cfg(not(target_arch = "wasm32"))]
144    Dropped(Put),
145    /// [`WRITE_QUEUE`] writes are already waiting: refused.
146    Full(Put),
147    /// Set a local PV's value.
148    Local(Put),
149    /// The PV takes no writes.
150    Refused,
151}
152
153thread_local! {
154    /// How many PVs' listeners this thread is calling, under their locks.
155    static CALLING: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
156    /// Work a listener left for when this thread holds no PV's lock.
157    static DEFERRED: std::cell::RefCell<std::collections::VecDeque<Box<dyn FnOnce()>>> =
158        const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
159    /// Whether this thread is running that work, and how many times it has
160    /// begun to (see [`drain_number`]).
161    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
165/// Call listeners in `f`, under a PV's lock; [`after_listeners`] once the
166/// lock is let go.
167fn 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
179/// Run the work listeners left, once this thread holds no PV's lock. Work
180/// that leaves more (and updates within it) joins this loop rather than
181/// nesting, so the stack stays flat however long a chain of formulas is.
182fn 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    // One at a time, so a panic leaves the rest for the next time.
196    while let Some(work) = DEFERRED.with(|d| d.borrow_mut().pop_front()) {
197        work();
198    }
199}
200
201/// Which run of left work this thread is in: work it leaves for itself
202/// (one formula evaluating another) stays in the same one.
203pub(crate) fn drain_number() -> u64 {
204    DRAINS.with(|n| n.get())
205}
206
207/// Run `work` once this thread holds no PV's lock: now, unless it is in a
208/// listener. Work that locks a PV (subscribing, listening, publishing)
209/// could otherwise wait for one this thread holds, or take two in another
210/// thread's order.
211pub(crate) fn outside_listeners(work: Box<dyn FnOnce()>) {
212    DEFERRED.with(|d| d.borrow_mut().push_back(work));
213    after_listeners();
214}
215
216/// State shared between a [`Pv`] handle and the task serving it.
217pub(crate) struct Shared {
218    ctl: Arc<Control>,
219    name: String,
220    state: Mutex<PvState>,
221    /// How many times the state has changed.
222    version: AtomicU64,
223    writer: Mutex<Option<Writer>>,
224    /// Writes made before the backend task set the writer, sent once it does.
225    pending: Mutex<Vec<Put>>,
226    /// Whether a backend task sets a writer (`ca://`, `pva://`), so writes
227    /// made before then wait for it.
228    backend_writes: bool,
229    /// Writes accepted but not yet answered by [`Shared::write_result`]: the
230    /// task serving the PV outlives its last [`Pv`] until they are.
231    #[cfg(not(target_arch = "wasm32"))]
232    in_flight: AtomicUsize,
233    notify: Arc<RwLock<Option<Notifier>>>,
234    /// The tags of the handles it was subscribed through, called after each
235    /// change while they are held.
236    tags: Mutex<Vec<Weak<Tag>>>,
237    listeners: Mutex<Vec<Weak<ListenFn>>>,
238    /// Set (under the state lock) once the PV has first connected.
239    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    /// Change the state and tell the UI about it.
262    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            // Under the state lock, so listeners see changes in order.
271            // A listener that panics is dropped, so it cannot stop the PV's
272            // task (or the UI thread writing a local PV) for good. Release
273            // builds abort on any panic instead (`panic = "abort"`).
274            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    /// Call `tag`'s notifier after each change, until it is dropped.
299    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    /// Whether the PV connected once and has since lost its connection;
311    /// not one that has yet to connect.
312    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    /// A put failed (or worked, with `None`): remember why, and tell the user.
322    #[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    /// A write counted in flight is done with: sent, failed or refused.
333    #[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    /// A put that the server does not answer was sent, or failed to be: a
350    /// failure is remembered as [`Shared::write_result`] does, but a send
351    /// confirms nothing, so it leaves an earlier failure shown.
352    #[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    /// Whether a write taken from the queue can go now; if not, say why.
362    /// One to a PV that has yet to connect goes, for the backend to wait
363    /// for or fail.
364    #[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    /// Whether it is connected, without copying its state.
374    #[cfg(not(target_arch = "wasm32"))]
375    pub(crate) fn connected(&self) -> bool {
376        self.state.lock().connected
377    }
378
379    /// Set where writes go, and send any made before now.
380    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        // Sent before the lock is released, so later writes follow them.
385        let mut failed: Vec<(Put, &str)> = Vec::new();
386        for put in pending {
387            match slot.as_ref() {
388                // Already counted in flight, so a failure is answered
389                // through `write_result`, which uncounts it.
390                #[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                // A backend sets only a channel (or the socket).
405                _ => 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    /// Send a new write through `writer`; with none yet, a `ca://` or
419    /// `pva://` PV (whose task is still starting) keeps it until there is
420    /// one. A write is counted in flight only once queued. Returns what is
421    /// left to do once the writer lock is released, so that it and the
422    /// state lock never nest.
423    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                // Counted before the task can answer it, and uncounted if
428                // it was not queued; a stopped task's write is answered (and
429                // uncounted) by `finish`.
430                self.in_flight.fetch_add(1, Ordering::AcqRel);
431                // The task serving the PV holds the receiver until it stops.
432                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                // Each write is a new sample, though its value be the same.
478                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/// What serves a PV: stopped when the last [`Pv`] is dropped.
496#[cfg(not(target_arch = "wasm32"))]
497type Task = tokio::task::JoinHandle<()>;
498#[cfg(target_arch = "wasm32")]
499type Task = pvws::Task;
500
501/// How long the task of a dropped PV may go on to send writes made on it,
502/// as a write to a PV that is not connected yet must first find it.
503#[cfg(not(target_arch = "wasm32"))]
504const WRITE_GRACE: std::time::Duration = std::time::Duration::from_secs(10);
505
506/// One subscription; the backend task stops when the last [`Pv`] is dropped,
507/// or once the writes made on it are sent.
508struct Entry {
509    name: PvName,
510    shared: Arc<Shared>,
511    task: Option<Task>,
512    /// The runtime the task runs on, to finish its writes after the drop.
513    #[cfg(not(target_arch = "wasm32"))]
514    rt: tokio::runtime::Handle,
515    /// A formula's inputs, followed for as long as it is held.
516    #[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        // A write is often made on a PV subscribed just for it and dropped
526        // straight after: let the task send it (or give up) first.
527        #[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/// A handle to a live PV. Cheap to clone.
545#[derive(Clone)]
546pub struct Pv(Arc<Entry>);
547
548impl Pv {
549    pub fn name(&self) -> &PvName {
550        &self.0.name
551    }
552
553    /// Whether the PV connected once and has since lost its connection. A
554    /// write to it is refused at once; one to a PV that has yet to connect
555    /// is queued.
556    pub fn dropped(&self) -> bool {
557        self.0.shared.dropped()
558    }
559
560    /// Mark the PV connected or not, as its backend would; for tests of
561    /// code that uses PVs.
562    #[doc(hidden)]
563    pub fn set_connected_for_test(&self, connected: bool) {
564        self.0.shared.update(|s| s.connected = connected);
565    }
566
567    /// The latest value, alarm and metadata.
568    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    /// Whether it is connected, without copying its state.
578    pub fn connected(&self) -> bool {
579        self.0.shared.state.lock().connected
580    }
581
582    /// A number that changes whenever its state does (bar read-only mode),
583    /// so a caller can skip work on a PV that has not changed.
584    pub fn version(&self) -> u64 {
585        self.0.shared.version.load(Ordering::Relaxed)
586    }
587
588    /// Whether read-only mode (see [`PvManager::set_read_only`]) stops writes
589    /// to this PV: all but local ones, which cannot reach a control system.
590    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    /// Call `listener` with the current state now and with each change
597    /// after, until the caller drops it: unlike [`Pv::state`] read once a
598    /// frame, this sees every monitor update.
599    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            // Drop dead ones here too: a PV that never changes is never updated.
606            listeners.retain(|l| l.strong_count() > 0);
607            listeners.push(Arc::downgrade(listener));
608        }
609        after_listeners();
610    }
611
612    /// Write a value given as text; the backend converts it to the PV's type.
613    pub fn write(&self, value: impl Into<String>) {
614        self.put(value.into(), false);
615    }
616
617    /// Write text as Phoebus does for a widget whose `format` is string: into
618    /// a char array as a long string (see [`long_string_bytes`]). Any other
619    /// PV is written as by [`Pv::write`].
620    ///
621    /// Over CA, as in Phoebus, a char array is a `DBF_CHAR` channel of more
622    /// than one element, and the text is cut to that element count with its
623    /// NUL kept. Over PVA and `loc://` it is any array of numbers. A PVA
624    /// array has no fixed element count to cut to, so an IOC that stores
625    /// only its first NELM elements may lose the NUL (and part of a
626    /// multi-byte character), as it does with Phoebus.
627    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        // Read first: the state and writer locks never nest.
641        #[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            // A write to a PV that has gone would wait for it to come back,
650            // and land late, perhaps long after the user moved on. One that
651            // has not connected yet is queued: it was most likely just
652            // subscribed, and the backend waits a while for it.
653            #[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/// Creates and shares PV subscriptions.
665#[derive(Clone)]
666pub struct PvManager {
667    inner: Arc<Inner>,
668    /// What the PVs subscribed through this handle are tagged with (see
669    /// [`PvManager::tagged`]).
670    tag: Option<Arc<Tag>>,
671}
672
673struct Inner {
674    /// Always `Some` until drop, which shuts it down without blocking.
675    #[cfg(not(target_arch = "wasm32"))]
676    rt: Option<tokio::runtime::Runtime>,
677    entries: Mutex<Entries>,
678    locals: Mutex<HashMap<String, Arc<Shared>>>,
679    // Shared with the PV tasks, which must not hold `Inner` itself: the last
680    // reference could then be dropped on a runtime thread.
681    #[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/// Every subscription by key, including dropped ones until the next prune.
699#[derive(Default)]
700struct Entries {
701    map: HashMap<String, Weak<Entry>>,
702    /// Prune dropped entries once the map is this big, so a batch of
703    /// subscriptions doesn't scan the map for each one.
704    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    /// Register a callback run after every PV change.
762    pub fn set_notifier(&self, notifier: Notifier) {
763        *self.inner.notify.write() = Some(notifier);
764    }
765
766    /// A handle to the same PVs that tags each PV subscribed through it with
767    /// `notifier`, called after each change of those PVs (and each new
768    /// notice) for as long as this handle or a clone of it is held. A UI
769    /// window gives its display one, to repaint only the windows showing a
770    /// PV that changed.
771    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    /// A handle with this one's notifier under a new tag: PVs subscribed
784    /// through it are tagged afresh, and those subscribed only through this
785    /// one stop calling the notifier once it and its clones are dropped (a
786    /// window showing another display).
787    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    /// Refuse (or allow again) writes to every PV but `loc://` ones; PVs then
795    /// report themselves not writable, so controls are disabled.
796    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    /// The latest message for the user about a refused or failed write, and a
805    /// number that changes with each new one (starting at 0 for none).
806    pub fn notice(&self) -> (u64, String) {
807        self.inner.ctl.notice.lock().clone()
808    }
809
810    /// The tokio runtime the PV tasks run on.
811    #[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    /// Get (or start) a subscription for `name`, e.g. `ca://X`, `sim://sine` or plain `X`.
817    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        // A macro left unexpanded makes a name that is not what was meant,
829        // and a local one would share its state with every other such PV.
830        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                // New, so no one is listening or drawing it yet.
846                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        // Its inputs are subscribed to with the entries unlocked.
865        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        // A CA put with no reply was only sent, not confirmed.
1009        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        // No backend task, so nothing else changes the PV's state.
1018        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        // One PV that both show, subscribed through each.
1052        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        // b shows another display: the PVs it showed no longer call it.
1061        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        // A new notice is shown in every window.
1071        pvs.subscribe("sys://time").write("1");
1072        assert_eq!(calls(), (5, 4));
1073        // An untagged handle tags nothing.
1074        let plain = pvs.subscribe("loc://tag_plain(0)");
1075        plain.write("1");
1076        assert_eq!(calls(), (5, 4));
1077    }
1078
1079    /// A PV whose writes go to a queue no backend reads.
1080    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        // Refused, so not counted as in flight: a dropped handle need not
1108        // wait for it.
1109        assert_eq!(pv.0.shared.in_flight.load(Ordering::Acquire), 0);
1110        // And one that disconnects while a write waits reports it.
1111        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        // The backend goes ahead with them, to wait for the PV or fail.
1128        assert!(pv.0.shared.can_write("1"));
1129        assert_eq!(pv.state().write_error, None);
1130        // Still bounded.
1131        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        // Only those queued are in flight.
1154        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        // Other writes are unchanged.
1172        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        // As the CA task answers a plain write that was sent.
1252        shared.write_sent(&put.text, None);
1253        assert_eq!(shared.in_flight.load(Ordering::Acquire), 0);
1254        // Sent is not confirmed: the earlier failure still shows.
1255        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}