1use core::mem::MaybeUninit;
10use std::{io::BufRead, path::Path};
11
12use compact_str::{CompactString, ToCompactString, format_compact}; use const_format::formatcp; use 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}; use tracing::{debug, trace}; pub const PTP_CLOCK_NAME_LEN: usize = 32;
28
29#[derive(Debug, ::thiserror::Error, Clone)]
30pub enum Error {
31 #[error("PTP not found: {0}")]
34 NotFound(Box<str>),
35 #[error("I/O error: {msg}")]
37 Io {
38 msg: Box<str>,
39 #[source]
40 err: Errno,
41 },
42 #[error("expected absolute path; found relative {0:?}")]
44 RelativePath(CompactString),
45 #[error("clock error: {msg}")]
47 Clock {
48 msg: Box<str>,
49 #[source]
50 err: Errno,
51 },
52}
53
54#[derive(Debug)]
58pub struct ClockSyncer {
59 src_clk_fd: OwnedFd,
61}
62
63impl ClockSyncer {
64 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 #[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 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 #[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 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
165pub 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
201pub 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 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
277pub 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 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 #[inline(always)]
400 pub const unsafe fn clockid_to_fd(clockid: i32) -> RawFd {
401 !(clockid >> 3) as u32 as _
402 }
403
404 pub unsafe fn ptp_clock_get_caps(
408 fd: impl AsRawFd + AsFd,
409 ) -> Result<::libc::ptp_clock_caps, Errno> {
410 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 #[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 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}