Skip to main content

rtime_clock/
phc.rs

1#[cfg(target_os = "linux")]
2mod linux_phc {
3    use std::os::fd::AsRawFd;
4    use std::sync::Mutex;
5
6    use nix::sys::time::TimeSpec;
7    use nix::time::ClockId;
8    use rtime_core::clock::{Clock, ClockError};
9    use rtime_core::timestamp::{NtpDuration, NtpTimestamp, PtpTimestamp};
10
11    /// Dynamic clock ID base used by the kernel for `/dev/ptpN`.
12    ///
13    /// The kernel maps `/dev/ptpN` file descriptors to clockid_t values using:
14    ///     clockid = ~(fd << 3) | 3
15    /// This allows `clock_gettime` to read PTP hardware clocks directly.
16    const CLOCKFD: libc::clockid_t = 3;
17
18    /// Convert a raw file descriptor to a `clockid_t` that can be passed to
19    /// `clock_gettime` / `clock_settime` / `clock_adjtime`.
20    fn fd_to_clockid(fd: libc::c_int) -> libc::clockid_t {
21        (!(fd as libc::clockid_t) << 3) | CLOCKFD
22    }
23
24    /// PTP Hardware Clock (PHC) accessed via `/dev/ptpN`.
25    ///
26    /// This provides direct access to the NIC's hardware clock through the Linux
27    /// PTP subsystem. The PHC can be read with nanosecond precision and disciplined
28    /// independently of the system clock.
29    ///
30    /// The internal Mutex ensures that read-modify-write operations like `step()`
31    /// are atomic with respect to concurrent callers.
32    pub struct PhcClock {
33        /// Owned handle; File's Drop closes the fd automatically.
34        file: std::fs::File,
35        clockid: libc::clockid_t,
36        device_path: String,
37        /// Guards clock mutation operations (step, adjust_frequency) to prevent
38        /// TOCTOU races from concurrent calls via &self.
39        discipline_lock: Mutex<()>,
40    }
41
42    impl std::fmt::Debug for PhcClock {
43        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44            f.debug_struct("PhcClock")
45                .field("fd", &self.file.as_raw_fd())
46                .field("clockid", &self.clockid)
47                .field("device_path", &self.device_path)
48                .finish()
49        }
50    }
51
52    impl PhcClock {
53        /// Open a PTP hardware clock device.
54        ///
55        /// `device` should be a path like `/dev/ptp0`. The device must exist and be
56        /// readable by the current process.
57        pub fn open(device: &str) -> Result<Self, ClockError> {
58            let file = std::fs::OpenOptions::new()
59                .read(true)
60                .write(true)
61                .open(device)
62                .map_err(|err| match err.raw_os_error() {
63                    Some(libc::ENOENT) | Some(libc::ENXIO) => ClockError::DeviceNotFound,
64                    Some(libc::EACCES) | Some(libc::EPERM) => ClockError::PermissionDenied,
65                    _ => ClockError::Os(err),
66                })?;
67
68            let clockid = fd_to_clockid(file.as_raw_fd());
69
70            Ok(Self {
71                file,
72                clockid,
73                device_path: device.to_string(),
74                discipline_lock: Mutex::new(()),
75            })
76        }
77
78        /// Read the PHC time as a `PtpTimestamp`.
79        ///
80        /// Uses `clock_gettime` with the dynamic clockid derived from the PHC file
81        /// descriptor.
82        pub fn read_time(&self) -> Result<PtpTimestamp, ClockError> {
83            let ts = nix::time::clock_gettime(ClockId::from_raw(self.clockid)).map_err(|e| {
84                let err: std::io::Error = e.into();
85                match err.raw_os_error() {
86                    Some(libc::EINVAL) => ClockError::DeviceNotFound,
87                    _ => ClockError::Os(err),
88                }
89            })?;
90
91            Ok(PtpTimestamp::new(ts.tv_sec() as u64, ts.tv_nsec() as u32))
92        }
93
94        /// Get the device path this clock was opened from.
95        pub fn device_path(&self) -> &str {
96            &self.device_path
97        }
98
99        /// Get the raw file descriptor (for advanced ioctl operations).
100        pub fn as_raw_fd(&self) -> libc::c_int {
101            self.file.as_raw_fd()
102        }
103
104        /// Get the dynamic clockid for this PHC.
105        pub fn clockid(&self) -> libc::clockid_t {
106            self.clockid
107        }
108    }
109
110    impl Clock for PhcClock {
111        fn now(&self) -> Result<NtpTimestamp, ClockError> {
112            let ptp_ts = self.read_time()?;
113            Ok(ptp_ts.to_ntp_timestamp())
114        }
115
116        fn step(&self, offset: NtpDuration) -> Result<(), ClockError> {
117            // Hold lock to prevent TOCTOU race in read-modify-write.
118            let _guard = self
119                .discipline_lock
120                .lock()
121                .map_err(|_| ClockError::Os(std::io::Error::other("discipline lock poisoned")))?;
122
123            let clock = ClockId::from_raw(self.clockid);
124            let ts = nix::time::clock_gettime(clock).map_err(|e| ClockError::Os(e.into()))?;
125
126            let nanos = offset.to_nanos();
127            let total_ns = ts.tv_sec() as i64 * 1_000_000_000 + ts.tv_nsec() as i64 + nanos;
128
129            if total_ns < 0 {
130                return Err(ClockError::Os(std::io::Error::new(
131                    std::io::ErrorKind::InvalidInput,
132                    "step would set clock to negative time",
133                )));
134            }
135
136            let new_ts = TimeSpec::new(
137                (total_ns / 1_000_000_000) as libc::time_t,
138                (total_ns % 1_000_000_000) as libc::c_long,
139            );
140
141            nix::time::clock_settime(clock, new_ts).map_err(|e| {
142                let err: std::io::Error = e.into();
143                match err.raw_os_error() {
144                    Some(libc::EPERM) => ClockError::PermissionDenied,
145                    _ => ClockError::Os(err),
146                }
147            })?;
148
149            Ok(())
150        }
151
152        fn adjust_frequency(&self, ppm: f64) -> Result<(), ClockError> {
153            let _guard = self
154                .discipline_lock
155                .lock()
156                .map_err(|_| ClockError::Os(std::io::Error::other("discipline lock poisoned")))?;
157
158            // Use clock_adjtime to adjust the PHC frequency.
159            let freq = (ppm * 65536.0) as i64;
160
161            let mut tx = crate::adjtime::Timex::new();
162            tx.0.modes = libc::ADJ_FREQUENCY;
163            tx.0.freq = freq;
164
165            crate::adjtime::clock_adjtime(self.clockid, &mut tx).map_err(|err| {
166                match err.raw_os_error() {
167                    Some(libc::EPERM) => ClockError::PermissionDenied,
168                    Some(libc::EOPNOTSUPP) => ClockError::NotSupported,
169                    _ => ClockError::Os(err),
170                }
171            })?;
172
173            Ok(())
174        }
175
176        fn frequency_offset(&self) -> Result<f64, ClockError> {
177            let mut tx = crate::adjtime::Timex::new(); // modes = 0 => query
178            crate::adjtime::clock_adjtime(self.clockid, &mut tx).map_err(ClockError::Os)?;
179            Ok(tx.0.freq as f64 / 65536.0)
180        }
181
182        fn resolution(&self) -> NtpDuration {
183            // PHC resolution varies by hardware; assume 1ns as a reasonable default.
184            NtpDuration::from_nanos(1)
185        }
186
187        fn max_frequency_adjustment(&self) -> f64 {
188            // Most PHCs support wider adjustments than the system clock.
189            // Common NICs (Intel i210, etc.) support up to ~1000 ppm.
190            1000.0
191        }
192
193        fn is_adjustable(&self) -> bool {
194            // If we could open it O_RDWR, it's likely adjustable (with CAP_SYS_TIME).
195            true
196        }
197    }
198
199    #[cfg(test)]
200    mod tests {
201        use super::*;
202
203        #[test]
204        fn fd_to_clockid_formula() {
205            // The formula is: clockid = (~fd << 3) | 3
206            // For fd=0: (~0 << 3) | 3 = (-1 << 3) | 3 = -8 | 3 = -5
207            let cid = fd_to_clockid(0);
208            assert_eq!(cid, -5);
209
210            // For fd=3: (~3 << 3) | 3 = (-4 << 3) | 3 = -32 | 3 = -29
211            let cid = fd_to_clockid(3);
212            assert_eq!(cid, (-4i32 << 3) | 3);
213            assert_eq!(cid, -29);
214        }
215
216        #[test]
217        fn open_nonexistent_device() {
218            let result = PhcClock::open("/dev/ptp_nonexistent_999");
219            assert!(result.is_err());
220            match result.unwrap_err() {
221                ClockError::DeviceNotFound => {}
222                ClockError::PermissionDenied => {
223                    // In some environments, /dev/ access might be denied entirely.
224                }
225                other => panic!("expected DeviceNotFound or PermissionDenied, got: {other}"),
226            }
227        }
228
229        #[test]
230        fn open_ptp0_if_available() {
231            // This test only runs meaningfully on systems with a PHC.
232            match PhcClock::open("/dev/ptp0") {
233                Ok(phc) => {
234                    // If we can open it, try reading.
235                    match phc.read_time() {
236                        Ok(ts) => {
237                            assert!(ts.seconds > 0, "PHC time should be non-zero");
238                        }
239                        Err(e) => {
240                            eprintln!("read_time failed (may be expected): {e}");
241                        }
242                    }
243                }
244                Err(_) => {
245                    // No PHC available -- that's fine.
246                }
247            }
248        }
249    }
250}
251
252#[cfg(target_os = "linux")]
253pub use linux_phc::PhcClock;
254
255/// Stub PhcClock for non-Linux platforms (PTP hardware clocks are Linux-specific).
256#[cfg(not(target_os = "linux"))]
257pub struct PhcClock;
258
259#[cfg(not(target_os = "linux"))]
260impl PhcClock {
261    /// PTP hardware clocks are not supported on this platform.
262    pub fn open(_device: &str) -> Result<Self, rtime_core::clock::ClockError> {
263        Err(rtime_core::clock::ClockError::NotSupported)
264    }
265}