gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

⎇ 5b761c17554ac412dde80ab2c2f77657fd1a90fe crates/uevent-relay/src/main.rs Rust · 338 líneas · 14.5 KB Historia Quién tocó qué crudo

feat(uevent-relay): que un accesorio enchufado DESPUES de arrancar aparezca 5b761c175 · sergio ·

1//! `uevent-relay` — que un accesorio enchufado DESPUÉS de arrancar aparezca.
2//!
3//! ══ EL DEFECTO, MEDIDO EN METAL EL 2026-10-15 ═════════════════════════════════════════════════
4//! El operador, con el escritorio abierto: *«los accesorios no aparecen cuando se conectan después
5//! de iniciar»*. No es el compositor y no es el kernel. Es una pieza que falta, y la ausencia es
6//! silenciosa en las tres capas:
7//!
8//!   · El kernel crea el nodo (`CONFIG_DEVTMPFS_MOUNT=y`) y emite su uevent al **grupo netlink 1**.
9//!   · `libudev-zero` —el libudev de la imagen, el único que sintetiza `ID_INPUT_*` sin `udevd`—
10//!     abre el monitor `"udev"` en el **grupo 0x4**, que es donde `udevd` reenvía lo ya procesado.
11//!   · `libinput` pide exactamente ese monitor (`udev_monitor_new_from_netlink(udev, "udev")`).
12//!
13//! Sin `udevd`, **nadie manda nunca nada al grupo 0x4**: el monitor no recibe un solo evento en
14//! toda la sesión. Y no es un bug de libudev-zero, es su contrato, escrito en su README:
15//!
16//!   «If you're developing your own device manager, you need to rebroadcast kernel uevents to the
17//!    `0x4` netlink group of NETLINK_KOBJECT_UEVENT. This is required because libudev-zero can't
18//!    simply listen to kernel uevents due to potential race conditions.»
19//!   «Hotplugging support is fully optional. You can skip this step…»
20//!
21//! takana se salteó ese paso. El candidato natural era `mdev`, y se **retiró** del defconfig de
22//! busybox el 2026-09-22 con este argumento: «`CONFIG_DEVTMPFS_MOUNT=y` hace que el kernel cree y
23//! monte `/dev`». Es cierto para los NODOS y no cubre el REENVÍO — y ahí se perdió el hotplug sin
24//! que ninguna prueba lo notara, porque todo lo que está enchufado al arrancar funciona igual.
25//!
26//! ══ POR QUÉ UN REENVIADOR Y NO UN PARCHE DE UNA LÍNEA ═════════════════════════════════════════
27//! Hacer que libudev-zero escuche el grupo 1 es una línea, y fue lo primero que pensé. El autor
28//! explica por qué no: **carrera**. El uevent del kernel puede llegar antes de que el nodo de `/dev`
29//! o los atributos de `/sys` estén listos, y entonces `libinput` abre un dispositivo a medio nacer y
30//! lo descarta — un hotplug que funciona nueve de cada diez veces es peor que ninguno, porque nadie
31//! lo puede diagnosticar. Este proceso **espera a que el dispositivo exista** antes de reenviarlo,
32//! que es lo que cierra esa carrera, y deja libudev-zero sin tocar (su hash no se mueve).
33//!
34//! ══ LO QUE NO HACE ════════════════════════════════════════════════════════════════════════════
35//! No crea nodos (lo hace devtmpfs), no aplica reglas, no monta nada. Un pendrive va a APARECER
36//! como `/dev/sdb1` y que alguien lo monte es otra conversación: esto es la campana, no la mano.
37
38use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
39use std::process::ExitCode;
40
41/// La familia netlink de los uevents. No está en `libc` para todos los targets, y es ABI estable.
42const NETLINK_KOBJECT_UEVENT: libc::c_int = 15;
43/// Donde emite el KERNEL. Un proceso de usuario puede escucharlo, pero no puede escribir ahí.
44const GRUPO_KERNEL: u32 = 1;
45/// Donde escucha `libudev-zero` el monitor `"udev"` (`UDEV_MONITOR_NLGRP` en su fuente).
46const GRUPO_UDEV: u32 = 4;
47/// Un `recvbuf` chico PIERDE eventos en ráfaga —enchufar un hub USB son veinte de golpe— y lo que
48/// se pierde no se reintenta nunca. udev pide 128 MiB; 8 cubre cualquier ráfaga de un escritorio.
49const RECVBUF: libc::c_int = 8 * 1024 * 1024;
50/// Cuánto se espera a que el dispositivo exista antes de reenviar (la carrera del README).
51const ESPERA_MAX_MS: u64 = 300;
52const ESPERA_PASO_MS: u64 = 10;
53
54fn main() -> ExitCode {
55    let verboso = std::env::var_os("UEVENT_RELAY_DEBUG").is_some();
56
57    // ⚠ Sin privilegio no se puede hacer multicast a un grupo netlink: el `sendmsg` sale con
58    // EPERM. Se dice ACÁ y con la causa, porque el síntoma de no decirlo es «el hotplug no anda»
59    // otra vez, con un proceso vivo que parece estar trabajando.
60    if unsafe { libc::geteuid() } != 0 {
61        eprintln!(
62            "uevent-relay: hace falta ser root (o CAP_NET_ADMIN) para emitir al grupo netlink {GRUPO_UDEV}; \
63             sin eso los accesorios enchufados en caliente no aparecen"
64        );
65        return ExitCode::from(1);
66    }
67
68    let rx = match abrir_socket(GRUPO_KERNEL) {
69        Ok(s) => s,
70        Err(e) => {
71            eprintln!("uevent-relay: no pude escuchar los uevents del kernel: {e}");
72            return ExitCode::from(1);
73        }
74    };
75    // El socket de salida se liga al grupo 0 —no escucha nada— sólo para que el kernel le asigne
76    // un `nl_pid`. Emitir por el MISMO socket con el que se escucha haría que cada reenvío volviera
77    // a entrar por el `recv`: un bucle que se multiplica solo. Medido en el diseño, no en producción.
78    let tx = match abrir_socket(0) {
79        Ok(s) => s,
80        Err(e) => {
81            eprintln!("uevent-relay: no pude abrir el socket de salida: {e}");
82            return ExitCode::from(1);
83        }
84    };
85
86    println!(
87        "uevent-relay: escuchando el grupo {GRUPO_KERNEL} (kernel) y reenviando al {GRUPO_UDEV} (el que mira libudev-zero)"
88    );
89
90    let mut buf = vec![0u8; 16 * 1024];
91    let mut reenviados: u64 = 0;
92    loop {
93        let n = unsafe {
94            libc::recv(rx.as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0)
95        };
96        if n <= 0 {
97            // EINTR no es un error: una señal no tiene por qué matar el hotplug de la máquina.
98            let err = std::io::Error::last_os_error();
99            if err.kind() == std::io::ErrorKind::Interrupted {
100                continue;
101            }
102            eprintln!("uevent-relay: recv falló ({err}); salgo para que arje me reinicie");
103            return ExitCode::from(1);
104        }
105        let n = n as usize;
106        let ev = Uevent::parsear(&buf[..n]);
107
108        // `libudev-zero` descarta todo mensaje cuyo contenido no traiga las cuatro claves que
109        // espera; reenviar lo que no es un uevent sería ruido en el socket de todos.
110        if !ev.util() {
111            continue;
112        }
113        if ev.accion.as_deref() == Some("add") {
114            esperar_a_que_exista(&ev, verboso);
115        }
116
117        let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
118        sa.nl_family = libc::AF_NETLINK as u16;
119        sa.nl_groups = GRUPO_UDEV;
120        let enviados = unsafe {
121            libc::sendto(
122                tx.as_raw_fd(),
123                buf.as_ptr() as *const libc::c_void,
124                n,
125                0,
126                &sa as *const libc::sockaddr_nl as *const libc::sockaddr,
127                std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
128            )
129        };
130        if enviados < 0 {
131            eprintln!(
132                "uevent-relay: no pude reenviar el evento de {}: {}",
133                ev.devpath.as_deref().unwrap_or("?"),
134                std::io::Error::last_os_error()
135            );
136            continue;
137        }
138        reenviados += 1;
139        if verboso {
140            eprintln!(
141                "uevent-relay: #{reenviados} {} {} {}",
142                ev.accion.as_deref().unwrap_or("?"),
143                ev.subsistema.as_deref().unwrap_or("?"),
144                ev.devpath.as_deref().unwrap_or("?")
145            );
146        }
147    }
148}
149
150fn abrir_socket(grupo: u32) -> std::io::Result<OwnedFd> {
151    let fd = unsafe {
152        libc::socket(
153            libc::AF_NETLINK,
154            libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
155            NETLINK_KOBJECT_UEVENT,
156        )
157    };
158    if fd < 0 {
159        return Err(std::io::Error::last_os_error());
160    }
161    let fd = unsafe { OwnedFd::from_raw_fd(fd) };
162    unsafe {
163        libc::setsockopt(
164            fd.as_raw_fd(),
165            libc::SOL_SOCKET,
166            libc::SO_RCVBUF,
167            &RECVBUF as *const libc::c_int as *const libc::c_void,
168            std::mem::size_of::<libc::c_int>() as libc::socklen_t,
169        );
170    }
171    let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
172    sa.nl_family = libc::AF_NETLINK as u16;
173    sa.nl_groups = grupo;
174    let r = unsafe {
175        libc::bind(
176            fd.as_raw_fd(),
177            &sa as *const libc::sockaddr_nl as *const libc::sockaddr,
178            std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
179        )
180    };
181    if r < 0 {
182        return Err(std::io::Error::last_os_error());
183    }
184    Ok(fd)
185}
186
187/// Lo que hace falta leer de un uevent para decidir si se reenvía y a qué hay que esperar.
188#[derive(Default, Debug, PartialEq)]
189struct Uevent {
190    accion: Option<String>,
191    devpath: Option<String>,
192    subsistema: Option<String>,
193    devname: Option<String>,
194    seqnum: bool,
195}
196
197impl Uevent {
198    /// El formato del alambre: `ACTION=add\0DEVPATH=/devices/...\0SUBSYSTEM=input\0...`, con un
199    /// primer campo `add@/devices/...` que no es un par y se salta.
200    fn parsear(buf: &[u8]) -> Uevent {
201        let mut ev = Uevent::default();
202        for campo in buf.split(|&b| b == 0) {
203            if campo.is_empty() {
204                continue;
205            }
206            let Ok(s) = std::str::from_utf8(campo) else { continue };
207            let Some((k, v)) = s.split_once('=') else { continue };
208            match k {
209                "ACTION" => ev.accion = Some(v.to_string()),
210                "DEVPATH" => ev.devpath = Some(v.to_string()),
211                "SUBSYSTEM" => ev.subsistema = Some(v.to_string()),
212                "DEVNAME" => ev.devname = Some(v.to_string()),
213                "SEQNUM" => ev.seqnum = true,
214                _ => {}
215            }
216        }
217        ev
218    }
219
220    /// Las cuatro claves que `libudev-zero` exige (`cnt != 4` ⇒ descarta el dispositivo). Reenviar
221    /// un mensaje que el otro lado va a tirar es gastar el socket de todos.
222    fn util(&self) -> bool {
223        self.accion.is_some() && self.devpath.is_some() && self.subsistema.is_some() && self.seqnum
224    }
225}
226
227/// La carrera que el README de libudev-zero nombra, cerrada: no se reenvía un `add` hasta que el
228/// dispositivo del que habla **existe**. Con devtmpfs el nodo suele estar antes que el evento, así
229/// que lo normal es que esto no espere ni un paso; cuando espera, es justo el caso que fallaba.
230fn esperar_a_que_exista(ev: &Uevent, verboso: bool) {
231    let mut esperado = 0;
232    while esperado < ESPERA_MAX_MS {
233        let sys_ok = ev
234            .devpath
235            .as_ref()
236            .map(|p| std::path::Path::new(&format!("/sys{p}")).exists())
237            .unwrap_or(true);
238        let dev_ok = ev
239            .devname
240            .as_ref()
241            .map(|n| std::path::Path::new(&format!("/dev/{n}")).exists())
242            .unwrap_or(true);
243        if sys_ok && dev_ok {
244            if esperado > 0 && verboso {
245                eprintln!("uevent-relay: esperé {esperado} ms a que naciera {}", ev.devpath.as_deref().unwrap_or("?"));
246            }
247            return;
248        }
249        std::thread::sleep(std::time::Duration::from_millis(ESPERA_PASO_MS));
250        esperado += ESPERA_PASO_MS;
251    }
252    // No se descarta: un dispositivo que tarda más que esto igual puede servirle a libinput, y
253    // tirarlo sería volver al síntoma. Se reenvía y se dice.
254    if verboso {
255        eprintln!(
256            "uevent-relay: {} no apareció en {ESPERA_MAX_MS} ms; lo reenvío igual",
257            ev.devpath.as_deref().unwrap_or("?")
258        );
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    /// Un uevent de verdad, copiado de la forma que emite el kernel al enchufar un ratón USB.
267    fn crudo(partes: &[&str]) -> Vec<u8> {
268        let mut v = Vec::new();
269        for p in partes {
270            v.extend_from_slice(p.as_bytes());
271            v.push(0);
272        }
273        v
274    }
275
276    #[test]
277    fn parsea_un_uevent_de_raton() {
278        let buf = crudo(&[
279            "add@/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0/0003:046D:C077.0003/input/input21/event5",
280            "ACTION=add",
281            "DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0/0003:046D:C077.0003/input/input21/event5",
282            "SUBSYSTEM=input",
283            "DEVNAME=input/event5",
284            "SEQNUM=4821",
285        ]);
286        let ev = Uevent::parsear(&buf);
287        assert_eq!(ev.accion.as_deref(), Some("add"));
288        assert_eq!(ev.subsistema.as_deref(), Some("input"));
289        assert_eq!(ev.devname.as_deref(), Some("input/event5"));
290        assert!(ev.devpath.unwrap().ends_with("event5"));
291        assert!(ev.seqnum);
292    }
293
294    /// El brazo de control: lo que `libudev-zero` va a tirar no se reenvía. Sin esta prueba, el
295    /// relay podría estar «funcionando» y reenviando sólo basura.
296    #[test]
297    fn lo_que_el_otro_lado_descarta_no_se_reenvia() {
298        // Sin SEQNUM: libudev-zero cuenta 3 de 4 claves y devuelve NULL.
299        let sin_seq = Uevent::parsear(&crudo(&["ACTION=add", "DEVPATH=/devices/x", "SUBSYSTEM=input"]));
300        assert!(!sin_seq.util(), "sin SEQNUM el otro lado lo tira");
301        // Sin SUBSYSTEM, idem.
302        let sin_sub = Uevent::parsear(&crudo(&["ACTION=add", "DEVPATH=/devices/x", "SEQNUM=1"]));
303        assert!(!sin_sub.util());
304        // Un mensaje que no es un uevent (los hay en ese socket) no pasa.
305        assert!(!Uevent::parsear(b"basura sin pares").util());
306        // Y el completo sí.
307        let ok = Uevent::parsear(&crudo(&[
308            "ACTION=remove",
309            "DEVPATH=/devices/x",
310            "SUBSYSTEM=usb",
311            "SEQNUM=9",
312        ]));
313        assert!(ok.util());
314    }
315
316    /// Un `remove` no espera a que nada exista —el dispositivo justamente ya no está— y ésa es la
317    /// razón de que la espera cuelgue sólo del `add`.
318    #[test]
319    fn el_remove_no_espera() {
320        let ev = Uevent::parsear(&crudo(&[
321            "ACTION=remove",
322            "DEVPATH=/devices/no-existe-jamas",
323            "SUBSYSTEM=input",
324            "DEVNAME=input/event99",
325            "SEQNUM=12",
326        ]));
327        assert_eq!(ev.accion.as_deref(), Some("remove"));
328        let t0 = std::time::Instant::now();
329        // La función se llama sólo para `add` en el bucle; acá se comprueba que si se llamara con
330        // un syspath inexistente, el techo es el declarado y no un cuelgue.
331        esperar_a_que_exista(&ev, false);
332        assert!(
333            t0.elapsed().as_millis() <= (ESPERA_MAX_MS as u128) + 200,
334            "la espera no tiene techo: {} ms",
335            t0.elapsed().as_millis()
336        );
337    }
338}