cadmus_core/
time_manager.rs1use anyhow::Error;
2use chrono::{DateTime, Utc};
3use sntpc::{NtpContext, StdTimestampGen};
4use sntpc_net_std::UdpSocketWrapper;
5use std::net::{ToSocketAddrs, UdpSocket};
6use std::sync::mpsc::Sender;
7use std::time::Duration;
8
9use crate::device::CURRENT_DEVICE;
10use crate::geolocation;
11use crate::geolocation::GeoLocation;
12use crate::http::Client as HttpClient;
13use crate::rtc::Rtc;
14use crate::view::{Event, NotificationEvent};
15
16const NTP_TIMEOUT: Duration = Duration::from_secs(5);
17
18pub struct TimeManager {
19 rtc: Rtc,
20}
21
22impl TimeManager {
23 pub fn new(rtc: Rtc) -> Self {
24 TimeManager { rtc }
25 }
26
27 pub fn sync(
28 &self,
29 ntp_host: &str,
30 manual: bool,
31 geolocation: Option<GeoLocation>,
32 hub: &Sender<Event>,
33 ) -> Result<(), Error> {
34 if let Err(e) = self.detect_and_set_timezone(geolocation) {
35 if manual {
36 hub.send(Event::Notification(NotificationEvent::Show(crate::fl!(
37 "notification-timezone-detection-failed"
38 ))))
39 .ok();
40 }
41 tracing::warn!(error = %e, "timezone detection failed");
42 }
43
44 let ntp_time = match self.query_ntp(ntp_host) {
45 Ok(t) => t,
46 Err(e) => {
47 if manual {
48 hub.send(Event::Notification(NotificationEvent::Show(crate::fl!(
49 "notification-time-sync-failed"
50 ))))
51 .ok();
52 } else {
53 tracing::warn!(error = %e, "ntp query failed");
54 }
55 return Err(e);
56 }
57 };
58
59 let result = self
60 .set_system_clock(ntp_time)
61 .and_then(|()| self.rtc.set_time(ntp_time));
62
63 match result {
64 Ok(()) => {
65 tracing::info!(time = %ntp_time, "time synced");
66 hub.send(Event::ClockTick).ok();
67 Ok(())
68 }
69 Err(e) => {
70 if manual {
71 hub.send(Event::Notification(NotificationEvent::Show(crate::fl!(
72 "notification-time-sync-failed"
73 ))))
74 .ok();
75 }
76 tracing::warn!(error = %e, "set_system_clock or rtc.set_time failed");
77 Err(e)
78 }
79 }
80 }
81
82 fn detect_and_set_timezone(&self, geolocation: Option<GeoLocation>) -> Result<(), Error> {
83 let geo = match geolocation {
84 Some(geo) => geo,
85 None => {
86 let client = HttpClient::new()?;
87
88 geolocation::fetch_geolocation(&client)?
89 }
90 };
91
92 CURRENT_DEVICE.set_system_timezone(geo.timezone)?;
93
94 Ok(())
95 }
96
97 fn query_ntp(&self, host: &str) -> Result<DateTime<Utc>, Error> {
98 query_ntp(host)
99 }
100
101 fn set_system_clock(&self, time: DateTime<Utc>) -> Result<(), Error> {
102 let tv = libc::timeval {
103 tv_sec: time.timestamp() as libc::time_t,
104 tv_usec: time.timestamp_subsec_micros() as libc::suseconds_t,
105 };
106 let ret = unsafe { libc::settimeofday(&tv, std::ptr::null()) };
107 if ret != 0 {
108 return Err(anyhow::anyhow!(
109 "settimeofday failed: {}",
110 std::io::Error::last_os_error()
111 ));
112 }
113 Ok(())
114 }
115}
116
117fn query_ntp(host: &str) -> Result<DateTime<Utc>, Error> {
118 let addrs: Vec<_> = host.to_socket_addrs()?.collect();
119
120 let mut last_err = None;
121 for addr in &addrs {
122 let bind_addr = match addr {
123 std::net::SocketAddr::V4(_) => "0.0.0.0:0",
124 std::net::SocketAddr::V6(_) => "[::]:0",
125 };
126
127 let socket = match UdpSocket::bind(bind_addr) {
128 Ok(s) => s,
129 Err(e) => {
130 last_err = Some(anyhow::anyhow!("UDP bind failed for {bind_addr}: {e}"));
131 continue;
132 }
133 };
134
135 if socket.set_read_timeout(Some(NTP_TIMEOUT)).is_err() {
136 continue;
137 }
138
139 let socket = UdpSocketWrapper::new(socket);
140 let context = NtpContext::new(StdTimestampGen::default());
141
142 match sntpc::sync::get_time(*addr, &socket, context) {
143 Ok(result) => {
144 let now = Utc::now();
145 let offset = chrono::Duration::microseconds(result.offset());
146 return Ok(now + offset);
147 }
148 Err(e) => {
149 last_err = Some(anyhow::anyhow!("NTP error: {e:?}"));
150 }
151 }
152 }
153
154 Err(last_err.unwrap_or_else(|| anyhow::anyhow!("DNS resolution failed for NTP host: {host}")))
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[ignore]
162 #[test]
163 fn ntp_query_with_hostname() {
164 let result = query_ntp("time.cloudflare.com:123");
165 assert!(result.is_ok(), "NTP query failed: {:?}", result.err());
166
167 let ntp_time = result.unwrap();
168 let now = Utc::now();
169 let diff = (now - ntp_time).num_seconds().abs();
170 assert!(diff < 60, "NTP time off by {diff}s, expected <60s");
171 }
172}