use core::mem::MaybeUninit;
use std::{io::BufRead, path::Path};
use compact_str::{CompactString, ToCompactString, format_compact}; use const_format::formatcp; use rustix::{
fd::{AsFd, OwnedFd},
fs::{Dir, FileType, Mode, OFlags, open, stat},
io::{Errno, read},
time::{ClockId, DynamicClockId, clock_gettime_dynamic, clock_settime},
}; use tracing::{debug, trace};
pub const PTP_CLOCK_NAME_LEN: usize = 32;
#[derive(Debug, ::thiserror::Error, Clone)]
pub enum Error {
#[error("PTP not found: {0}")]
NotFound(Box<str>),
#[error("I/O error: {msg}")]
Io {
msg: Box<str>,
#[source]
err: Errno,
},
#[error("expected absolute path; found relative {0:?}")]
RelativePath(CompactString),
#[error("clock error: {msg}")]
Clock {
msg: Box<str>,
#[source]
err: Errno,
},
}
#[derive(Debug)]
pub struct ClockSyncer {
src_clk_fd: OwnedFd,
}
impl ClockSyncer {
pub fn with_ptp_clock(dev_ptp_path: impl AsRef<Path>) -> Result<Self, Error> {
let src_clk_fd =
open(dev_ptp_path.as_ref(), OFlags::RDONLY, Mode::empty()).map_err(|err| {
Error::Io {
msg: format!(
"failed to open('{}', O_RDONLY)",
dev_ptp_path.as_ref().display()
)
.into_boxed_str(),
err,
}
})?;
Ok(Self { src_clk_fd })
}
#[inline]
pub fn with_kvm_clock() -> Result<Self, Error> {
find_ptp_kvm().and_then(Self::with_ptp_clock)
}
#[cfg(feature = "clock-utils")]
pub fn ptp_clock_get_caps(&self) -> Result<::libc::ptp_clock_caps, Error> {
unsafe { clock_utils::ptp_clock_get_caps(self.src_clk_fd.as_fd()) }.map_err(|err| {
Error::Io {
msg: "failed to ioctl(PTP_CLOCK_GETCAPS) the PTP clock".into(),
err,
}
})
}
#[inline]
pub fn sync(&self) -> Result<(), Error> {
let src_clk_id = DynamicClockId::Dynamic(self.src_clk_fd.as_fd());
let now = clock_gettime_dynamic(src_clk_id).map_err(|err| Error::Clock {
msg: "source PTP clock: failed to clock_gettime(2)".into(),
err,
})?;
clock_settime(ClockId::Realtime, now).map_err(|err| Error::Clock {
msg: "system-wide real-time clock: failed to clock_settime(2)".into(),
err,
})
}
pub fn try_clone(&self) -> Result<Self, Error> {
Ok(Self {
src_clk_fd: self.src_clk_fd.try_clone().map_err(|err| Error::Io {
msg: "failed to dup(2) source PTP clock's fd".into(),
err: Errno::from_io_error(&err).expect("dup(2) does not return weird error value"),
})?,
})
}
}
pub fn procfs_find_ptp_major() -> Result<u32, Error> {
const PROC_DEVICES: &str = "/proc/devices";
const BUF_SZ: usize = 1024;
let mut buf = [MaybeUninit::<u8>::uninit(); BUF_SZ];
let fd = open(PROC_DEVICES, OFlags::RDONLY, Mode::empty()).map_err(|err| Error::Io {
msg: formatcp!("failed to open('{PROC_DEVICES}', RDONLY)").into(),
err,
})?;
let (buf, _uninit_buf) = read(&fd, &mut buf).map_err(|err| Error::Io {
msg: formatcp!("failed to read('{PROC_DEVICES}', {BUF_SZ})").into(),
err,
})?;
buf.lines()
.find_map(|line| {
let line = line.expect("Linux returns valid ASCII");
let mut words = line.trim_start().split_ascii_whitespace();
words
.next()
.and_then(|major| words.next().and_then(|dev| (dev == "ptp").then_some(major)))
.and_then(|major| major.parse::<u32>().ok())
})
.ok_or_else(|| Error::NotFound("could not find information about the PTP driver".into()))
}
pub fn find_ptp_kvm() -> Result<CompactString, Error> {
const DEVTMPFS_CDEV_BY_DEVNO: &str = "/dev/char";
const SYSFS_CDEV_BY_DEVNO: &str = "/sys/dev/char";
let ptp_major = procfs_find_ptp_major()
.inspect_err(|err| debug!(error = ?err, "Failed to procfs_find_ptp_major: {err:#}"))?;
let ptp_major_ascii = ptp_major.to_compact_string();
let dir = open(
DEVTMPFS_CDEV_BY_DEVNO,
OFlags::RDONLY | OFlags::DIRECTORY,
Mode::empty(),
)
.and_then(Dir::new)
.map_err(|err| Error::Io {
msg: formatcp!("failed to open(RDONLY) directory '{DEVTMPFS_CDEV_BY_DEVNO}'").into(),
err,
})?;
let mut buf = [MaybeUninit::<u8>::uninit(); PTP_CLOCK_NAME_LEN];
for dev_dirent in dir.into_iter() {
let dev_dirent = dev_dirent.map_err(|err| Error::Io {
msg: formatcp!("error reading direntry in {DEVTMPFS_CDEV_BY_DEVNO}").into(),
err,
})?;
let dev_dirent_name = dev_dirent.file_name();
if !dev_dirent_name
.to_bytes()
.starts_with(ptp_major_ascii.as_bytes())
{
trace!(?dev_dirent, "Ignoring...");
continue;
}
::tracing::info!(?dev_dirent, "KEEPER!");
let dev_dirent_name = dev_dirent_name.to_str().expect("should be all ASCII");
let sys_clkname_path = format!("{SYSFS_CDEV_BY_DEVNO}/{dev_dirent_name}/clock_name");
let sys_clkname_fd =
open(&sys_clkname_path, OFlags::RDONLY, Mode::empty()).map_err(|err| Error::Io {
msg: format!("failed to open('{sys_clkname_path}', RDONLY)").into_boxed_str(),
err,
})?;
let (buf, _uninit_buf) = read(sys_clkname_fd, &mut buf).map_err(|err| Error::Io {
msg: format!("failed to read('{sys_clkname_path}')").into_boxed_str(),
err,
})?;
buf.make_ascii_lowercase();
let clk_name = unsafe { CompactString::from_utf8_unchecked(buf.trim_ascii_end()) };
if !clk_name.contains("kvm") {
trace!(?dev_dirent, "Ignoring (due to clock name '{clk_name}')...");
continue;
}
return Ok(format_compact!(
"{DEVTMPFS_CDEV_BY_DEVNO}/{dev_dirent_name}"
));
}
Err(Error::NotFound(
"could not find KVM virtual PTP clock".into(),
))
}
pub fn verify_ptp_dev(dev_path: impl AsRef<Path>, ptp_major: Option<u32>) -> Result<bool, Error> {
#[derive(Debug, Clone, Copy)]
struct DevInfo {
typ: FileType,
major: u32,
#[allow(dead_code)]
minor: u32,
}
fn stat_dev_info(dev_path: impl AsRef<Path>) -> Result<DevInfo, Error> {
if !dev_path.as_ref().is_absolute() {
return Err(Error::RelativePath(
dev_path.as_ref().to_string_lossy().to_compact_string(),
));
}
let st = stat(dev_path.as_ref()).map_err(|err| Error::Io {
msg: format!("failed to stat('{}')", dev_path.as_ref().display()).into_boxed_str(),
err,
})?;
let dev_no = ::rustix::fs::Dev::from(st.st_rdev);
Ok(DevInfo {
typ: FileType::from_raw_mode(st.st_mode),
major: ::rustix::fs::major(dev_no),
minor: ::rustix::fs::minor(dev_no),
})
}
let ptp_major = ptp_major.map(Ok).unwrap_or_else(procfs_find_ptp_major)?;
let dev_info = stat_dev_info(dev_path)?;
Ok(dev_info.typ.is_char_device() && dev_info.major == ptp_major)
}
#[cfg(test)]
mod tests {
use std::time::Instant;
use anyhow::Result;
use tracing::info;
use tracing_test::traced_test;
use crate::{find_ptp_kvm, procfs_find_ptp_major, verify_ptp_dev};
#[test]
#[traced_test]
fn test_procfs_find_ptp_major() -> Result<()> {
let _start = Instant::now();
let x = procfs_find_ptp_major();
let elapsed = _start.elapsed();
info!("{elapsed:?}");
info!("{x:#?}");
Ok(())
}
#[test]
#[traced_test]
fn test_find_ptp_kvm() -> Result<()> {
let _start = Instant::now();
let x = find_ptp_kvm();
let elapsed = _start.elapsed();
info!("{elapsed:?}");
info!("{x:?}");
Ok(())
}
#[test]
#[traced_test]
fn test_verify_ptp() -> Result<()> {
let _start = Instant::now();
let x = verify_ptp_dev("/dev/ptp0", None)?;
let elapsed = _start.elapsed();
info!("- verify_ptp('/dev/ptp0', None) == {x} and ran for {elapsed:?}");
let ptp_major = procfs_find_ptp_major()?;
let _start = Instant::now();
let x = verify_ptp_dev("/dev/ptp0", Some(ptp_major))?;
let elapsed = _start.elapsed();
info!("- verify_ptp('/dev/ptp0', Some(..)) == {x} and ran for {elapsed:?}");
Ok(())
}
}
#[cfg(feature = "clock-utils")]
pub mod clock_utils {
use rustix::{
fd::{AsFd, AsRawFd, RawFd},
io::Errno,
ioctl,
};
const CLOCKFD: i32 = 3;
#[inline(always)]
pub const fn fd_to_clockid(fd: RawFd) -> i32 {
(!fd << 3) | CLOCKFD
}
#[inline(always)]
pub const unsafe fn clockid_to_fd(clockid: i32) -> RawFd {
!(clockid >> 3) as u32 as _
}
pub unsafe fn ptp_clock_get_caps(
fd: impl AsRawFd + AsFd,
) -> Result<::libc::ptp_clock_caps, Errno> {
unsafe {
ioctl::ioctl(
fd,
ioctl::Getter::<{ ::libc::PTP_CLOCK_GETCAPS }, ::libc::ptp_clock_caps>::new(),
)
}
}
#[cfg(test)]
mod tests {
use anyhow::{Context, Result};
use rustix::{
fd::{AsFd, RawFd},
fs::{Mode, OFlags, open},
};
use tracing::{info, trace};
use tracing_test::traced_test;
use super::{clockid_to_fd, fd_to_clockid, ptp_clock_get_caps};
#[test]
#[traced_test]
fn conversions() {
let fd: RawFd = 4;
info!("orig: {fd:?}");
let to_clkid = fd_to_clockid(fd);
info!("fd_to_clockid( {fd:?} ) -> {to_clkid:?}");
let back_to_fd = unsafe { clockid_to_fd(to_clkid) };
info!("clockid_to_fd( {to_clkid:?} ) -> {back_to_fd:?}");
assert_eq!(fd, back_to_fd);
}
#[test]
#[traced_test]
fn test_ioctl_get_caps() -> Result<()> {
let fd = open("/dev/ptp0", OFlags::RDONLY, Mode::empty()).context("open")?;
trace!("{fd:?}");
let bfd = fd.as_fd();
trace!("{bfd:?}");
let caps = unsafe { ptp_clock_get_caps(fd.as_fd()).context("ioctl") }?;
info!(
"Caps {{ pps: {}, max_adj: {}, n_alarm: {}, n_ext_rs: {}, n_pins: {} }}",
caps.pps, caps.max_adj, caps.n_alarm, caps.n_ext_ts, caps.n_pins,
);
Ok(())
}
}
}