⎇ a388ab14b76921e9da0cd25d822d22353a220b69
crates/compat/arje-timedated-compat/src/main.rs
Historia
Quién tocó qué
crudo
fix(compat): auditoría de stubs — los métodos que mentían dejan de mentir
1//! ente-timedated-compat: shim de `org.freedesktop.timedate1`.
2//!
3//! GNOME settings panel "Date & Time" llama aquí. Properties read-only se
4//! mapean a syscalls/lecturas del sistema; setters log + forward.
5
6use arje_bus::{BusClient, BusRequest, BusResponse};
7use arje_card::Capability;
8use arje_compat_common::atomic_write;
9use std::time::{SystemTime, UNIX_EPOCH};
10use tokio::signal::unix::{signal, SignalKind};
11use tracing::{info, warn};
12use tracing_subscriber::EnvFilter;
13use zbus::{fdo, interface};
14
15const BUS_NAME: &str = "org.freedesktop.timedate1";
16const OBJ_PATH: &str = "/org/freedesktop/timedate1";
17
18#[tokio::main(flavor = "current_thread")]
19async fn main() -> anyhow::Result<()> {
20 init_tracing();
21 info!("ente-timedated-compat: arrancando");
22 announce_to_fractal().await;
23
24 let manager = TimedateManager::default();
25 let conn_result = zbus::connection::Builder::system()
26 .and_then(|b| b.name(BUS_NAME))
27 .and_then(|b| b.serve_at(OBJ_PATH, manager));
28 match conn_result {
29 Ok(builder) => match builder.build().await {
30 Ok(_conn) => {
31 info!(name = BUS_NAME, "name acquired, sirviendo");
32 wait_for_term().await
33 }
34 Err(e) => {
35 warn!(?e, "build conn falló — modo idle");
36 wait_for_term().await
37 }
38 },
39 Err(e) => {
40 warn!(?e, "builder D-Bus falló — modo idle");
41 wait_for_term().await
42 }
43 }
44}
45
46#[derive(Default)]
47struct TimedateManager;
48
49#[interface(name = "org.freedesktop.timedate1")]
50impl TimedateManager {
51 // ----- Properties -----
52
53 /// Timezone configurada. Por defecto leemos el target de /etc/localtime
54 /// (un symlink a /usr/share/zoneinfo/<TZ>).
55 #[zbus(property)]
56 async fn timezone(&self) -> String {
57 std::fs::read_link("/etc/localtime")
58 .ok()
59 .and_then(|p| {
60 let s = p.to_string_lossy().into_owned();
61 s.strip_prefix("/usr/share/zoneinfo/").map(String::from)
62 .or_else(|| s.split("/zoneinfo/").nth(1).map(String::from))
63 })
64 .unwrap_or_else(|| "UTC".into())
65 }
66
67 /// True si el RTC del hardware está en local time. Convención moderna
68 /// es UTC (false). Reportamos false como default.
69 #[zbus(property)]
70 async fn local_rtc(&self) -> bool { false }
71
72 /// Si el sistema puede gestionar NTP. `false`: arje no administra un
73 /// daemon de sincronización propio, así que GNOME deja el toggle
74 /// «hora automática» deshabilitado en vez de fingir que funciona.
75 #[zbus(property)]
76 async fn can_ntp(&self) -> bool { false }
77
78 /// Si NTP está activo. Sin daemon real bajo nuestro control no podemos
79 /// consultarlo con precisión — false como default seguro.
80 #[zbus(property)]
81 async fn ntp(&self) -> bool { false }
82
83 #[zbus(property)]
84 async fn ntpsynchronized(&self) -> bool { false }
85
86 /// Timestamp actual en microsegundos desde epoch.
87 #[zbus(property)]
88 async fn time_usec(&self) -> u64 {
89 SystemTime::now().duration_since(UNIX_EPOCH)
90 .map(|d| d.as_micros() as u64)
91 .unwrap_or(0)
92 }
93
94 #[zbus(property)]
95 async fn rtctime_usec(&self) -> u64 {
96 // El RTC real requiere ioctl a /dev/rtc — usamos system clock como aprox.
97 SystemTime::now().duration_since(UNIX_EPOCH)
98 .map(|d| d.as_micros() as u64)
99 .unwrap_or(0)
100 }
101
102 // ----- Setters -----
103
104 async fn set_time(&self, usec_utc: i64, relative: bool, _interactive: bool) -> fdo::Result<()> {
105 // `relative`: el valor es un delta sobre el reloj actual.
106 let target = if relative {
107 let now = SystemTime::now()
108 .duration_since(UNIX_EPOCH)
109 .map(|d| d.as_micros() as i64)
110 .unwrap_or(0);
111 now + usec_utc
112 } else {
113 usec_utc
114 };
115 if target < 0 {
116 return Err(fdo::Error::InvalidArgs("el tiempo resultante es negativo".into()));
117 }
118 let ts = libc::timespec {
119 tv_sec: (target / 1_000_000) as libc::time_t,
120 tv_nsec: ((target % 1_000_000) * 1_000) as _,
121 };
122 // SEGURIDAD: `clock_settime` con un timespec válido vivo en la
123 // pila durante la llamada.
124 let r = unsafe { libc::clock_settime(libc::CLOCK_REALTIME, &ts) };
125 if r != 0 {
126 let e = std::io::Error::last_os_error();
127 warn!(%e, "clock_settime falló (¿falta CAP_SYS_TIME?)");
128 return Err(fdo::Error::Failed(format!("clock_settime: {e}")));
129 }
130 info!(target, "SetTime aplicado al reloj del sistema");
131 Ok(())
132 }
133
134 async fn set_timezone(&self, timezone: String, _interactive: bool) -> fdo::Result<()> {
135 // Validar contra zoneinfo: el archivo destino debe existir.
136 let zoneinfo = format!("/usr/share/zoneinfo/{timezone}");
137 if !std::path::Path::new(&zoneinfo).exists() {
138 return Err(fdo::Error::InvalidArgs(format!("timezone desconocida: {timezone}")));
139 }
140 // Atomic relink: crear localtime.tmp como symlink, rename.
141 let tmp = "/etc/localtime.tmp";
142 let _ = std::fs::remove_file(tmp);
143 if let Err(e) = std::os::unix::fs::symlink(&zoneinfo, tmp) {
144 return Err(fdo::Error::Failed(format!("symlink: {e}")));
145 }
146 if let Err(e) = std::fs::rename(tmp, "/etc/localtime") {
147 return Err(fdo::Error::Failed(format!("rename: {e}")));
148 }
149 info!(%timezone, "SetTimezone → /etc/localtime");
150 Ok(())
151 }
152
153 async fn set_local_rtc(&self, local_rtc: bool, _fix_system: bool, _interactive: bool) -> fdo::Result<()> {
154 let mode = if local_rtc { "LOCAL" } else { "UTC" };
155 // `/etc/adjtime` tiene tres líneas; sólo la tercera (UTC|LOCAL)
156 // nos concierne. Se conservan las dos primeras si ya existían.
157 let existing = std::fs::read_to_string("/etc/adjtime").unwrap_or_default();
158 let mut lines = existing.lines();
159 let l0 = lines.next().unwrap_or("0.0 0 0.0");
160 let l1 = lines.next().unwrap_or("0");
161 atomic_write("/etc/adjtime", format!("{l0}\n{l1}\n{mode}\n").as_bytes())
162 .map_err(|e| fdo::Error::Failed(format!("write /etc/adjtime: {e}")))?;
163 info!(local_rtc, "SetLocalRTC → /etc/adjtime");
164 Ok(())
165 }
166
167 async fn set_ntp(&self, ntp: bool, _interactive: bool) -> fdo::Result<()> {
168 // arje no administra un daemon NTP propio. En vez de fingir
169 // éxito, se rechaza con honestidad — y como `can_ntp` es `false`,
170 // GNOME deja el toggle deshabilitado y normalmente no llega acá.
171 warn!(ntp, "SetNTP rechazado — arje no gestiona un daemon NTP");
172 Err(fdo::Error::NotSupported(
173 "arje no gestiona un daemon NTP (timesyncd/chrony)".into(),
174 ))
175 }
176
177 async fn list_timezones(&self) -> fdo::Result<Vec<String>> {
178 // Listar /usr/share/zoneinfo recursivamente. Hacemos un best-effort.
179 let mut out = Vec::new();
180 if let Ok(rd) = std::fs::read_dir("/usr/share/zoneinfo") {
181 for entry in rd.flatten() {
182 if let Ok(name) = entry.file_name().into_string() {
183 if !name.starts_with(|c: char| c.is_lowercase()) && name != "posix" && name != "right" {
184 out.push(name);
185 }
186 }
187 }
188 }
189 Ok(out)
190 }
191}
192
193async fn announce_to_fractal() {
194 if let Ok(mut client) = BusClient::from_env().await {
195 let req = BusRequest::Announce {
196 capabilities: vec![Capability::Endpoint {
197 interface: arje_card::InterfaceId([0xa1; 16]),
198 version: 1,
199 }],
200 };
201 match client.call(req).await {
202 Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
203 Ok(other) => warn!(?other, "Announce respuesta inesperada"),
204 Err(e) => warn!(?e, "Announce falló"),
205 }
206 }
207}
208
209async fn wait_for_term() -> anyhow::Result<()> {
210 let mut term = signal(SignalKind::terminate())?;
211 let mut int_ = signal(SignalKind::interrupt())?;
212 tokio::select! {
213 _ = term.recv() => info!("SIGTERM"),
214 _ = int_.recv() => info!("SIGINT"),
215 }
216 Ok(())
217}
218
219fn init_tracing() {
220 let filter = EnvFilter::try_from_default_env()
221 .unwrap_or_else(|_| EnvFilter::new("arje_timedated_compat=info"));
222 tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
223}