tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definiciones

⎇ 19d04a276699d974a7a1be88ec2ffb5c18df58de crates/compat/arje-logind-compat/src/main.rs Rust · 369 líneas · 12.5 KB Historia Quién tocó qué crudo

feat(compat): logind — Inhibit real (fd vivo) + ListInhibitors 19d04a276 · sergio ·

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}