use std::{
os::unix::io::{AsRawFd, RawFd},
path::Path,
};
use crate::{Error, EPOCH_OFFSET};
use ntp_proto::{NtpClock, NtpDuration, NtpLeapIndicator, NtpTimestamp, PollInterval};
#[cfg(all(target_os = "linux", target_env = "gnu"))]
pub(crate) 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(crate) 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(crate) 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,
};
#[derive(Debug, Default, Clone, Copy)]
pub struct UnixNtpClock {
clock: libc::clockid_t,
}
impl UnixNtpClock {
pub fn realtime() -> Self {
Self::custom(libc::CLOCK_REALTIME)
}
pub fn custom(id: libc::clockid_t) -> Self {
Self { clock: id }
}
pub fn from_path(path: &Path) -> Result<Self, Error> {
match std::fs::File::options().read(true).write(true).open(path) {
Err(_) => Err(convert_errno()),
Ok(file) => {
let fd = file.as_raw_fd();
std::mem::forget(file);
Ok(Self::from_file_descriptor(fd))
}
}
}
pub fn from_file_descriptor(fd: RawFd) -> Self {
let id = ((!(fd as libc::clockid_t)) << 3) | 0b11;
Self::custom(id)
}
#[cfg_attr(target_os = "linux", allow(unused))]
fn clock_gettime(&self) -> Result<libc::timespec, Error> {
let mut timespec = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
cerr(unsafe { libc::clock_gettime(self.clock, &mut timespec) })?;
Ok(timespec)
}
#[cfg_attr(target_os = "linux", allow(unused))]
fn clock_settime(&self, mut timespec: libc::timespec) -> Result<(), Error> {
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, ×pec))? };
Ok(())
}
fn clock_adjtime(&self, timex: &mut libc::timex) -> Result<(), Error> {
#[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<(), Error> {
#[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(())
}
}
pub(crate) fn adjtime(&self, timex: &mut libc::timex) -> Result<(), Error> {
if self.clock == libc::CLOCK_REALTIME {
Self::ntp_adjtime(timex)
} else {
self.clock_adjtime(timex)
}
}
#[cfg_attr(target_os = "linux", allow(unused))]
fn step_clock_timespec(&self, offset: ntp_proto::NtpDuration) -> Result<NtpTimestamp, Error> {
let (offset_secs, offset_nanos) = offset.as_seconds_nanos();
let mut timespec = self.clock_gettime()?;
#[cfg_attr(target_env = "musl", allow(deprecated))]
{
timespec.tv_sec += offset_secs as libc::time_t;
timespec.tv_nsec += offset_nanos as libc::c_long;
}
self.clock_settime(timespec)?;
Ok(current_time_timespec(timespec, Precision::Nano))
}
#[cfg(target_os = "linux")]
fn step_clock_timex(&self, offset: ntp_proto::NtpDuration) -> Result<NtpTimestamp, Error> {
let (secs, nanos) = offset.as_seconds_nanos();
let mut timex = libc::timex {
modes: libc::ADJ_SETOFFSET | libc::MOD_NANO,
time: libc::timeval {
tv_sec: secs as _,
tv_usec: nanos as libc::suseconds_t,
},
..crate::unix::EMPTY_TIMEX
};
self.adjtime(&mut timex)?;
self.extract_current_time(&timex)
}
fn extract_current_time(&self, _timex: &libc::timex) -> Result<NtpTimestamp, Error> {
#[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))
}
}
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() -> Error {
match error_number() {
libc::EINVAL => Error::Invalid,
libc::ENODEV => Error::NoDevice,
libc::EOPNOTSUPP => Error::NotSupported,
libc::EPERM => Error::NoPermission,
other => {
let error = std::io::Error::from_raw_os_error(other);
unreachable!("error code `{other}` ({error:?}) should not occur")
}
}
}
fn cerr(c_int: libc::c_int) -> Result<(), Error> {
if c_int == -1 {
Err(convert_errno())
} else {
Ok(())
}
}
fn duration_in_nanos(duration: NtpDuration) -> libc::c_long {
let (secs, nanos) = duration.as_seconds_nanos();
(secs as libc::c_long) * 1_000_000_000 + (nanos as libc::c_long)
}
pub(crate) enum Precision {
Nano,
#[cfg_attr(any(target_os = "freebsd", target_os = "macos"), allow(unused))]
Micro,
}
fn micros_to_nanos(micros: u32) -> u32 {
let msg = "microseconds out of range (this can happen when initializing hardware clocks)";
match micros.checked_mul(1000) {
Some(v) => v,
None => {
tracing::debug!(msg);
0
}
}
}
#[cfg_attr(target_os = "linux", allow(unused))]
fn current_time_timespec(timespec: libc::timespec, precision: Precision) -> NtpTimestamp {
let mut seconds = timespec.tv_sec as u32;
let mut nanos = match precision {
Precision::Nano => timespec.tv_nsec as u32,
Precision::Micro => micros_to_nanos(timespec.tv_nsec as u32),
};
while nanos > 1_000_000_000 {
seconds = seconds.wrapping_add(1);
nanos -= 1_000_000_000;
}
NtpTimestamp::from_seconds_nanos_since_ntp_era(seconds.wrapping_add(EPOCH_OFFSET), nanos)
}
#[cfg_attr(not(target_os = "linux"), allow(unused))]
fn current_time_timeval(timespec: libc::timeval, precision: Precision) -> NtpTimestamp {
NtpTimestamp::from_seconds_nanos_since_ntp_era(
(timespec.tv_sec as u32).wrapping_add(EPOCH_OFFSET),
match precision {
Precision::Nano => timespec.tv_usec as u32,
Precision::Micro => micros_to_nanos(timespec.tv_usec as u32),
},
)
}
fn ignore_not_supported(res: Result<(), Error>) -> Result<(), Error> {
match res {
Err(Error::NotSupported) => Ok(()),
other => other,
}
}
impl NtpClock for UnixNtpClock {
type Error = Error;
fn now(&self) -> Result<ntp_proto::NtpTimestamp, Error> {
let mut ntp_kapi_timex = EMPTY_TIMEX;
self.adjtime(&mut ntp_kapi_timex)?;
self.extract_current_time(&ntp_kapi_timex)
}
fn set_frequency(&self, freq: f64) -> Result<NtpTimestamp, Self::Error> {
let mut ntp_kapi_timex = EMPTY_TIMEX;
ntp_kapi_timex.modes = libc::MOD_FREQUENCY;
ntp_kapi_timex.freq = (freq * 65536e6) as libc::c_long;
ntp_kapi_timex.status =
!libc::STA_PLL & !libc::STA_PPSFREQ & !libc::STA_FLL & !libc::STA_PPSTIME;
self.adjtime(&mut ntp_kapi_timex)?;
self.extract_current_time(&ntp_kapi_timex)
}
#[cfg(target_os = "linux")]
fn step_clock(&self, offset: ntp_proto::NtpDuration) -> Result<NtpTimestamp, Self::Error> {
self.step_clock_timex(offset)
}
#[cfg(any(target_os = "freebsd", target_os = "macos"))]
fn step_clock(&self, offset: ntp_proto::NtpDuration) -> Result<NtpTimestamp, Self::Error> {
self.step_clock_timespec(offset)
}
fn enable_ntp_algorithm(&self) -> Result<(), Self::Error> {
let mut timex = EMPTY_TIMEX;
self.adjtime(&mut timex)?;
timex.modes = libc::MOD_STATUS;
timex.status |= libc::STA_PLL;
timex.status &= !libc::STA_FLL & !libc::STA_PPSTIME & !libc::STA_PPSFREQ;
self.adjtime(&mut timex)
}
fn disable_ntp_algorithm(&self) -> Result<(), Self::Error> {
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))
}
fn ntp_algorithm_update(
&self,
offset: NtpDuration,
poll_interval: PollInterval,
) -> Result<(), Self::Error> {
let mut timex = EMPTY_TIMEX;
timex.modes = libc::MOD_OFFSET | libc::MOD_TIMECONST;
timex.offset = duration_in_nanos(offset);
timex.constant = poll_interval.as_log() as libc::c_long;
ignore_not_supported(self.adjtime(&mut timex))
}
fn error_estimate_update(
&self,
est_error: NtpDuration,
max_error: NtpDuration,
) -> Result<(), Self::Error> {
let mut timex = EMPTY_TIMEX;
timex.modes = libc::MOD_ESTERROR | libc::MOD_MAXERROR;
timex.esterror = duration_in_nanos(est_error) / 1000;
timex.maxerror = duration_in_nanos(max_error) / 1000;
ignore_not_supported(self.adjtime(&mut timex))
}
fn status_update(&self, leap_status: NtpLeapIndicator) -> Result<(), Self::Error> {
let mut timex = EMPTY_TIMEX;
self.adjtime(&mut timex)?;
timex.modes = libc::MOD_STATUS;
timex.status &= !libc::STA_INS & !libc::STA_DEL & !libc::STA_UNSYNC;
match leap_status {
NtpLeapIndicator::NoWarning => {}
NtpLeapIndicator::Leap61 => timex.status |= libc::STA_INS,
NtpLeapIndicator::Leap59 => timex.status |= libc::STA_DEL,
NtpLeapIndicator::Unknown => timex.status |= libc::STA_UNSYNC,
}
ignore_not_supported(self.adjtime(&mut timex))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_now_does_not_crash() {
let clock = UnixNtpClock::realtime();
assert_ne!(
clock.now().unwrap(),
NtpTimestamp::from_seconds_nanos_since_ntp_era(0, 0)
);
}
#[test]
fn realtime_gettime() {
let clock = UnixNtpClock::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 = UnixNtpClock::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() {
UnixNtpClock::realtime()
.step_clock(NtpDuration::from_seconds(0.0))
.unwrap();
}
}