⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229
crates/ente-kernel/src/uevent.rs
Historia
Quién tocó qué
crudo
Pausa: 11 crates del fractal Ente #0 con cerebro completo
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}