gioser
⎇ main crates/ente-kernel/src/uevent.rs Rust · 146 líneas · 4.7 KB Historia Quién tocó qué crudo

Pausa: 11 crates del fractal Ente #0 con cerebro completo d6b8f18b4 · Sergio ·

1//! Stream de uevents del kernel vía NETLINK_KOBJECT_UEVENT.
2//!
3//! Bind requiere CAP_NET_ADMIN. En dev mode normal eso no está disponible —
4//! el caller debe estar preparado para que `spawn_uevent_stream` falle, y
5//! continuar sin grafo de dispositivos.
6
7use anyhow::Context;
8use ente_card::DeviceClass;
9use nix::sys::socket::{bind, socket, AddressFamily, NetlinkAddr, SockFlag, SockProtocol, SockType};
10use std::collections::HashMap;
11use std::os::fd::{AsRawFd, OwnedFd};
12use tokio::io::unix::AsyncFd;
13use tokio::sync::mpsc;
14use tracing::{trace, warn};
15
16#[derive(Debug, Clone)]
17pub struct UEvent {
18    pub action: UAction,
19    pub devpath: String,
20    pub subsystem: Option<String>,
21    pub device_class: Option<DeviceClass>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum UAction {
26    Add, Remove, Change, Move, Online, Offline, Bind, Unbind, Unknown,
27}
28
29pub fn spawn_uevent_stream() -> anyhow::Result<mpsc::Receiver<UEvent>> {
30    let fd: OwnedFd = socket(
31        AddressFamily::Netlink,
32        SockType::Datagram,
33        SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC,
34        SockProtocol::NetlinkKObjectUEvent,
35    ).context("netlink socket")?;
36
37    // pid=0 → kernel asigna; groups=1 → multicast group del kernel uevent.
38    let addr = NetlinkAddr::new(0, 1);
39    bind(fd.as_raw_fd(), &addr).context("bind netlink uevent (CAP_NET_ADMIN?)")?;
40
41    let async_fd = AsyncFd::new(NetlinkHandle(fd)).context("AsyncFd::new(netlink)")?;
42    let (tx, rx) = mpsc::channel(128);
43
44    tokio::spawn(async move {
45        let mut buf = vec![0u8; 16 * 1024];
46        loop {
47            let mut guard = match async_fd.readable().await {
48                Ok(g) => g,
49                Err(e) => { warn!(?e, "netlink readable"); return; }
50            };
51            let raw_fd = guard.get_inner().as_raw_fd();
52            loop {
53                let n = unsafe {
54                    libc::recv(raw_fd, buf.as_mut_ptr() as *mut _, buf.len(), 0)
55                };
56                if n <= 0 { break; }
57                if let Some(evt) = parse_uevent(&buf[..n as usize]) {
58                    trace!(?evt.action, devpath = %evt.devpath, "uevent");
59                    if tx.send(evt).await.is_err() { return; }
60                }
61            }
62            guard.clear_ready();
63        }
64    });
65
66    Ok(rx)
67}
68
69struct NetlinkHandle(OwnedFd);
70impl AsRawFd for NetlinkHandle {
71    fn as_raw_fd(&self) -> std::os::fd::RawFd { self.0.as_raw_fd() }
72}
73
74fn parse_uevent(buf: &[u8]) -> Option<UEvent> {
75    // udev re-emite mensajes con magic "libudev\0..." al multicast group 2.
76    // Si llegan al grupo 1 (improbable con bind groups=1) los filtramos igual.
77    if buf.starts_with(b"libudev\0") {
78        return None;
79    }
80    let mut parts = buf.split(|b| *b == 0).filter(|s| !s.is_empty());
81    let header = std::str::from_utf8(parts.next()?).ok()?;
82    let (action_s, devpath) = header.split_once('@')?;
83    let mut env: HashMap<String, String> = HashMap::new();
84    for kv in parts {
85        if let Ok(s) = std::str::from_utf8(kv) {
86            if let Some((k, v)) = s.split_once('=') {
87                env.insert(k.to_string(), v.to_string());
88            }
89        }
90    }
91    let subsystem = env.remove("SUBSYSTEM");
92    let device_class = subsystem.as_deref().and_then(map_device_class);
93    Some(UEvent {
94        action: parse_action(action_s),
95        devpath: devpath.to_string(),
96        subsystem,
97        device_class,
98    })
99}
100
101fn parse_action(s: &str) -> UAction {
102    match s {
103        "add" => UAction::Add,
104        "remove" => UAction::Remove,
105        "change" => UAction::Change,
106        "move" => UAction::Move,
107        "online" => UAction::Online,
108        "offline" => UAction::Offline,
109        "bind" => UAction::Bind,
110        "unbind" => UAction::Unbind,
111        _ => UAction::Unknown,
112    }
113}
114
115fn map_device_class(subsys: &str) -> Option<DeviceClass> {
116    Some(match subsys {
117        "block" => DeviceClass::Block,
118        "tty" => DeviceClass::Tty,
119        "input" => DeviceClass::Input,
120        "drm" => DeviceClass::Drm,
121        "net" => DeviceClass::Net,
122        "hidraw" => DeviceClass::Hidraw,
123        _ => return None,
124    })
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn parse_minimal_uevent() {
133        let buf = b"add@/devices/foo\0ACTION=add\0DEVPATH=/devices/foo\0SUBSYSTEM=block\0";
134        let evt = parse_uevent(buf).expect("parsed");
135        assert_eq!(evt.action, UAction::Add);
136        assert_eq!(evt.devpath, "/devices/foo");
137        assert_eq!(evt.subsystem.as_deref(), Some("block"));
138        assert!(matches!(evt.device_class, Some(DeviceClass::Block)));
139    }
140
141    #[test]
142    fn libudev_messages_filtered() {
143        let buf = b"libudev\0\xfe\xed\xca\xfeadd@/devices/foo\0";
144        assert!(parse_uevent(buf).is_none());
145    }
146}