Initial commit

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-09-01 13:01:13 +00:00
commit 5163bdc367
34 changed files with 2625 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "lxdeviced"
version = "0.1.0"
edition = "2024"
[dependencies]
ipc = { path = "../libs/ipc" }
kobject-uevent = { path = "../libs/kobject-uevent" }
libc = "0.2"
nix = { version = "0.31", features = ["fs", "user"] }
rule = { path = "../libs/rule" }
rustc-hash = "2"
serde = "1"
serde_json = "1"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "fs", "signal", "sync"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+209
View File
@@ -0,0 +1,209 @@
use kobject_uevent::KobjectUevent;
use rule::{engine::Engine, parser::When};
use rustc_hash::FxHashMap;
use std::{
fs::Permissions,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
sync::{Arc, LazyLock, RwLock},
};
use tokio::sync::Mutex;
static DEVICES: LazyLock<RwLock<FxHashMap<String, ArcDevice>>> =
LazyLock::new(|| Default::default());
pub type ArcDevice = Arc<Device>;
#[derive(Debug)]
pub struct Device {
lock: Mutex<()>,
engine: Engine,
}
impl Device {
pub fn new() -> Self {
let engine = Engine::new();
engine.set_property("DEVROOT".into(), dev_root().to_string_lossy().into());
Self {
lock: Mutex::new(()),
engine,
}
}
fn apply_uevent(&self, ev: &KobjectUevent) {
for (k, v) in ev.inner().iter() {
self.engine.set_property(k.into(), v.into());
}
self.update_derived_properties();
}
fn update_derived_properties(&self) {
if let Some(devname) = self.engine.get_property("DEVNAME") {
self.engine.set_property(
"DEVNODE".into(),
format!("{}/{devname}", dev_root().display()),
);
}
}
async fn update_devnode_creds(&self) {
let Some(devnode) = self.engine.get_property("DEVNODE") else {
return;
};
let uid = crate::util::uid_by_string(self.engine.get_property("OWNER").as_deref());
let gid = crate::util::gid_by_string(self.engine.get_property("GROUP").as_deref());
let mode = crate::util::parse_mode(self.engine.get_property("MODE").as_deref());
if let Err(err) = crate::util::set_ownership(&devnode, uid, gid).await {
tracing::warn!("{devnode}: failed to set ownership: {err}");
}
if let Some(mode) = mode
&& let Err(err) =
tokio::fs::set_permissions(&devnode, Permissions::from_mode(mode as _)).await
{
tracing::warn!("{devnode}: failed to set permissions: {err}");
}
}
async fn update_symlinks(&self) {
let Some(devnode) = self.engine.get_property("DEVNODE") else {
return;
};
for i in self.engine.get_list("SYMLINKS") {
if let Ok(x) = tokio::fs::read_link(&i).await
&& x == Path::new(&devnode)
{
return;
}
if let Some(parent) = Path::new(&i).parent() {
_ = tokio::fs::create_dir_all(parent).await;
}
if let Err(err) = tokio::fs::symlink(&devnode, &i).await {
tracing::warn!("{devnode}: failed to create symbolic link \"{i}\": {err}");
}
}
}
async fn remove_symlinks(&self) {
for i in self.engine.get_list("SYMLINKS") {
if let Err(err) = tokio::fs::remove_file(&i).await {
tracing::warn!("failed to remove symbolic link \"{i}\": {err}");
}
}
}
}
pub async fn handle_uevent(ev: &KobjectUevent) {
match ev.action() {
Some("add") => add(ev).await,
Some("remove") => remove(ev).await,
Some("change") => change(ev).await,
Some(other) => {
tracing::warn!("Unrecognized uevent action \"{other}\", should we update lxdeviced?");
}
None => (),
}
}
async fn add(ev: &KobjectUevent) {
assert_eq!(ev.action(), Some("add"));
let Some(devpath) = ev.devpath() else {
return;
};
let device = Device::new();
device.apply_uevent(&ev);
device.engine.exec(&crate::rules::get()).await;
device.update_devnode_creds().await;
device.update_symlinks().await;
DEVICES
.write()
.unwrap()
.insert(devpath.into(), Arc::new(device));
}
async fn remove(ev: &KobjectUevent) {
assert_eq!(ev.action(), Some("remove"));
let Some(devpath) = ev.devpath() else {
return;
};
let Some(device) = DEVICES.read().unwrap().get(devpath).cloned() else {
return;
};
let _guard = device.lock.lock().await;
device.apply_uevent(ev);
device.engine.exec(&crate::rules::get()).await;
device.remove_symlinks().await;
DEVICES.write().unwrap().remove(devpath);
}
async fn change(ev: &KobjectUevent) {
assert_eq!(ev.action(), Some("change"));
let Some(devpath) = ev.devpath() else {
return;
};
let Some(device) = DEVICES.read().unwrap().get(devpath).cloned() else {
return;
};
let _guard = device.lock.lock().await;
device.apply_uevent(ev);
device.engine.exec(&crate::rules::get()).await;
}
pub async fn update_devnode(ev: &KobjectUevent) {
if std::env::var("LXDEVICED_HAS_DEVTMPFS").as_deref() == Ok("1") {
return;
}
let Some(devname) = ev.devname() else {
return;
};
let devnode = dev_root().join(devname);
if ev.action() == Some("add") {
if let Some(parent) = devnode.parent() {
_ = tokio::fs::create_dir_all(&parent).await;
}
let Some(major) = ev.major() else {
return;
};
let Some(minor) = ev.minor() else {
return;
};
let mode = ev.devmode().unwrap_or(0o600);
let dev = libc::makedev(major, minor);
if ev.subsystem() == Some("block") {
_ = crate::util::create_blockdev(devnode, dev, mode).await;
} else {
_ = crate::util::create_chardev(devnode, dev, mode).await;
}
} else if ev.action() == Some("remove") {
_ = tokio::fs::remove_file(&devnode).await;
}
}
pub async fn list_devices(when: When) -> Vec<String> {
let mut ret = Vec::new();
let keys: Vec<String> = DEVICES.read().unwrap().keys().map(Into::into).collect();
for key in keys {
let Some(device) = DEVICES.read().unwrap().get(&key).cloned() else {
continue;
};
if device.engine.exec_condition(&when).unwrap_or_default() {
ret.push(key);
}
}
ret
}
fn dev_root() -> PathBuf {
std::env::var("LXDEVICED_DEV_ROOT")
.as_deref()
.unwrap_or("/dev")
.into()
}
+67
View File
@@ -0,0 +1,67 @@
use ipc::{
repr::{Error, Request},
server::{Connection, Listener},
};
use rule::parser::When;
use serde::Serialize;
use std::pin::Pin;
pub fn start_server() -> std::io::Result<()> {
let listener = Listener::bind()?;
tokio::spawn(server(listener));
Ok(())
}
async fn server(listener: Listener) {
loop {
let Ok(connection) = listener.accept().await else {
continue;
};
tracing::debug!(
"Established connection with {{ uid={}, gid={}, pid={} }}",
connection.uid,
connection.gid,
connection.pid
);
tokio::spawn(handle_connection(connection));
}
}
async fn handle_connection(mut connection: Connection) -> std::io::Result<()> {
loop {
let request = connection.recv().await?;
let handler = match request {
Request::Trigger(set, action) => handler(trigger(set, action)),
Request::ReloadRules => handler(reload_rules()),
Request::ListDevices(when) => handler(list_devices(when)),
};
let reply = handler.await;
connection.send(reply).await?;
}
}
async fn trigger(set: String, action: String) -> Result<(), Error> {
if !crate::trigger::VALID_ACTIONS.contains(&action.as_str()) {
return Err(Error::BadRequest);
}
crate::trigger::trigger(&set, &action).await;
Ok(())
}
async fn reload_rules() -> Result<(), Error> {
crate::rules::reload();
Ok(())
}
async fn list_devices(when: When) -> Result<Vec<String>, Error> {
Ok(crate::device::list_devices(when).await)
}
fn handler<R: Serialize>(
fut: impl Future<Output = Result<R, Error>> + Send + 'static,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, Error>> + Send>> {
Box::pin(async move {
let reply = fut.await;
reply.map(|x| serde_json::to_value(x).unwrap())
})
}
+25
View File
@@ -0,0 +1,25 @@
mod device;
mod ipc;
mod rules;
mod trigger;
mod uevent;
mod util;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
if let Err(err) = ipc::start_server() {
tracing::error!("Failed to start IPC server: {err}");
std::process::exit(1);
}
if let Err(err) = uevent::start_monitor() {
tracing::error!("Failed to start KOBJECT_UEVENT monitor: {err}");
std::process::exit(1);
}
rules::reload();
_ = tokio::signal::ctrl_c().await;
}
+42
View File
@@ -0,0 +1,42 @@
use rule::engine::RuleSet;
use std::{
path::PathBuf,
sync::{LazyLock, RwLock},
};
static RULE_SET: LazyLock<RwLock<RuleSet>> = LazyLock::new(|| RwLock::new(RuleSet::empty()));
pub fn reload() {
*RULE_SET.write().unwrap() = RuleSet::open_dirs(rule_dirs().iter());
}
pub fn get() -> RuleSet {
RULE_SET.read().unwrap().clone()
}
pub fn rule_dirs() -> Vec<PathBuf> {
let mut all = Vec::with_capacity(1);
if let Some(path) = bundle_rule_dir() {
all.push(path);
}
if let Ok(val) = std::env::var("LXDEVICED_EXTRA_RULE_DIRS") {
for path in val.split(':') {
all.push(path.into());
}
}
all.push("/share/device/rules".into());
all.push("/usr/share/device/rules".into());
all.push("/etc/device/rules".into());
all.push("/var/config/device/rules".into());
all
}
fn bundle_rule_dir() -> Option<PathBuf> {
// current_exe: "/bundle/bin/lxdeviced"
std::fs::canonicalize(std::env::current_exe().ok()?)
.ok()?
.parent()?
.parent()?
.join("share/device/rules")
.into()
}
+62
View File
@@ -0,0 +1,62 @@
use std::path::{Path, PathBuf};
pub const VALID_ACTIONS: &[&str] = &["add", "remove", "change"];
pub async fn trigger(set: &str, action: &str) {
assert!(VALID_ACTIONS.contains(&action));
for i in set_to_dirs(set) {
let path = &sysfs_root().join(i);
if trigger_dir(path, action).await.is_err() {
tracing::warn!(
"\"{}\" is inaccessible. Check your system configuration.",
path.display()
);
}
}
}
async fn trigger_dir(path: &Path, action: &str) -> std::io::Result<()> {
let mut read_dir = tokio::fs::read_dir(path).await?;
while let Ok(Some(dirent)) = read_dir.next_entry().await {
if dirent.file_name().as_encoded_bytes() == b"uevent" {
match tokio::fs::write(dirent.path(), action.as_bytes()).await {
Ok(()) => {
tracing::debug!(
"Triggering uevent \"{action}\" for \"{}\".",
dirent.path().display()
);
}
Err(err) => {
tracing::warn!(
"Failed to trigger uevent \"{action}\" for \"{}\": {err}",
dirent.path().display()
);
}
};
} else if dirent
.file_type()
.await
.map(|x| x.is_dir())
.unwrap_or_default()
{
_ = Box::pin(trigger_dir(&dirent.path(), action)).await;
}
}
Ok(())
}
fn set_to_dirs(set: &str) -> Vec<PathBuf> {
if set == "all" {
vec![sysfs_root().into()]
} else if set == "devices" {
vec![sysfs_root().join("devices")]
} else {
vec![]
}
}
fn sysfs_root() -> PathBuf {
std::env::var("LXDEVICED_SYSFS_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| "/sys".into())
}
+22
View File
@@ -0,0 +1,22 @@
use kobject_uevent::KobjectUeventSocket;
pub fn start_monitor() -> std::io::Result<()> {
let socket = KobjectUeventSocket::connect(1)?;
tokio::spawn(async move {
loop {
let ev = match socket.recv().await {
Ok(ev) => ev,
Err(err) => {
tracing::warn!("Failed to receive KOBJECT_UEVENT: {err}");
continue;
}
};
tracing::debug!("Received KOBJECT_UEVENT: {ev:?}");
crate::device::update_devnode(&ev).await;
tokio::spawn(async move {
crate::device::handle_uevent(&ev).await;
});
}
});
Ok(())
}
+88
View File
@@ -0,0 +1,88 @@
use nix::{
sys::stat::{Mode, SFlag},
unistd::{Group, User},
};
use std::{os::unix::fs::lchown, path::PathBuf};
pub async fn create_blockdev(path: impl Into<PathBuf>, dev: u64, mode: u16) -> std::io::Result<()> {
create_nodefile(path, SFlag::S_IFBLK, dev, mode).await
}
pub async fn create_chardev(path: impl Into<PathBuf>, dev: u64, mode: u16) -> std::io::Result<()> {
create_nodefile(path, SFlag::S_IFCHR, dev, mode).await
}
pub async fn set_ownership(
path: impl Into<PathBuf>,
uid: Option<u32>,
gid: Option<u32>,
) -> std::io::Result<()> {
let path = path.into();
tokio::task::spawn_blocking(move || lchown(&path, uid, gid))
.await
.unwrap()
.map_err(Into::into)
}
pub fn uid_by_string(user: Option<&str>) -> Option<u32> {
let mut uid = user.and_then(|x| {
x.parse::<u32>().ok().or_else(|| {
User::from_name(x)
.ok()
.flatten()
.map(|user| user.uid.as_raw())
})
});
if uid.is_none()
&& let Some(user) = user
{
uid = Some(0);
tracing::warn!("Unrecognized user \"{user}\", fallbacking to root",);
}
uid
}
pub fn gid_by_string(group: Option<&str>) -> Option<u32> {
let mut gid = group.and_then(|x| {
x.parse::<u32>().ok().or_else(|| {
Group::from_name(x)
.ok()
.flatten()
.map(|group| group.gid.as_raw())
})
});
if gid.is_none()
&& let Some(group) = group
{
gid = Some(0);
tracing::warn!("Unrecognized group \"{group}\", fallbacking to root",);
}
gid
}
pub fn parse_mode(mode_raw: Option<&str>) -> Option<u16> {
let mode = mode_raw
.and_then(|x| x.strip_prefix("0o"))
.and_then(|x| u16::from_str_radix(x, 8).ok());
if mode.is_none()
&& let Some(mode_raw) = mode_raw
{
tracing::warn!("Invalid mode \"{mode_raw}\", fallbacking to the default value");
}
mode
}
async fn create_nodefile(
path: impl Into<PathBuf>,
sflag: SFlag,
dev: u64,
mode: u16,
) -> std::io::Result<()> {
let path = path.into();
tokio::task::spawn_blocking(move || {
nix::sys::stat::mknod(&path, sflag, Mode::from_bits_retain(mode as _), dev)
})
.await
.unwrap()
.map_err(Into::into)
}