gioser
⎇ d88a9c57914ac7870b43a7a4f616510161278237 crates/ente-timedated-compat/src/main.rs Rust · 182 siq'ikuna · 6.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

Persistencia setters, compat-resolved, journald→CAS, compat-polkit d88a9c579 · 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 ente_bus::{BusClient, BusRequest, BusResponse};
7use ente_card::Capability;
8use std::time::{SystemTime, UNIX_EPOCH};
9use tokio::signal::unix::{signal, SignalKind};
10use tracing::{info, warn};
11use tracing_subscriber::EnvFilter;
12use zbus::{fdo, interface};
13
14const BUS_NAME: &str = "org.freedesktop.timedate1";
15const OBJ_PATH: &str = "/org/freedesktop/timedate1";
16
17#[tokio::main(flavor = "current_thread")]
18async fn main() -> anyhow::Result<()> {
19    init_tracing();
20    info!("ente-timedated-compat: arrancando");
21    announce_to_fractal().await;
22
23    let manager = TimedateManager::default();
24    let conn_result = zbus::connection::Builder::system()
25        .and_then(|b| b.name(BUS_NAME))
26        .and_then(|b| b.serve_at(OBJ_PATH, manager));
27    match conn_result {
28        Ok(builder) => match builder.build().await {
29            Ok(_conn) => {
30                info!(name = BUS_NAME, "name acquired, sirviendo");
31                wait_for_term().await
32            }
33            Err(e) => {
34                warn!(?e, "build conn falló — modo idle");
35                wait_for_term().await
36            }
37        },
38        Err(e) => {
39            warn!(?e, "builder D-Bus falló — modo idle");
40            wait_for_term().await
41        }
42    }
43}
44
45#[derive(Default)]
46struct TimedateManager;
47
48#[interface(name = "org.freedesktop.timedate1")]
49impl TimedateManager {
50    // ----- Properties -----
51
52    /// Timezone configurada. Por defecto leemos el target de /etc/localtime
53    /// (un symlink a /usr/share/zoneinfo/<TZ>).
54    #[zbus(property)]
55    async fn timezone(&self) -> String {
56        std::fs::read_link("/etc/localtime")
57            .ok()
58            .and_then(|p| {
59                let s = p.to_string_lossy().into_owned();
60                s.strip_prefix("/usr/share/zoneinfo/").map(String::from)
61                    .or_else(|| s.split("/zoneinfo/").nth(1).map(String::from))
62            })
63            .unwrap_or_else(|| "UTC".into())
64    }
65
66    /// True si el RTC del hardware está en local time. Convención moderna
67    /// es UTC (false). Reportamos false como default.
68    #[zbus(property)]
69    async fn local_rtc(&self) -> bool { false }
70
71    /// Si NTP es soportado. Reportamos true (asumimos systemd-timesyncd
72    /// o chrony están disponibles en el host).
73    #[zbus(property)]
74    async fn can_ntp(&self) -> bool { true }
75
76    /// Si NTP está activo. Sin daemon real bajo nuestro control no podemos
77    /// consultarlo con precisión — false como default seguro.
78    #[zbus(property)]
79    async fn ntp(&self) -> bool { false }
80
81    #[zbus(property)]
82    async fn ntpsynchronized(&self) -> bool { false }
83
84    /// Timestamp actual en microsegundos desde epoch.
85    #[zbus(property)]
86    async fn time_usec(&self) -> u64 {
87        SystemTime::now().duration_since(UNIX_EPOCH)
88            .map(|d| d.as_micros() as u64)
89            .unwrap_or(0)
90    }
91
92    #[zbus(property)]
93    async fn rtctime_usec(&self) -> u64 {
94        // El RTC real requiere ioctl a /dev/rtc — usamos system clock como aprox.
95        SystemTime::now().duration_since(UNIX_EPOCH)
96            .map(|d| d.as_micros() as u64)
97            .unwrap_or(0)
98    }
99
100    // ----- Setters -----
101
102    async fn set_time(&self, usec_utc: i64, _relative: bool, _interactive: bool) -> fdo::Result<()> {
103        info!(usec_utc, "SetTime (stub: requiere CAP_SYS_TIME para aplicar)");
104        Ok(())
105    }
106
107    async fn set_timezone(&self, timezone: String, _interactive: bool) -> fdo::Result<()> {
108        // Validar contra zoneinfo: el archivo destino debe existir.
109        let zoneinfo = format!("/usr/share/zoneinfo/{timezone}");
110        if !std::path::Path::new(&zoneinfo).exists() {
111            return Err(fdo::Error::InvalidArgs(format!("timezone desconocida: {timezone}")));
112        }
113        // Atomic relink: crear localtime.tmp como symlink, rename.
114        let tmp = "/etc/localtime.tmp";
115        let _ = std::fs::remove_file(tmp);
116        if let Err(e) = std::os::unix::fs::symlink(&zoneinfo, tmp) {
117            return Err(fdo::Error::Failed(format!("symlink: {e}")));
118        }
119        if let Err(e) = std::fs::rename(tmp, "/etc/localtime") {
120            return Err(fdo::Error::Failed(format!("rename: {e}")));
121        }
122        info!(%timezone, "SetTimezone → /etc/localtime");
123        Ok(())
124    }
125
126    async fn set_local_rtc(&self, local_rtc: bool, _fix_system: bool, _interactive: bool) -> fdo::Result<()> {
127        info!(local_rtc, "SetLocalRTC (stub)");
128        Ok(())
129    }
130
131    async fn set_ntp(&self, ntp: bool, _interactive: bool) -> fdo::Result<()> {
132        info!(ntp, "SetNTP (stub: no controlamos timesyncd)");
133        Ok(())
134    }
135
136    async fn list_timezones(&self) -> fdo::Result<Vec<String>> {
137        // Listar /usr/share/zoneinfo recursivamente. Hacemos un best-effort.
138        let mut out = Vec::new();
139        if let Ok(rd) = std::fs::read_dir("/usr/share/zoneinfo") {
140            for entry in rd.flatten() {
141                if let Ok(name) = entry.file_name().into_string() {
142                    if !name.starts_with(|c: char| c.is_lowercase()) && name != "posix" && name != "right" {
143                        out.push(name);
144                    }
145                }
146            }
147        }
148        Ok(out)
149    }
150}
151
152async fn announce_to_fractal() {
153    if let Ok(mut client) = BusClient::from_env().await {
154        let req = BusRequest::Announce {
155            capabilities: vec![Capability::Endpoint {
156                interface: ente_card::InterfaceId([0xa1; 16]),
157                version: 1,
158            }],
159        };
160        match client.call(req).await {
161            Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
162            Ok(other) => warn!(?other, "Announce respuesta inesperada"),
163            Err(e) => warn!(?e, "Announce falló"),
164        }
165    }
166}
167
168async fn wait_for_term() -> anyhow::Result<()> {
169    let mut term = signal(SignalKind::terminate())?;
170    let mut int_ = signal(SignalKind::interrupt())?;
171    tokio::select! {
172        _ = term.recv() => info!("SIGTERM"),
173        _ = int_.recv() => info!("SIGINT"),
174    }
175    Ok(())
176}
177
178fn init_tracing() {
179    let filter = EnvFilter::try_from_default_env()
180        .unwrap_or_else(|_| EnvFilter::new("ente_timedated_compat=info"));
181    tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
182}