1#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum BootIdSource {
41 Unknown,
43 Native,
45 StateBacked,
47 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#[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#[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 host_filesystem_prefix: Option<PathBuf>,
99 pub monotonic_now: Option<Arc<dyn Fn() -> io::Result<u64> + Send + Sync>>,
100 pub monotonic_label: Option<String>,
101}
102
103impl LoadOptions {
104 pub fn with_state_dir(mut self, path: impl Into<PathBuf>) -> Self {
105 self.state_dir = Some(path.into());
106 self
107 }
108
109 pub fn with_state_file_name(mut self, name: impl Into<String>) -> Self {
110 self.state_file_name = Some(name.into());
111 self
112 }
113
114 pub fn with_state_path(mut self, path: impl Into<PathBuf>) -> Self {
115 self.state_path = Some(path.into());
116 self
117 }
118
119 pub fn with_host_filesystem_prefix(mut self, path: impl Into<PathBuf>) -> Self {
126 self.host_filesystem_prefix = Some(path.into());
127 self
128 }
129
130 pub fn with_monotonic_now(
131 mut self,
132 label: impl Into<String>,
133 source: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
134 ) -> Self {
135 self.monotonic_label = Some(label.into());
136 self.monotonic_now = Some(source);
137 self
138 }
139
140 #[cfg(any(target_os = "freebsd", windows))]
141 pub(crate) fn resolve_state_path(
142 &self,
143 default_dir: impl AsRef<std::path::Path>,
144 default_file_name: impl AsRef<str>,
145 ) -> PathBuf {
146 if let Some(path) = &self.state_path {
147 return path.clone();
148 }
149 let dir = self
150 .state_dir
151 .clone()
152 .unwrap_or_else(|| default_dir.as_ref().to_path_buf());
153 let file_name = self
154 .state_file_name
155 .clone()
156 .unwrap_or_else(|| default_file_name.as_ref().to_string());
157 dir.join(file_name)
158 }
159}
160
161pub struct LocalJournalProvider {
163 machine_id: Uuid,
164 boot_id: Uuid,
165 diagnostics: Diagnostics,
166 monotonic_now: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
167 monotonic_label: String,
168 last_monotonic: Mutex<u64>,
169}
170
171impl LocalJournalProvider {
172 pub fn load(options: LoadOptions) -> io::Result<Self> {
173 platform::load(options)
174 }
175
176 pub fn machine_id(&self) -> Uuid {
177 self.machine_id
178 }
179
180 pub fn boot_id(&self) -> Uuid {
181 self.boot_id
182 }
183
184 pub fn diagnostics(&self) -> &Diagnostics {
185 &self.diagnostics
186 }
187
188 pub fn monotonic_source(&self) -> &str {
189 &self.monotonic_label
190 }
191
192 pub fn realtime_usec(&self) -> io::Result<u64> {
193 realtime_usec()
194 }
195
196 pub fn monotonic_usec(&self) -> io::Result<u64> {
200 let mut now = (self.monotonic_now)()?;
201 let mut last = self
202 .last_monotonic
203 .lock()
204 .map_err(|_| io::Error::other("monotonic mutex poisoned"))?;
205 if now <= *last {
206 now = last
207 .checked_add(1)
208 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "monotonic overflow"))?;
209 }
210 *last = now;
211 Ok(now)
212 }
213
214 pub fn entry_timestamps(&self) -> io::Result<EntryTimestamps> {
216 Ok(EntryTimestamps::default()
217 .with_entry_realtime_usec(self.realtime_usec()?)
218 .with_entry_monotonic_usec(self.monotonic_usec()?))
219 }
220
221 pub(crate) fn new(
222 machine_id: Uuid,
223 boot_id: Uuid,
224 diagnostics: Diagnostics,
225 monotonic_label: impl Into<String>,
226 monotonic_now: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
227 ) -> Self {
228 let monotonic_label = monotonic_label.into();
229 let mut diagnostics = diagnostics;
230 diagnostics.monotonic_source = monotonic_label.clone();
231 if diagnostics.monotonic_source_detail.is_empty() {
232 diagnostics.monotonic_source_detail = monotonic_label.clone();
233 }
234 Self {
235 machine_id,
236 boot_id,
237 diagnostics,
238 monotonic_now,
239 monotonic_label,
240 last_monotonic: Mutex::new(0),
241 }
242 }
243}
244
245pub fn load(options: LoadOptions) -> io::Result<LocalJournalProvider> {
247 LocalJournalProvider::load(options)
248}
249
250pub fn must_load(options: LoadOptions) -> LocalJournalProvider {
252 load(options).unwrap_or_else(|err| panic!("journal host helper failed: {err}"))
253}
254
255pub(crate) fn realtime_usec() -> io::Result<u64> {
256 let duration = SystemTime::now()
257 .duration_since(UNIX_EPOCH)
258 .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
259 u64::try_from(duration.as_micros())
260 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "realtime microseconds overflow"))
261}
262
263pub(crate) fn parse_uuid_text(text: &str) -> io::Result<Uuid> {
264 Uuid::try_parse(text.trim())
265 .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
266 .and_then(reject_zero_uuid)
267}
268
269pub(crate) fn reject_zero_uuid(id: Uuid) -> io::Result<Uuid> {
270 if id.is_nil() {
271 return Err(io::Error::new(
272 io::ErrorKind::InvalidData,
273 "uuid is all zeros",
274 ));
275 }
276 Ok(id)
277}
278
279#[cfg(any(target_os = "freebsd", windows, test))]
280pub(crate) fn uuid_compact(id: Uuid) -> String {
281 id.as_simple().to_string()
282}
283
284#[cfg(unix)]
285pub(crate) fn clock_gettime_usec(clock_id: libc::clockid_t) -> io::Result<u64> {
286 let mut ts = std::mem::MaybeUninit::<libc::timespec>::uninit();
287 let rc = unsafe { libc::clock_gettime(clock_id, ts.as_mut_ptr()) };
290 if rc != 0 {
291 return Err(io::Error::last_os_error());
292 }
293 let ts = unsafe { ts.assume_init() };
296 let sec = u64::try_from(ts.tv_sec).map_err(|_| {
297 io::Error::new(
298 io::ErrorKind::InvalidData,
299 "clock_gettime returned negative seconds",
300 )
301 })?;
302 let nsec = u64::try_from(ts.tv_nsec).map_err(|_| {
303 io::Error::new(
304 io::ErrorKind::InvalidData,
305 "clock_gettime returned negative nanoseconds",
306 )
307 })?;
308 sec.checked_mul(1_000_000)
309 .and_then(|value| value.checked_add(nsec / 1_000))
310 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "clock microseconds overflow"))
311}
312
313#[cfg(any(target_os = "freebsd", target_os = "macos"))]
314pub(crate) fn sysctl_string(name: &str) -> io::Result<String> {
315 let raw = sysctl_raw(name)?;
316 let end = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len());
317 String::from_utf8(raw[..end].to_vec())
318 .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
319}
320
321#[cfg(any(target_os = "freebsd", target_os = "macos"))]
322pub(crate) fn sysctl_raw(name: &str) -> io::Result<Vec<u8>> {
323 let name = std::ffi::CString::new(name)
324 .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
325 let mut len = 0usize;
326 let rc = unsafe {
329 libc::sysctlbyname(
330 name.as_ptr(),
331 std::ptr::null_mut(),
332 &mut len,
333 std::ptr::null_mut(),
334 0,
335 )
336 };
337 if rc != 0 {
338 return Err(io::Error::last_os_error());
339 }
340 let mut buf = vec![0u8; len];
341 let rc = unsafe {
344 libc::sysctlbyname(
345 name.as_ptr(),
346 buf.as_mut_ptr().cast(),
347 &mut len,
348 std::ptr::null_mut(),
349 0,
350 )
351 };
352 if rc != 0 {
353 return Err(io::Error::last_os_error());
354 }
355 buf.truncate(len);
356 Ok(buf)
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use std::sync::atomic::{AtomicU64, Ordering};
363
364 #[test]
365 fn provider_monotonic_is_strictly_increasing() {
366 let counter = Arc::new(AtomicU64::new(5));
367 let source = {
368 let counter = Arc::clone(&counter);
369 Arc::new(move || Ok(counter.load(Ordering::SeqCst)))
370 };
371 let provider = LocalJournalProvider::new(
372 Uuid::from_u128(1),
373 Uuid::from_u128(2),
374 Diagnostics::default(),
375 "test",
376 source,
377 );
378 assert_eq!(provider.monotonic_usec().unwrap(), 5);
379 assert_eq!(provider.monotonic_usec().unwrap(), 6);
380 counter.store(10, Ordering::SeqCst);
381 assert_eq!(provider.monotonic_usec().unwrap(), 10);
382 }
383}