Skip to main content

journal_host/
lib.rs

1//! Optional local-host identity helpers for journal writers.
2//!
3//! This crate is intentionally separate from the core file-format writer. It
4//! probes the local host only when callers explicitly opt in and then pass the
5//! returned values to the writer themselves.
6
7#[cfg(any(target_os = "freebsd", windows, test))]
8mod state;
9
10#[cfg(target_os = "linux")]
11#[path = "platform/linux.rs"]
12mod platform;
13#[cfg(target_os = "freebsd")]
14#[path = "platform/freebsd.rs"]
15mod platform;
16#[cfg(target_os = "macos")]
17#[path = "platform/macos.rs"]
18mod platform;
19#[cfg(windows)]
20#[path = "platform/windows.rs"]
21mod platform;
22#[cfg(not(any(
23    target_os = "linux",
24    target_os = "freebsd",
25    target_os = "macos",
26    windows
27)))]
28#[path = "platform/other.rs"]
29mod platform;
30
31use journal_common::EntryTimestamps;
32use std::io;
33use std::path::PathBuf;
34use std::sync::{Arc, Mutex};
35use std::time::{SystemTime, UNIX_EPOCH};
36use uuid::Uuid;
37
38/// Classifies how the helper obtained the boot ID.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum BootIdSource {
41    /// No boot ID has been loaded yet.
42    Unknown,
43    /// A native per-boot kernel/OS UUID was read.
44    Native,
45    /// A state-backed boot ID was read from or written to healthy state.
46    StateBacked,
47    /// A valid fresh UUID was generated after native/state discovery failed.
48    ///
49    /// Degraded IDs satisfy the strict writer contract for this provider
50    /// instance, but they do not claim cross-process same-boot stability.
51    Degraded,
52}
53
54impl BootIdSource {
55    pub fn as_str(self) -> &'static str {
56        match self {
57            Self::Unknown => "unknown",
58            Self::Native => "native",
59            Self::StateBacked => "state-backed",
60            Self::Degraded => "degraded",
61        }
62    }
63}
64
65/// Diagnostics for the values returned by [`LocalJournalProvider`].
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Diagnostics {
68    pub machine_id_source: String,
69    pub boot_id_source: BootIdSource,
70    pub boot_id_path: Option<PathBuf>,
71    pub monotonic_source: String,
72    pub monotonic_source_detail: String,
73    pub degraded_reason: Option<String>,
74}
75
76impl Default for Diagnostics {
77    fn default() -> Self {
78        Self {
79            machine_id_source: String::new(),
80            boot_id_source: BootIdSource::Unknown,
81            boot_id_path: None,
82            monotonic_source: String::new(),
83            monotonic_source_detail: String::new(),
84            degraded_reason: None,
85        }
86    }
87}
88
89/// Configures local-host helper loading.
90///
91/// The zero value uses platform defaults. State paths are used only on Windows
92/// and on FreeBSD when native `kern.boot_id` is unavailable.
93#[derive(Clone, Default)]
94pub struct LoadOptions {
95    pub state_dir: Option<PathBuf>,
96    pub state_file_name: Option<String>,
97    pub state_path: Option<PathBuf>,
98    pub monotonic_now: Option<Arc<dyn Fn() -> io::Result<u64> + Send + Sync>>,
99    pub monotonic_label: Option<String>,
100}
101
102impl LoadOptions {
103    pub fn with_state_dir(mut self, path: impl Into<PathBuf>) -> Self {
104        self.state_dir = Some(path.into());
105        self
106    }
107
108    pub fn with_state_file_name(mut self, name: impl Into<String>) -> Self {
109        self.state_file_name = Some(name.into());
110        self
111    }
112
113    pub fn with_state_path(mut self, path: impl Into<PathBuf>) -> Self {
114        self.state_path = Some(path.into());
115        self
116    }
117
118    pub fn with_monotonic_now(
119        mut self,
120        label: impl Into<String>,
121        source: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
122    ) -> Self {
123        self.monotonic_label = Some(label.into());
124        self.monotonic_now = Some(source);
125        self
126    }
127
128    #[cfg(any(target_os = "freebsd", windows))]
129    pub(crate) fn resolve_state_path(
130        &self,
131        default_dir: impl AsRef<std::path::Path>,
132        default_file_name: impl AsRef<str>,
133    ) -> PathBuf {
134        if let Some(path) = &self.state_path {
135            return path.clone();
136        }
137        let dir = self
138            .state_dir
139            .clone()
140            .unwrap_or_else(|| default_dir.as_ref().to_path_buf());
141        let file_name = self
142            .state_file_name
143            .clone()
144            .unwrap_or_else(|| default_file_name.as_ref().to_string());
145        dir.join(file_name)
146    }
147}
148
149/// Local-host values that callers may pass explicitly to the writer.
150pub struct LocalJournalProvider {
151    machine_id: Uuid,
152    boot_id: Uuid,
153    diagnostics: Diagnostics,
154    monotonic_now: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
155    monotonic_label: String,
156    last_monotonic: Mutex<u64>,
157}
158
159impl LocalJournalProvider {
160    pub fn load(options: LoadOptions) -> io::Result<Self> {
161        platform::load(options)
162    }
163
164    pub fn machine_id(&self) -> Uuid {
165        self.machine_id
166    }
167
168    pub fn boot_id(&self) -> Uuid {
169        self.boot_id
170    }
171
172    pub fn diagnostics(&self) -> &Diagnostics {
173        &self.diagnostics
174    }
175
176    pub fn monotonic_source(&self) -> &str {
177        &self.monotonic_label
178    }
179
180    pub fn realtime_usec(&self) -> io::Result<u64> {
181        realtime_usec()
182    }
183
184    /// Returns a boot-anchored monotonic microsecond timestamp.
185    ///
186    /// Values are clamped to strictly increase within this provider instance.
187    pub fn monotonic_usec(&self) -> io::Result<u64> {
188        let mut now = (self.monotonic_now)()?;
189        let mut last = self
190            .last_monotonic
191            .lock()
192            .map_err(|_| io::Error::other("monotonic mutex poisoned"))?;
193        if now <= *last {
194            now = last
195                .checked_add(1)
196                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "monotonic overflow"))?;
197        }
198        *last = now;
199        Ok(now)
200    }
201
202    /// Builds the high-level Rust log-writer timestamp options.
203    pub fn entry_timestamps(&self) -> io::Result<EntryTimestamps> {
204        Ok(EntryTimestamps::default()
205            .with_entry_realtime_usec(self.realtime_usec()?)
206            .with_entry_monotonic_usec(self.monotonic_usec()?))
207    }
208
209    pub(crate) fn new(
210        machine_id: Uuid,
211        boot_id: Uuid,
212        diagnostics: Diagnostics,
213        monotonic_label: impl Into<String>,
214        monotonic_now: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
215    ) -> Self {
216        let monotonic_label = monotonic_label.into();
217        let mut diagnostics = diagnostics;
218        diagnostics.monotonic_source = monotonic_label.clone();
219        if diagnostics.monotonic_source_detail.is_empty() {
220            diagnostics.monotonic_source_detail = monotonic_label.clone();
221        }
222        Self {
223            machine_id,
224            boot_id,
225            diagnostics,
226            monotonic_now,
227            monotonic_label,
228            last_monotonic: Mutex::new(0),
229        }
230    }
231}
232
233/// Loads local-host identity and clock values using platform defaults.
234pub fn load(options: LoadOptions) -> io::Result<LocalJournalProvider> {
235    LocalJournalProvider::load(options)
236}
237
238/// Loads local-host identity and panics on failure.
239pub fn must_load(options: LoadOptions) -> LocalJournalProvider {
240    load(options).unwrap_or_else(|err| panic!("journal host helper failed: {err}"))
241}
242
243pub(crate) fn realtime_usec() -> io::Result<u64> {
244    let duration = SystemTime::now()
245        .duration_since(UNIX_EPOCH)
246        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
247    u64::try_from(duration.as_micros())
248        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "realtime microseconds overflow"))
249}
250
251pub(crate) fn parse_uuid_text(text: &str) -> io::Result<Uuid> {
252    Uuid::try_parse(text.trim())
253        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
254        .and_then(reject_zero_uuid)
255}
256
257pub(crate) fn reject_zero_uuid(id: Uuid) -> io::Result<Uuid> {
258    if id.is_nil() {
259        return Err(io::Error::new(
260            io::ErrorKind::InvalidData,
261            "uuid is all zeros",
262        ));
263    }
264    Ok(id)
265}
266
267#[cfg(any(target_os = "freebsd", windows, test))]
268pub(crate) fn uuid_compact(id: Uuid) -> String {
269    id.as_simple().to_string()
270}
271
272#[cfg(unix)]
273pub(crate) fn clock_gettime_usec(clock_id: libc::clockid_t) -> io::Result<u64> {
274    let mut ts = std::mem::MaybeUninit::<libc::timespec>::uninit();
275    // SAFETY: `ts` points to valid storage for clock_gettime to initialize.
276    let rc = unsafe { libc::clock_gettime(clock_id, ts.as_mut_ptr()) };
277    if rc != 0 {
278        return Err(io::Error::last_os_error());
279    }
280    // SAFETY: clock_gettime succeeded and initialized the whole timespec.
281    let ts = unsafe { ts.assume_init() };
282    let sec = u64::try_from(ts.tv_sec).map_err(|_| {
283        io::Error::new(
284            io::ErrorKind::InvalidData,
285            "clock_gettime returned negative seconds",
286        )
287    })?;
288    let nsec = u64::try_from(ts.tv_nsec).map_err(|_| {
289        io::Error::new(
290            io::ErrorKind::InvalidData,
291            "clock_gettime returned negative nanoseconds",
292        )
293    })?;
294    sec.checked_mul(1_000_000)
295        .and_then(|value| value.checked_add(nsec / 1_000))
296        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "clock microseconds overflow"))
297}
298
299#[cfg(any(target_os = "freebsd", target_os = "macos"))]
300pub(crate) fn sysctl_string(name: &str) -> io::Result<String> {
301    let raw = sysctl_raw(name)?;
302    let end = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len());
303    String::from_utf8(raw[..end].to_vec())
304        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
305}
306
307#[cfg(any(target_os = "freebsd", target_os = "macos"))]
308pub(crate) fn sysctl_raw(name: &str) -> io::Result<Vec<u8>> {
309    let name = std::ffi::CString::new(name)
310        .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
311    let mut len = 0usize;
312    // SAFETY: the name is NUL-terminated and len points to valid storage.
313    let rc = unsafe {
314        libc::sysctlbyname(
315            name.as_ptr(),
316            std::ptr::null_mut(),
317            &mut len,
318            std::ptr::null_mut(),
319            0,
320        )
321    };
322    if rc != 0 {
323        return Err(io::Error::last_os_error());
324    }
325    let mut buf = vec![0u8; len];
326    // SAFETY: the output buffer is valid for len bytes and sysctl writes into it.
327    let rc = unsafe {
328        libc::sysctlbyname(
329            name.as_ptr(),
330            buf.as_mut_ptr().cast(),
331            &mut len,
332            std::ptr::null_mut(),
333            0,
334        )
335    };
336    if rc != 0 {
337        return Err(io::Error::last_os_error());
338    }
339    buf.truncate(len);
340    Ok(buf)
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use std::sync::atomic::{AtomicU64, Ordering};
347
348    #[test]
349    fn provider_monotonic_is_strictly_increasing() {
350        let counter = Arc::new(AtomicU64::new(5));
351        let source = {
352            let counter = Arc::clone(&counter);
353            Arc::new(move || Ok(counter.load(Ordering::SeqCst)))
354        };
355        let provider = LocalJournalProvider::new(
356            Uuid::from_u128(1),
357            Uuid::from_u128(2),
358            Diagnostics::default(),
359            "test",
360            source,
361        );
362        assert_eq!(provider.monotonic_usec().unwrap(), 5);
363        assert_eq!(provider.monotonic_usec().unwrap(), 6);
364        counter.store(10, Ordering::SeqCst);
365        assert_eq!(provider.monotonic_usec().unwrap(), 10);
366    }
367}