use 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 { entities.iter().copied().find(|entity| { world .get::(*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 { entities.iter().copied().find(|entity| { world .get::(*entity) .is_some_and(|objects| objects.0.iter().any(|candidate| candidate == object)) }) } pub fn get_or_create( world: &mut world::World, entities: &mut Vec, session_entities: &[entity::Entity], uid: u32, ) -> Option { 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::(entity).map(|state| state.0)) .collect::>(); 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::(entity).map(|uid| uid.0) else { return; }; let lingering = world .get::(entity) .is_some_and(|lingering| lingering.0); let state = compute_state(world, session_entities, uid, lingering); let changed = world .get::(entity) .is_none_or(|current| current.0 != state); if !changed { return; } if let Some(mut component) = world.get_mut::(entity) { component.0 = state; } if let Some(objects) = world.get::(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::(entity) { object.send_uid(uid.0); } if let Some(username) = world.get::(entity) { object.send_username(&username.0); } if let Some(lingering) = world.get::(entity) { object.send_lingering(u32::from(lingering.0)); } if let Some(state) = world.get::(entity) { object.send_state(state.0); } object.send_done(); if let Some(mut objects) = world.get_mut::(entity) { objects.0.push(object); } } impl hyprwire::Dispatch for crate::Sessiond { fn event( &mut self, object: &session_core_user::SessionCoreUser, event: ::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::(entity) { objects.0.retain(|candidate| candidate != object); } return; } let Some(client) = object.client() else { return; }; let uid = self.world.get::(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::(entity) { component.0 = lingering != 0; } if let Some(objects) = self.world.get::(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> { let uids = fs::read_dir(LINGER_DIR)? .filter_map(|entry| entry.ok()?.file_name().to_str()?.parse::().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::(entity)) .is_some_and(|lingering| lingering.0) }