use crate::quality::SyncQuality;
use std::path::Path;
const NTP_MARKERS: [&str; 4] = [
"/run/systemd/timesync/synchronized",
"/run/chrony/chronyd.pid",
"/var/run/ntpd.pid",
"/run/ntpd.pid",
];
#[must_use]
pub fn detect_sync_quality() -> SyncQuality {
detect_sync_quality_in(Path::new("/sys/class/ptp"))
}
#[must_use]
pub fn detect_sync_quality_in(root: &Path) -> SyncQuality {
let ptp_device = ptp_device_present(root);
let tai = tai_offset_coherent();
let ntp_hint = ntp_runtime_hint();
classify_sync_quality(ptp_device, tai, ntp_hint)
}
#[must_use]
pub fn classify_sync_quality(ptp_device: bool, tai_coherent: bool, ntp_hint: bool) -> SyncQuality {
if ptp_device {
if tai_coherent {
SyncQuality::Ptp
} else {
SyncQuality::Fallback
}
} else if tai_coherent || ntp_hint {
SyncQuality::Ntp
} else {
SyncQuality::Fallback
}
}
#[must_use]
pub fn tai_offset_ns() -> Option<i64> {
tai_offset_once()
}
#[must_use]
pub fn tai_offset_coherent() -> bool {
#[cfg(target_os = "linux")]
{
const SANE_MAX: i64 = 65_535 * 1_000_000_000;
const STABLE_NS: i64 = 1_000_000; let Some(a) = tai_offset_once() else {
return false;
};
let Some(b) = tai_offset_once() else {
return false;
};
(0..=SANE_MAX).contains(&a) && (a - b).abs() <= STABLE_NS
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
#[cfg(target_os = "linux")]
fn tai_offset_once() -> Option<i64> {
let realtime = crate::low::clock_gettime_ns_i64(libc::CLOCK_REALTIME)?;
let tai = crate::low::clock_gettime_ns_i64(libc::CLOCK_TAI)?;
Some(tai.wrapping_sub(realtime))
}
#[cfg(not(target_os = "linux"))]
fn tai_offset_once() -> Option<i64> {
None
}
fn ptp_device_present(root: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(root) else {
return false;
};
entries
.filter_map(core::result::Result::ok)
.any(|e| is_ptp_device_name(&e.file_name().to_string_lossy()))
}
fn is_ptp_device_name(name: &str) -> bool {
match name.strip_prefix("ptp") {
Some(rest) => !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()),
None => false,
}
}
fn ntp_runtime_hint() -> bool {
NTP_MARKERS.iter().any(|p| std::path::Path::new(p).exists())
}
#[cfg(test)]
mod tests {
use super::{classify_sync_quality, is_ptp_device_name};
use crate::quality::SyncQuality;
#[test]
fn classification_truth_table() {
use SyncQuality::{Fallback, Ntp, Ptp};
assert_eq!(classify_sync_quality(true, true, false), Ptp);
assert_eq!(classify_sync_quality(true, true, true), Ptp);
assert_eq!(classify_sync_quality(true, false, false), Fallback); assert_eq!(classify_sync_quality(true, false, true), Fallback);
assert_eq!(classify_sync_quality(false, true, false), Ntp);
assert_eq!(classify_sync_quality(false, false, true), Ntp);
assert_eq!(classify_sync_quality(false, false, false), Fallback);
}
#[test]
fn ptp_name_parsing_is_minimal() {
assert!(is_ptp_device_name("ptp0"));
assert!(is_ptp_device_name("ptp42"));
assert!(!is_ptp_device_name("ptp"));
assert!(!is_ptp_device_name("ptpX"));
assert!(!is_ptp_device_name("ptp0a"));
assert!(!is_ptp_device_name("clock0"));
assert!(!is_ptp_device_name(""));
}
}