@@ -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"] }
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user