⎇ d88a9c57914ac7870b43a7a4f616510161278237
crates/ente-timedated-compat/src/main.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
Persistencia setters, compat-resolved, journald→CAS, compat-polkit
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}