Skip to main content

leenfetch_core/modules/linux/info/
os_age.rs

1use std::fmt::Write;
2use std::fs;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use crate::modules::enums::OsAgeShorthand;
6
7/// Returns the OS "age" (time since root FS creation/install) formatted per shorthand.
8/// Mirrors the style of your `get_uptime` function.
9pub fn get_os_age(shorthand: OsAgeShorthand) -> Option<String> {
10    let install_epoch = read_install_epoch_seconds()?;
11    let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
12
13    // Guard against clock skew or unknown/invalid install time
14    let seconds = now.saturating_sub(install_epoch);
15
16    Some(format_age(seconds, shorthand))
17}
18
19/// Best-effort detection of install time (epoch seconds) of the root filesystem.
20/// Strategy:
21/// 1) Try `metadata("/").created()` (when supported by platform/fs)
22/// 2) Fallback to `libc::stat` syscalls for birth time
23fn read_install_epoch_seconds() -> Option<u64> {
24    // Try std first (portable when supported)
25    if let Ok(md) = fs::metadata("/") {
26        if let Ok(created) = md.created() {
27            if let Ok(dur) = created.duration_since(UNIX_EPOCH) {
28                let secs = dur.as_secs();
29                if secs > 0 {
30                    return Some(secs);
31                }
32            }
33        }
34    }
35
36    // Fallback: Use libc::stat to get st_birthtime (Linux specific)
37    // Try to get st_ctime as fallback (inode change time, close to install time)
38    let root_cstr = std::ffi::CString::new("/").ok()?;
39    let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
40
41    if unsafe { libc::stat(root_cstr.as_ptr(), &mut stat_buf) } == 0 {
42        // Try st_birthtime if available (BSD/macOS, some Linux with new glibc)
43        #[cfg(any(target_os = "freebsd", target_os = "macos"))]
44        {
45            let birth = stat_buf.st_birthtime;
46            if birth > 0 {
47                return Some(birth as u64);
48            }
49        }
50
51        // Fallback: use st_ctime (inode change time - often close to install)
52        // This is not ideal but better than spawning a process
53        let ctime = stat_buf.st_ctime as u64;
54        if ctime > 0 {
55            // Only use if it seems reasonable (at least 1 day old)
56            let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
57            if now > ctime && (now - ctime) > 86400 {
58                return Some(ctime);
59            }
60        }
61    }
62
63    None
64}
65
66fn format_age(seconds: u64, shorthand: OsAgeShorthand) -> String {
67    let days = seconds / 86_400;
68    let hours = (seconds / 3_600) % 24;
69    let minutes = (seconds / 60) % 60;
70
71    let mut buf = String::with_capacity(32);
72
73    match shorthand {
74        OsAgeShorthand::Full => {
75            if days > 0 {
76                let _ = write!(buf, "{} day{}, ", days, if days != 1 { "s" } else { "" });
77            }
78            if hours > 0 {
79                let _ = write!(buf, "{} hour{}, ", hours, if hours != 1 { "s" } else { "" });
80            }
81            if minutes > 0 {
82                let _ = write!(
83                    buf,
84                    "{} minute{}",
85                    minutes,
86                    if minutes != 1 { "s" } else { "" }
87                );
88            }
89            if buf.is_empty() {
90                let _ = write!(buf, "{} seconds", seconds);
91            }
92        }
93        OsAgeShorthand::Tiny => {
94            if days > 0 {
95                let _ = write!(buf, "{} days", days);
96            }
97        }
98        OsAgeShorthand::Seconds => {
99            let _ = write!(buf, "{}s", seconds);
100        }
101    }
102
103    buf.trim_end_matches([' ', ','].as_ref()).to_string()
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn formats_full_age() {
112        let result = format_age(86_400 * 2 + 3_600 + 120, OsAgeShorthand::Full);
113        assert!(
114            result.contains("2 days") && result.contains("1 hour") && result.contains("2 minutes"),
115            "unexpected format: {result}"
116        );
117    }
118
119    #[test]
120    fn formats_tiny_age() {
121        let result = format_age(86_400 * 5 + 300, OsAgeShorthand::Tiny);
122        assert_eq!(result, "5 days");
123    }
124
125    #[test]
126    fn formats_seconds_age() {
127        let result = format_age(42, OsAgeShorthand::Seconds);
128        assert_eq!(result, "42s");
129    }
130}