Release 1.0

This commit is contained in:
Breadway 2026-05-11 11:56:03 +08:00
parent 009ea6da0e
commit 730a8b61d7
32 changed files with 6629 additions and 0 deletions

265
breadd/src/adapters/udev.rs Normal file
View 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 &current_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)
}