⎇ 19d04a276699d974a7a1be88ec2ffb5c18df58de
crates/compat/arje-logind-compat/src/main.rs
Historia
Quién tocó qué
crudo
feat(compat): logind — Inhibit real (fd vivo) + ListInhibitors
1//! ente-logind-compat: el Ente que se hace pasar por systemd-logind.
2//!
3//! Vive FUERA de PID 1 — un parser D-Bus en el Init es una bomba con CVEs
4//! históricos. Ejecutado como hijo del Ente #0 con `Restart` supervision.
5//!
6//! Implementa el subset del interface `org.freedesktop.login1.Manager` que
7//! GNOME/KDE consultan en arranque. Cada método se traduce internamente en
8//! una request al bus interno del fractal — capacidades tipadas, no nombres
9//! D-Bus opacos hacia abajo.
10//!
11//! ## Lo que GNOME/KDE realmente llaman al boot
12//! - ListSessions, ListUsers, GetSession*
13//! - Inhibit (mantiene un fd vivo mientras la app está activa)
14//! - CanPowerOff/CanReboot/CanSuspend
15//! - PowerOff/Reboot/Suspend
16//! - Properties: IdleHint, Docked, etc.
17//!
18//! El stub responde "no hay sesiones" y "sí puedo apagar" — suficiente para
19//! que GNOME complete arranque sin marcar fallo.
20
21use arje_bus::{BusClient, BusRequest, BusResponse};
22use arje_card::Capability;
23use std::sync::atomic::{AtomicU32, Ordering};
24use std::sync::{Arc, Mutex};
25use std::time::Duration;
26use tokio::signal::unix::{signal, SignalKind};
27use tracing::{info, warn};
28use tracing_subscriber::EnvFilter;
29use zbus::{fdo, interface, zvariant::OwnedObjectPath, Connection};
30
31const BUS_NAME: &str = "org.freedesktop.login1";
32const MANAGER_PATH: &str = "/org/freedesktop/login1";
33
34#[tokio::main(flavor = "current_thread")]
35async fn main() -> anyhow::Result<()> {
36 init_tracing();
37 info!("ente-logind-compat: arrancando");
38
39 let bus_addr = std::env::var("DBUS_SYSTEM_BUS_ADDRESS")
40 .unwrap_or_else(|_| "unix:path=/var/run/dbus/system_bus_socket".into());
41 let bus_path = bus_addr.strip_prefix("unix:path=").unwrap_or(&bus_addr);
42 let bus_present = std::path::Path::new(bus_path).exists();
43 info!(bus_addr, bus_present, "configuración D-Bus");
44
45 // Anunciamos nuestra presencia al bus interno del fractal antes de
46 // intentar registrar el nombre D-Bus. Esto sirve como handshake "estoy
47 // vivo" independiente del estado del system bus.
48 announce_to_fractal().await;
49
50 if !bus_present {
51 warn!("system bus no disponible — modo idle (esperando SIGTERM)");
52 return wait_for_term().await;
53 }
54
55 let conn = match build_connection().await {
56 Ok(c) => c,
57 Err(e) => {
58 warn!(?e, "fallo al registrar org.freedesktop.login1 — modo idle");
59 // No retornamos error: la supervisión Restart entraría en bucle
60 // si systemd-logind real ya posee el nombre. Esperar señal y salir.
61 return wait_for_term().await;
62 }
63 };
64
65 info!("logind compat corriendo — esperando señales");
66 let _ = conn; // mantener viva la conexión hasta SIGTERM
67 wait_for_term().await
68}
69
70async fn build_connection() -> anyhow::Result<Connection> {
71 let manager = LogindManager::default();
72 let conn = zbus::connection::Builder::system()?
73 .name(BUS_NAME)?
74 .serve_at(MANAGER_PATH, manager)?
75 .build()
76 .await?;
77 info!(name = BUS_NAME, path = MANAGER_PATH, "name acquired + manager served");
78 Ok(conn)
79}
80
81async fn announce_to_fractal() {
82 match BusClient::from_env().await {
83 Ok(mut client) => {
84 let req = BusRequest::Announce {
85 capabilities: vec![Capability::LegacyLogind],
86 };
87 match client.call(req).await {
88 Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
89 Ok(other) => warn!(?other, "Announce respuesta inesperada"),
90 Err(e) => warn!(?e, "Announce falló"),
91 }
92 }
93 Err(e) => warn!(?e, "no se pudo conectar al bus interno"),
94 }
95}
96
97async fn forward_to_fractal(req: BusRequest) -> fdo::Result<()> {
98 let mut client = BusClient::from_env().await
99 .map_err(|e| fdo::Error::Failed(format!("bus client: {e}")))?;
100 match client.call(req).await {
101 Ok(BusResponse::Ok) => Ok(()),
102 Ok(BusResponse::Error(s)) => Err(fdo::Error::Failed(s)),
103 Ok(other) => Err(fdo::Error::Failed(format!("respuesta inesperada: {other:?}"))),
104 Err(e) => Err(fdo::Error::Failed(format!("bus call: {e}"))),
105 }
106}
107
108async fn wait_for_term() -> anyhow::Result<()> {
109 let mut term = signal(SignalKind::terminate())?;
110 let mut int_ = signal(SignalKind::interrupt())?;
111 let mut tick = tokio::time::interval(Duration::from_secs(60));
112 tick.tick().await; // descartar el primer tick inmediato
113 loop {
114 tokio::select! {
115 _ = term.recv() => { info!("SIGTERM — cierre ordenado"); return Ok(()); }
116 _ = int_.recv() => { info!("SIGINT — cierre"); return Ok(()); }
117 _ = tick.tick() => { info!("heartbeat"); }
118 }
119 }
120}
121
122fn init_tracing() {
123 let filter = EnvFilter::try_from_default_env()
124 .unwrap_or_else(|_| EnvFilter::new("arje_logind_compat=info"));
125 tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
126}
127
128/// Un inhibidor activo: el cliente sostiene un fd vivo; mientras no lo
129/// cierre, esta entrada permanece en la tabla.
130struct Inhibitor {
131 id: u32,
132 what: String,
133 who: String,
134 why: String,
135 mode: String,
136 uid: u32,
137 pid: u32,
138}
139
140/// Une los `what` de los inhibidores de un `mode` dado en una lista de
141/// tokens únicos separados por `:` — el formato de las propiedades
142/// `BlockInhibited` / `DelayInhibited`.
143fn inhibited_what(inhibitors: &[Inhibitor], mode: &str) -> String {
144 let mut tokens: Vec<&str> = Vec::new();
145 for inh in inhibitors.iter().filter(|i| i.mode == mode) {
146 for t in inh.what.split(':').filter(|t| !t.is_empty()) {
147 if !tokens.contains(&t) {
148 tokens.push(t);
149 }
150 }
151 }
152 tokens.join(":")
153}
154
155#[derive(Default)]
156struct LogindManager {
157 /// Contador monótono — fuente de ids de inhibidores.
158 inhibit_counter: AtomicU32,
159 /// Inhibidores activos. La tarea guardiana del fd de cada uno quita
160 /// su entrada cuando el cliente cierra el descriptor.
161 inhibitors: Arc<Mutex<Vec<Inhibitor>>>,
162}
163
164/// Tipos del wire format de `org.freedesktop.login1.Manager`.
165type SessionTuple = (String, u32, String, String, OwnedObjectPath);
166type UserTuple = (u32, String, OwnedObjectPath);
167
168#[interface(name = "org.freedesktop.login1.Manager")]
169impl LogindManager {
170 // ---- Listado / lookup ----
171
172 async fn list_sessions(&self) -> fdo::Result<Vec<SessionTuple>> {
173 Ok(vec![])
174 }
175
176 async fn list_users(&self) -> fdo::Result<Vec<UserTuple>> {
177 Ok(vec![])
178 }
179
180 async fn get_session(&self, _session_id: String) -> fdo::Result<OwnedObjectPath> {
181 Err(fdo::Error::Failed("no sessions in fractal".into()))
182 }
183
184 async fn get_session_by_pid(&self, _pid: u32) -> fdo::Result<OwnedObjectPath> {
185 Err(fdo::Error::Failed("no sessions in fractal".into()))
186 }
187
188 async fn get_user(&self, _uid: u32) -> fdo::Result<OwnedObjectPath> {
189 Err(fdo::Error::Failed("no users in fractal".into()))
190 }
191
192 async fn get_user_by_pid(&self, _pid: u32) -> fdo::Result<OwnedObjectPath> {
193 Err(fdo::Error::Failed("no users in fractal".into()))
194 }
195
196 // ---- Inhibit ----
197 //
198 // Devuelve un fd que el cliente mantiene abierto mientras quiere
199 // inhibir. Un pipe: el cliente se queda el extremo de escritura;
200 // este shim, el de lectura. Cuando el cliente cierra el suyo —o
201 // muere—, nuestra lectura ve EOF y la tarea guardiana retira el
202 // inhibidor de la tabla.
203
204 async fn inhibit(
205 &self,
206 what: String,
207 who: String,
208 why: String,
209 mode: String,
210 ) -> fdo::Result<zbus::zvariant::OwnedFd> {
211 let (reader, writer) = std::io::pipe()
212 .map_err(|e| fdo::Error::Failed(format!("pipe: {e}")))?;
213 let id = self.inhibit_counter.fetch_add(1, Ordering::Relaxed);
214 // uid/pid del llamante quedan en 0/0: obtenerlos exige consultar
215 // las credenciales de la conexión D-Bus, y para el shim no es
216 // crítico (ListInhibitors los expone sólo de forma informativa).
217 self.inhibitors.lock().unwrap().push(Inhibitor {
218 id,
219 what: what.clone(),
220 who: who.clone(),
221 why: why.clone(),
222 mode: mode.clone(),
223 uid: 0,
224 pid: 0,
225 });
226 info!(id, %what, %who, %why, %mode, "Inhibit registrado");
227 let inhibitors = self.inhibitors.clone();
228 tokio::task::spawn_blocking(move || {
229 use std::io::Read;
230 let mut reader = reader;
231 let mut buf = [0u8; 64];
232 loop {
233 match reader.read(&mut buf) {
234 Ok(0) | Err(_) => break, // EOF: el cliente soltó el fd
235 Ok(_) => continue, // datos espurios: seguir
236 }
237 }
238 inhibitors.lock().unwrap().retain(|i| i.id != id);
239 info!(id, "inhibidor liberado (el cliente cerró el fd)");
240 });
241 let raw: std::os::fd::OwnedFd = writer.into();
242 Ok(raw.into())
243 }
244
245 /// Los inhibidores activos: `(what, who, why, mode, uid, pid)`.
246 async fn list_inhibitors(
247 &self,
248 ) -> fdo::Result<Vec<(String, String, String, String, u32, u32)>> {
249 Ok(self
250 .inhibitors
251 .lock()
252 .unwrap()
253 .iter()
254 .map(|i| {
255 (
256 i.what.clone(),
257 i.who.clone(),
258 i.why.clone(),
259 i.mode.clone(),
260 i.uid,
261 i.pid,
262 )
263 })
264 .collect())
265 }
266
267 // ---- Power management ----
268
269 async fn power_off(&self, interactive: bool) -> fdo::Result<()> {
270 info!(interactive, "PowerOff D-Bus → bus interno");
271 forward_to_fractal(BusRequest::PowerOff { interactive }).await
272 }
273
274 async fn reboot(&self, interactive: bool) -> fdo::Result<()> {
275 info!(interactive, "Reboot D-Bus → bus interno");
276 forward_to_fractal(BusRequest::Reboot { interactive }).await
277 }
278
279 async fn suspend(&self, interactive: bool) -> fdo::Result<()> {
280 info!(interactive, "Suspend D-Bus → bus interno");
281 forward_to_fractal(BusRequest::Suspend { interactive }).await
282 }
283
284 async fn hibernate(&self, interactive: bool) -> fdo::Result<()> {
285 info!(interactive, "Hibernate D-Bus → bus interno");
286 forward_to_fractal(BusRequest::Hibernate { interactive }).await
287 }
288
289 async fn can_power_off(&self) -> fdo::Result<String> {
290 Ok("yes".into())
291 }
292
293 async fn can_reboot(&self) -> fdo::Result<String> {
294 Ok("yes".into())
295 }
296
297 async fn can_suspend(&self) -> fdo::Result<String> {
298 // "challenge" = válido, requiere autenticación. GNOME muestra el
299 // botón pero pide PIN/contraseña antes de invocar Suspend.
300 Ok("challenge".into())
301 }
302
303 async fn can_hibernate(&self) -> fdo::Result<String> {
304 Ok("challenge".into())
305 }
306
307 // ---- Properties mínimas ----
308
309 #[zbus(property)]
310 async fn idle_hint(&self) -> bool { false }
311
312 #[zbus(property)]
313 async fn idle_since_hint(&self) -> u64 { 0 }
314
315 #[zbus(property)]
316 async fn idle_since_hint_monotonic(&self) -> u64 { 0 }
317
318 #[zbus(property)]
319 async fn block_inhibited(&self) -> String {
320 inhibited_what(&self.inhibitors.lock().unwrap(), "block")
321 }
322
323 #[zbus(property)]
324 async fn delay_inhibited(&self) -> String {
325 inhibited_what(&self.inhibitors.lock().unwrap(), "delay")
326 }
327
328 #[zbus(property)]
329 async fn docked(&self) -> bool { false }
330
331 #[zbus(property)]
332 async fn lid_closed(&self) -> bool { false }
333
334 #[zbus(property)]
335 async fn on_external_power(&self) -> bool { true }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 fn inh(what: &str, mode: &str) -> Inhibitor {
343 Inhibitor {
344 id: 0,
345 what: what.into(),
346 who: "app".into(),
347 why: "test".into(),
348 mode: mode.into(),
349 uid: 0,
350 pid: 0,
351 }
352 }
353
354 #[test]
355 fn inhibited_what_une_tokens_unicos_por_modo() {
356 let v = vec![
357 inh("sleep:shutdown", "block"),
358 inh("idle", "delay"),
359 inh("shutdown:handle-lid-switch", "block"),
360 ];
361 let block = inhibited_what(&v, "block");
362 // sleep, shutdown, handle-lid-switch — `shutdown` no se duplica.
363 assert_eq!(block.split(':').count(), 3, "block = {block}");
364 assert!(block.contains("sleep"));
365 assert!(block.contains("handle-lid-switch"));
366 assert_eq!(inhibited_what(&v, "delay"), "idle");
367 assert_eq!(inhibited_what(&[], "block"), "");
368 }
369}