⎇ a388ab14b76921e9da0cd25d822d22353a220b69
crates/compat/arje-systemd1-compat/src/main.rs
History
Who touched what
raw
fix(compat): auditoría de stubs — los métodos que mentían dejan de mentir
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 arje_bus::{BusClient, BusRequest, BusResponse};
22use arje_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 // No se finge éxito: detener una Card por nombre de unit exige
147 // una capability del bus del fractal que aún no existe. Hasta
148 // entonces, se rechaza con honestidad (igual que StartUnit).
149 warn!(%name, "StopUnit rechazado — el fractal no detiene Cards por nombre de unit");
150 Err(fdo::Error::NotSupported(
151 "StopUnit: el fractal supervisa Cards; no se detienen por nombre de unit".into(),
152 ))
153 }
154
155 async fn restart_unit(&self, name: String, _mode: String) -> fdo::Result<OwnedObjectPath> {
156 warn!(%name, "RestartUnit rechazado — sin StopUnit honesto no hay reinicio");
157 Err(fdo::Error::NotSupported(
158 "RestartUnit: el fractal no reinicia Cards por nombre de unit".into(),
159 ))
160 }
161
162 async fn reload_unit(&self, name: String, _mode: String) -> fdo::Result<OwnedObjectPath> {
163 info!(%name, "ReloadUnit (no-op — Cards inmutables)");
164 let path = ObjectPath::try_from("/").unwrap();
165 Ok(path.into())
166 }
167
168 async fn kill_unit(&self, name: String, _who: String, _signal: i32) -> fdo::Result<()> {
169 warn!(%name, "KillUnit rechazado — no implementado");
170 Err(fdo::Error::NotSupported(
171 "KillUnit: enviar señales a una Card por nombre de unit no está implementado".into(),
172 ))
173 }
174
175 async fn subscribe(&self) -> fdo::Result<()> { Ok(()) }
176 async fn unsubscribe(&self) -> fdo::Result<()> { Ok(()) }
177
178 async fn reload(&self) -> fdo::Result<()> {
179 info!("Reload: trigger re-read (no-op — Cards no se recargan tras boot)");
180 Ok(())
181 }
182
183 async fn list_unit_files(&self) -> fdo::Result<Vec<(String, String)>> {
184 // Empty: no usamos unit files. Cards en su lugar.
185 Ok(vec![])
186 }
187
188 async fn list_jobs(&self) -> fdo::Result<Vec<(u32, String, String, String, OwnedObjectPath, OwnedObjectPath)>> {
189 Ok(vec![])
190 }
191
192 async fn get_default_target(&self) -> fdo::Result<String> {
193 Ok("multi-user.target".into())
194 }
195
196 async fn set_default_target(&self, _name: String, _force: bool) -> fdo::Result<(Vec<String>, Vec<String>, Vec<String>)> {
197 Err(fdo::Error::NotSupported("default target gestionado por Card de Semilla".into()))
198 }
199
200 // ----- Properties -----
201
202 #[zbus(property)]
203 async fn version(&self) -> String { format!("ente-systemd1-compat {}", env!("CARGO_PKG_VERSION")) }
204
205 #[zbus(property)]
206 async fn architecture(&self) -> String { std::env::consts::ARCH.into() }
207
208 #[zbus(property)]
209 async fn features(&self) -> String { "+ENTE-FRACTAL".into() }
210
211 #[zbus(property)]
212 async fn virtualization(&self) -> String { String::new() }
213
214 #[zbus(property)]
215 async fn confined(&self) -> bool { false }
216
217 #[zbus(property)]
218 async fn environment(&self) -> Vec<String> {
219 std::env::vars().map(|(k, v)| format!("{k}={v}")).collect()
220 }
221
222 #[zbus(property)]
223 async fn n_names(&self) -> u32 { 0 }
224
225 #[zbus(property)]
226 async fn n_jobs(&self) -> u32 { 0 }
227
228 #[zbus(property)]
229 async fn progress(&self) -> f64 { 1.0 }
230}
231
232/// Pregunta al bus interno por la lista de Entes vivos.
233async fn query_list_entes() -> Option<Vec<arje_bus::EnteInfo>> {
234 let mut client = match BusClient::from_env().await {
235 Ok(c) => c,
236 Err(e) => { warn!(?e, "no bus client — devuelvo vacío"); return None; }
237 };
238 match client.call(BusRequest::ListEntes).await {
239 Ok(BusResponse::Entes(entes)) => Some(entes),
240 Ok(other) => { warn!(?other, "ListEntes respuesta inesperada"); None }
241 Err(e) => { warn!(?e, "ListEntes call falló"); None }
242 }
243}
244
245/// Escape de nombres de units para object paths según convención systemd:
246/// `.` → `_2e`, `-` → `_2d`, etc. Para el demo usamos un escape simple.
247fn escape_unit_name(name: &str) -> String {
248 name.chars().map(|c| match c {
249 c if c.is_ascii_alphanumeric() => c.to_string(),
250 c => format!("_{:02x}", c as u32),
251 }).collect()
252}
253
254async fn announce_to_fractal() {
255 if let Ok(mut client) = BusClient::from_env().await {
256 let req = BusRequest::Announce {
257 capabilities: vec![Capability::Endpoint {
258 interface: arje_card::InterfaceId([0xa6; 16]),
259 version: 1,
260 }],
261 };
262 match client.call(req).await {
263 Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
264 Ok(other) => warn!(?other, "Announce respuesta inesperada"),
265 Err(e) => warn!(?e, "Announce falló"),
266 }
267 }
268}
269
270async fn wait_for_term() -> anyhow::Result<()> {
271 let mut term = signal(SignalKind::terminate())?;
272 let mut int_ = signal(SignalKind::interrupt())?;
273 tokio::select! {
274 _ = term.recv() => info!("SIGTERM"),
275 _ = int_.recv() => info!("SIGINT"),
276 }
277 Ok(())
278}
279
280fn init_tracing() {
281 let filter = EnvFilter::try_from_default_env()
282 .unwrap_or_else(|_| EnvFilter::new("arje_systemd1_compat=info"));
283 tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
284}
285
286#[allow(dead_code)]
287fn _suppress(_: HashMap<String, OwnedValue>) {} // mantener import si se reduce