modular session management daemon
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640use 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::<ecs::ProtocolObjects>() .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::<ecs::ProtocolObjects>() .add_device_object(entity, ecs::DeviceObject { object, id });
entity}
impl hyprwire::Dispatch<device::Device> for crate::Sessiond { fn event( &mut self, object: &device::Device, event: <device::Device as hyprwire::Object>::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<session_core_seat_device::SessionCoreSeatDevice> for crate::Sessiond { fn event( &mut self, object: &session_core_seat_device::SessionCoreSeatDevice, event: <session_core_seat_device::SessionCoreSeatDevice as hyprwire::Object>::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::<ecs::ProtocolObjects>() .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::<ecs::ProtocolObjects>() .remove_seat_device_objects_by_id(device_entity, device_id);}
pub fn devices(world: &world::World, client_entity: entity::Entity) -> Vec<entity::Entity> { let Some(mut devices) = world.try_query_filtered::<(Entity, &ecs::OwnerClient), With<ecs::Device>>() 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::<ecs::SessionId>(client_entity) .map(|session| session.0) .ok_or_else(|| anyhow::anyhow!("client has no session id"))?; let name = world .get::<ecs::SeatName>(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::<ecs::ProtocolObjects>() .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::<usize>(); 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(|_| "<unknown>".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::<io::Errno>() .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::<c_int>(b'E', 0x91);const HIDIOCREVOKE: ioctl::Opcode = ioctl::opcode::write::<c_int>(b'H', 0x0d);
fn drm_set_master<Fd: fd::AsFd>(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::NoArg::<DRM_SET_MASTER>::new()) }}
fn drm_drop_master<Fd: fd::AsFd>(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::NoArg::<DRM_DROP_MASTER>::new()) }}
fn evdev_revoke<Fd: fd::AsFd>(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::IntegerSetter::<EVIOCREVOKE>::new_usize(0)) }}
fn hidraw_revoke<Fd: fd::AsFd>(fd: Fd) -> io::Result<()> { unsafe { ioctl::ioctl(fd, ioctl::IntegerSetter::<HIDIOCREVOKE>::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<Self> { 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::<ecs::DeviceActive>(entity).is_some() { return Ok(()); }
let kind = kind(world, entity)?; match kind { DeviceKind::Drm => { let fd = world .get::<ecs::DeviceFd>(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::<ecs::DeviceActive>(entity).is_none() { return Ok(()); }
let kind = kind(world, entity)?; match kind { DeviceKind::Drm => { let fd = world .get::<ecs::DeviceFd>(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::<ecs::DeviceFd>(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::<ecs::DeviceFd>(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::DeviceActive>(); 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::<ecs::DevicePath>(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::<ecs::DeviceRefCount>(entity) { ref_count.0 += 1; } world .resource_mut::<ecs::ProtocolObjects>() .add_device_object(entity, ecs::DeviceObject { object, id });}
pub fn max_id(world: &world::World, entity: entity::Entity) -> Option<u32> { world .resource::<ecs::ProtocolObjects>() .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::<ecs::ProtocolObjects>() .device_objects(entity) .map_or(0, <[_]>::len)}
pub fn kind(world: &world::World, entity: entity::Entity) -> anyhow::Result<DeviceKind> { world .get::<ecs::DeviceKind>(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<path::PathBuf> { world .get::<ecs::DevicePath>(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( |_| "<unknown>".to_string(), |path| path.display().to_string(), )}
pub fn absolute_path_string( world: &world::World, entity: entity::Entity,) -> anyhow::Result<String> { 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::<ecs::ProtocolObjects>() .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::<ecs::ProtocolObjects>() .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::<ecs::ProtocolObjects>() .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::<ecs::DeviceRefCount>(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::<ecs::ProtocolObjects>() .device_objects(entity) .and_then(|objects| objects.last()) else { return Ok(()); }; let path = absolute_path_string(world, entity)?; let fd = world .get::<ecs::DeviceFd>(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<entity::Entity> { devices(world, client_entity) .into_iter() .find(|device_entity| { world .resource::<ecs::ProtocolObjects>() .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<entity::Entity> { devices(world, client_entity) .into_iter() .find(|device_entity| { world .resource::<ecs::ProtocolObjects>() .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::<ecs::DeviceKind>(device_entity) { object.send_kind(kind_event(kind.0)); } if world.get::<ecs::DeviceActive>(device_entity).is_some() { object.send_active(1); } else { object.send_active(0); } object.send_done();
world .resource_mut::<ecs::ProtocolObjects>() .add_seat_device_object( device_entity, ecs::SeatDeviceObject { object, id: device_id, }, );}