Release 1.0
This commit is contained in:
parent
009ea6da0e
commit
730a8b61d7
32 changed files with 6629 additions and 0 deletions
265
breadd/src/adapters/udev.rs
Normal file
265
breadd/src/adapters/udev.rs
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use bread_shared::{now_unix_ms, AdapterSource, RawEvent};
|
||||
use serde_json::json;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::adapters::Adapter;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UdevAdapter {
|
||||
subsystems: Vec<String>,
|
||||
}
|
||||
|
||||
impl UdevAdapter {
|
||||
pub fn new(subsystems: Vec<String>) -> Self {
|
||||
Self { subsystems }
|
||||
}
|
||||
|
||||
pub async fn enumerate_existing(&self, tx: &mpsc::Sender<RawEvent>) -> Result<()> {
|
||||
let devices = enumerate_with_udev(&self.subsystems).unwrap_or_else(|_| {
|
||||
scan_devices(&self.subsystems).unwrap_or_default()
|
||||
});
|
||||
|
||||
for device in devices {
|
||||
tx.send(RawEvent {
|
||||
source: AdapterSource::Udev,
|
||||
kind: "udev.enumerate".to_string(),
|
||||
payload: json!({
|
||||
"action": "add",
|
||||
"id": device.id,
|
||||
"name": device.name,
|
||||
"subsystem": device.subsystem,
|
||||
}),
|
||||
timestamp: now_unix_ms(),
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Adapter for UdevAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
"udev"
|
||||
}
|
||||
|
||||
async fn run(&self, tx: mpsc::Sender<RawEvent>) -> Result<()> {
|
||||
debug!("udev adapter started");
|
||||
if let Ok(()) = run_udev_monitor(self.subsystems.clone(), tx.clone()).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Fallback for environments where monitor sockets are unavailable.
|
||||
let mut known: HashMap<String, ScannedDevice> = scan_devices(&self.subsystems)?
|
||||
.into_iter()
|
||||
.map(|d| (d.id.clone(), d))
|
||||
.collect();
|
||||
|
||||
loop {
|
||||
let current = scan_devices(&self.subsystems)?;
|
||||
let current_map: HashMap<String, ScannedDevice> = current
|
||||
.into_iter()
|
||||
.map(|d| (d.id.clone(), d))
|
||||
.collect();
|
||||
|
||||
for (id, dev) in ¤t_map {
|
||||
if !known.contains_key(id) {
|
||||
tx.send(raw_change_event("add", dev)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
for (id, dev) in &known {
|
||||
if !current_map.contains_key(id) {
|
||||
tx.send(raw_change_event("remove", dev)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
known = current_map;
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ScannedDevice {
|
||||
id: String,
|
||||
name: String,
|
||||
subsystem: String,
|
||||
}
|
||||
|
||||
async fn run_udev_monitor(subsystems: Vec<String>, tx: mpsc::Sender<RawEvent>) -> Result<()> {
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
let mut builder = udev::MonitorBuilder::new()?;
|
||||
for subsystem in &subsystems {
|
||||
builder = builder.match_subsystem(subsystem)?;
|
||||
}
|
||||
let monitor = builder.listen()?;
|
||||
|
||||
for event in monitor.iter() {
|
||||
let action = event
|
||||
.action()
|
||||
.map(|a| a.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "change".to_string());
|
||||
let subsystem = event
|
||||
.subsystem()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let name = event
|
||||
.property_value("ID_MODEL")
|
||||
.or_else(|| event.property_value("NAME"))
|
||||
.map(|v| v.to_string_lossy().to_string())
|
||||
.or_else(|| event.devnode().map(|n| n.display().to_string()))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let id = event
|
||||
.syspath()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let msg = RawEvent {
|
||||
source: AdapterSource::Udev,
|
||||
kind: "udev.change".to_string(),
|
||||
payload: json!({
|
||||
"action": action,
|
||||
"id": id,
|
||||
"name": name,
|
||||
"subsystem": subsystem,
|
||||
}),
|
||||
timestamp: now_unix_ms(),
|
||||
};
|
||||
|
||||
if tx.blocking_send(msg).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enumerate_with_udev(subsystems: &[String]) -> Result<Vec<ScannedDevice>> {
|
||||
let mut enumerator = udev::Enumerator::new()?;
|
||||
for subsystem in subsystems {
|
||||
enumerator.match_subsystem(subsystem)?;
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
for dev in enumerator.scan_devices()? {
|
||||
let subsystem = dev
|
||||
.subsystem()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let name = dev
|
||||
.property_value("ID_MODEL")
|
||||
.or_else(|| dev.property_value("NAME"))
|
||||
.map(|v| v.to_string_lossy().to_string())
|
||||
.or_else(|| dev.sysname().to_str().map(ToString::to_string))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let id = dev.syspath().to_string_lossy().to_string();
|
||||
|
||||
out.push(ScannedDevice {
|
||||
id,
|
||||
name,
|
||||
subsystem,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn raw_change_event(action: &str, dev: &ScannedDevice) -> RawEvent {
|
||||
RawEvent {
|
||||
source: AdapterSource::Udev,
|
||||
kind: "udev.change".to_string(),
|
||||
payload: json!({
|
||||
"action": action,
|
||||
"id": dev.id,
|
||||
"name": dev.name,
|
||||
"subsystem": dev.subsystem,
|
||||
}),
|
||||
timestamp: now_unix_ms(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_devices(subsystems: &[String]) -> Result<Vec<ScannedDevice>> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
if subsystems.iter().any(|s| s == "drm") {
|
||||
let drm_dir = Path::new("/sys/class/drm");
|
||||
if drm_dir.exists() {
|
||||
for entry in fs::read_dir(drm_dir)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if !name.contains('-') {
|
||||
continue;
|
||||
}
|
||||
let status = fs::read_to_string(entry.path().join("status")).unwrap_or_default();
|
||||
if status.trim() == "connected" {
|
||||
out.push(ScannedDevice {
|
||||
id: format!("drm:{name}"),
|
||||
name,
|
||||
subsystem: "drm".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subsystems.iter().any(|s| s == "input") {
|
||||
let input_dir = Path::new("/dev/input/by-id");
|
||||
if input_dir.exists() {
|
||||
for entry in fs::read_dir(input_dir)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
out.push(ScannedDevice {
|
||||
id: format!("input:{name}"),
|
||||
name,
|
||||
subsystem: "input".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subsystems.iter().any(|s| s == "power_supply") {
|
||||
let pwr_dir = Path::new("/sys/class/power_supply");
|
||||
if pwr_dir.exists() {
|
||||
for entry in fs::read_dir(pwr_dir)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
out.push(ScannedDevice {
|
||||
id: format!("power_supply:{name}"),
|
||||
name,
|
||||
subsystem: "power_supply".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subsystems.iter().any(|s| s == "usb") {
|
||||
let usb_dir = Path::new("/sys/bus/usb/devices");
|
||||
if usb_dir.exists() {
|
||||
for entry in fs::read_dir(usb_dir)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if !name.contains(':') && name.chars().any(|c| c.is_ascii_digit()) {
|
||||
out.push(ScannedDevice {
|
||||
id: format!("usb:{name}"),
|
||||
name,
|
||||
subsystem: "usb".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue