hayson-engine 0.14.0

Storage engine for hayson document databases — the embeddable core behind hayson-server, hayson-cli and hayson-browser
Documentation
//! Everything about the lock that only some platforms can answer. Each function returns `None`
//! for "this platform cannot tell", and a caller must read that as "not provably dead" — never as
//! permission to act.
//!
//! On `wasm32-wasip1` there is no pid to ask for — `std::process::id()` panics rather than failing
//! — so `pid` and `hostname` answer from the WASI environment instead, out of the two variables a
//! host sets at instantiation to name the process it is running this module inside. A host that
//! sets neither leaves the lock anonymous, exactly as before. `process_alive` stays `None` there
//! whatever the host said: WASI has no way to ask after a process, so a lock is only ever reclaimed
//! by its lease expiring.

#[cfg(unix)]
pub fn hostname() -> Option<String> {
    let mut buf = [0u8; 256];
    // SAFETY: gethostname writes at most buf.len() bytes into a buffer we own for the whole call,
    // and the result is only read up to the first NUL (or the buffer end, if it wrote no NUL).
    let result = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
    if result != 0 {
        return None;
    }
    let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
    Some(String::from_utf8_lossy(&buf[..end]).into_owned())
}

/// Names the pid of the process the module runs inside. Decimal, and read once.
#[cfg(target_os = "wasi")]
pub const HOST_PID_VAR: &str = "HAYSON_HOST_PID";

/// Names that process's host, in the spelling `gethostname` gives — otherwise a native reader on
/// the same machine reads the lock as belonging to somewhere else and never reclaims it.
#[cfg(target_os = "wasi")]
pub const HOST_NAME_VAR: &str = "HAYSON_HOST_NAME";

#[cfg(target_os = "wasi")]
fn from_env(name: &str) -> Option<String> {
    std::env::var(name).ok().filter(|value| !value.is_empty())
}

#[cfg(target_os = "wasi")]
pub fn hostname() -> Option<String> {
    static HOST: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
    HOST.get_or_init(|| from_env(HOST_NAME_VAR)).clone()
}

#[cfg(all(not(unix), not(target_os = "wasi")))]
pub fn hostname() -> Option<String> {
    None
}

#[cfg(unix)]
pub fn pid() -> Option<u32> {
    Some(std::process::id())
}

/// The WASI environment is fixed when the host instantiates the module, so this is stable for the
/// whole life of the instance and every handle opened through it reports the same process.
#[cfg(target_os = "wasi")]
pub fn pid() -> Option<u32> {
    static PID: std::sync::OnceLock<Option<u32>> = std::sync::OnceLock::new();
    *PID.get_or_init(|| from_env(HOST_PID_VAR).and_then(|raw| raw.parse().ok()))
}

// NOTE: std::process::id() PANICS on wasm32-wasip1 ("no pids on this platform") — never call it.
#[cfg(all(not(unix), not(target_os = "wasi")))]
pub fn pid() -> Option<u32> {
    None
}

#[cfg(unix)]
pub fn process_alive(pid: u32) -> Option<bool> {
    #[cfg(test)]
    if !tests::accelerator_enabled() {
        return None;
    }
    // SAFETY: signal 0 performs the permission and existence checks without delivering anything.
    Some(unsafe { libc::kill(pid as libc::pid_t, 0) == 0 })
}

#[cfg(not(unix))]
pub fn process_alive(_pid: u32) -> Option<bool> {
    None
}

#[cfg(test)]
pub mod tests {
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};

    static ENABLED: AtomicBool = AtomicBool::new(true);
    /// The switch below is process-wide, so a test that turns it off must not overlap a test that
    /// counts on it being on: one takes the write side, the others the read side.
    static SWITCH: RwLock<()> = RwLock::new(());

    pub(super) fn accelerator_enabled() -> bool {
        ENABLED.load(Ordering::SeqCst)
    }

    pub struct WithoutPidLiveness(#[allow(dead_code)] RwLockWriteGuard<'static, ()>);

    impl Drop for WithoutPidLiveness {
        fn drop(&mut self) {
            ENABLED.store(true, Ordering::SeqCst);
        }
    }

    /// Turns the pid-liveness accelerator off for as long as the returned guard lives, so a test
    /// can exercise the lease-only path every platform without pids is limited to.
    pub fn without_pid_liveness() -> WithoutPidLiveness {
        let guard = SWITCH.write().unwrap_or_else(PoisonError::into_inner);
        ENABLED.store(false, Ordering::SeqCst);
        WithoutPidLiveness(guard)
    }

    /// Keeps the accelerator on for as long as the returned guard lives — for the tests that are
    /// about the accelerator itself and would otherwise be broken by a parallel [`without_pid_liveness`].
    pub fn with_pid_liveness() -> RwLockReadGuard<'static, ()> {
        SWITCH.read().unwrap_or_else(PoisonError::into_inner)
    }
}