exfiltrate 0.4.0

An embeddable debug tool for Rust.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Implements the `uptime` command.
use crate::command::{Command, Response};
use std::sync::LazyLock;
use wasm_lite_std::time::{Instant, SystemTime, UNIX_EPOCH};

/// When `exfiltrate::begin` ran.
///
/// A monotonic clock rather than the wall clock, because the question this
/// command answers — has the program made progress, or is it wedged — must not
/// be affected by an NTP step or a laptop waking up.
static STARTED: LazyLock<(Instant, SystemTime)> =
    LazyLock::new(|| (Instant::now(), SystemTime::now()));

/// Records the start instant. Called from `begin_with` so the clock starts with
/// the server rather than with the first `uptime` call.
pub(crate) fn mark_start() {
    LazyLock::force(&STARTED);
}

/// The `uptime` command.
pub(crate) struct Uptime;

impl Command for Uptime {
    fn name(&self) -> &'static str {
        "uptime"
    }

    fn short_description(&self) -> &'static str {
        "Reports how long the program has been running and what time it thinks it is.  Use this to tell a hung program from a slow one."
    }

    fn full_description(&self) -> &'static str {
        "Reports elapsed time since `exfiltrate::begin` and the current wall clock.

Elapsed time comes from a monotonic clock, so it is unaffected by clock steps or by
the machine sleeping. Comparing the wall clock reported here against your own is how
you notice that a remote or browser target disagrees with you about what time it is.

The most common use is telling a hung program from a slow one: run it twice."
    }

    fn execute(&self, _args: Vec<String>) -> Result<Response, Response> {
        let (started_monotonic, started_wall) = *STARTED;
        let elapsed = started_monotonic.elapsed();
        let now = SystemTime::now();

        let mut out = String::new();
        out.push_str(&format!("uptime:      {}\n", format_duration(elapsed)));
        out.push_str(&format!(
            "started:     {}\n",
            format_unix(started_wall.duration_since(UNIX_EPOCH).ok())
        ));
        out.push_str(&format!(
            "wall clock:  {}\n",
            format_unix(now.duration_since(UNIX_EPOCH).ok())
        ));
        out.push_str(&format!("monotonic:   {:?}\n", elapsed));
        Ok(out.into())
    }
}

/// Formats a duration the way a human reads an uptime, not the way `Debug` does.
fn format_duration(duration: std::time::Duration) -> String {
    let total = duration.as_secs();
    let (days, hours, minutes, seconds) = (
        total / 86_400,
        (total % 86_400) / 3600,
        (total % 3600) / 60,
        total % 60,
    );
    if days > 0 {
        format!("{days}d {hours:02}h {minutes:02}m {seconds:02}s")
    } else if hours > 0 {
        format!("{hours}h {minutes:02}m {seconds:02}s")
    } else if minutes > 0 {
        format!("{minutes}m {seconds:02}s")
    } else {
        format!("{}.{:03}s", seconds, duration.subsec_millis())
    }
}

/// Formats seconds-since-the-epoch as `YYYY-MM-DDTHH:MM:SSZ`.
///
/// `None` means the clock is before the epoch, which a browser can genuinely
/// report; saying "unknown" is better than printing a nonsense date.
fn format_unix(since_epoch: Option<std::time::Duration>) -> String {
    let Some(since_epoch) = since_epoch else {
        return "unknown".to_string();
    };
    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
    )
}

/// Hinnant's `civil_from_days`: days since the Unix epoch to a Gregorian date.
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::*;
    use std::time::Duration;

    #[test]
    fn durations_read_the_way_an_uptime_reads() {
        assert_eq!(format_duration(Duration::from_millis(1500)), "1.500s");
        assert_eq!(format_duration(Duration::from_secs(65)), "1m 05s");
        assert_eq!(format_duration(Duration::from_secs(3725)), "1h 02m 05s");
        assert_eq!(
            format_duration(Duration::from_secs(90_061)),
            "1d 01h 01m 01s"
        );
    }

    #[test]
    fn the_epoch_and_a_known_date_both_format_correctly() {
        assert_eq!(
            format_unix(Some(Duration::from_secs(0))),
            "1970-01-01T00:00:00Z"
        );
        assert_eq!(
            format_unix(Some(Duration::from_secs(1_787_056_549))),
            "2026-08-18T12:35:49Z"
        );
        // A leap day, which is where a hand-rolled calendar goes wrong.
        assert_eq!(
            format_unix(Some(Duration::from_secs(1_709_164_800))),
            "2024-02-29T00:00:00Z"
        );
    }

    #[test]
    fn a_clock_before_the_epoch_says_unknown_rather_than_guessing() {
        assert_eq!(format_unix(None), "unknown");
    }

    #[test]
    fn the_command_reports_all_three_clocks() {
        let text = Uptime.execute(Vec::new()).unwrap().into_string();
        assert!(text.contains("uptime:"), "{text}");
        assert!(text.contains("started:"), "{text}");
        assert!(text.contains("wall clock:"), "{text}");
    }
}