ptp-sync 0.0.2

Synchronize the system-wide real-time clock based on a source PTP clock
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
//! A (probably only Linux-compatible) crate that provides [`ClockSyncer`], which can synchronize
//! on-demand the system-wide real-time (i.e., wall-clock time) clock using a specified PTP clock
//! as the source of truth.
//!
//! It was originally developed to be used with [KVM's paravirtualized PTP clock][1] as source.
//!
//! [1]: https://elixir.bootlin.com/linux/v6.15-rc7/source/drivers/ptp/ptp_kvm_common.c

use core::mem::MaybeUninit;
use std::{io::BufRead, path::Path};

use compact_str::{CompactString, ToCompactString, format_compact}; // supports no_std
use const_format::formatcp; // supports no_std
use rustix::{
    fd::{AsFd, OwnedFd},
    fs::{Dir, FileType, Mode, OFlags, open, stat},
    io::{Errno, read},
    time::{ClockId, DynamicClockId, clock_gettime_dynamic, clock_settime},
}; // supports no_std
use tracing::{debug, trace}; // supports no_std

/// The maximum length of a PTP clock's name in bytes, based on [Linux source code][1], which
/// equals to [`32` bytes][2].
///
/// [1]: https://elixir.bootlin.com/linux/v6.15-rc7/source/include/linux/ptp_clock_kernel.h#L170
/// [2]: https://elixir.bootlin.com/linux/v6.15-rc7/source/include/linux/ptp_clock_kernel.h#L17
pub const PTP_CLOCK_NAME_LEN: usize = 32;

#[derive(Debug, ::thiserror::Error, Clone)]
pub enum Error {
    /// Either no information about the PTP driver could be found at all, or no KVM virtual PTP
    /// clock could be found.
    #[error("PTP not found: {0}")]
    NotFound(Box<str>),
    /// I/O error.
    #[error("I/O error: {msg}")]
    Io {
        msg: Box<str>,
        #[source]
        err: Errno,
    },
    /// Expected an absolute path; found a relative one.
    #[error("expected absolute path; found relative {0:?}")]
    RelativePath(CompactString),
    /// Failed to get or set the time of a clock.
    #[error("clock error: {msg}")]
    Clock {
        msg: Box<str>,
        #[source]
        err: Errno,
    },
}

/// Stores the state of the source PTP clock (i.e., an open file descriptor to it),
/// and provides [`ClockSyncer::sync`] to set on-demand the system-wide real-time
/// (i.e., wall-clock time) clock to the time of that source PTP clock.
#[derive(Debug)]
pub struct ClockSyncer {
    /// An open file descriptor referring to the source PTP clock.
    src_clk_fd: OwnedFd,
}

impl ClockSyncer {
    /// Create a new `ClockSyncer`, assuming the provided `dev_ptp_path` is the path to
    /// the device node (typically under `/dev/`) that refers to the source PTP clock.
    ///
    /// # Errors
    ///
    /// - On failure to `open(2)` the provided source PTP clock device node.
    ///
    /// # Notes
    ///
    /// It is up to the caller to provide a path to a device node that refers to a valid
    /// instance of a PTP clock. This is not checked upon `ClockSyncer` initialization,
    /// and failure to provide so will be manifested through [`Error::Clock`] errors in
    /// subsequent [`ClockSyncer::sync`] calls.
    ///
    /// The auxiliary function [`verify_ptp_dev`] attempts to verify whether a path
    /// refers to a device node that corresponds to a PTP clock device.
    ///
    /// Use [`ClockSyncer::with_kvm_clock`] (which calls [`find_ptp_kvm`]) to attempt to
    /// automatically find a device node of [KVM's paravirtualized PTP clock][1] before
    /// constructing the `ClockSyncer`.
    ///
    /// [1]: https://elixir.bootlin.com/linux/v6.15-rc7/source/drivers/ptp/ptp_kvm_common.c
    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 })
    }

    /// Create a new `ClockSyncer`, by searching for the KVM PTP clock to be used as the source.
    ///
    /// # Errors
    ///
    /// - On failure while looking for the KVM PTP clock via [`find_ptp_kvm`].
    /// - On failure to `open(2)` the KVM PTP clock device node via
    ///   [`with_ptp_clock`](Self::with_ptp_clock).
    #[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> {
        // SAFETY: Linux just gave us `fd` by successfully open(2)ing `/dev/ptpX`.
        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,
            }
        })
    }

    /// Set the system-wide real-time clock to the time provided by the source PTP clock of this
    /// `ClockSyncer`.
    ///
    /// # Errors
    ///
    /// - If `clock_gettime(2)` fails for the source PTP clock.
    /// - If `clock_settime(2)` fails for the system-wide real-time clock.
    #[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,
        })
    }

    /// Attempt to clone the `ClockSyncer`, by also `dup(2)`ing the underlying file descriptor
    /// of source PTP clock.
    ///
    /// # Errors
    ///
    /// If [`OwnedFd::try_clone`] fails.
    ///
    /// # Panics
    ///
    /// If `dup(2)` returns an error value not in `[1, 4095]`.
    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"),
            })?,
        })
    }
}

/// Search `procfs(5)` for the major number assigned to the PTP driver.
///
/// # Errors
///
/// - On failure to `open(2)` or `read(2)` the `/proc/devices` file.
/// - If no entry for the PTP driver is found at all.
///
/// # Panics
///
/// If Linux returns invalid UTF-8 when reading `/proc/devices`.
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()))
}

/// Attempt to find the path to a device node (under `/dev/`) that corresponds to the KVM PTP
/// clock.
///
/// # Errors
///
/// - On [`procfs_find_ptp_major`] failure.
/// - On failure while traversing `/dev/char/` or `/sys/dev/char/`, and reading their (and their
///   entries') contents.
///
/// # Panics
///
/// If Linux returns invalid UTF-8.
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();
        // SAFETY: Clock's name in Linux should be valid UTF-8
        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(),
    ))
}

/// Returns `true` if the provided (absolute) `path` refers to a PTP clock device node (typically
/// under `/dev/`), or `false` otherwise.
///
/// By optionally providing the `ptp_major`, the verification is accelerated by skipping the
/// reading and processing of `procfs(5)` to figure out which major number has been assigned
/// to the PTP driver by the system.
///
/// # Errors
///
/// - If the provided `path` is not absolute.
/// - If `stat(2)`ing the provided `path` fails.
/// - If `procfs_find_ptp_major` fails.
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,
    }

    /// Basically a wrapper for `stat(2)`.
    ///
    /// # Errors
    ///
    /// - If the provided `path` is not absolute.
    /// - If `stat(2)`ing the provided `path` fails.
    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
    }

    /// # Safety
    ///
    /// `clockid` must have been produced by [`fd_to_clockid`] or similar (and thus correspond
    /// to a valid, open file descriptor).
    #[inline(always)]
    pub const unsafe fn clockid_to_fd(clockid: i32) -> RawFd {
        !(clockid >> 3) as u32 as _
    }

    /// # Safety
    ///
    /// `fd` must be a valid, open file descriptor referring to a PTP clock device.
    pub unsafe fn ptp_clock_get_caps(
        fd: impl AsRawFd + AsFd,
    ) -> Result<::libc::ptp_clock_caps, Errno> {
        // SAFETY: Caller pinkie-promised that `fd` is valid.
        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);
        }

        /// NOTE(ckatsak): May need root privileges to read `/dev/ptp0`.
        #[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:?}");

            // SAFETY: Linux just gave us `fd` by successfully open(2)ing `/dev/ptp0`.
            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(())
        }
    }
}