Skip to main content

sail/
notice.rs

1//! Process-wide delivery for actionable server notices.
2
3use std::sync::{Arc, Mutex};
4
5/// Dedup key for notices. At most one notice per kind is delivered per
6/// process: the notice is best-effort, while the structured API fields
7/// (warning headers, `SailboxInfo.deprecation`) carry the full picture for
8/// every affected request or sailbox.
9#[derive(Clone, Copy, PartialEq, Eq)]
10pub(crate) enum NoticeKind {
11    /// This client build is deprecated (server warning headers).
12    ClientVersion,
13    /// A Sailbox runtime should be upgraded before a deadline.
14    GuestDeprecation,
15}
16
17type NoticeHandler = Arc<dyn Fn(&str) + Send + Sync + 'static>;
18
19static HANDLER: Mutex<Option<NoticeHandler>> = Mutex::new(None);
20static SEEN: Mutex<Vec<NoticeKind>> = Mutex::new(Vec::new());
21
22/// Replace the process-wide callback used for actionable notices returned by
23/// the Sail API. Each kind of notice (deprecated client build, deprecated
24/// Sailbox runtime) is delivered at most once per process.
25pub fn set_notice_handler(callback: impl Fn(&str) + Send + Sync + 'static) {
26    // Bind the displaced handler so its captured state drops after the lock
27    // guard: a panicking Drop in caller state then cannot poison the mutex.
28    let _previous = HANDLER.lock().unwrap().replace(Arc::new(callback));
29}
30
31/// Remove the process-wide notice callback. While no handler is installed,
32/// notices are dropped without consuming their once-per-process delivery, so
33/// a later notice of the same kind can still be delivered once a handler is
34/// reinstalled. Lets a full-screen TUI silence stderr notices while it owns
35/// the terminal without losing them for the rest of the process.
36pub fn clear_notice_handler() {
37    let _previous = HANDLER.lock().unwrap().take();
38}
39
40pub(crate) fn notify(kind: NoticeKind, message: &str) {
41    let callback = HANDLER.lock().unwrap().clone();
42    let Some(callback) = callback else {
43        // Do not consume the notice before a binding has installed its sink.
44        return;
45    };
46    {
47        let mut seen = SEEN.lock().unwrap();
48        if seen.contains(&kind) {
49            return;
50        }
51        seen.push(kind);
52    }
53    callback(message);
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    // One test covers the whole lifecycle: the handler and seen list are
61    // process-wide, so parallel test functions would race on them.
62    #[test]
63    fn delivers_each_kind_at_most_once_and_holds_them_while_cleared() {
64        let delivered = Arc::new(Mutex::new(Vec::new()));
65        let sink = Arc::clone(&delivered);
66        let install = move || {
67            let sink = Arc::clone(&sink);
68            set_notice_handler(move |message| sink.lock().unwrap().push(message.to_string()));
69        };
70
71        // Without a handler the notice is dropped but not consumed.
72        notify(NoticeKind::GuestDeprecation, "too early");
73
74        install();
75        notify(NoticeKind::GuestDeprecation, "upgrade sb_1");
76        notify(NoticeKind::GuestDeprecation, "upgrade sb_2");
77
78        // Clearing holds notices un-consumed until a handler returns.
79        clear_notice_handler();
80        notify(NoticeKind::ClientVersion, "cli notice while cleared");
81        install();
82        notify(NoticeKind::ClientVersion, "cli notice after reinstall");
83        notify(NoticeKind::ClientVersion, "cli notice repeat");
84
85        assert_eq!(
86            *delivered.lock().unwrap(),
87            vec![
88                "upgrade sb_1".to_string(),
89                "cli notice after reinstall".to_string()
90            ]
91        );
92
93        // Displacing a handler whose captured state panics on drop must not
94        // poison the mutex: the old handler drops outside the lock guard.
95        struct PanicOnDrop;
96        impl Drop for PanicOnDrop {
97            fn drop(&mut self) {
98                panic!("panic in dropped handler state");
99            }
100        }
101        let bomb = PanicOnDrop;
102        set_notice_handler(move |_| {
103            let _ = &bomb;
104        });
105        assert!(
106            std::panic::catch_unwind(|| set_notice_handler(|_| {})).is_err(),
107            "displacing the bomb handler should panic outside the lock"
108        );
109        install();
110    }
111}