sail-rs 0.4.2

Official Rust SDK for Sail: create and drive sailboxes (sandboxed cloud VMs) with lifecycle, streaming exec, file transfer, and ingress.
Documentation
//! Process-wide delivery for actionable server notices.

use std::sync::{Arc, Mutex};

/// Dedup key for notices. At most one notice per kind is delivered per
/// process: the notice is best-effort, while the structured API fields
/// (warning headers, `SailboxInfo.deprecation`) carry the full picture for
/// every affected request or sailbox.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum NoticeKind {
    /// This client build is deprecated (server warning headers).
    ClientVersion,
    /// A Sailbox runtime should be upgraded before a deadline.
    GuestDeprecation,
}

type NoticeHandler = Arc<dyn Fn(&str) + Send + Sync + 'static>;

static HANDLER: Mutex<Option<NoticeHandler>> = Mutex::new(None);
static SEEN: Mutex<Vec<NoticeKind>> = Mutex::new(Vec::new());

/// Replace the process-wide callback used for actionable notices returned by
/// the Sail API. Each kind of notice (deprecated client build, deprecated
/// Sailbox runtime) is delivered at most once per process.
pub fn set_notice_handler(callback: impl Fn(&str) + Send + Sync + 'static) {
    // Bind the displaced handler so its captured state drops after the lock
    // guard: a panicking Drop in caller state then cannot poison the mutex.
    let _previous = HANDLER.lock().unwrap().replace(Arc::new(callback));
}

/// Remove the process-wide notice callback. While no handler is installed,
/// notices are dropped without consuming their once-per-process delivery, so
/// a later notice of the same kind can still be delivered once a handler is
/// reinstalled. Lets a full-screen TUI silence stderr notices while it owns
/// the terminal without losing them for the rest of the process.
pub fn clear_notice_handler() {
    let _previous = HANDLER.lock().unwrap().take();
}

pub(crate) fn notify(kind: NoticeKind, message: &str) {
    let callback = HANDLER.lock().unwrap().clone();
    let Some(callback) = callback else {
        // Do not consume the notice before a binding has installed its sink.
        return;
    };
    {
        let mut seen = SEEN.lock().unwrap();
        if seen.contains(&kind) {
            return;
        }
        seen.push(kind);
    }
    callback(message);
}

#[cfg(test)]
mod tests {
    use super::*;

    // One test covers the whole lifecycle: the handler and seen list are
    // process-wide, so parallel test functions would race on them.
    #[test]
    fn delivers_each_kind_at_most_once_and_holds_them_while_cleared() {
        let delivered = Arc::new(Mutex::new(Vec::new()));
        let sink = Arc::clone(&delivered);
        let install = move || {
            let sink = Arc::clone(&sink);
            set_notice_handler(move |message| sink.lock().unwrap().push(message.to_string()));
        };

        // Without a handler the notice is dropped but not consumed.
        notify(NoticeKind::GuestDeprecation, "too early");

        install();
        notify(NoticeKind::GuestDeprecation, "upgrade sb_1");
        notify(NoticeKind::GuestDeprecation, "upgrade sb_2");

        // Clearing holds notices un-consumed until a handler returns.
        clear_notice_handler();
        notify(NoticeKind::ClientVersion, "cli notice while cleared");
        install();
        notify(NoticeKind::ClientVersion, "cli notice after reinstall");
        notify(NoticeKind::ClientVersion, "cli notice repeat");

        assert_eq!(
            *delivered.lock().unwrap(),
            vec![
                "upgrade sb_1".to_string(),
                "cli notice after reinstall".to_string()
            ]
        );

        // Displacing a handler whose captured state panics on drop must not
        // poison the mutex: the old handler drops outside the lock guard.
        struct PanicOnDrop;
        impl Drop for PanicOnDrop {
            fn drop(&mut self) {
                panic!("panic in dropped handler state");
            }
        }
        let bomb = PanicOnDrop;
        set_notice_handler(move |_| {
            let _ = &bomb;
        });
        assert!(
            std::panic::catch_unwind(|| set_notice_handler(|_| {})).is_err(),
            "displacing the bomb handler should panic outside the lock"
        );
        install();
    }
}