modular session management daemon
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274use crate::{ecs, session, session_core_user, session_core_v1};use bevy_ecs::{entity, world};use std::{fs, io, path};
pub fn create_runtime_dir(uid: u32) { let runtime_dir = format!("/run/user/{uid}"); if let Err(e) = fs::create_dir_all(&runtime_dir) { log::error!(uid, error = e.to_string().as_str(); "failed to create runtime dir"); } if let Err(e) = rustix::fs::chown(&runtime_dir, Some(rustix::fs::Uid::from_raw(uid)), None) { log::error!(uid, error = e.to_string().as_str(); "failed to chown runtime dir"); } if let Err(e) = rustix::fs::chmod(&runtime_dir, rustix::fs::Mode::from_bits_truncate(0o700)) { log::error!(uid, error = e.to_string().as_str(); "failed to chmod runtime dir"); }}
pub fn remove_runtime_dir(uid: u32) -> io::Result<()> { match fs::remove_dir(format!("/run/user/{uid}")) { Ok(()) => Ok(()), Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), Err(e) => Err(e), }}
pub fn entity_by_uid( world: &world::World, entities: &[entity::Entity], uid: u32,) -> Option<entity::Entity> { entities.iter().copied().find(|entity| { world .get::<ecs::Uid>(*entity) .is_some_and(|entity_uid| entity_uid.0 == uid) })}
pub fn entity_by_object( world: &world::World, entities: &[entity::Entity], object: &session_core_user::SessionCoreUser,) -> Option<entity::Entity> { entities.iter().copied().find(|entity| { world .get::<ecs::UserObjects>(*entity) .is_some_and(|objects| objects.0.iter().any(|candidate| candidate == object)) })}
pub fn get_or_create( world: &mut world::World, entities: &mut Vec<entity::Entity>, session_entities: &[entity::Entity], uid: u32,) -> Option<entity::Entity> { if let Some(entity) = entity_by_uid(world, entities, uid) { return Some(entity); }
let user = users::get_user_by_uid(uid)?; let lingering = linger_path(uid).exists(); let state = compute_state(world, session_entities, uid, lingering); let entity = ecs::insert_user( world, uid, user.name().to_string_lossy().into_owned(), lingering, state, ); entities.push(entity); Some(entity)}
fn compute_state( world: &world::World, session_entities: &[entity::Entity], uid: u32, lingering: bool,) -> session_core_v1::UserState { let states = session::entities_by_uid(world, session_entities, uid) .filter_map(|entity| world.get::<ecs::ActiveState>(entity).map(|state| state.0)) .collect::<Vec<_>>();
if states.contains(&session_core_v1::ActiveState::Active) { return session_core_v1::UserState::Active; }
if !states.is_empty() { return if states .iter() .all(|state| *state == session_core_v1::ActiveState::Closing) { session_core_v1::UserState::Closing } else { session_core_v1::UserState::Online }; }
if lingering { session_core_v1::UserState::Lingering } else { session_core_v1::UserState::Offline }}
pub fn sync_all( world: &mut world::World, user_entities: &[entity::Entity], session_entities: &[entity::Entity],) { for entity in user_entities.iter().copied() { sync(world, session_entities, entity); }}
fn sync(world: &mut world::World, session_entities: &[entity::Entity], entity: entity::Entity) { let Some(uid) = world.get::<ecs::Uid>(entity).map(|uid| uid.0) else { return; }; let lingering = world .get::<ecs::Lingering>(entity) .is_some_and(|lingering| lingering.0); let state = compute_state(world, session_entities, uid, lingering);
let changed = world .get::<ecs::UserState>(entity) .is_none_or(|current| current.0 != state); if !changed { return; }
if let Some(mut component) = world.get_mut::<ecs::UserState>(entity) { component.0 = state; } if let Some(objects) = world.get::<ecs::UserObjects>(entity) { for object in &objects.0 { object.send_state(state); } }}
pub fn add_user_object( world: &mut world::World, entity: entity::Entity, object: session_core_user::SessionCoreUser,) { if let Some(uid) = world.get::<ecs::Uid>(entity) { object.send_uid(uid.0); } if let Some(username) = world.get::<ecs::Username>(entity) { object.send_username(&username.0); } if let Some(lingering) = world.get::<ecs::Lingering>(entity) { object.send_lingering(u32::from(lingering.0)); } if let Some(state) = world.get::<ecs::UserState>(entity) { object.send_state(state.0); } object.send_done();
if let Some(mut objects) = world.get_mut::<ecs::UserObjects>(entity) { objects.0.push(object); }}
impl hyprwire::Dispatch<session_core_user::SessionCoreUser> for crate::Sessiond { fn event( &mut self, object: &session_core_user::SessionCoreUser, event: <session_core_user::SessionCoreUser as hyprwire::Object>::Event<'_>, ) { let Some(entity) = entity_by_object(&self.world, &self.user_entities, object) else { return; };
if matches!(event, session_core_user::Event::Destroy) { if let Some(mut objects) = self.world.get_mut::<ecs::UserObjects>(entity) { objects.0.retain(|candidate| candidate != object); } return; }
let Some(client) = object.client() else { return; }; let uid = self.world.get::<ecs::Uid>(entity).map_or(0, |uid| uid.0); if !client.creds().uid.is_root() && client.creds().uid.as_raw() != uid { object.send_error( session_core_v1::ErrorCode::PermissionDenied, "permission denied", ); return; }
if let session_core_user::Event::SetLingering { lingering } = event { if let Err(err) = set_lingering(uid, lingering != 0) { object.send_error( session_core_v1::ErrorCode::Io, format!("failed to set lingering: {err}"), ); return; }
match ( lingering != 0, session::entities_by_uid(&self.world, &self.session_entities, uid).count() == 0, ) { // Clean up when lingering was turned off and no sessions are active (false, true) => { if let Err(e) = self.cgroup_manager.remove_user_cgroup(uid) { log::error!(uid, error = e.to_string().as_str(); "failed to remove user cgroup"); } if let Err(e) = remove_runtime_dir(uid) { log::error!(uid, error = e.to_string().as_str(); "failed to remove runtime dir"); } } // Create runtime dir and cgroup if lingering // was enabled for user with no active sessions (true, true) => { create_runtime_dir(uid); if let Err(e) = self.cgroup_manager.ensure_user_cgroup(uid) { log::error!(uid, error = e.to_string().as_str(); "failed to create user cgroup"); } } _ => {} }
log::info!(uid, lingering; "lingering updated"); if let Some(mut component) = self.world.get_mut::<ecs::Lingering>(entity) { component.0 = lingering != 0; } if let Some(objects) = self.world.get::<ecs::UserObjects>(entity) { for object in &objects.0 { object.send_lingering(lingering); object.send_ok(); } } } }}
const LINGER_DIR: &str = "/var/lib/sessiond";
fn linger_path(uid: u32) -> path::PathBuf { path::Path::new(LINGER_DIR).join(uid.to_string())}
pub fn lingering_uids() -> io::Result<Vec<u32>> { let uids = fs::read_dir(LINGER_DIR)? .filter_map(|entry| entry.ok()?.file_name().to_str()?.parse::<u32>().ok()) .collect();
Ok(uids)}
fn set_lingering(uid: u32, lingering: bool) -> io::Result<()> { if lingering { fs::File::create(linger_path(uid))?; return Ok(()); }
match fs::remove_file(linger_path(uid)) { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()), Err(err) => Err(err), }}
pub fn is_lingering(world: &world::World, user_entities: &[entity::Entity], uid: u32) -> bool { entity_by_uid(world, user_entities, uid) .and_then(|entity| world.get::<ecs::Lingering>(entity)) .is_some_and(|lingering| lingering.0)}