exfiltrate 0.3.0

An embeddable debug tool for Rust.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Advertising this process in the runtime instance registry.
//!
//! See [`exfiltrate_internal::instances`] for what the registry is and why an
//! ephemeral listen port is unusable without it.
#![cfg(not(target_arch = "wasm32"))]

use exfiltrate_internal::instances::Instance;
use std::sync::atomic::{AtomicBool, Ordering};

/// Whether this process wrote an instance file that it should clean up.
static ADVERTISED: AtomicBool = AtomicBool::new(false);

/// Writes this process's instance file.
///
/// Failing to write is reported and then ignored: a read-only or missing runtime
/// directory should cost you discoverability, not the debug server.
pub(crate) fn advertise(name: &str, addr: &str) {
    // Only one process can hold a given address, so any other file claiming it
    // is a corpse. This matters more than it used to: a Unix socket left behind
    // by a crash is now recovered and rebound, which means the dead process's
    // file would otherwise keep listing — and keep looking reachable, because
    // the thing answering at that address is us.
    for stale in exfiltrate_internal::instances::read_all() {
        if stale.addr == addr && stale.pid != std::process::id() {
            exfiltrate_internal::instances::remove(stale.pid);
        }
    }
    let instance = Instance {
        pid: std::process::id(),
        name: name.to_string(),
        addr: addr.to_string(),
        started_at: now_rfc3339(),
    };
    match exfiltrate_internal::instances::write(&instance) {
        Ok(_) => {
            ADVERTISED.store(true, Ordering::Relaxed);
        }
        Err(error) => crate::diagnostic(&format!(
            "exfiltrate: could not advertise this instance in {}: {error}. \
             `exfiltrate instances` will not list it; the server itself is unaffected.",
            exfiltrate_internal::instances::directory().display()
        )),
    }
}

/// Removes this process's instance file, if it wrote one.
///
/// There is no portable "run this at exit" hook that survives `abort` or a
/// signal, so this is called from the paths that *do* know the process is
/// ending — notably `terminate` — and readers prune whatever is left behind.
pub(crate) fn withdraw() {
    if ADVERTISED.swap(false, Ordering::Relaxed) {
        exfiltrate_internal::instances::remove(std::process::id());
    }
}

/// The current time as `YYYY-MM-DDTHH:MM:SSZ`, or empty if the clock is broken.
fn now_rfc3339() -> String {
    let Ok(since_epoch) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) else {
        return String::new();
    };
    let seconds = since_epoch.as_secs() as i64;
    let days = seconds.div_euclid(86_400);
    let time_of_day = seconds.rem_euclid(86_400);
    let (year, month, day) = civil_from_days(days);
    format!(
        "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
        time_of_day / 3600,
        (time_of_day % 3600) / 60,
        time_of_day % 60
    )
}

fn civil_from_days(days: i64) -> (i64, u32, u32) {
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let day_of_era = z.rem_euclid(146_097);
    let year_of_era =
        (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
    let year = year_of_era + era * 400;
    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
    let shifted_month = (5 * day_of_year + 2) / 153;
    let day = (day_of_year - (153 * shifted_month + 2) / 5 + 1) as u32;
    let month = if shifted_month < 10 {
        shifted_month + 3
    } else {
        shifted_month - 9
    } as u32;
    (if month <= 2 { year + 1 } else { year }, month, day)
}

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

    #[test]
    fn the_timestamp_is_rfc_3339_shaped() {
        let stamp = now_rfc3339();
        assert_eq!(stamp.len(), 20, "{stamp}");
        assert!(stamp.ends_with('Z'), "{stamp}");
        assert_eq!(&stamp[4..5], "-", "{stamp}");
        assert_eq!(&stamp[10..11], "T", "{stamp}");
    }

    #[test]
    fn withdrawing_without_advertising_does_nothing() {
        // Specifically: it must not delete an instance file belonging to a real
        // server that happens to share this pid space.
        ADVERTISED.store(false, Ordering::Relaxed);
        withdraw();
        assert!(!ADVERTISED.load(Ordering::Relaxed));
    }
}