tawasuyu

tawasuyu/brahman

Rust 307TOML 110JSON 14Markdown 113965 Definiciones

⎇ a388ab14b76921e9da0cd25d822d22353a220b69 crates/compat/arje-timedated-compat/src/main.rs Rust · 223 líneas · 8.4 KB Historia Quién tocó qué crudo

fix(compat): auditoría de stubs — los métodos que mentían dejan de mentir a388ab14b · sergio ·

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}