ptp_sync/
lib.rs

1//! A (probably only Linux-compatible) crate that provides [`ClockSyncer`], which can synchronize
2//! on-demand the system-wide real-time (i.e., wall-clock time) clock using a specified PTP clock
3//! as the source of truth.
4//!
5//! It was originally developed to be used with [KVM's paravirtualized PTP clock][1] as source.
6//!
7//! [1]: https://elixir.bootlin.com/linux/v6.15-rc7/source/drivers/ptp/ptp_kvm_common.c
8
9use core::mem::MaybeUninit;
10use std::{io::BufRead, path::Path};
11
12use compact_str::{CompactString, ToCompactString, format_compact}; // supports no_std
13use const_format::formatcp; // supports no_std
14use rustix::{
15    fd::{AsFd, OwnedFd},
16    fs::{Dir, FileType, Mode, OFlags, open, stat},
17    io::{Errno, read},
18    time::{ClockId, DynamicClockId, clock_gettime_dynamic, clock_settime},
19}; // supports no_std
20use tracing::{debug, trace}; // supports no_std
21
22/// The maximum length of a PTP clock's name in bytes, based on [Linux source code][1], which
23/// equals to [`32` bytes][2].
24///
25/// [1]: https://elixir.bootlin.com/linux/v6.15-rc7/source/include/linux/ptp_clock_kernel.h#L170
26/// [2]: https://elixir.bootlin.com/linux/v6.15-rc7/source/include/linux/ptp_clock_kernel.h#L17
27pub const PTP_CLOCK_NAME_LEN: usize = 32;
28
29#[derive(Debug, ::thiserror::Error, Clone)]
30pub enum Error {
31    /// Either no information about the PTP driver could be found at all, or no KVM virtual PTP
32    /// clock could be found.
33    #[error("PTP not found: {0}")]
34    NotFound(Box<str>),
35    /// I/O error.
36    #[error("I/O error: {msg}")]
37    Io {
38        msg: Box<str>,
39        #[source]
40        err: Errno,
41    },
42    /// Expected an absolute path; found a relative one.
43    #[error("expected absolute path; found relative {0:?}")]
44    RelativePath(CompactString),
45    /// Failed to get or set the time of a clock.
46    #[error("clock error: {msg}")]
47    Clock {
48        msg: Box<str>,
49        #[source]
50        err: Errno,
51    },
52}
53
54/// Stores the state of the source PTP clock (i.e., an open file descriptor to it),
55/// and provides [`ClockSyncer::sync`] to set on-demand the system-wide real-time
56/// (i.e., wall-clock time) clock to the time of that source PTP clock.
57#[derive(Debug)]
58pub struct ClockSyncer {
59    /// An open file descriptor referring to the source PTP clock.
60    src_clk_fd: OwnedFd,
61}
62
63impl ClockSyncer {
64    /// Create a new `ClockSyncer`, assuming the provided `dev_ptp_path` is the path to
65    /// the device node (typically under `/dev/`) that refers to the source PTP clock.
66    ///
67    /// # Errors
68    ///
69    /// - On failure to `open(2)` the provided source PTP clock device node.
70    ///
71    /// # Notes
72    ///
73    /// It is up to the caller to provide a path to a device node that refers to a valid
74    /// instance of a PTP clock. This is not checked upon `ClockSyncer` initialization,
75    /// and failure to provide so will be manifested through [`Error::Clock`] errors in
76    /// subsequent [`ClockSyncer::sync`] calls.
77    ///
78    /// The auxiliary function [`verify_ptp_dev`] attempts to verify whether a path
79    /// refers to a device node that corresponds to a PTP clock device.
80    ///
81    /// Use [`ClockSyncer::with_kvm_clock`] (which calls [`find_ptp_kvm`]) to attempt to
82    /// automatically find a device node of [KVM's paravirtualized PTP clock][1] before
83    /// constructing the `ClockSyncer`.
84    ///
85    /// [1]: https://elixir.bootlin.com/linux/v6.15-rc7/source/drivers/ptp/ptp_kvm_common.c
86    pub fn with_ptp_clock(dev_ptp_path: impl AsRef<Path>) -> Result<Self, Error> {
87        let src_clk_fd =
88            open(dev_ptp_path.as_ref(), OFlags::RDONLY, Mode::empty()).map_err(|err| {
89                Error::Io {
90                    msg: format!(
91                        "failed to open('{}', O_RDONLY)",
92                        dev_ptp_path.as_ref().display()
93                    )
94                    .into_boxed_str(),
95                    err,
96                }
97            })?;
98
99        Ok(Self { src_clk_fd })
100    }
101
102    /// Create a new `ClockSyncer`, by searching for the KVM PTP clock to be used as the source.
103    ///
104    /// # Errors
105    ///
106    /// - On failure while looking for the KVM PTP clock via [`find_ptp_kvm`].
107    /// - On failure to `open(2)` the KVM PTP clock device node via
108    ///   [`with_ptp_clock`](Self::with_ptp_clock).
109    #[inline]
110    pub fn with_kvm_clock() -> Result<Self, Error> {
111        find_ptp_kvm().and_then(Self::with_ptp_clock)
112    }
113
114    #[cfg(feature = "clock-utils")]
115    pub fn ptp_clock_get_caps(&self) -> Result<::libc::ptp_clock_caps, Error> {
116        // SAFETY: Linux just gave us `fd` by successfully open(2)ing `/dev/ptpX`.
117        unsafe { clock_utils::ptp_clock_get_caps(self.src_clk_fd.as_fd()) }.map_err(|err| {
118            Error::Io {
119                msg: "failed to ioctl(PTP_CLOCK_GETCAPS) the PTP clock".into(),
120                err,
121            }
122        })
123    }
124
125    /// Set the system-wide real-time clock to the time provided by the source PTP clock of this
126    /// `ClockSyncer`.
127    ///
128    /// # Errors
129    ///
130    /// - If `clock_gettime(2)` fails for the source PTP clock.
131    /// - If `clock_settime(2)` fails for the system-wide real-time clock.
132    #[inline]
133    pub fn sync(&self) -> Result<(), Error> {
134        let src_clk_id = DynamicClockId::Dynamic(self.src_clk_fd.as_fd());
135        let now = clock_gettime_dynamic(src_clk_id).map_err(|err| Error::Clock {
136            msg: "source PTP clock: failed to clock_gettime(2)".into(),
137            err,
138        })?;
139        clock_settime(ClockId::Realtime, now).map_err(|err| Error::Clock {
140            msg: "system-wide real-time clock: failed to clock_settime(2)".into(),
141            err,
142        })
143    }
144
145    /// Attempt to clone the `ClockSyncer`, by also `dup(2)`ing the underlying file descriptor
146    /// of source PTP clock.
147    ///
148    /// # Errors
149    ///
150    /// If [`OwnedFd::try_clone`] fails.
151    ///
152    /// # Panics
153    ///
154    /// If `dup(2)` returns an error value not in `[1, 4095]`.
155    pub fn try_clone(&self) -> Result<Self, Error> {
156        Ok(Self {
157            src_clk_fd: self.src_clk_fd.try_clone().map_err(|err| Error::Io {
158                msg: "failed to dup(2) source PTP clock's fd".into(),
159                err: Errno::from_io_error(&err).expect("dup(2) does not return weird error value"),
160            })?,
161        })
162    }
163}
164
165/// Search `procfs(5)` for the major number assigned to the PTP driver.
166///
167/// # Errors
168///
169/// - On failure to `open(2)` or `read(2)` the `/proc/devices` file.
170/// - If no entry for the PTP driver is found at all.
171///
172/// # Panics
173///
174/// If Linux returns invalid UTF-8 when reading `/proc/devices`.
175pub fn procfs_find_ptp_major() -> Result<u32, Error> {
176    const PROC_DEVICES: &str = "/proc/devices";
177    const BUF_SZ: usize = 1024;
178
179    let mut buf = [MaybeUninit::<u8>::uninit(); BUF_SZ];
180    let fd = open(PROC_DEVICES, OFlags::RDONLY, Mode::empty()).map_err(|err| Error::Io {
181        msg: formatcp!("failed to open('{PROC_DEVICES}', RDONLY)").into(),
182        err,
183    })?;
184    let (buf, _uninit_buf) = read(&fd, &mut buf).map_err(|err| Error::Io {
185        msg: formatcp!("failed to read('{PROC_DEVICES}', {BUF_SZ})").into(),
186        err,
187    })?;
188
189    buf.lines()
190        .find_map(|line| {
191            let line = line.expect("Linux returns valid ASCII");
192            let mut words = line.trim_start().split_ascii_whitespace();
193            words
194                .next()
195                .and_then(|major| words.next().and_then(|dev| (dev == "ptp").then_some(major)))
196                .and_then(|major| major.parse::<u32>().ok())
197        })
198        .ok_or_else(|| Error::NotFound("could not find information about the PTP driver".into()))
199}
200
201/// Attempt to find the path to a device node (under `/dev/`) that corresponds to the KVM PTP
202/// clock.
203///
204/// # Errors
205///
206/// - On [`procfs_find_ptp_major`] failure.
207/// - On failure while traversing `/dev/char/` or `/sys/dev/char/`, and reading their (and their
208///   entries') contents.
209///
210/// # Panics
211///
212/// If Linux returns invalid UTF-8.
213pub fn find_ptp_kvm() -> Result<CompactString, Error> {
214    const DEVTMPFS_CDEV_BY_DEVNO: &str = "/dev/char";
215    const SYSFS_CDEV_BY_DEVNO: &str = "/sys/dev/char";
216
217    let ptp_major = procfs_find_ptp_major()
218        .inspect_err(|err| debug!(error = ?err, "Failed to procfs_find_ptp_major: {err:#}"))?;
219    let ptp_major_ascii = ptp_major.to_compact_string();
220
221    let dir = open(
222        DEVTMPFS_CDEV_BY_DEVNO,
223        OFlags::RDONLY | OFlags::DIRECTORY,
224        Mode::empty(),
225    )
226    .and_then(Dir::new)
227    .map_err(|err| Error::Io {
228        msg: formatcp!("failed to open(RDONLY) directory '{DEVTMPFS_CDEV_BY_DEVNO}'").into(),
229        err,
230    })?;
231
232    let mut buf = [MaybeUninit::<u8>::uninit(); PTP_CLOCK_NAME_LEN];
233    for dev_dirent in dir.into_iter() {
234        let dev_dirent = dev_dirent.map_err(|err| Error::Io {
235            msg: formatcp!("error reading direntry in {DEVTMPFS_CDEV_BY_DEVNO}").into(),
236            err,
237        })?;
238
239        let dev_dirent_name = dev_dirent.file_name();
240        if !dev_dirent_name
241            .to_bytes()
242            .starts_with(ptp_major_ascii.as_bytes())
243        {
244            trace!(?dev_dirent, "Ignoring...");
245            continue;
246        }
247        ::tracing::info!(?dev_dirent, "KEEPER!");
248
249        let dev_dirent_name = dev_dirent_name.to_str().expect("should be all ASCII");
250        let sys_clkname_path = format!("{SYSFS_CDEV_BY_DEVNO}/{dev_dirent_name}/clock_name");
251        let sys_clkname_fd =
252            open(&sys_clkname_path, OFlags::RDONLY, Mode::empty()).map_err(|err| Error::Io {
253                msg: format!("failed to open('{sys_clkname_path}', RDONLY)").into_boxed_str(),
254                err,
255            })?;
256        let (buf, _uninit_buf) = read(sys_clkname_fd, &mut buf).map_err(|err| Error::Io {
257            msg: format!("failed to read('{sys_clkname_path}')").into_boxed_str(),
258            err,
259        })?;
260        buf.make_ascii_lowercase();
261        // SAFETY: Clock's name in Linux should be valid UTF-8
262        let clk_name = unsafe { CompactString::from_utf8_unchecked(buf.trim_ascii_end()) };
263        if !clk_name.contains("kvm") {
264            trace!(?dev_dirent, "Ignoring (due to clock name '{clk_name}')...");
265            continue;
266        }
267
268        return Ok(format_compact!(
269            "{DEVTMPFS_CDEV_BY_DEVNO}/{dev_dirent_name}"
270        ));
271    }
272    Err(Error::NotFound(
273        "could not find KVM virtual PTP clock".into(),
274    ))
275}
276
277/// Returns `true` if the provided (absolute) `path` refers to a PTP clock device node (typically
278/// under `/dev/`), or `false` otherwise.
279///
280/// By optionally providing the `ptp_major`, the verification is accelerated by skipping the
281/// reading and processing of `procfs(5)` to figure out which major number has been assigned
282/// to the PTP driver by the system.
283///
284/// # Errors
285///
286/// - If the provided `path` is not absolute.
287/// - If `stat(2)`ing the provided `path` fails.
288/// - If `procfs_find_ptp_major` fails.
289pub fn verify_ptp_dev(dev_path: impl AsRef<Path>, ptp_major: Option<u32>) -> Result<bool, Error> {
290    #[derive(Debug, Clone, Copy)]
291    struct DevInfo {
292        typ: FileType,
293        major: u32,
294        #[allow(dead_code)]
295        minor: u32,
296    }
297
298    /// Basically a wrapper for `stat(2)`.
299    ///
300    /// # Errors
301    ///
302    /// - If the provided `path` is not absolute.
303    /// - If `stat(2)`ing the provided `path` fails.
304    fn stat_dev_info(dev_path: impl AsRef<Path>) -> Result<DevInfo, Error> {
305        if !dev_path.as_ref().is_absolute() {
306            return Err(Error::RelativePath(
307                dev_path.as_ref().to_string_lossy().to_compact_string(),
308            ));
309        }
310
311        let st = stat(dev_path.as_ref()).map_err(|err| Error::Io {
312            msg: format!("failed to stat('{}')", dev_path.as_ref().display()).into_boxed_str(),
313            err,
314        })?;
315
316        let dev_no = ::rustix::fs::Dev::from(st.st_rdev);
317        Ok(DevInfo {
318            typ: FileType::from_raw_mode(st.st_mode),
319            major: ::rustix::fs::major(dev_no),
320            minor: ::rustix::fs::minor(dev_no),
321        })
322    }
323
324    let ptp_major = ptp_major.map(Ok).unwrap_or_else(procfs_find_ptp_major)?;
325
326    let dev_info = stat_dev_info(dev_path)?;
327    Ok(dev_info.typ.is_char_device() && dev_info.major == ptp_major)
328}
329
330#[cfg(test)]
331mod tests {
332    use std::time::Instant;
333
334    use anyhow::Result;
335    use tracing::info;
336    use tracing_test::traced_test;
337
338    use crate::{find_ptp_kvm, procfs_find_ptp_major, verify_ptp_dev};
339
340    #[test]
341    #[traced_test]
342    fn test_procfs_find_ptp_major() -> Result<()> {
343        let _start = Instant::now();
344        let x = procfs_find_ptp_major();
345        let elapsed = _start.elapsed();
346        info!("{elapsed:?}");
347        info!("{x:#?}");
348        Ok(())
349    }
350
351    #[test]
352    #[traced_test]
353    fn test_find_ptp_kvm() -> Result<()> {
354        let _start = Instant::now();
355        let x = find_ptp_kvm();
356        let elapsed = _start.elapsed();
357        info!("{elapsed:?}");
358        info!("{x:?}");
359        Ok(())
360    }
361
362    #[test]
363    #[traced_test]
364    fn test_verify_ptp() -> Result<()> {
365        let _start = Instant::now();
366        let x = verify_ptp_dev("/dev/ptp0", None)?;
367        let elapsed = _start.elapsed();
368        info!("- verify_ptp('/dev/ptp0', None) == {x} and ran for {elapsed:?}");
369
370        let ptp_major = procfs_find_ptp_major()?;
371        let _start = Instant::now();
372        let x = verify_ptp_dev("/dev/ptp0", Some(ptp_major))?;
373        let elapsed = _start.elapsed();
374        info!("- verify_ptp('/dev/ptp0', Some(..)) == {x} and ran for {elapsed:?}");
375
376        Ok(())
377    }
378}
379
380#[cfg(feature = "clock-utils")]
381pub mod clock_utils {
382    use rustix::{
383        fd::{AsFd, AsRawFd, RawFd},
384        io::Errno,
385        ioctl,
386    };
387
388    const CLOCKFD: i32 = 3;
389
390    #[inline(always)]
391    pub const fn fd_to_clockid(fd: RawFd) -> i32 {
392        (!fd << 3) | CLOCKFD
393    }
394
395    /// # Safety
396    ///
397    /// `clockid` must have been produced by [`fd_to_clockid`] or similar (and thus correspond
398    /// to a valid, open file descriptor).
399    #[inline(always)]
400    pub const unsafe fn clockid_to_fd(clockid: i32) -> RawFd {
401        !(clockid >> 3) as u32 as _
402    }
403
404    /// # Safety
405    ///
406    /// `fd` must be a valid, open file descriptor referring to a PTP clock device.
407    pub unsafe fn ptp_clock_get_caps(
408        fd: impl AsRawFd + AsFd,
409    ) -> Result<::libc::ptp_clock_caps, Errno> {
410        // SAFETY: Caller pinkie-promised that `fd` is valid.
411        unsafe {
412            ioctl::ioctl(
413                fd,
414                ioctl::Getter::<{ ::libc::PTP_CLOCK_GETCAPS }, ::libc::ptp_clock_caps>::new(),
415            )
416        }
417    }
418
419    #[cfg(test)]
420    mod tests {
421        use anyhow::{Context, Result};
422        use rustix::{
423            fd::{AsFd, RawFd},
424            fs::{Mode, OFlags, open},
425        };
426        use tracing::{info, trace};
427        use tracing_test::traced_test;
428
429        use super::{clockid_to_fd, fd_to_clockid, ptp_clock_get_caps};
430
431        #[test]
432        #[traced_test]
433        fn conversions() {
434            let fd: RawFd = 4;
435            info!("orig: {fd:?}");
436
437            let to_clkid = fd_to_clockid(fd);
438            info!("fd_to_clockid( {fd:?} ) -> {to_clkid:?}");
439
440            let back_to_fd = unsafe { clockid_to_fd(to_clkid) };
441            info!("clockid_to_fd( {to_clkid:?} ) -> {back_to_fd:?}");
442
443            assert_eq!(fd, back_to_fd);
444        }
445
446        /// NOTE(ckatsak): May need root privileges to read `/dev/ptp0`.
447        #[test]
448        #[traced_test]
449        fn test_ioctl_get_caps() -> Result<()> {
450            let fd = open("/dev/ptp0", OFlags::RDONLY, Mode::empty()).context("open")?;
451            trace!("{fd:?}");
452            let bfd = fd.as_fd();
453            trace!("{bfd:?}");
454
455            // SAFETY: Linux just gave us `fd` by successfully open(2)ing `/dev/ptp0`.
456            let caps = unsafe { ptp_clock_get_caps(fd.as_fd()).context("ioctl") }?;
457            info!(
458                "Caps {{ pps: {}, max_adj: {}, n_alarm: {}, n_ext_rs: {}, n_pins: {} }}",
459                caps.pps, caps.max_adj, caps.n_alarm, caps.n_ext_ts, caps.n_pins,
460            );
461
462            Ok(())
463        }
464    }
465}