use statime_base::{Clock, ClockError, Duration, LeapStatus, Timestamp, TAI, UTC};
#[cfg(target_os = "linux")]
use crate::linux_ioctls::{ptp_clock_getcaps, PtpClockCaps};
use std::marker::PhantomData;
#[cfg(target_os = "linux")]
use std::mem::MaybeUninit;
#[cfg(target_os = "linux")]
use std::{
os::unix::io::{IntoRawFd, RawFd},
path::Path,
};
pub struct UnixClock<Timescale> {
clock: libc::clockid_t,
#[cfg(target_os = "linux")]
fd: Option<RawFd>,
_timescale: PhantomData<Timescale>,
}
impl<Timescale> std::fmt::Debug for UnixClock<Timescale> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(target_os = "linux")]
{
f.debug_struct("UnixClock")
.field("clock", &self.clock)
.field("fd", &self.fd)
.field("_timescale", &self._timescale)
.finish()
}
#[cfg(not(target_os = "linux"))]
{
f.debug_struct("UnixClock")
.field("clock", &self.clock)
.field("_timescale", &self._timescale)
.finish()
}
}
}
impl<Timescale> Clone for UnixClock<Timescale> {
fn clone(&self) -> Self {
*self
}
}
impl<Timescale> Copy for UnixClock<Timescale> {}
impl UnixClock<UTC> {
pub const CLOCK_REALTIME: Self = UnixClock {
clock: libc::CLOCK_REALTIME,
#[cfg(target_os = "linux")]
fd: None,
_timescale: PhantomData,
};
}
impl UnixClock<TAI> {
#[cfg(target_os = "linux")]
pub const CLOCK_TAI: Self = UnixClock {
clock: libc::CLOCK_TAI,
fd: None,
_timescale: PhantomData,
};
#[cfg(target_os = "linux")]
pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
let file = std::fs::OpenOptions::new()
.write(true)
.read(true)
.open(path)?;
let clock = Self::safe_from_raw_fd(file.into_raw_fd());
Ok(clock)
}
#[cfg(target_os = "linux")]
fn safe_from_raw_fd(fd: RawFd) -> Self {
let clock = ((!(fd as libc::clockid_t)) << 3) | 3;
Self {
clock,
fd: Some(fd),
_timescale: PhantomData,
}
}
#[cfg(target_os = "linux")]
#[expect(clippy::type_complexity, reason = "FIXME: improve this")]
#[allow(
clippy::cast_possible_truncation,
reason = "Casts for timestamp conversion aren't lossless on all platforms"
)]
pub fn system_offset(
&self,
) -> Result<(Timestamp<TAI>, Timestamp<TAI>, Timestamp<TAI>), ClockError> {
use libc::{ptp_clock_time, ptp_sys_offset, PTP_SYS_OFFSET};
let Some(fd) = self.fd else {
return Err(ClockError::InvalidValue);
};
let default_ptp_clock_time = ptp_clock_time {
sec: 0,
nsec: 0,
reserved: 0,
};
let mut offset = ptp_sys_offset {
n_samples: 1,
rsv: [0u32; 3],
ts: [default_ptp_clock_time; 51],
};
if unsafe { libc::ioctl(fd, PTP_SYS_OFFSET as _, &mut offset) } != 0 {
let t1 = Self::CLOCK_TAI.now();
let tp = self.now();
let t2 = Self::CLOCK_TAI.now();
Ok((t1?, tp?, t2?))
} else {
let tai_offset = Self::CLOCK_TAI.get_tai()?;
Ok((
Timestamp::from_seconds_nanos_since_unix_epoch(
(offset.ts[0].sec + i64::from(tai_offset)).cast_unsigned(),
offset.ts[0].nsec as _,
),
Timestamp::from_seconds_nanos_since_unix_epoch(
offset.ts[1].sec.cast_unsigned(),
offset.ts[1].nsec as _,
),
Timestamp::from_seconds_nanos_since_unix_epoch(
(offset.ts[2].sec + i64::from(tai_offset)).cast_unsigned(),
offset.ts[2].nsec as _,
),
))
}
}
}
impl<Timescale> UnixClock<Timescale> {
pub fn disable_kernel(&self) -> Result<(), ClockError> {
let mut timex = EMPTY_TIMEX;
self.adjtime(&mut timex)?;
timex.modes = libc::MOD_STATUS;
timex.status &= !(libc::STA_PLL | libc::STA_FLL | libc::STA_PPSTIME | libc::STA_PPSFREQ);
ignore_not_supported(self.adjtime(&mut timex))
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "Allows a consistent interface between platforms."
)]
fn clock_adjtime(&self, timex: &mut libc::timex) -> Result<(), ClockError> {
#[cfg(target_os = "linux")]
use libc::clock_adjtime as adjtime;
#[cfg(any(target_os = "freebsd", target_os = "macos"))]
unsafe fn adjtime(clk_id: libc::clockid_t, buf: *mut libc::timex) -> libc::c_int {
assert_eq!(
clk_id,
libc::CLOCK_REALTIME,
"only the REALTIME clock is supported"
);
libc::ntp_adjtime(buf)
}
if unsafe { adjtime(self.clock, timex) } == -1 {
Err(convert_errno())
} else {
Ok(())
}
}
fn ntp_adjtime(timex: &mut libc::timex) -> Result<(), ClockError> {
#[cfg(any(target_os = "freebsd", target_os = "macos", target_env = "gnu"))]
use libc::ntp_adjtime as adjtime;
#[cfg(all(target_os = "linux", target_env = "musl"))]
use libc::adjtimex as adjtime;
if unsafe { adjtime(timex) } == -1 {
Err(convert_errno())
} else {
Ok(())
}
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "Allows a consistent interface between platforms."
)]
fn adjtime(&self, timex: &mut libc::timex) -> Result<(), ClockError> {
if self.clock == libc::CLOCK_REALTIME {
Self::ntp_adjtime(timex)
} else {
self.clock_adjtime(timex)
}
}
#[cfg_attr(target_os = "linux", allow(unused))]
fn clock_gettime(&self) -> Result<libc::timespec, ClockError> {
let mut timespec = libc::timespec::default();
cerr(unsafe { libc::clock_gettime(self.clock, &raw mut timespec) })?;
Ok(timespec)
}
#[cfg_attr(target_os = "linux", allow(unused))]
fn clock_settime(&self, mut timespec: libc::timespec) -> Result<(), ClockError> {
while timespec.tv_nsec > 1_000_000_000 {
timespec.tv_sec += 1;
timespec.tv_nsec -= 1_000_000_000;
}
unsafe { cerr(libc::clock_settime(self.clock, &raw const timespec))? };
Ok(())
}
#[cfg_attr(target_os = "linux", allow(unused))]
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "Allows a consistent interface between platforms."
)]
fn step_clock_by_timespec(&self, offset: Duration) -> Result<Timestamp<Timescale>, ClockError> {
let steps = offset.as_raw_steps();
#[cfg_attr(
target_env = "musl",
allow(
deprecated,
reason = "see https://github.com/rust-lang/libc/issues/1848"
)
)]
let seconds: libc::time_t = (steps >> 64)
.try_into()
.map_err(|_| ClockError::InvalidValue)?;
#[allow(
clippy::cast_possible_truncation,
reason = "the resulting value will always fit in an u32"
)]
let nanos = (((steps.cast_unsigned() & u128::from(u64::MAX)) * 1_000_000_000) >> 64)
as libc::c_long;
let mut timespec = self.clock_gettime()?;
#[cfg_attr(target_env = "musl", allow(deprecated))]
#[allow(
clippy::cast_possible_wrap,
reason = "The actual value is always possible, but not unsigned on all platforms."
)]
#[allow(
clippy::cast_lossless,
reason = "The cast can be done lossless on some, but not all, platforms."
)]
{
timespec.tv_sec += seconds;
timespec.tv_nsec += nanos;
}
self.clock_settime(timespec)?;
Ok(current_time_timespec(timespec, Precision::Nano))
}
fn error_estimate_timex(esterror: Duration, maxerror: Duration) -> libc::timex {
let modes = libc::MOD_ESTERROR | libc::MOD_MAXERROR;
let esterror =
libc::c_long::try_from((esterror.as_nanos() + 500) / 1000).unwrap_or(libc::c_long::MAX);
let maxerror =
libc::c_long::try_from((maxerror.as_nanos() + 500) / 1000).unwrap_or(libc::c_long::MAX);
libc::timex {
modes,
esterror,
maxerror,
..EMPTY_TIMEX
}
}
#[cfg(target_os = "linux")]
fn step_clock_timex(offset: Duration) -> Result<libc::timex, ClockError> {
let modes = libc::ADJ_SETOFFSET | libc::ADJ_NANO;
let nanos = offset.as_nanos();
let seconds = nanos
.div_euclid(1_000_000_000)
.try_into()
.map_err(|_| ClockError::InvalidValue)?;
#[allow(
clippy::cast_possible_truncation,
reason = "the resulting value will always fit in an u32"
)]
let nanos = nanos.rem_euclid(1_000_000_000) as _;
let time = libc::timeval {
tv_sec: seconds,
tv_usec: nanos,
};
Ok(libc::timex {
modes,
time,
..EMPTY_TIMEX
})
}
#[cfg(target_os = "linux")]
fn step_clock_by_timex(&self, offset: Duration) -> Result<Timestamp<Timescale>, ClockError> {
let mut timex = Self::step_clock_timex(offset)?;
self.adjtime(&mut timex)?;
self.extract_current_time(&timex)
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "Allows a consistent interface between platforms."
)]
fn extract_current_time(
&self,
#[cfg_attr(target_os = "macos", allow(unused))] timex: &libc::timex,
) -> Result<Timestamp<Timescale>, ClockError> {
#[cfg(target_os = "linux")]
if timex.time.tv_sec != 0 && timex.time.tv_usec != 0 {
let precision = match timex.status & libc::STA_NANO {
0 => Precision::Micro,
_ => Precision::Nano,
};
return Ok(current_time_timeval(timex.time, precision));
}
let timespec = self.clock_gettime()?;
Ok(current_time_timespec(timespec, Precision::Nano))
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "Allows a consistent interface between platforms."
)]
fn update_timex<F>(&self, f: F) -> Result<(), ClockError>
where
F: FnOnce(libc::timex) -> libc::timex,
{
let mut timex = EMPTY_TIMEX;
self.adjtime(&mut timex)?;
timex = f(timex);
self.adjtime(&mut timex)
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "Allows a consistent interface between platforms."
)]
fn update_status<F>(&self, f: F) -> Result<(), ClockError>
where
F: FnOnce(libc::c_int) -> libc::c_int,
{
self.update_timex(|mut timex| {
timex.modes = libc::MOD_STATUS;
timex.status = f(timex.status);
timex
})
}
#[expect(
clippy::cast_possible_truncation,
reason = "Truncating the frequency is acceptable here."
)]
fn set_frequency_timex(ppm: f64) -> libc::timex {
let mut timex = EMPTY_TIMEX;
timex.modes = libc::MOD_FREQUENCY;
let frequency = (ppm * 65536.0).round() as libc::c_long;
timex.freq = frequency.clamp(-32_768_000 + 1, 32_768_000 - 1);
timex
}
#[cfg(target_os = "linux")]
pub fn set_tai(&self, tai_offset: i32) -> Result<(), ClockError> {
#[allow(
clippy::useless_conversion,
reason = "This conversion is not useless on all platforms."
)]
let mut timex = libc::timex {
modes: libc::ADJ_TAI,
constant: tai_offset.into(),
..EMPTY_TIMEX
};
self.clock_adjtime(&mut timex)
}
#[cfg(not(target_os = "linux"))]
pub fn set_tai(&self, _tai_offset: i32) -> Result<(), ClockError> {
Err(ClockError::NotSupported)
}
#[cfg(target_os = "linux")]
pub fn get_tai(&self) -> Result<i32, ClockError> {
let mut timex = EMPTY_TIMEX;
if self.clock_adjtime(&mut timex).is_ok() {
Ok(timex.tai)
} else {
Ok(0)
}
}
#[cfg(not(target_os = "linux"))]
pub fn get_tai(&self) -> Result<i32, ClockError> {
Err(ClockError::NotSupported)
}
pub fn resolution(&self) -> Result<Duration, ClockError> {
let mut timespec = libc::timespec::default();
cerr(unsafe { libc::clock_getres(self.clock, &raw mut timespec) })?;
Ok(if timespec.tv_nsec == 0 && timespec.tv_sec == 0 {
Duration::from_seconds_nanos(0, 1)
} else {
#[allow(
clippy::cast_possible_truncation,
reason = "tv_nsec is guaranteed to be between 0 and 1_000_000_000, truncation doesn't happen on all platforms though."
)]
#[expect(
clippy::cast_sign_loss,
reason = "tv_nsec is guaranteed to be between 0 and 1_000_000_000"
)]
#[allow(
clippy::useless_conversion,
reason = "Conversion is not useless on all platforms."
)]
Duration::from_seconds_nanos(timespec.tv_sec.into(), timespec.tv_nsec as u32)
})
}
#[cfg(target_os = "linux")]
fn fetch_max_frequency_adjustment(&self) -> Result<f64, ClockError> {
if let Some(fd) = self.fd {
let mut caps = MaybeUninit::<PtpClockCaps>::zeroed();
if unsafe { ptp_clock_getcaps(fd, caps.as_mut_ptr()) } == 0 {
let caps = unsafe { caps.assume_init() };
Ok(f64::from(caps.max_adj) * 1e-9)
} else if error_number() == libc::ENOTTY {
Ok(500e-6)
} else {
Err(convert_errno())
}
} else {
Ok(500e-6)
}
}
#[cfg(not(target_os = "linux"))]
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "Allows a consistent interface between platforms."
)]
#[allow(
clippy::unnecessary_wraps,
reason = "Allows a consistent interface between platforms."
)]
fn fetch_max_frequency_adjustment(&self) -> Result<f64, ClockError> {
Ok(500e-6)
}
}
impl<Timescale: Send + 'static> Clock<Timescale> for UnixClock<Timescale> {
fn now(&self) -> Result<Timestamp<Timescale>, ClockError> {
let mut ntp_kapi_timex = EMPTY_TIMEX;
if self.adjtime(&mut ntp_kapi_timex).is_ok() {
Ok(self.extract_current_time(&ntp_kapi_timex)?)
} else {
Ok(self
.clock_gettime()
.map(|ts| current_time_timespec(ts, Precision::Nano))?)
}
}
fn set_frequency(&self, freq: f64) -> Result<Timestamp<Timescale>, ClockError> {
let mut timex = Self::set_frequency_timex(freq * 1e6);
self.adjtime(&mut timex)?;
self.extract_current_time(&timex)
}
#[allow(
clippy::cast_precision_loss,
reason = "Precision loss is acceptable here."
)]
#[allow(
clippy::cast_lossless,
reason = "Cast is not lossless on all platforms."
)]
fn get_frequency(&self) -> Result<f64, ClockError> {
let mut timex = EMPTY_TIMEX;
self.adjtime(&mut timex)?;
Ok((timex.freq as f64) / 65536.0e6)
}
fn max_frequency(&self) -> Result<f64, ClockError> {
self.fetch_max_frequency_adjustment()
}
#[cfg(target_os = "linux")]
fn step_clock(&self, offset: Duration) -> Result<Timestamp<Timescale>, ClockError> {
self.step_clock_by_timex(offset)
}
#[cfg(not(target_os = "linux"))]
fn step_clock(&self, offset: Duration) -> Result<Timestamp<Timescale>, ClockError> {
self.step_clock_by_timespec(offset)
}
fn error_estimate_update(
&self,
est_error: Duration,
max_error: Duration,
) -> Result<(), ClockError> {
let mut timex = Self::error_estimate_timex(est_error, max_error);
ignore_not_supported(self.adjtime(&mut timex))
}
fn leap_update(&self, leap_status: LeapStatus) -> Result<(), ClockError> {
self.update_status(|status| {
(status & !(libc::STA_INS | libc::STA_DEL))
| match leap_status {
LeapStatus::None => 0,
LeapStatus::Leap59 => libc::STA_DEL,
LeapStatus::Leap61 => libc::STA_INS,
}
})
}
fn synchronization_update(&self, synchronized: bool) -> Result<(), ClockError> {
self.update_status(|status| {
if synchronized {
status & !libc::STA_UNSYNC
} else {
status | libc::STA_UNSYNC
}
})
}
}
fn ignore_not_supported(result: Result<(), ClockError>) -> Result<(), ClockError> {
match result {
Err(ClockError::InvalidValue) => Ok(()),
result => result,
}
}
fn error_number() -> libc::c_int {
#[cfg(target_os = "linux")]
unsafe {
*libc::__errno_location()
}
#[cfg(not(target_os = "linux"))]
unsafe {
*libc::__error()
}
}
fn convert_errno() -> ClockError {
match error_number() {
libc::EINVAL | libc::ERANGE => ClockError::InvalidValue,
libc::ENODEV => ClockError::NoDevice,
libc::EOPNOTSUPP => ClockError::NotSupported,
libc::EPERM | libc::EACCES => ClockError::PermissionDenied,
_ => ClockError::Unknown,
}
}
fn cerr(c_int: libc::c_int) -> Result<(), ClockError> {
if c_int == -1 {
Err(convert_errno())
} else {
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Precision {
Nano,
#[cfg_attr(any(target_os = "freebsd", target_os = "macos"), allow(unused))]
Micro,
}
#[cfg_attr(target_os = "linux", allow(unused))]
#[allow(
clippy::cast_possible_truncation,
reason = "tv_nsec is always less than 1_000_000_000, which fits in an i32"
)]
#[allow(
clippy::useless_conversion,
reason = "Conversion is necessary on some platforms."
)]
fn current_time_timespec<Timescale>(
timespec: libc::timespec,
precision: Precision,
) -> Timestamp<Timescale> {
let mut seconds = timespec.tv_sec;
let nanos: i32 = timespec.tv_nsec as _;
let mut nanos = match precision {
Precision::Nano => nanos,
Precision::Micro => nanos.checked_mul(1000).unwrap_or_default(),
};
while nanos > 1_000_000_000 {
seconds = seconds.wrapping_add(1);
nanos -= 1_000_000_000;
}
while nanos < 0 {
seconds = seconds.wrapping_sub(1);
nanos += 1_000_000_000;
}
Timestamp::from_seconds_nanos_since_unix_epoch(
seconds.cast_unsigned().into(),
nanos.cast_unsigned(),
)
}
#[cfg_attr(not(target_os = "linux"), allow(unused))]
#[allow(
clippy::cast_possible_truncation,
reason = "tv_usec is always less than 1_000_000_000, which fits in an i32"
)]
#[expect(clippy::cast_sign_loss, reason = "tv_usec is guaranteed to be >=0")]
fn current_time_timeval<Timescale>(
timespec: libc::timeval,
precision: Precision,
) -> Timestamp<Timescale> {
let seconds = timespec.tv_sec;
let nanos = match precision {
Precision::Nano => timespec.tv_usec as u32,
Precision::Micro => (timespec.tv_usec as u32)
.checked_mul(1000)
.unwrap_or_default(),
};
Timestamp::from_seconds_nanos_since_unix_epoch(seconds as _, nanos)
}
#[cfg(all(target_os = "linux", target_env = "gnu"))]
pub const EMPTY_TIMEX: libc::timex = libc::timex {
modes: 0,
offset: 0,
freq: 0,
maxerror: 0,
esterror: 0,
status: 0,
constant: 0,
precision: 0,
tolerance: 0,
time: libc::timeval {
tv_sec: 0,
tv_usec: 0,
},
tick: 0,
ppsfreq: 0,
jitter: 0,
shift: 0,
stabil: 0,
jitcnt: 0,
calcnt: 0,
errcnt: 0,
stbcnt: 0,
tai: 0,
__unused1: 0,
__unused2: 0,
__unused3: 0,
__unused4: 0,
__unused5: 0,
__unused6: 0,
__unused7: 0,
__unused8: 0,
__unused9: 0,
__unused10: 0,
__unused11: 0,
};
#[cfg(all(target_os = "linux", target_env = "musl"))]
pub const EMPTY_TIMEX: libc::timex = libc::timex {
modes: 0,
offset: 0,
freq: 0,
maxerror: 0,
esterror: 0,
status: 0,
constant: 0,
precision: 0,
tolerance: 0,
time: libc::timeval {
tv_sec: 0,
tv_usec: 0,
},
tick: 0,
ppsfreq: 0,
jitter: 0,
shift: 0,
stabil: 0,
jitcnt: 0,
calcnt: 0,
errcnt: 0,
stbcnt: 0,
tai: 0,
__padding: [0; 11],
};
#[cfg(any(target_os = "freebsd", target_os = "macos"))]
pub const EMPTY_TIMEX: libc::timex = libc::timex {
modes: 0,
offset: 0,
freq: 0,
maxerror: 0,
esterror: 0,
status: 0,
constant: 0,
precision: 0,
tolerance: 0,
ppsfreq: 0,
jitter: 0,
shift: 0,
stabil: 0,
jitcnt: 0,
calcnt: 0,
errcnt: 0,
stbcnt: 0,
};
#[cfg(test)]
mod tests {
use statime_base::{Clock, Duration, Timestamp, UTC};
use super::UnixClock;
#[test]
fn test_time_now_does_not_crash() {
let clock = UnixClock::CLOCK_REALTIME;
assert_ne!(clock.now().unwrap(), Timestamp::UNIX_EPOCH,);
}
#[test]
fn realtime_gettime() {
let clock = UnixClock::CLOCK_REALTIME;
let time = clock.clock_gettime().unwrap();
assert_ne!((time.tv_sec, time.tv_nsec), (0, 0));
}
#[test]
#[ignore = "requires permissions, useful for testing permissions"]
fn ptp0_gettime() {
let clock = UnixClock::CLOCK_REALTIME;
let time = clock.clock_gettime().unwrap();
assert_ne!((time.tv_sec, time.tv_nsec), (0, 0));
}
#[test]
#[ignore = "requires permissions, useful for testing permissions"]
fn step_clock() {
UnixClock::CLOCK_REALTIME
.step_clock(Duration::from_seconds_nanos(0, 0))
.unwrap();
}
#[cfg(target_os = "linux")]
#[test]
fn test_step_clock() {
let offset = Duration::from_seconds_nanos(1, 200_000_000);
let timex = UnixClock::<UTC>::step_clock_timex(offset).unwrap();
assert_eq!(timex.modes, libc::ADJ_SETOFFSET | libc::ADJ_NANO);
assert_eq!(timex.time.tv_sec, 1);
assert_eq!(timex.time.tv_usec, 200_000_000);
}
#[test]
fn test_error_estimate() {
let est_error = Duration::from_seconds_nanos(0, 500_000_000);
let max_error = Duration::from_seconds_nanos(1, 200_000_000);
let timex = UnixClock::<UTC>::error_estimate_timex(est_error, max_error);
assert_eq!(timex.modes, libc::MOD_ESTERROR | libc::MOD_MAXERROR);
assert_eq!(timex.esterror, 500_000);
assert_eq!(timex.maxerror, 1_200_000);
}
#[test]
fn test_now() {
let resolution = UnixClock::CLOCK_REALTIME.now().unwrap();
assert_ne!(resolution, Timestamp::UNIX_EPOCH);
}
#[test]
fn test_resolution() {
let resolution = UnixClock::CLOCK_REALTIME.resolution().unwrap();
assert_ne!(resolution, Duration::ZERO);
}
#[test]
fn test_capabilities() {
let clock = UnixClock::CLOCK_REALTIME;
assert!(clock.max_frequency().is_ok());
}
}