Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
18 kB · 582 lines
Rust
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583use bevy_ecs::prelude::*;use rustix::{fs as rfs, io, ioctl};use std::collections::HashMap;use std::os::fd::{AsFd, OwnedFd};use std::os::unix::fs::{FileTypeExt, MetadataExt};use std::{fs, path};
const MAX_SEAT_DEVICES: usize = 128;
#[derive(Resource, Default)]pub struct DeviceIndex { by_object: HashMap<crate::seat_v1::device::Device, DeviceHandle>, by_client_path: HashMap<(Entity, path::PathBuf), u64>, devices: HashMap<u64, DeviceEntry>, next_device: u64, next_id: u32,}
struct DeviceHandle { client: Entity, device: u64, id: u32,}
struct DeviceEntry { client: Entity, path: path::PathBuf, kind: DeviceKind, fd: OwnedFd, handles: usize, active: bool,}
#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct DeviceInfo { pub id: u32, pub path: path::PathBuf, pub kind: DeviceKind, pub active: bool,}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum DeviceKind { Evdev, Drm, Wscons, Hidraw,}
impl DeviceKind { pub fn classify(metadata: &DeviceMetadata, path: &path::Path) -> Option<Self> { match (metadata.subsystem.as_str(), metadata.sysname.as_str()) { ("input", name) if name.starts_with("event") => Some(Self::Evdev), ("drm", _) => Some(Self::Drm), ("hidraw", _) => Some(Self::Hidraw), _ if path.to_str().is_some_and(|value| { value.starts_with("/dev/wskbd") || value.starts_with("/dev/wsmouse") || value.starts_with("/dev/wsmux") }) => { Some(Self::Wscons) } _ => None, } }}
#[derive(Debug, Clone, PartialEq, Eq)]pub struct DeviceMetadata { pub subsystem: String, pub sysname: String, pub syspath: path::PathBuf,}
pub fn metadata_for_path(path: &path::Path) -> anyhow::Result<DeviceMetadata> { let metadata = fs::metadata(path)?; anyhow::ensure!( metadata.file_type().is_char_device(), "device is not a character device" ); metadata_for_devnum( path::Path::new("/sys/dev/char"), rfs::major(metadata.rdev()), rfs::minor(metadata.rdev()), )}
fn metadata_for_devnum( root: &path::Path, major: u32, minor: u32,) -> anyhow::Result<DeviceMetadata> { let syspath = fs::canonicalize(root.join(format!("{major}:{minor}")))?; let sysname = syspath .file_name() .and_then(|name| name.to_str()) .ok_or_else(|| anyhow::anyhow!("sysfs device has no valid sysname"))? .to_owned(); let subsystem = fs::canonicalize(syspath.join("subsystem"))? .file_name() .and_then(|name| name.to_str()) .ok_or_else(|| anyhow::anyhow!("sysfs device has no valid subsystem"))? .to_owned();
Ok(DeviceMetadata { subsystem, sysname, syspath, })}
pub fn open( world: &mut World, client_entity: Entity, object: crate::seat_v1::device::Device, requested_path: &str,) { if !client_can_open(world, client_entity) { object.send_failed( crate::seat_v1::DeviceError::SeatDisabled, io::Errno::PERM.raw_os_error().cast_unsigned(), "open is only valid for the active seat client", ); return; }
let path = match fs::canonicalize(requested_path) { Ok(path) => path, Err(error) => { object.send_failed( crate::seat_v1::DeviceError::OpenFailed, error .raw_os_error() .unwrap_or(io::Errno::NOENT.raw_os_error()) .cast_unsigned(), "could not canonicalize device path", ); return; } };
let metadata = match metadata_for_path(&path) { Ok(metadata) => metadata, Err(error) => { object.send_failed( crate::seat_v1::DeviceError::OpenFailed, io::Errno::NODEV.raw_os_error().cast_unsigned(), format!("could not resolve sysfs device: {error}"), ); return; } };
let Some(kind) = DeviceKind::classify(&metadata, &path) else { object.send_failed( crate::seat_v1::DeviceError::OpenFailed, io::Errno::INVAL.raw_os_error().cast_unsigned(), "unsupported device type", ); return; };
let assigned_seat = { let config = world.non_send::<crate::config::Config>(); config.seats.seat_for_device( &config.lua, crate::config::Device { subsystem: metadata.subsystem.clone(), sysname: metadata.sysname.clone(), syspath: metadata.syspath.to_string_lossy().into_owned(), }, ) }; let assigned_seat = match assigned_seat { Ok(seat) => seat, Err(error) => { object.send_failed( crate::seat_v1::DeviceError::OpenFailed, io::Errno::IO.raw_os_error().cast_unsigned(), format!("device matcher failed: {error}"), ); return; } };
let client_seat = world .get::<crate::ecs::ClientSession>(client_entity) .and_then(|session| world.get::<crate::ecs::SessionSeat>(session.0)) .and_then(|seat| world.get::<crate::ecs::SeatName>(seat.0)) .map(|name| name.0.as_str()); if client_seat != Some(assigned_seat.as_str()) { object.send_failed( crate::seat_v1::DeviceError::OpenFailed, io::Errno::PERM.raw_os_error().cast_unsigned(), "device belongs to another seat", ); return; }
let client_device_count = world .resource::<DeviceIndex>() .by_object .values() .filter(|handle| handle.client == client_entity) .count(); if client_device_count >= MAX_SEAT_DEVICES { object.send_failed( crate::seat_v1::DeviceError::OpenFailed, io::Errno::MFILE.raw_os_error().cast_unsigned(), "client exceeded max seat devices", ); return; }
let existing = world .resource::<DeviceIndex>() .by_client_path .get(&(client_entity, path.clone())) .copied(); if let Some(device_key) = existing { let mut index = world.resource_mut::<DeviceIndex>(); let id = index.allocate_id(); let Some(entry) = index.devices.get_mut(&device_key) else { index.by_client_path.remove(&(client_entity, path)); object.send_failed( crate::seat_v1::DeviceError::OpenFailed, io::Errno::IO.raw_os_error().cast_unsigned(), "invalid shared device state", ); return; }; entry.handles += 1; let path_string = entry.path.to_string_lossy().into_owned(); object.send_opened(id, &entry.fd, &path_string); index.by_object.insert( object, DeviceHandle { client: client_entity, device: device_key, id, }, ); log::info!(client:? = client_entity, device_id = id, path = path_string.as_str(); "device handle reused"); return; }
let fd = match rfs::open( &path, rfs::OFlags::RDWR | rfs::OFlags::NOCTTY | rfs::OFlags::NOFOLLOW | rfs::OFlags::CLOEXEC | rfs::OFlags::NONBLOCK, rfs::Mode::empty(), ) { Ok(fd) => fd, Err(error) => { object.send_failed( crate::seat_v1::DeviceError::OpenFailed, error.raw_os_error().cast_unsigned(), "could not open device", ); return; } };
if kind == DeviceKind::Drm && let Err(error) = drm_set_master(&fd) { object.send_failed( crate::seat_v1::DeviceError::OpenFailed, error.raw_os_error().cast_unsigned(), "could not acquire DRM master", ); return; }
let path_string = path.to_string_lossy().into_owned(); let mut index = world.resource_mut::<DeviceIndex>(); let id = index.allocate_id(); index.next_device = index.next_device.saturating_add(1).max(1); let device_key = index.next_device; object.send_opened(id, &fd, &path_string); index.devices.insert( device_key, DeviceEntry { client: client_entity, path: path.clone(), kind, fd, handles: 1, active: true, }, ); index .by_client_path .insert((client_entity, path), device_key); index.by_object.insert( object, DeviceHandle { client: client_entity, device: device_key, id, }, );
log::info!(client:? = client_entity, device_id = id, path = path_string.as_str(); "device opened");}
impl DeviceIndex { pub(crate) fn ids_for_client(&self, client: Entity) -> Vec<u32> { self.by_object .values() .filter(|handle| handle.client == client) .map(|handle| handle.id) .collect() }
pub(crate) fn info_for_client_id(&self, client: Entity, id: u32) -> Option<DeviceInfo> { self.by_object.values().find_map(|handle| { (handle.client == client && handle.id == id).then(|| { let entry = self.devices.get(&handle.device)?; Some(DeviceInfo { id, path: entry.path.clone(), kind: entry.kind, active: entry.active, }) })? }) }
fn allocate_id(&mut self) -> u32 { self.next_id = self.next_id.saturating_add(1).max(1); self.next_id }
fn release_handle(&mut self, handle: DeviceHandle) { let Some(entry) = self.devices.get_mut(&handle.device) else { return; }; entry.handles = entry.handles.saturating_sub(1); log::info!(client:? = handle.client, device_id = handle.id, path = entry.path.display().to_string().as_str(); "device handle closed"); if entry.handles != 0 { return; }
let Some(entry) = self.devices.remove(&handle.device) else { return; }; let path_key = (entry.client, entry.path.clone()); if self.by_client_path.get(&path_key) == Some(&handle.device) { self.by_client_path.remove(&path_key); } release_entry(&entry); }}
pub fn close(world: &mut World, object: &crate::seat_v1::device::Device) { let Some(handle) = world.resource_mut::<DeviceIndex>().by_object.remove(object) else { log::warn!("received destroy for unknown device object"); return; }; world.resource_mut::<DeviceIndex>().release_handle(handle);}
pub fn close_client_devices(index: &mut DeviceIndex, client: Entity) { let objects: Vec<_> = index .by_object .iter() .filter_map(|(object, handle)| (handle.client == client).then_some(object.clone())) .collect(); for object in objects { if let Some(handle) = index.by_object.remove(&object) { index.release_handle(handle); } }}
pub fn deactivate_client_devices(index: &mut DeviceIndex, client: Entity) { let keys: Vec<_> = index .devices .iter() .filter_map(|(key, entry)| (entry.client == client && entry.active).then_some(*key)) .collect(); for key in keys { let Some(entry) = index.devices.get_mut(&key) else { continue; }; let result = match entry.kind { DeviceKind::Drm => drm_drop_master(&entry.fd), DeviceKind::Evdev => evdev_revoke(&entry.fd), DeviceKind::Hidraw => hidraw_revoke(&entry.fd), DeviceKind::Wscons => Ok(()), }; if let Err(error) = result { log::warn!(path = entry.path.display().to_string().as_str(), error = error.to_string().as_str(); "failed to revoke device"); } entry.active = false; if matches!(entry.kind, DeviceKind::Evdev | DeviceKind::Hidraw) { index .by_client_path .remove(&(entry.client, entry.path.clone())); } }}
pub fn activate_client_devices(index: &mut DeviceIndex, client: Entity) -> anyhow::Result<()> { for entry in index .devices .values_mut() .filter(|entry| entry.client == client && !entry.active) { match entry.kind { DeviceKind::Drm => drm_set_master(&entry.fd)?, DeviceKind::Wscons => {} DeviceKind::Evdev | DeviceKind::Hidraw => continue, } entry.active = true; } Ok(())}
fn release_entry(entry: &DeviceEntry) { if entry.active && entry.kind == DeviceKind::Drm && let Err(error) = drm_drop_master(&entry.fd) { log::warn!(path = entry.path.display().to_string().as_str(), error = error.to_string().as_str(); "failed to release DRM master"); }}
fn client_can_open(world: &World, client: Entity) -> bool { if world .get::<crate::ecs::ClientState>(client) .is_none_or(|state| *state != crate::ecs::ClientState::Enabled) { return false; } let Some(session) = world .get::<crate::ecs::ClientSession>(client) .map(|value| value.0) else { return false; }; let Some(seat) = world .get::<crate::ecs::SessionSeat>(session) .map(|value| value.0) else { return false; }; world .get::<crate::ecs::CurrentClient>(seat) .is_some_and(|current| current.0 == Some(client)) && world .get::<crate::ecs::ActiveState>(session) .is_some_and(|state| state.0 == crate::session_core_v1::ActiveState::Active)}
const DRM_SET_MASTER: ioctl::Opcode = ioctl::opcode::none(b'd', 0x1e);const DRM_DROP_MASTER: ioctl::Opcode = ioctl::opcode::none(b'd', 0x1f);const EVIOCREVOKE: ioctl::Opcode = ioctl::opcode::write::<i32>(b'E', 0x91);const HIDIOCREVOKE: ioctl::Opcode = ioctl::opcode::write::<i32>(b'H', 0x0d);
fn drm_set_master<Fd: AsFd>(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::NoArg::<DRM_SET_MASTER>::new()) }}
fn drm_drop_master<Fd: AsFd>(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::NoArg::<DRM_DROP_MASTER>::new()) }}
fn evdev_revoke<Fd: AsFd>(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::IntegerSetter::<EVIOCREVOKE>::new_usize(0)) }}
fn hidraw_revoke<Fd: AsFd>(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::IntegerSetter::<HIDIOCREVOKE>::new_usize(0)) }}
#[cfg(test)]mod tests { use super::*; use std::os::unix::fs::symlink;
#[test] fn classifies_supported_devices() { let metadata = |subsystem: &str, sysname: &str| DeviceMetadata { subsystem: subsystem.to_owned(), sysname: sysname.to_owned(), syspath: path::PathBuf::from("/sys/devices/test"), }; assert_eq!( DeviceKind::classify( &metadata("input", "event4"), path::Path::new("/dev/input/event4") ), Some(DeviceKind::Evdev) ); assert_eq!( DeviceKind::classify(&metadata("drm", "card0"), path::Path::new("/dev/dri/card0")), Some(DeviceKind::Drm) ); assert_eq!( DeviceKind::classify( &metadata("hidraw", "hidraw0"), path::Path::new("/dev/hidraw0") ), Some(DeviceKind::Hidraw) ); assert_eq!( DeviceKind::classify( &metadata("sound", "controlC0"), path::Path::new("/dev/snd/controlC0") ), None ); }
#[test] fn resolves_sysfs_metadata() -> anyhow::Result<()> { let root = std::env::temp_dir().join(format!("sessiond-seat-sysfs-{}", std::process::id())); let device = root.join("devices/pci/card0"); let subsystem = root.join("class/drm"); fs::create_dir_all(&device)?; fs::create_dir_all(&subsystem)?; fs::create_dir_all(root.join("dev/char"))?; symlink(&subsystem, device.join("subsystem"))?; symlink(&device, root.join("dev/char/226:0"))?;
let metadata = metadata_for_devnum(&root.join("dev/char"), 226, 0)?; assert_eq!(metadata.subsystem, "drm"); assert_eq!(metadata.sysname, "card0"); assert_eq!(metadata.syspath, fs::canonicalize(device)?); fs::remove_dir_all(root)?; Ok(()) }
#[test] fn final_handle_releases_underlying_device() -> anyhow::Result<()> { let mut index = DeviceIndex::default(); let client = Entity::from_bits(1); let path = path::PathBuf::from("/dev/null"); let fd = rfs::open(&path, rfs::OFlags::RDWR, rfs::Mode::empty())?; index.devices.insert( 1, DeviceEntry { client, path: path.clone(), kind: DeviceKind::Wscons, fd, handles: 2, active: true, }, ); index.by_client_path.insert((client, path), 1);
index.release_handle(DeviceHandle { client, device: 1, id: 1, }); assert_eq!(index.devices.get(&1).map(|entry| entry.handles), Some(1)); index .by_client_path .insert((client, path::PathBuf::from("/dev/null")), 2); index.release_handle(DeviceHandle { client, device: 1, id: 2, }); assert!(!index.devices.contains_key(&1)); assert_eq!( index .by_client_path .get(&(client, path::PathBuf::from("/dev/null"))), Some(&2) ); Ok(()) }}