gioser
⎇ 227715a464b872a0ecd918a069c667e05c95ec27 crates/ente-systemd1-compat/src/main.rs Rust · 280 líneas · 10.5 KB Historia Quién tocó qué crudo

compat-systemd1, NOTIFY_SOCKET, binfmt y timer 227715a46 · Sergio ·

1//! ente-systemd1-compat: shim de `org.freedesktop.systemd1.Manager`.
2//!
3//! Centro de control que `systemctl` consulta. Sin esto, `systemctl list-units`
4//! falla con `Failed to connect to bus` aunque el sistema funcione.
5//!
6//! Mapeo: cada Ente vivo del fractal aparece como una "unit" cuyo nombre es
7//! `<label>.service`. Estados:
8//!   - `loaded` siempre (porque está en el grafo)
9//!   - `active` si tiene PID o es Wasm corriendo, `inactive` si está virtual
10//!   - sub_state: `running`/`exited`/`virtual`
11//!
12//! Métodos cubiertos del subset que `systemctl` típicamente llama al boot:
13//!   - ListUnits (basis de `systemctl list-units`)
14//!   - GetUnit / GetUnitByPID (object-path lookup; no servimos métodos del unit)
15//!   - StartUnit / StopUnit / RestartUnit (forwardea al bus interno)
16//!   - Subscribe / Unsubscribe (no-op)
17//!   - Reload (no-op — Cards inmutables)
18//!   - ListUnitFiles (vacío)
19//!   - GetVersion / Environment / Architecture (properties)
20
21use ente_bus::{BusClient, BusRequest, BusResponse};
22use ente_card::Capability;
23use std::collections::HashMap;
24use tokio::signal::unix::{signal, SignalKind};
25use tracing::{info, warn};
26use tracing_subscriber::EnvFilter;
27use zbus::{fdo, interface, zvariant::{ObjectPath, OwnedObjectPath, OwnedValue}};
28
29const BUS_NAME: &str = "org.freedesktop.systemd1";
30const OBJ_PATH: &str = "/org/freedesktop/systemd1";
31
32#[tokio::main(flavor = "current_thread")]
33async fn main() -> anyhow::Result<()> {
34    init_tracing();
35    info!("ente-systemd1-compat: arrancando");
36    announce_to_fractal().await;
37
38    let manager = SystemdManager;
39    let conn_result = zbus::connection::Builder::system()
40        .and_then(|b| b.name(BUS_NAME))
41        .and_then(|b| b.serve_at(OBJ_PATH, manager));
42    match conn_result {
43        Ok(builder) => match builder.build().await {
44            Ok(_conn) => {
45                info!(name = BUS_NAME, "name acquired, sirviendo systemctl");
46                wait_for_term().await
47            }
48            Err(e) => { warn!(?e, "build conn falló — modo idle"); wait_for_term().await }
49        },
50        Err(e) => { warn!(?e, "builder D-Bus falló — modo idle"); wait_for_term().await }
51    }
52}
53
54struct SystemdManager;
55
56/// Wire format de un unit en `ListUnits`:
57/// (name, description, load_state, active_state, sub_state, followed,
58///  unit_path, job_id, job_type, job_path)
59type UnitInfo = (
60    String, String, String, String, String, String,
61    OwnedObjectPath, u32, String, OwnedObjectPath,
62);
63
64#[interface(name = "org.freedesktop.systemd1.Manager")]
65impl SystemdManager {
66    async fn list_units(&self) -> fdo::Result<Vec<UnitInfo>> {
67        let entes = match query_list_entes().await {
68            Some(es) => es,
69            None => return Ok(vec![]),
70        };
71        let unit_path = ObjectPath::try_from("/org/freedesktop/systemd1/unit/_invalid")
72            .map_err(|e| fdo::Error::Failed(format!("path: {e}")))?;
73        let job_path = ObjectPath::try_from("/")
74            .map_err(|e| fdo::Error::Failed(format!("path: {e}")))?;
75
76        let mut out = Vec::with_capacity(entes.len());
77        for e in entes {
78            let name = format!("{}.service", e.label);
79            let description = format!("Ente: {} ({})", e.label, e.id);
80            let active_state = if e.pid.is_some() { "active" } else { "active" };
81            let sub_state = match e.pid {
82                Some(_) => "running",
83                None => "virtual",
84            };
85            out.push((
86                name,
87                description,
88                "loaded".to_string(),
89                active_state.to_string(),
90                sub_state.to_string(),
91                String::new(),  // followed_unit
92                unit_path.clone().into(),
93                0u32,           // job_id
94                String::new(),  // job_type
95                job_path.clone().into(),
96            ));
97        }
98        info!(count = out.len(), "ListUnits");
99        Ok(out)
100    }
101
102    async fn list_units_filtered(&self, _states: Vec<String>) -> fdo::Result<Vec<UnitInfo>> {
103        // Subset simple: ignoramos el filtro y devolvemos todas.
104        self.list_units().await
105    }
106
107    async fn list_units_by_names(&self, names: Vec<String>) -> fdo::Result<Vec<UnitInfo>> {
108        let all = self.list_units().await?;
109        let want: std::collections::HashSet<&String> = names.iter().collect();
110        Ok(all.into_iter().filter(|u| want.contains(&u.0)).collect())
111    }
112
113    async fn get_unit(&self, name: String) -> fdo::Result<OwnedObjectPath> {
114        if let Some(entes) = query_list_entes().await {
115            if entes.iter().any(|e| format!("{}.service", e.label) == name) {
116                let path = format!("/org/freedesktop/systemd1/unit/{}", escape_unit_name(&name));
117                return ObjectPath::try_from(path)
118                    .map(OwnedObjectPath::from)
119                    .map_err(|e| fdo::Error::Failed(format!("path: {e}")));
120            }
121        }
122        Err(fdo::Error::Failed(format!("Unit {name} not found")))
123    }
124
125    async fn get_unit_by_pid(&self, pid: u32) -> fdo::Result<OwnedObjectPath> {
126        if let Some(entes) = query_list_entes().await {
127            if let Some(e) = entes.iter().find(|e| e.pid == Some(pid as i32)) {
128                let path = format!("/org/freedesktop/systemd1/unit/{}",
129                    escape_unit_name(&format!("{}.service", e.label)));
130                return ObjectPath::try_from(path)
131                    .map(OwnedObjectPath::from)
132                    .map_err(|e| fdo::Error::Failed(format!("path: {e}")));
133            }
134        }
135        Err(fdo::Error::Failed(format!("PID {pid} not in any unit")))
136    }
137
138    async fn start_unit(&self, name: String, _mode: String) -> fdo::Result<OwnedObjectPath> {
139        warn!(%name, "StartUnit no implementado — Cards no se 'start' tras boot");
140        Err(fdo::Error::NotSupported(
141            "StartUnit: el fractal usa Cards cargadas al boot, no unit files dinámicos".into()
142        ))
143    }
144
145    async fn stop_unit(&self, name: String, _mode: String) -> fdo::Result<OwnedObjectPath> {
146        warn!(%name, "StopUnit (stub: TODO via bus capability)");
147        // TODO: bus → graph → kill PID por label. Por ahora no-op.
148        let path = ObjectPath::try_from("/").unwrap();
149        Ok(path.into())
150    }
151
152    async fn restart_unit(&self, name: String, mode: String) -> fdo::Result<OwnedObjectPath> {
153        info!(%name, "RestartUnit (delega a StopUnit)");
154        self.stop_unit(name, mode).await
155    }
156
157    async fn reload_unit(&self, name: String, _mode: String) -> fdo::Result<OwnedObjectPath> {
158        info!(%name, "ReloadUnit (no-op — Cards inmutables)");
159        let path = ObjectPath::try_from("/").unwrap();
160        Ok(path.into())
161    }
162
163    async fn kill_unit(&self, name: String, _who: String, _signal: i32) -> fdo::Result<()> {
164        warn!(%name, "KillUnit (stub)");
165        Ok(())
166    }
167
168    async fn subscribe(&self) -> fdo::Result<()> { Ok(()) }
169    async fn unsubscribe(&self) -> fdo::Result<()> { Ok(()) }
170
171    async fn reload(&self) -> fdo::Result<()> {
172        info!("Reload: trigger re-read (no-op — Cards no se recargan tras boot)");
173        Ok(())
174    }
175
176    async fn list_unit_files(&self) -> fdo::Result<Vec<(String, String)>> {
177        // Empty: no usamos unit files. Cards en su lugar.
178        Ok(vec![])
179    }
180
181    async fn list_jobs(&self) -> fdo::Result<Vec<(u32, String, String, String, OwnedObjectPath, OwnedObjectPath)>> {
182        Ok(vec![])
183    }
184
185    async fn get_default_target(&self) -> fdo::Result<String> {
186        Ok("multi-user.target".into())
187    }
188
189    async fn set_default_target(&self, _name: String, _force: bool) -> fdo::Result<(Vec<String>, Vec<String>, Vec<String>)> {
190        Err(fdo::Error::NotSupported("default target gestionado por Card de Semilla".into()))
191    }
192
193    // ----- Properties -----
194
195    #[zbus(property)]
196    async fn version(&self) -> String { format!("ente-systemd1-compat {}", env!("CARGO_PKG_VERSION")) }
197
198    #[zbus(property)]
199    async fn architecture(&self) -> String { std::env::consts::ARCH.into() }
200
201    #[zbus(property)]
202    async fn features(&self) -> String { "+ENTE-FRACTAL".into() }
203
204    #[zbus(property)]
205    async fn virtualization(&self) -> String { String::new() }
206
207    #[zbus(property)]
208    async fn confined(&self) -> bool { false }
209
210    #[zbus(property)]
211    async fn environment(&self) -> Vec<String> {
212        std::env::vars().map(|(k, v)| format!("{k}={v}")).collect()
213    }
214
215    #[zbus(property)]
216    async fn n_names(&self) -> u32 { 0 }
217
218    #[zbus(property)]
219    async fn n_jobs(&self) -> u32 { 0 }
220
221    #[zbus(property)]
222    async fn progress(&self) -> f64 { 1.0 }
223}
224
225/// Pregunta al bus interno por la lista de Entes vivos.
226async fn query_list_entes() -> Option<Vec<ente_bus::EnteInfo>> {
227    let mut client = match BusClient::from_env().await {
228        Ok(c) => c,
229        Err(e) => { warn!(?e, "no bus client — devuelvo vacío"); return None; }
230    };
231    match client.call(BusRequest::ListEntes).await {
232        Ok(BusResponse::Entes(entes)) => Some(entes),
233        Ok(other) => { warn!(?other, "ListEntes respuesta inesperada"); None }
234        Err(e) => { warn!(?e, "ListEntes call falló"); None }
235    }
236}
237
238/// Escape de nombres de units para object paths según convención systemd:
239/// `.` → `_2e`, `-` → `_2d`, etc. Para el demo usamos un escape simple.
240fn escape_unit_name(name: &str) -> String {
241    name.chars().map(|c| match c {
242        c if c.is_ascii_alphanumeric() => c.to_string(),
243        c => format!("_{:02x}", c as u32),
244    }).collect()
245}
246
247async fn announce_to_fractal() {
248    if let Ok(mut client) = BusClient::from_env().await {
249        let req = BusRequest::Announce {
250            capabilities: vec![Capability::Endpoint {
251                interface: ente_card::InterfaceId([0xa6; 16]),
252                version: 1,
253            }],
254        };
255        match client.call(req).await {
256            Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
257            Ok(other) => warn!(?other, "Announce respuesta inesperada"),
258            Err(e) => warn!(?e, "Announce falló"),
259        }
260    }
261}
262
263async fn wait_for_term() -> anyhow::Result<()> {
264    let mut term = signal(SignalKind::terminate())?;
265    let mut int_ = signal(SignalKind::interrupt())?;
266    tokio::select! {
267        _ = term.recv() => info!("SIGTERM"),
268        _ = int_.recv() => info!("SIGINT"),
269    }
270    Ok(())
271}
272
273fn init_tracing() {
274    let filter = EnvFilter::try_from_default_env()
275        .unwrap_or_else(|_| EnvFilter::new("ente_systemd1_compat=info"));
276    tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
277}
278
279#[allow(dead_code)]
280fn _suppress(_: HashMap<String, OwnedValue>) {} // mantener import si se reduce