Skip to main content

clock_steering/
unix.rs

1use statime_base::{Clock, ClockError, Duration, LeapStatus, Timestamp, TAI, UTC};
2
3#[cfg(target_os = "linux")]
4use crate::linux_ioctls::{ptp_clock_getcaps, PtpClockCaps};
5use std::marker::PhantomData;
6#[cfg(target_os = "linux")]
7use std::mem::MaybeUninit;
8#[cfg(target_os = "linux")]
9use std::{
10    os::unix::io::{IntoRawFd, RawFd},
11    path::Path,
12};
13
14/// A Unix OS clock
15pub struct UnixClock<Timescale> {
16    clock: libc::clockid_t,
17    #[cfg(target_os = "linux")]
18    fd: Option<RawFd>,
19    _timescale: PhantomData<Timescale>,
20}
21
22impl<Timescale> std::fmt::Debug for UnixClock<Timescale> {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        #[cfg(target_os = "linux")]
25        {
26            f.debug_struct("UnixClock")
27                .field("clock", &self.clock)
28                .field("fd", &self.fd)
29                .field("_timescale", &self._timescale)
30                .finish()
31        }
32
33        #[cfg(not(target_os = "linux"))]
34        {
35            f.debug_struct("UnixClock")
36                .field("clock", &self.clock)
37                .field("_timescale", &self._timescale)
38                .finish()
39        }
40    }
41}
42
43impl<Timescale> Clone for UnixClock<Timescale> {
44    fn clone(&self) -> Self {
45        *self
46    }
47}
48
49impl<Timescale> Copy for UnixClock<Timescale> {}
50
51impl UnixClock<UTC> {
52    /// The standard realtime clock on unix systems.
53    ///
54    /// ```no_run
55    /// use clock_steering::unix::UnixClock;
56    /// use statime_base::Clock;
57    ///
58    /// fn main() {
59    ///     let clock = UnixClock::CLOCK_REALTIME;
60    ///     let now = clock.now().unwrap();
61    ///
62    ///     println!("{now:?}");
63    /// }
64    /// ```
65    pub const CLOCK_REALTIME: Self = UnixClock {
66        clock: libc::CLOCK_REALTIME,
67        #[cfg(target_os = "linux")]
68        fd: None,
69        _timescale: PhantomData,
70    };
71}
72
73impl UnixClock<TAI> {
74    /// TAI time on linux systems.
75    ///
76    /// ```no_run
77    /// use clock_steering::unix::UnixClock;
78    /// use statime_base::Clock;
79    ///
80    /// fn main() {
81    ///     let clock = UnixClock::CLOCK_TAI;
82    ///     let now = clock.now().unwrap();
83    ///
84    ///     println!("{now:?}");
85    /// }
86    /// ```
87    #[cfg(target_os = "linux")]
88    pub const CLOCK_TAI: Self = UnixClock {
89        clock: libc::CLOCK_TAI,
90        fd: None,
91        _timescale: PhantomData,
92    };
93
94    /// Open a clock device.
95    ///
96    /// ```no_run
97    /// use clock_steering::unix::UnixClock;
98    /// use statime_base::Clock;
99    ///
100    /// fn main() {
101    ///     let clock = UnixClock::open("/dev/ptp0").unwrap();
102    ///     let now = clock.now().unwrap();
103    ///
104    ///     println!("{now:?}");
105    /// }
106    /// ```
107    ///
108    /// # Errors
109    /// Returns an error if the given path cannot be opened.
110    #[cfg(target_os = "linux")]
111    pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
112        let file = std::fs::OpenOptions::new()
113            .write(true)
114            .read(true)
115            .open(path)?;
116
117        // we need an owned fd. the file will be closed when the process exits.
118        let clock = Self::safe_from_raw_fd(file.into_raw_fd());
119
120        Ok(clock)
121    }
122
123    // Consume an fd and produce a clock id. Clock id is only valid
124    // so long as the fd is open, so the RawFd here should
125    // not be borrowed.
126    #[cfg(target_os = "linux")]
127    fn safe_from_raw_fd(fd: RawFd) -> Self {
128        let clock = ((!(fd as libc::clockid_t)) << 3) | 3;
129
130        Self {
131            clock,
132            fd: Some(fd),
133            _timescale: PhantomData,
134        }
135    }
136
137    /// Determine offset between file clock and TAI clock (if any)
138    /// Returns two system timestamps sandwhiching a timestamp from the
139    /// hardware clock.
140    ///
141    /// # Errors
142    /// Returns an error fi the system offset could not be provided.
143    #[cfg(target_os = "linux")]
144    #[expect(clippy::type_complexity, reason = "FIXME: improve this")]
145    #[allow(
146        clippy::cast_possible_truncation,
147        reason = "Casts for timestamp conversion aren't lossless on all platforms"
148    )]
149    pub fn system_offset(
150        &self,
151    ) -> Result<(Timestamp<TAI>, Timestamp<TAI>, Timestamp<TAI>), ClockError> {
152        use libc::{ptp_clock_time, ptp_sys_offset, PTP_SYS_OFFSET};
153
154        let Some(fd) = self.fd else {
155            return Err(ClockError::InvalidValue);
156        };
157
158        let default_ptp_clock_time = ptp_clock_time {
159            sec: 0,
160            nsec: 0,
161            reserved: 0,
162        };
163
164        let mut offset = ptp_sys_offset {
165            n_samples: 1,
166            rsv: [0u32; 3],
167            ts: [default_ptp_clock_time; 51],
168        };
169
170        // # Safety
171        //
172        // PTP_SYS_OFFSET receives a valid ptp_sys_offset mutable pointer
173        if unsafe { libc::ioctl(fd, PTP_SYS_OFFSET as _, &mut offset) } != 0 {
174            let t1 = Self::CLOCK_TAI.now();
175            let tp = self.now();
176            let t2 = Self::CLOCK_TAI.now();
177
178            Ok((t1?, tp?, t2?))
179        } else {
180            let tai_offset = Self::CLOCK_TAI.get_tai()?;
181
182            Ok((
183                Timestamp::from_seconds_nanos_since_unix_epoch(
184                    (offset.ts[0].sec + i64::from(tai_offset)).cast_unsigned(),
185                    offset.ts[0].nsec as _,
186                ),
187                Timestamp::from_seconds_nanos_since_unix_epoch(
188                    offset.ts[1].sec.cast_unsigned(),
189                    offset.ts[1].nsec as _,
190                ),
191                Timestamp::from_seconds_nanos_since_unix_epoch(
192                    (offset.ts[2].sec + i64::from(tai_offset)).cast_unsigned(),
193                    offset.ts[2].nsec as _,
194                ),
195            ))
196        }
197    }
198}
199
200impl<Timescale> UnixClock<Timescale> {
201    /// Disable any kernel disciplining of this clock.
202    ///
203    /// # Errors
204    /// May error if the clock has gone away.
205    pub fn disable_kernel(&self) -> Result<(), ClockError> {
206        let mut timex = EMPTY_TIMEX;
207        self.adjtime(&mut timex)?;
208
209        // We are setting the status bits
210        timex.modes = libc::MOD_STATUS;
211
212        // Disable all kernel time control loops (phase lock, frequency lock, pps time and pps frequency).
213        timex.status &= !(libc::STA_PLL | libc::STA_FLL | libc::STA_PPSTIME | libc::STA_PPSFREQ);
214
215        // ignore if we cannot disable the kernel time control loops (e.g. external clocks)
216        ignore_not_supported(self.adjtime(&mut timex))
217    }
218
219    #[allow(
220        clippy::trivially_copy_pass_by_ref,
221        reason = "Allows a consistent interface between platforms."
222    )]
223    fn clock_adjtime(&self, timex: &mut libc::timex) -> Result<(), ClockError> {
224        // We don't care about the time status, so the non-error
225        // information in the return value of clock_adjtime can be ignored.
226        //
227        // # Safety
228        //
229        // The clock_adjtime call is safe because the reference always
230        // points to a valid libc::timex.
231        //
232        // using an invalid clock id is safe. `clock_adjtime` will return an EINVAL
233        // error https://man.archlinux.org/man/clock_adjtime.2.en#EINVAL~4
234        #[cfg(target_os = "linux")]
235        use libc::clock_adjtime as adjtime;
236
237        #[cfg(any(target_os = "freebsd", target_os = "macos"))]
238        unsafe fn adjtime(clk_id: libc::clockid_t, buf: *mut libc::timex) -> libc::c_int {
239            assert_eq!(
240                clk_id,
241                libc::CLOCK_REALTIME,
242                "only the REALTIME clock is supported"
243            );
244
245            libc::ntp_adjtime(buf)
246        }
247
248        if unsafe { adjtime(self.clock, timex) } == -1 {
249            Err(convert_errno())
250        } else {
251            Ok(())
252        }
253    }
254
255    fn ntp_adjtime(timex: &mut libc::timex) -> Result<(), ClockError> {
256        #[cfg(any(target_os = "freebsd", target_os = "macos", target_env = "gnu"))]
257        use libc::ntp_adjtime as adjtime;
258
259        // ntp_adjtime is equivalent to adjtimex for our purposes
260        //
261        // https://man7.org/linux/man-pages/man2/adjtimex.2.html
262        #[cfg(all(target_os = "linux", target_env = "musl"))]
263        use libc::adjtimex as adjtime;
264
265        // We don't care about the time status, so the non-error
266        // information in the return value of ntp_adjtime can be ignored.
267        // The ntp_adjtime call is safe because the reference always
268        // points to a valid libc::timex.
269        if unsafe { adjtime(timex) } == -1 {
270            Err(convert_errno())
271        } else {
272            Ok(())
273        }
274    }
275
276    /// Adjust the clock state with a [`libc::timex`] specifying the desired changes.
277    ///
278    /// This is a lowlevel function. If possible, use more specialized (trait) methods.
279    ///
280    /// Note that [`libc::timex`] has a different layout between different operating systems, and
281    /// not all fields are available on all operating systems. Keep this in mind when writing
282    /// platform-independent code.
283    #[allow(
284        clippy::trivially_copy_pass_by_ref,
285        reason = "Allows a consistent interface between platforms."
286    )]
287    fn adjtime(&self, timex: &mut libc::timex) -> Result<(), ClockError> {
288        if self.clock == libc::CLOCK_REALTIME {
289            Self::ntp_adjtime(timex)
290        } else {
291            self.clock_adjtime(timex)
292        }
293    }
294
295    #[cfg_attr(target_os = "linux", allow(unused))]
296    fn clock_gettime(&self) -> Result<libc::timespec, ClockError> {
297        let mut timespec = libc::timespec::default();
298
299        // # Safety
300        //
301        // using an invalid clock id is safe. `clock_adjtime` will return an EINVAL
302        // error https://linux.die.net/man/3/clock_gettime
303        //
304        // The timespec pointer is valid.
305        cerr(unsafe { libc::clock_gettime(self.clock, &raw mut timespec) })?;
306
307        Ok(timespec)
308    }
309
310    #[cfg_attr(target_os = "linux", allow(unused))]
311    fn clock_settime(&self, mut timespec: libc::timespec) -> Result<(), ClockError> {
312        while timespec.tv_nsec > 1_000_000_000 {
313            timespec.tv_sec += 1;
314            timespec.tv_nsec -= 1_000_000_000;
315        }
316
317        // # Safety
318        //
319        // using an invalid clock id is safe. `clock_adjtime` will return an EINVAL
320        // error https://linux.die.net/man/3/clock_settime
321        //
322        // The timespec pointer is valid.
323        unsafe { cerr(libc::clock_settime(self.clock, &raw const timespec))? };
324
325        Ok(())
326    }
327
328    #[cfg_attr(target_os = "linux", allow(unused))]
329    #[allow(
330        clippy::trivially_copy_pass_by_ref,
331        reason = "Allows a consistent interface between platforms."
332    )]
333    fn step_clock_by_timespec(&self, offset: Duration) -> Result<Timestamp<Timescale>, ClockError> {
334        let steps = offset.as_raw_steps();
335        #[cfg_attr(
336            target_env = "musl",
337            allow(
338                deprecated,
339                reason = "see https://github.com/rust-lang/libc/issues/1848"
340            )
341        )]
342        let seconds: libc::time_t = (steps >> 64)
343            .try_into()
344            .map_err(|_| ClockError::InvalidValue)?;
345        #[allow(
346            clippy::cast_possible_truncation,
347            reason = "the resulting value will always fit in an u32"
348        )]
349        let nanos = (((steps.cast_unsigned() & u128::from(u64::MAX)) * 1_000_000_000) >> 64)
350            as libc::c_long;
351
352        let mut timespec = self.clock_gettime()?;
353
354        // see https://github.com/rust-lang/libc/issues/1848
355        #[cfg_attr(target_env = "musl", allow(deprecated))]
356        #[allow(
357            clippy::cast_possible_wrap,
358            reason = "The actual value is always possible, but not unsigned on all platforms."
359        )]
360        #[allow(
361            clippy::cast_lossless,
362            reason = "The cast can be done lossless on some, but not all, platforms."
363        )]
364        {
365            timespec.tv_sec += seconds;
366            timespec.tv_nsec += nanos;
367        }
368
369        self.clock_settime(timespec)?;
370
371        Ok(current_time_timespec(timespec, Precision::Nano))
372    }
373
374    fn error_estimate_timex(esterror: Duration, maxerror: Duration) -> libc::timex {
375        let modes = libc::MOD_ESTERROR | libc::MOD_MAXERROR;
376
377        // these fields are always in microseconds
378        let esterror =
379            libc::c_long::try_from((esterror.as_nanos() + 500) / 1000).unwrap_or(libc::c_long::MAX);
380        let maxerror =
381            libc::c_long::try_from((maxerror.as_nanos() + 500) / 1000).unwrap_or(libc::c_long::MAX);
382
383        libc::timex {
384            modes,
385            esterror,
386            maxerror,
387            ..EMPTY_TIMEX
388        }
389    }
390
391    #[cfg(target_os = "linux")]
392    fn step_clock_timex(offset: Duration) -> Result<libc::timex, ClockError> {
393        // we provide the offset in nanoseconds
394        let modes = libc::ADJ_SETOFFSET | libc::ADJ_NANO;
395
396        let nanos = offset.as_nanos();
397        let seconds = nanos
398            .div_euclid(1_000_000_000)
399            .try_into()
400            .map_err(|_| ClockError::InvalidValue)?;
401        #[allow(
402            clippy::cast_possible_truncation,
403            reason = "the resulting value will always fit in an u32"
404        )]
405        let nanos = nanos.rem_euclid(1_000_000_000) as _;
406
407        let time = libc::timeval {
408            tv_sec: seconds,
409            tv_usec: nanos,
410        };
411
412        Ok(libc::timex {
413            modes,
414            time,
415            ..EMPTY_TIMEX
416        })
417    }
418
419    #[cfg(target_os = "linux")]
420    fn step_clock_by_timex(&self, offset: Duration) -> Result<Timestamp<Timescale>, ClockError> {
421        let mut timex = Self::step_clock_timex(offset)?;
422        self.adjtime(&mut timex)?;
423        self.extract_current_time(&timex)
424    }
425
426    #[allow(
427        clippy::trivially_copy_pass_by_ref,
428        reason = "Allows a consistent interface between platforms."
429    )]
430    fn extract_current_time(
431        &self,
432        #[cfg_attr(target_os = "macos", allow(unused))] timex: &libc::timex,
433    ) -> Result<Timestamp<Timescale>, ClockError> {
434        #[cfg(target_os = "linux")]
435        // hardware clocks may not report the timestamp
436        if timex.time.tv_sec != 0 && timex.time.tv_usec != 0 {
437            // in a timex, the status flag determines precision
438            let precision = match timex.status & libc::STA_NANO {
439                0 => Precision::Micro,
440                _ => Precision::Nano,
441            };
442
443            return Ok(current_time_timeval(timex.time, precision));
444        }
445
446        // clock_gettime always gives nanoseconds
447        let timespec = self.clock_gettime()?;
448        Ok(current_time_timespec(timespec, Precision::Nano))
449    }
450
451    #[allow(
452        clippy::trivially_copy_pass_by_ref,
453        reason = "Allows a consistent interface between platforms."
454    )]
455    fn update_timex<F>(&self, f: F) -> Result<(), ClockError>
456    where
457        F: FnOnce(libc::timex) -> libc::timex,
458    {
459        let mut timex = EMPTY_TIMEX;
460        self.adjtime(&mut timex)?;
461
462        timex = f(timex);
463
464        self.adjtime(&mut timex)
465    }
466
467    #[allow(
468        clippy::trivially_copy_pass_by_ref,
469        reason = "Allows a consistent interface between platforms."
470    )]
471    fn update_status<F>(&self, f: F) -> Result<(), ClockError>
472    where
473        F: FnOnce(libc::c_int) -> libc::c_int,
474    {
475        self.update_timex(|mut timex| {
476            // We are setting the status bits
477            timex.modes = libc::MOD_STATUS;
478
479            // update the status flags
480            timex.status = f(timex.status);
481
482            timex
483        })
484    }
485
486    #[expect(
487        clippy::cast_possible_truncation,
488        reason = "Truncating the frequency is acceptable here."
489    )]
490    fn set_frequency_timex(ppm: f64) -> libc::timex {
491        // We do an offset with precision
492        let mut timex = EMPTY_TIMEX;
493
494        // set the frequency (MOD_FREQUENCY is an alias for ADJ_FREQUENCY on linux)
495        timex.modes = libc::MOD_FREQUENCY;
496
497        // NTP Kapi expects frequency adjustment in units of 2^-16 ppm
498        // but our input is in units of seconds drift per second, so convert.
499        let frequency = (ppm * 65536.0).round() as libc::c_long;
500
501        // Since Linux 2.6.26, the supplied value is clamped to the range (-32768000,
502        // +32768000). In older kernels, an EINVAL error occurs if the supplied value is
503        // out of range. (32768000 is 500 << 16)
504        timex.freq = frequency.clamp(-32_768_000 + 1, 32_768_000 - 1);
505
506        timex
507    }
508
509    /// Set the TAI offset on the given clock.
510    ///
511    /// # Errors
512    /// May error if the clock does not support setting the TAI offset.
513    #[cfg(target_os = "linux")]
514    pub fn set_tai(&self, tai_offset: i32) -> Result<(), ClockError> {
515        #[allow(
516            clippy::useless_conversion,
517            reason = "This conversion is not useless on all platforms."
518        )]
519        let mut timex = libc::timex {
520            modes: libc::ADJ_TAI,
521            constant: tai_offset.into(),
522            ..EMPTY_TIMEX
523        };
524
525        self.clock_adjtime(&mut timex)
526    }
527
528    /// Set the TAI offset on the given clock.
529    ///
530    /// # Errors
531    /// May error if the clock does not support setting the TAI offset.
532    #[cfg(not(target_os = "linux"))]
533    pub fn set_tai(&self, _tai_offset: i32) -> Result<(), ClockError> {
534        Err(ClockError::NotSupported)
535    }
536
537    /// Get the TAI offset on the given clock.
538    ///
539    /// # Errors
540    /// May error if the clock does not support getting the TAI offset.
541    #[cfg(target_os = "linux")]
542    pub fn get_tai(&self) -> Result<i32, ClockError> {
543        let mut timex = EMPTY_TIMEX;
544        if self.clock_adjtime(&mut timex).is_ok() {
545            Ok(timex.tai)
546        } else {
547            // hardware clock which doesn't have an offset anyway
548            Ok(0)
549        }
550    }
551
552    /// Get the TAI offset on the given clock.
553    ///
554    /// # Errors
555    /// May error if the clock does not support getting the TAI offset.
556    #[cfg(not(target_os = "linux"))]
557    pub fn get_tai(&self) -> Result<i32, ClockError> {
558        Err(ClockError::NotSupported)
559    }
560
561    /// Get the clock's resolution.
562    ///
563    /// The output [`Timestamp`] will be all zeros when the resolution is
564    /// unavailable.
565    ///
566    /// # Errors
567    /// May error if the clcok has gone away
568    pub fn resolution(&self) -> Result<Duration, ClockError> {
569        let mut timespec = libc::timespec::default();
570
571        cerr(unsafe { libc::clock_getres(self.clock, &raw mut timespec) })?;
572
573        Ok(if timespec.tv_nsec == 0 && timespec.tv_sec == 0 {
574            Duration::from_seconds_nanos(0, 1)
575        } else {
576            #[allow(
577                clippy::cast_possible_truncation,
578                reason = "tv_nsec is guaranteed to be between 0 and 1_000_000_000, truncation doesn't happen on all platforms though."
579            )]
580            #[expect(
581                clippy::cast_sign_loss,
582                reason = "tv_nsec is guaranteed to be between 0 and 1_000_000_000"
583            )]
584            #[allow(
585                clippy::useless_conversion,
586                reason = "Conversion is not useless on all platforms."
587            )]
588            Duration::from_seconds_nanos(timespec.tv_sec.into(), timespec.tv_nsec as u32)
589        })
590    }
591
592    #[cfg(target_os = "linux")]
593    fn fetch_max_frequency_adjustment(&self) -> Result<f64, ClockError> {
594        if let Some(fd) = self.fd {
595            let mut caps = MaybeUninit::<PtpClockCaps>::zeroed();
596
597            if unsafe { ptp_clock_getcaps(fd, caps.as_mut_ptr()) } == 0 {
598                let caps = unsafe { caps.assume_init() };
599
600                Ok(f64::from(caps.max_adj) * 1e-9)
601            } else if error_number() == libc::ENOTTY {
602                // Not a PTP device, use system clock defaults
603                Ok(500e-6)
604            } else {
605                Err(convert_errno())
606            }
607        } else {
608            // System clock, use system defaults
609            Ok(500e-6)
610        }
611    }
612
613    #[cfg(not(target_os = "linux"))]
614    #[allow(
615        clippy::trivially_copy_pass_by_ref,
616        reason = "Allows a consistent interface between platforms."
617    )]
618    #[allow(
619        clippy::unnecessary_wraps,
620        reason = "Allows a consistent interface between platforms."
621    )]
622    fn fetch_max_frequency_adjustment(&self) -> Result<f64, ClockError> {
623        Ok(500e-6)
624    }
625}
626
627impl<Timescale: Send + 'static> Clock<Timescale> for UnixClock<Timescale> {
628    fn now(&self) -> Result<Timestamp<Timescale>, ClockError> {
629        let mut ntp_kapi_timex = EMPTY_TIMEX;
630
631        if self.adjtime(&mut ntp_kapi_timex).is_ok() {
632            Ok(self.extract_current_time(&ntp_kapi_timex)?)
633        } else {
634            Ok(self
635                .clock_gettime()
636                .map(|ts| current_time_timespec(ts, Precision::Nano))?)
637        }
638    }
639
640    fn set_frequency(&self, freq: f64) -> Result<Timestamp<Timescale>, ClockError> {
641        let mut timex = Self::set_frequency_timex(freq * 1e6);
642        self.adjtime(&mut timex)?;
643        self.extract_current_time(&timex)
644    }
645
646    #[allow(
647        clippy::cast_precision_loss,
648        reason = "Precision loss is acceptable here."
649    )]
650    #[allow(
651        clippy::cast_lossless,
652        reason = "Cast is not lossless on all platforms."
653    )]
654    fn get_frequency(&self) -> Result<f64, ClockError> {
655        let mut timex = EMPTY_TIMEX;
656        self.adjtime(&mut timex)?;
657
658        Ok((timex.freq as f64) / 65536.0e6)
659    }
660
661    fn max_frequency(&self) -> Result<f64, ClockError> {
662        self.fetch_max_frequency_adjustment()
663    }
664
665    #[cfg(target_os = "linux")]
666    fn step_clock(&self, offset: Duration) -> Result<Timestamp<Timescale>, ClockError> {
667        self.step_clock_by_timex(offset)
668    }
669
670    #[cfg(not(target_os = "linux"))]
671    fn step_clock(&self, offset: Duration) -> Result<Timestamp<Timescale>, ClockError> {
672        self.step_clock_by_timespec(offset)
673    }
674
675    fn error_estimate_update(
676        &self,
677        est_error: Duration,
678        max_error: Duration,
679    ) -> Result<(), ClockError> {
680        let mut timex = Self::error_estimate_timex(est_error, max_error);
681        ignore_not_supported(self.adjtime(&mut timex))
682    }
683
684    fn leap_update(&self, leap_status: LeapStatus) -> Result<(), ClockError> {
685        self.update_status(|status| {
686            (status & !(libc::STA_INS | libc::STA_DEL))
687                | match leap_status {
688                    LeapStatus::None => 0,
689                    LeapStatus::Leap59 => libc::STA_DEL,
690                    LeapStatus::Leap61 => libc::STA_INS,
691                }
692        })
693    }
694
695    fn synchronization_update(&self, synchronized: bool) -> Result<(), ClockError> {
696        self.update_status(|status| {
697            if synchronized {
698                status & !libc::STA_UNSYNC
699            } else {
700                status | libc::STA_UNSYNC
701            }
702        })
703    }
704}
705
706fn ignore_not_supported(result: Result<(), ClockError>) -> Result<(), ClockError> {
707    match result {
708        Err(ClockError::InvalidValue) => Ok(()),
709        result => result,
710    }
711}
712
713fn error_number() -> libc::c_int {
714    #[cfg(target_os = "linux")]
715    unsafe {
716        *libc::__errno_location()
717    }
718
719    #[cfg(not(target_os = "linux"))]
720    unsafe {
721        *libc::__error()
722    }
723}
724
725// Convert those error numbers that can occur for calls to the following
726// functions
727// - ntp_adjtimex https://man7.org/linux/man-pages/man3/ntp_adjtime.3.html
728// - clock_gettime & clock_settime https://man7.org/linux/man-pages/man3/clock_gettime.3.html
729fn convert_errno() -> ClockError {
730    match error_number() {
731        libc::EINVAL | libc::ERANGE => ClockError::InvalidValue,
732        // The documentation is a bit unclear if this can happen with
733        // non-dynamic clocks like the ntp kapi clock, however deal with it just in case.
734        libc::ENODEV => ClockError::NoDevice,
735        libc::EOPNOTSUPP => ClockError::NotSupported,
736        libc::EPERM | libc::EACCES => ClockError::PermissionDenied,
737        _ => ClockError::Unknown,
738    }
739}
740
741fn cerr(c_int: libc::c_int) -> Result<(), ClockError> {
742    if c_int == -1 {
743        Err(convert_errno())
744    } else {
745        Ok(())
746    }
747}
748
749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750pub(crate) enum Precision {
751    Nano,
752    #[cfg_attr(any(target_os = "freebsd", target_os = "macos"), allow(unused))]
753    Micro,
754}
755
756#[cfg_attr(target_os = "linux", allow(unused))]
757#[allow(
758    clippy::cast_possible_truncation,
759    reason = "tv_nsec is always less than 1_000_000_000, which fits in an i32"
760)]
761#[allow(
762    clippy::useless_conversion,
763    reason = "Conversion is necessary on some platforms."
764)]
765fn current_time_timespec<Timescale>(
766    timespec: libc::timespec,
767    precision: Precision,
768) -> Timestamp<Timescale> {
769    let mut seconds = timespec.tv_sec;
770
771    let nanos: i32 = timespec.tv_nsec as _;
772
773    let mut nanos = match precision {
774        Precision::Nano => nanos,
775        Precision::Micro => nanos.checked_mul(1000).unwrap_or_default(),
776    };
777
778    // on macOS (at least) we've observed higher nanosecond counts than appear valid
779    while nanos > 1_000_000_000 {
780        seconds = seconds.wrapping_add(1);
781        nanos -= 1_000_000_000;
782    }
783
784    // we disallow negative nanoseconds
785    while nanos < 0 {
786        seconds = seconds.wrapping_sub(1);
787        nanos += 1_000_000_000;
788    }
789
790    Timestamp::from_seconds_nanos_since_unix_epoch(
791        seconds.cast_unsigned().into(),
792        nanos.cast_unsigned(),
793    )
794}
795
796#[cfg_attr(not(target_os = "linux"), allow(unused))]
797#[allow(
798    clippy::cast_possible_truncation,
799    reason = "tv_usec is always less than 1_000_000_000, which fits in an i32"
800)]
801#[expect(clippy::cast_sign_loss, reason = "tv_usec is guaranteed to be >=0")]
802fn current_time_timeval<Timescale>(
803    timespec: libc::timeval,
804    precision: Precision,
805) -> Timestamp<Timescale> {
806    let seconds = timespec.tv_sec;
807    let nanos = match precision {
808        Precision::Nano => timespec.tv_usec as u32,
809        Precision::Micro => (timespec.tv_usec as u32)
810            .checked_mul(1000)
811            .unwrap_or_default(),
812    };
813
814    Timestamp::from_seconds_nanos_since_unix_epoch(seconds as _, nanos)
815}
816
817/// Convenient way of getting an empty [`libc::timex`] struct.
818// Libc has no good other way of obtaining this, so let's at least make our
819// functions more readable.
820#[cfg(all(target_os = "linux", target_env = "gnu"))]
821pub const EMPTY_TIMEX: libc::timex = libc::timex {
822    modes: 0,
823    offset: 0,
824    freq: 0,
825    maxerror: 0,
826    esterror: 0,
827    status: 0,
828    constant: 0,
829    precision: 0,
830    tolerance: 0,
831    time: libc::timeval {
832        tv_sec: 0,
833        tv_usec: 0,
834    },
835    tick: 0,
836    ppsfreq: 0,
837    jitter: 0,
838    shift: 0,
839    stabil: 0,
840    jitcnt: 0,
841    calcnt: 0,
842    errcnt: 0,
843    stbcnt: 0,
844    tai: 0,
845    __unused1: 0,
846    __unused2: 0,
847    __unused3: 0,
848    __unused4: 0,
849    __unused5: 0,
850    __unused6: 0,
851    __unused7: 0,
852    __unused8: 0,
853    __unused9: 0,
854    __unused10: 0,
855    __unused11: 0,
856};
857
858/// Convenient way of getting an empty [`libc::timex`] struct.
859#[cfg(all(target_os = "linux", target_env = "musl"))]
860pub const EMPTY_TIMEX: libc::timex = libc::timex {
861    modes: 0,
862    offset: 0,
863    freq: 0,
864    maxerror: 0,
865    esterror: 0,
866    status: 0,
867    constant: 0,
868    precision: 0,
869    tolerance: 0,
870    time: libc::timeval {
871        tv_sec: 0,
872        tv_usec: 0,
873    },
874    tick: 0,
875    ppsfreq: 0,
876    jitter: 0,
877    shift: 0,
878    stabil: 0,
879    jitcnt: 0,
880    calcnt: 0,
881    errcnt: 0,
882    stbcnt: 0,
883    tai: 0,
884    __padding: [0; 11],
885};
886
887/// Convenient way of getting an empty [`libc::timex`] struct.
888#[cfg(any(target_os = "freebsd", target_os = "macos"))]
889pub const EMPTY_TIMEX: libc::timex = libc::timex {
890    modes: 0,
891    offset: 0,
892    freq: 0,
893    maxerror: 0,
894    esterror: 0,
895    status: 0,
896    constant: 0,
897    precision: 0,
898    tolerance: 0,
899    ppsfreq: 0,
900    jitter: 0,
901    shift: 0,
902    stabil: 0,
903    jitcnt: 0,
904    calcnt: 0,
905    errcnt: 0,
906    stbcnt: 0,
907};
908
909#[cfg(test)]
910mod tests {
911    use statime_base::{Clock, Duration, Timestamp, UTC};
912
913    use super::UnixClock;
914
915    #[test]
916    fn test_time_now_does_not_crash() {
917        let clock = UnixClock::CLOCK_REALTIME;
918        assert_ne!(clock.now().unwrap(), Timestamp::UNIX_EPOCH,);
919    }
920
921    #[test]
922    fn realtime_gettime() {
923        let clock = UnixClock::CLOCK_REALTIME;
924        let time = clock.clock_gettime().unwrap();
925
926        assert_ne!((time.tv_sec, time.tv_nsec), (0, 0));
927    }
928
929    #[test]
930    #[ignore = "requires permissions, useful for testing permissions"]
931    fn ptp0_gettime() {
932        let clock = UnixClock::CLOCK_REALTIME;
933        let time = clock.clock_gettime().unwrap();
934
935        assert_ne!((time.tv_sec, time.tv_nsec), (0, 0));
936    }
937
938    #[test]
939    #[ignore = "requires permissions, useful for testing permissions"]
940    fn step_clock() {
941        UnixClock::CLOCK_REALTIME
942            .step_clock(Duration::from_seconds_nanos(0, 0))
943            .unwrap();
944    }
945
946    #[cfg(target_os = "linux")]
947    #[test]
948    fn test_step_clock() {
949        let offset = Duration::from_seconds_nanos(1, 200_000_000);
950        let timex = UnixClock::<UTC>::step_clock_timex(offset).unwrap();
951
952        assert_eq!(timex.modes, libc::ADJ_SETOFFSET | libc::ADJ_NANO);
953
954        assert_eq!(timex.time.tv_sec, 1);
955        assert_eq!(timex.time.tv_usec, 200_000_000);
956    }
957
958    #[test]
959    fn test_error_estimate() {
960        let est_error = Duration::from_seconds_nanos(0, 500_000_000);
961        let max_error = Duration::from_seconds_nanos(1, 200_000_000);
962        let timex = UnixClock::<UTC>::error_estimate_timex(est_error, max_error);
963
964        assert_eq!(timex.modes, libc::MOD_ESTERROR | libc::MOD_MAXERROR);
965
966        // these fields are always in microseconds
967        assert_eq!(timex.esterror, 500_000);
968        assert_eq!(timex.maxerror, 1_200_000);
969    }
970
971    #[test]
972    fn test_now() {
973        let resolution = UnixClock::CLOCK_REALTIME.now().unwrap();
974
975        assert_ne!(resolution, Timestamp::UNIX_EPOCH);
976    }
977
978    #[test]
979    fn test_resolution() {
980        let resolution = UnixClock::CLOCK_REALTIME.resolution().unwrap();
981
982        assert_ne!(resolution, Duration::ZERO);
983    }
984
985    #[test]
986    fn test_capabilities() {
987        let clock = UnixClock::CLOCK_REALTIME;
988
989        assert!(clock.max_frequency().is_ok());
990    }
991}