use crate::seat_v1::device; use crate::session_core_v1; use crate::session_core_v1::session_core_seat_device; use crate::{client, ecs, reconcile, seat, seat_v1}; use bevy_ecs::prelude::*; use bevy_ecs::{entity, world}; use rustix::{io, ioctl}; use std::ffi::c_int; use std::os::fd; use std::os::fd::AsFd; use std::{fs, path}; const MAX_SEAT_DEVICES: u8 = 128; pub fn remove_protocol_objects(world: &mut world::World, device_entity: entity::Entity) { world .resource_mut::() .remove_device_objects(device_entity); } #[allow(clippy::too_many_arguments)] pub fn spawn( world: &mut world::World, object: seat_v1::device::Device, path: path::PathBuf, kind: DeviceKind, id: u32, active: bool, fd: fd::OwnedFd, owner: entity::Entity, ) -> entity::Entity { let mut cmd = world.spawn(ecs::DeviceBundle { marker: ecs::Device, path: ecs::DevicePath(path), ref_count: ecs::DeviceRefCount(1), kind: ecs::DeviceKind(kind), fd: ecs::DeviceFd(fd), owner: ecs::OwnerClient(owner), }); if active { cmd.insert(ecs::DeviceActive); } let entity = cmd.id(); world .resource_mut::() .add_device_object(entity, ecs::DeviceObject { object, id }); entity } impl hyprwire::Dispatch for crate::Sessiond { fn event( &mut self, object: &device::Device, event: ::Event<'_>, ) { let device::Event::Destroy = event; let Some(client_entity) = client::entity_by_device_object(&self.world, object) else { return; }; if let Err(err) = close(&mut self.world, client_entity, object) { log::error!("Could not close device: {err}"); } } } impl hyprwire::Dispatch for crate::Sessiond { fn event( &mut self, object: &session_core_seat_device::SessionCoreSeatDevice, event: ::Event<'_>, ) { let session_core_seat_device::Event::Destroy = event; let Some(client_entity) = client::entity_by_core_device_object(&self.world, object) else { return; }; let Some(device_entity) = entity_by_core_object(&self.world, client_entity, object) else { return; }; remove_core_object(&mut self.world, device_entity, object); } } pub fn remove_core_object( world: &mut world::World, device_entity: entity::Entity, object: &session_core_seat_device::SessionCoreSeatDevice, ) { world .resource_mut::() .remove_seat_device_object(device_entity, object); } pub fn remove_id_objects(world: &mut world::World, device_entity: entity::Entity, device_id: u32) { world .resource_mut::() .remove_seat_device_objects_by_id(device_entity, device_id); } pub fn devices(world: &world::World, client_entity: entity::Entity) -> Vec { let Some(mut devices) = world.try_query_filtered::<(Entity, &ecs::OwnerClient), With>() else { return Vec::new(); }; devices .iter(world) .filter_map(|(entity, client)| (client.0 == client_entity).then_some(entity)) .collect() } fn close( world: &mut world::World, client_entity: entity::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 name = world .get::(client_entity) .map(|seat| seat.0.clone()) .ok_or_else(|| anyhow::anyhow!("client has no seat"))?; let device_entity = devices(world, client_entity) .into_iter() .find(|device_entity| contains_object(world, *device_entity, object)) .ok_or_else(|| anyhow::anyhow!("device not found"))?; let device_id = world .resource::() .device_objects(device_entity) .and_then(|objects| { objects .iter() .find(|candidate| candidate.object == *object) .map(|candidate| candidate.id) }) .ok_or_else(|| anyhow::anyhow!("device object not found"))?; remove_object(world, device_entity, object); let path = path(world, device_entity)?; log::debug!( "Closing device {} for client {} on {}", path.display(), session_id.as_raw(), name ); reconcile::client_device_removed(world, client_entity, device_id); reconcile::device_id_destroyed(world, device_entity, device_id); remove_id_objects(world, device_entity, device_id); if decrement_ref(world, device_entity) > 0 { return Ok(()); } if let Err(err) = deactivate(world, device_entity, false) { log::error!( "Could not deactivate device {} for client {} on {}: {err}", path.display(), session_id.as_raw(), name ); } reconcile::device_destroyed(world, device_entity); remove_protocol_objects(world, device_entity); world.despawn(device_entity); Ok(()) } pub fn open( world: &mut world::World, seat_entity: entity::Entity, client_entity: entity::Entity, device: seat_v1::device::Device, path: &str, ) { if !client::is_active(world, seat_entity, client_entity) { device.send_failed( seat_v1::DeviceError::SeatDisabled, io::Errno::PERM.raw_os_error().cast_unsigned(), "open is only valid for the active seat client", ); return; } let Ok(sanitized_path) = fs::canonicalize(path) else { device.send_failed( seat_v1::DeviceError::OpenFailed, io::Errno::NOENT.raw_os_error().cast_unsigned(), "could not canonicalize device path", ); return; }; if seat::state(world, client_entity).ok() != Some(seat::SeatState::Enabled) { device.send_failed( seat_v1::DeviceError::SeatDisabled, io::Errno::BUSY.raw_os_error().cast_unsigned(), "open is only valid while the seat is enabled", ); return; } let Some(kind) = DeviceKind::from_path(&sanitized_path) else { log::error!( "{} is not a supported device type ", sanitized_path.display() ); device.send_failed( seat_v1::DeviceError::OpenFailed, io::Errno::INVAL.raw_os_error().cast_unsigned(), "unsupported device type", ); return; }; let device_entities = devices(world, client_entity); let open_handle_count = device_entities .iter() .map(|device_entity| object_count(world, *device_entity)) .sum::(); if open_handle_count >= usize::from(MAX_SEAT_DEVICES) { log::error!("Client exceeded max seat devices"); device.send_failed( seat_v1::DeviceError::OpenFailed, io::Errno::MFILE.raw_os_error().cast_unsigned(), "client exceeded max seat devices", ); return; } let device_id = device_entities .iter() .filter_map(|device_entity| max_id(world, *device_entity)) .max() .map_or(1, |id| id + 1); if let Some(old_device) = device_entities .iter() .find(|device_entity| matches_path(world, **device_entity, &sanitized_path)) .copied() { add_object(world, old_device, device, device_id); reconcile::client_device_added(world, client_entity, device_id); if let Err(err) = send_opened(world, old_device) { log::error!("Could not send opened device event: {err}"); } return; } let session_id = client::session_id(world, client_entity) .map(crate::session::SessionId::as_raw) .unwrap_or_default(); let name = seat::name(world, seat_entity).unwrap_or_else(|_| "".to_string()); log::debug!( "Opening device {} for client {} on {}", sanitized_path.display(), session_id, name ); let fd = match rustix::fs::open( &sanitized_path, rustix::fs::OFlags::RDWR | rustix::fs::OFlags::NOCTTY | rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::CLOEXEC | rustix::fs::OFlags::NONBLOCK, rustix::fs::Mode::empty(), ) { Ok(fd) => fd, Err(err) => { log::error!("Could not open file: {err}"); device.send_failed( seat_v1::DeviceError::OpenFailed, err.raw_os_error().cast_unsigned(), "could not open device", ); return; } }; let device_entity = spawn( world, device, sanitized_path, kind, device_id, matches!(kind, DeviceKind::Evdev | DeviceKind::Hidraw), fd, client_entity, ); if let Err(err) = activate(world, device_entity) { let path = path_display(world, device_entity); log::error!("Could not activate {path}: {err}"); let errno = err .root_cause() .downcast_ref::() .copied() .unwrap_or(io::Errno::INVAL); if let Some(object) = last_object(world, device_entity) { object.send_failed( seat_v1::DeviceError::OpenFailed, errno.raw_os_error().cast_unsigned(), "could not activate device", ); } remove_protocol_objects(world, device_entity); world.despawn(device_entity); return; } if let Err(err) = send_opened(world, device_entity) { log::error!("Could not send opened device event: {err}"); } reconcile::client_device_added(world, client_entity, device_id); } 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.to_str().is_some_and(|s| { s.starts_with("/dev/wskbd") || s.starts_with("/dev/wsmouse") || s.starts_with("/dev/wsmux") }) { Some(Self::Wscons) } else if path.to_str().is_some_and(|s| s.starts_with("/dev/hidraw")) { Some(Self::Hidraw) } else { None } } } pub 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 world::World, entity: entity::Entity) -> anyhow::Result<()> { if world.get::(entity).is_some() { return Ok(()); } let kind = 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.entity_mut(entity).insert(ecs::DeviceActive); } DeviceKind::Evdev | DeviceKind::Hidraw => { return Err(anyhow::anyhow!("device type cannot be activated")); } DeviceKind::Wscons => { world.entity_mut(entity).insert(ecs::DeviceActive); } } Ok(()) } pub fn deactivate( world: &mut world::World, entity: entity::Entity, // EVIOCREVOKE/HIDIOCREVOKE revoke all open clients for the device, so // only use them when disabling a seat, not when one client closes its fd. revoke: bool, ) -> anyhow::Result<()> { if world.get::(entity).is_none() { return Ok(()); } let kind = 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 if revoke => { 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 if revoke => { 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::Evdev | DeviceKind::Hidraw => {} DeviceKind::Wscons => {} } world.entity_mut(entity).remove::(); ecs::push_device_active_notification(world, entity, false); Ok(()) } pub fn matches_path(world: &world::World, entity: entity::Entity, path: &path::Path) -> bool { world .get::(entity) .is_some_and(|device_path| device_path.0 == path) } pub fn add_object( world: &mut world::World, entity: entity::Entity, object: device::Device, id: u32, ) { if let Some(mut ref_count) = world.get_mut::(entity) { ref_count.0 += 1; } world .resource_mut::() .add_device_object(entity, ecs::DeviceObject { object, id }); } pub fn max_id(world: &world::World, entity: entity::Entity) -> Option { world .resource::() .device_objects(entity) .and_then(|objects| objects.iter().map(|handle| handle.id).max()) } pub fn object_count(world: &world::World, entity: entity::Entity) -> usize { world .resource::() .device_objects(entity) .map_or(0, <[_]>::len) } pub fn kind(world: &world::World, entity: entity::Entity) -> anyhow::Result { world .get::(entity) .map(|kind| kind.0) .ok_or_else(|| anyhow::anyhow!("device has no kind")) } pub fn path(world: &world::World, entity: entity::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: &world::World, entity: entity::Entity) -> String { path(world, entity).map_or_else( |_| "".to_string(), |path| path.display().to_string(), ) } pub fn absolute_path_string( world: &world::World, entity: entity::Entity, ) -> anyhow::Result { let path = path(world, entity)?; Ok(path::absolute(&path).unwrap_or(path).display().to_string()) } pub fn last_object(world: &world::World, entity: entity::Entity) -> Option<&device::Device> { world .resource::() .device_objects(entity) .and_then(|objects| objects.last()) .map(|handle| &handle.object) } pub fn contains_object( world: &world::World, entity: entity::Entity, object: &device::Device, ) -> bool { world .resource::() .device_objects(entity) .is_some_and(|objects| objects.iter().any(|candidate| candidate.object == *object)) } pub fn remove_object(world: &mut world::World, entity: entity::Entity, object: &device::Device) { world .resource_mut::() .remove_device_object(entity, object); } pub fn decrement_ref(world: &mut world::World, entity: entity::Entity) -> usize { let Some(mut 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: &world::World, entity: entity::Entity) -> anyhow::Result<()> { let Some(handle) = world .resource::() .device_objects(entity) .and_then(|objects| objects.last()) else { return Ok(()); }; let path = absolute_path_string(world, entity)?; let fd = world .get::(entity) .ok_or_else(|| anyhow::anyhow!("device has no fd"))?; handle.object.send_opened(handle.id, fd.0.as_fd(), path); Ok(()) } pub fn entity_by_id( world: &world::World, client_entity: entity::Entity, device_id: u32, ) -> Option { devices(world, client_entity) .into_iter() .find(|device_entity| { world .resource::() .device_objects(*device_entity) .is_some_and(|objects| objects.iter().any(|object| object.id == device_id)) }) } pub fn entity_by_core_object( world: &world::World, client_entity: entity::Entity, object: &session_core_seat_device::SessionCoreSeatDevice, ) -> Option { devices(world, client_entity) .into_iter() .find(|device_entity| { world .resource::() .seat_device_objects(*device_entity) .is_some_and(|objects| objects.iter().any(|candidate| candidate.object == *object)) }) } pub const fn kind_event(kind: DeviceKind) -> session_core_v1::SeatDeviceKind { match kind { DeviceKind::Drm => session_core_v1::SeatDeviceKind::Drm, DeviceKind::Evdev => session_core_v1::SeatDeviceKind::Evdev, DeviceKind::Hidraw => session_core_v1::SeatDeviceKind::Hidraw, DeviceKind::Wscons => session_core_v1::SeatDeviceKind::Wscons, } } pub fn add_core_object( world: &mut world::World, device_entity: entity::Entity, device_id: u32, object: session_core_seat_device::SessionCoreSeatDevice, ) { object.send_device_id(device_id); if let Ok(path) = absolute_path_string(world, device_entity) { object.send_path(path); } if let Some(kind) = world.get::(device_entity) { object.send_kind(kind_event(kind.0)); } if world.get::(device_entity).is_some() { object.send_active(1); } else { object.send_active(0); } object.send_done(); world .resource_mut::() .add_seat_device_object( device_entity, ecs::SeatDeviceObject { object, id: device_id, }, ); }