haematite-cli 0.21.9

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use std::io;
use std::sync::{
    Arc, Condvar, Mutex,
    atomic::{AtomicUsize, Ordering},
};

pub(super) struct Clients {
    pub(super) count: AtomicUsize,
    changed: Mutex<()>,
    idle: Condvar,
}
impl Clients {
    pub(super) const fn new(active: usize) -> Self {
        Self {
            count: AtomicUsize::new(active),
            changed: Mutex::new(()),
            idle: Condvar::new(),
        }
    }
    pub(super) fn drain(&self) -> io::Result<()> {
        let mut guard = self
            .changed
            .lock()
            .map_err(|error| io::Error::other(format!("client_drain_failed: {error}")))?;
        let count = self.count.load(Ordering::Acquire);
        if count > 0 {
            eprintln!("serve_draining_clients: {count} before stdin shutdown");
        }
        while self.count.load(Ordering::Acquire) != 0 {
            guard = self
                .idle
                .wait(guard)
                .map_err(|error| io::Error::other(format!("client_drain_failed: {error}")))?;
        }
        drop(guard);
        Ok(())
    }
}

pub(super) struct Permit(pub(super) Arc<Clients>);
impl Drop for Permit {
    fn drop(&mut self) {
        let guard = match self.0.changed.lock() {
            Ok(guard) => guard,
            Err(poisoned) => {
                eprintln!("client_drain_failed: {poisoned}");
                // Even failed clients must release their counted admission.
                poisoned.into_inner()
            }
        };
        self.0.count.fetch_sub(1, Ordering::AcqRel);
        drop(guard);
        self.0.idle.notify_all();
    }
}