use crate::seat_v1::device; use crate::{ecs, seat, seat_v1}; use rustix::{io, ioctl}; use std::ffi::c_int; use std::os::fd; use std::os::fd::AsFd; use std::path; impl hyprwire::Dispatch for crate::Sessiond { fn event( &mut self, object: &device::Device, event: ::Event<'_>, ) { let device::Event::Destroy = event; let Some(client_entity) = seat::client_entity_by_device_object(&self.world, &self.client_entities, object) else { return; }; if let Err(err) = close_device(&mut self.world, client_entity, object) { log::error!("Could not close device: {err}"); } } } pub fn close_device( world: &mut ecs::World, client_entity: ecs::Entity, object: &seat_v1::device::Device, ) -> anyhow::Result<()> { let session_id = world .get::(client_entity) .map(|session| session.0) .ok_or_else(|| anyhow::anyhow!("client has no session id"))?; let seat_name = world .get::(client_entity) .map(|seat| seat.0.clone()) .ok_or_else(|| anyhow::anyhow!("client has no seat"))?; let devices = world .get::(client_entity) .ok_or_else(|| anyhow::anyhow!("client has no devices"))?; let idx = devices .0 .iter() .position(|device_entity| contains_object(world, *device_entity, object)) .ok_or_else(|| anyhow::anyhow!("device not found"))?; let device_entity = devices.0[idx]; remove_object(world, device_entity, object); let device_path = device_path(world, device_entity)?; log::debug!( "Closing device {} for client {} on {}", device_path.display(), session_id.as_raw(), seat_name ); if decrement_ref(world, device_entity) > 0 { return Ok(()); } if let Err(err) = deactivate(world, device_entity) { log::error!( "Could not deactivate device {} for client {} on {}: {err}", device_path.display(), session_id.as_raw(), seat_name ); } if let Some(devices) = world.get_mut::(client_entity) { devices.0.remove(idx); } world.remove_entity(device_entity); Ok(()) } 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::(b'E', 0x91); const HIDIOCREVOKE: ioctl::Opcode = ioctl::opcode::write::(b'H', 0x0d); fn drm_set_master(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::NoArg::::new()) } } fn drm_drop_master(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::NoArg::::new()) } } fn evdev_revoke(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::IntegerSetter::::new_usize(0)) } } fn hidraw_revoke(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::IntegerSetter::::new_usize(0)) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeviceKind { Evdev, Drm, Wscons, Hidraw, } impl DeviceKind { pub fn from_path(path: &path::Path) -> Option { if matches_named_device(path, "/dev/input", "event") { Some(Self::Evdev) } else if path.starts_with("/dev/dri/") || path.starts_with("/dev/drm/") { Some(Self::Drm) } else if path.starts_with("/dev/wskbd") || path.starts_with("/dev/wsmouse") || path.starts_with("/dev/wsmux") { Some(Self::Wscons) } else if path.starts_with("/dev/hidraw") { Some(Self::Hidraw) } else { None } } } fn matches_named_device(path: &path::Path, parent: &str, prefix: &str) -> bool { path.parent() == Some(path::Path::new(parent)) && path .file_name() .and_then(|name| name.to_str()) .is_some_and(|name| name.starts_with(prefix)) } pub fn activate(world: &mut ecs::World, entity: ecs::Entity) -> anyhow::Result<()> { if world .get::(entity) .is_some_and(|active| active.0) { return Ok(()); } let kind = device_kind(world, entity)?; match kind { DeviceKind::Drm => { let fd = world .get::(entity) .ok_or_else(|| anyhow::anyhow!("device has no fd"))?; drm_set_master(&fd.0) .map_err(|err| anyhow::anyhow!("Could not make device fd drm master: {err}"))?; world.insert(entity, ecs::DeviceActive(true)); } DeviceKind::Evdev | DeviceKind::Hidraw => { return Err(anyhow::anyhow!("device type cannot be activated")); } DeviceKind::Wscons => { world.insert(entity, ecs::DeviceActive(true)); } } Ok(()) } pub fn deactivate(world: &mut ecs::World, entity: ecs::Entity) -> anyhow::Result<()> { if !world .get::(entity) .is_some_and(|active| active.0) { return Ok(()); } let kind = device_kind(world, entity)?; match kind { DeviceKind::Drm => { let fd = world .get::(entity) .ok_or_else(|| anyhow::anyhow!("device has no fd"))?; if let Err(err) = drm_drop_master(&fd.0) { log::error!("Could not revoke drm master on device fd: {err}"); return Err(err.into()); } } DeviceKind::Evdev => { let fd = world .get::(entity) .ok_or_else(|| anyhow::anyhow!("device has no fd"))?; if let Err(err) = evdev_revoke(&fd.0) { log::error!("Could not revoke evdev on device fd: {err}"); return Err(err.into()); } } DeviceKind::Hidraw => { let fd = world .get::(entity) .ok_or_else(|| anyhow::anyhow!("device has no fd"))?; if let Err(err) = hidraw_revoke(&fd.0) { log::error!("Could not revoke hidraw on device fd: {err}"); return Err(err.into()); } } DeviceKind::Wscons => {} } world.insert(entity, ecs::DeviceActive(false)); Ok(()) } pub fn matches_path(world: &ecs::World, entity: ecs::Entity, path: &path::Path) -> bool { world .get::(entity) .is_some_and(|device_path| device_path.0 == path) } pub fn add_object(world: &mut ecs::World, entity: ecs::Entity, object: device::Device) { if let Some(ref_count) = world.get_mut::(entity) { ref_count.0 += 1; } if let Some(objects) = world.get_mut::(entity) { objects.0.push(object); } } pub fn id(world: &ecs::World, entity: ecs::Entity) -> Option { world.get::(entity).map(|id| id.0) } pub fn device_kind(world: &ecs::World, entity: ecs::Entity) -> anyhow::Result { world .get::(entity) .map(|kind| kind.0) .ok_or_else(|| anyhow::anyhow!("device has no kind")) } pub fn device_path(world: &ecs::World, entity: ecs::Entity) -> anyhow::Result { world .get::(entity) .map(|path| path.0.clone()) .ok_or_else(|| anyhow::anyhow!("device has no path")) } pub fn path_display(world: &ecs::World, entity: ecs::Entity) -> String { device_path(world, entity).map_or_else( |_| "".to_string(), |path| path.display().to_string(), ) } pub fn absolute_path_string(world: &ecs::World, entity: ecs::Entity) -> anyhow::Result { let device_path = device_path(world, entity)?; Ok(path::absolute(&device_path) .unwrap_or(device_path) .display() .to_string()) } pub fn last_object(world: &ecs::World, entity: ecs::Entity) -> Option<&device::Device> { world .get::(entity) .and_then(|objects| objects.0.last()) } pub fn contains_object(world: &ecs::World, entity: ecs::Entity, object: &device::Device) -> bool { world .get::(entity) .is_some_and(|objects| objects.0.iter().any(|candidate| candidate == object)) } pub fn remove_object(world: &mut ecs::World, entity: ecs::Entity, object: &device::Device) { if let Some(objects) = world.get_mut::(entity) { objects.0.retain(|candidate| candidate != object); } } pub fn send_revoked(world: &ecs::World, entity: ecs::Entity) { if let Some(objects) = world.get::(entity) { for object in &objects.0 { object.send_revoked(); } } } pub fn decrement_ref(world: &mut ecs::World, entity: ecs::Entity) -> usize { let Some(ref_count) = world.get_mut::(entity) else { return 0; }; ref_count.0 = ref_count.0.saturating_sub(1); ref_count.0 } pub fn send_opened(world: &ecs::World, entity: ecs::Entity) -> anyhow::Result<()> { let Some(object) = last_object(world, entity) else { return Ok(()); }; let id = id(world, entity).ok_or_else(|| anyhow::anyhow!("device has no id"))?; let path = absolute_path_string(world, entity)?; let fd = world .get::(entity) .ok_or_else(|| anyhow::anyhow!("device has no fd"))?; object.send_opened(id, fd.0.as_fd(), path); Ok(()) } #[cfg(test)] mod tests { use super::{DeviceKind, matches_named_device}; use std::path::Path; #[test] fn recognizes_evdev_event_nodes() { assert_eq!( DeviceKind::from_path(Path::new("/dev/input/event0")), Some(DeviceKind::Evdev) ); assert_eq!( DeviceKind::from_path(Path::new("/dev/input/event12")), Some(DeviceKind::Evdev) ); } #[test] fn does_not_match_partial_parent_paths() { assert!(!matches_named_device( Path::new("/dev/input/by-path/event0"), "/dev/input", "event" )); assert_eq!(DeviceKind::from_path(Path::new("/dev/input")), None); } }