modular session management daemon
Something went wrong. Try again.
Rust
at consolekit
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764mod session_management_v1 { hyprwire::include_protocol!("session_management_v1"); pub use server::*;}mod session_core_v1 { hyprwire::include_protocol!("session_core_v1"); pub use server::*; pub use spec::*;}mod seat_v1 { hyprwire::include_protocol!("seat_v1"); pub use server::*; pub use spec::*;}
mod cgroup;#[cfg(feature = "consolekit")]mod consolekit;mod device;mod ecs;mod seat;mod session;mod terminal;
use calloop::{generic, signals};use clap::Parser;use hyprwire::server;use rustix::{net, process, system};use session_core_v1::{session_core_manager, session_core_session};use session_management_v1::session_manager;use std::os::fd::AsRawFd;use std::os::unix::fs::PermissionsExt;use std::{env, fs, path, time};
#[derive(Parser)]struct Args { #[arg( long, value_name = "USER", default_value = "root", help = "Allow USER to bind seat_v1" )] seat_user: String, #[arg( long, value_name = "GROUP", default_value = "root", help = "Allow members of GROUP to bind seat_v1" )] seat_group: String,}
struct SeatAccess { user: process::Uid, group: process::Gid,}
impl SeatAccess { const fn new(user: process::Uid, group: process::Gid) -> Self { Self { user, group } }
fn allows(&self, creds: &net::UCred) -> bool { if self.user == creds.uid || self.group == creds.gid { return true; }
Self::process_groups(creds.pid.as_raw_pid().cast_unsigned()) .is_ok_and(|groups| groups.contains(&self.group)) }
fn process_groups(pid: u32) -> anyhow::Result<Vec<process::Gid>> { let status = fs::read_to_string(format!("/proc/{pid}/status"))?; let Some(groups) = status.lines().find_map(|line| line.strip_prefix("Groups:")) else { return Ok(Vec::new()); };
groups .split_whitespace() .map(|group| { group .parse::<process::RawGid>() .map(process::Gid::from_raw) .map_err(Into::into) }) .collect() }}
struct Sessiond { next_session_id: session::SessionId, info_managers: Vec<session_core_manager::SessionCoreManager>, cgroup_manager: cgroup::CgroupManager, session_entities: Vec<ecs::Entity>, seat_entities: Vec<ecs::Entity>, client_entities: Vec<ecs::Entity>, seat_access: SeatAccess, #[cfg(feature = "consolekit")] consolekit: consolekit::Server, shutting_down: bool, loop_signal: calloop::LoopSignal, world: ecs::World,}
impl Sessiond { fn remove_session(&mut self, session_id: session::SessionId) -> Option<ecs::Entity> { let idx = self.session_entities.iter().position(|entity| { self.world .get::<ecs::Session>(*entity) .is_some_and(|session| session.0 == session_id) })?; let entity = self.session_entities.remove(idx); self.world.remove_entity(entity); #[cfg(feature = "consolekit")] self.consolekit.remove_session(session_id); Some(entity) }
#[cfg(feature = "consolekit")] fn sync_consolekit_session(&self, session_id: session::SessionId, entity: ecs::Entity) { if let Some(info) = consolekit::SessionInfo::from_world(&self.world, entity, session_id) { self.consolekit.upsert_session(info); } }}
impl session_management_v1::SessionManagementV1Handler for Sessiond {}
impl session_core_v1::SessionCoreV1Handler for Sessiond { fn bind(&mut self, object: session_core_manager::SessionCoreManager) { session::sync_vt_session_states( &mut self.world, &self.seat_entities, &self.session_entities, );
let session_ids = session::entities(&self.world, &self.session_entities) .filter_map(|entity| session::id(&self.world, entity)) .map(session::SessionId::as_raw) .collect::<Vec<_>>(); object.send_sessions(&session_ids); object.send_done(); self.info_managers.push(object); }}
impl hyprwire::Dispatch<session_manager::SessionManager> for Sessiond { fn event( &mut self, object: &session_manager::SessionManager, event: <session_manager::SessionManager as hyprwire::Object>::Event<'_>, ) { let Some(client) = object.client() else { return; };
if !client.creds().uid.is_root() { log::warn!(uid = client.creds().uid.as_raw(); "rejected request from non-root client"); return; }
match event { session_manager::Event::CreateSession { uid, username, vt, is_local, remote_host, } => { let session_id = self.next_session_id; self.next_session_id = self.next_session_id.next(); let is_local = is_local != 0; log::info!(session_id = session_id.as_raw(), uid, username = username.as_str(), vt, is_local, remote_host = remote_host.as_str(); "session created");
let leader_id = client.creds().pid.as_raw_pid();
if let Err(e) = self .cgroup_manager .create_session_cgroup(session_id, leader_id) { log::error!(session_id = session_id.as_raw(), error = e.to_string().as_str(); "failed to create session cgroup"); }
seat::refresh_current_vts(&mut self.world, &self.seat_entities);
let mut session_seat_entity = self.seat_entities.first().copied(); let mut session_seat = session_seat_entity.and_then(|seat_entity| { self.world .get::<ecs::SeatName>(seat_entity) .map(|seat| seat.0.clone()) .filter(|seat| !seat.is_empty()) }); let mut active_state = session_core_v1::ActiveState::Online;
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"); }
if let Some((seat_entity, seat)) = self.seat_entities.iter().copied().find_map(|seat_entity| { let mode = self.world.get::<ecs::SeatMode>(seat_entity)?; if mode.0 != ecs::SeatKind::VtBound { return None; } let current_vt = self.world.get::<ecs::CurrentVt>(seat_entity)?.0; (current_vt == vt).then(|| { self.world .get::<ecs::SeatName>(seat_entity) .map(|seat| (seat_entity, seat.0.clone())) })? }) { session_seat_entity = Some(seat_entity); session_seat = Some(seat); active_state = session_core_v1::ActiveState::Active; }
let session_entity = self.world.insert_session( session_id, uid, username, vt, session_seat.clone(), session_seat_entity, time::SystemTime::now(), leader_id, is_local, remote_host, Vec::new(), active_state, ); self.session_entities.push(session_entity); #[cfg(feature = "consolekit")] self.sync_consolekit_session(session_id, session_entity);
object.send_session_created( session_id.as_raw(), session_seat.as_deref().unwrap_or_default(), );
for manager in &self.info_managers { manager.send_session_added(session_id.as_raw()); } } session_manager::Event::DestroySession { session_id } => { let session_id = session::SessionId::from_raw(session_id); let Some(entity) = session::entity(&self.world, &self.session_entities, session_id) else { log::warn!(session_id = session_id.as_raw(); "destroy requested for unknown session"); return; };
let uid = self.world.get::<ecs::Uid>(entity).map_or(0, |uid| uid.0); let username = self .world .get::<ecs::Username>(entity) .map_or("", |username| username.0.as_str()); log::info!(session_id = session_id.as_raw(), uid, username; "session destroyed");
if let Some(active_state) = self.world.get_mut::<ecs::ActiveState>(entity) { active_state.0 = session_core_v1::ActiveState::Closing; } #[cfg(feature = "consolekit")] self.sync_consolekit_session(session_id, entity); if let Some(objects) = self.world.get::<ecs::SessionObjects>(entity) { for object in &objects.0 { object.send_active(session_core_v1::ActiveState::Closing); } } if let Some(objects) = self.world.get::<ecs::SessionObjects>(entity) { for object in &objects.0 { object.send_destroyed(); } } let _ = self.remove_session(session_id);
for manager in &self.info_managers { manager.send_session_removed(session_id.as_raw()); }
if let Err(e) = self.cgroup_manager.poll_destroy(session_id) { log::error!(session_id = session_id.as_raw(), error = e.to_string().as_str(); "failed to poll destroy session cgroup"); }
if !session::entities(&self.world, &self.session_entities).any(|entity| { self.world .get::<ecs::Uid>(entity) .is_some_and(|session_uid| session_uid.0 == uid) }) && let Err(e) = fs::remove_dir(format!("/run/user/{uid}")) { log::error!(session_id = session_id.as_raw(), uid, error = e.to_string().as_str(); "failed to remove runtime dir"); } } session_manager::Event::Destroy => {} } }}
impl hyprwire::Dispatch<session_core_manager::SessionCoreManager> for Sessiond { fn event( &mut self, object: &session_core_manager::SessionCoreManager, event: <session_core_manager::SessionCoreManager as hyprwire::Object>::Event<'_>, ) { match event { session_core_manager::Event::GetSession { session_core_session, session_id, } => { session::sync_vt_session_states( &mut self.world, &self.seat_entities, &self.session_entities, );
let session_id = session::SessionId::from_raw(session_id); if let Some(entity) = session::entity(&self.world, &self.session_entities, session_id) { session::add_session_object( &mut self.world, session_id, entity, session_core_session, ); return; }
log::warn!(session_id = session_id.as_raw(); "get_session requested for unknown session"); session_core_session.send_destroyed(); } session_core_manager::Event::GetSessionByPid { session_core_session, pid, } => { session::sync_vt_session_states( &mut self.world, &self.seat_entities, &self.session_entities, );
let Ok(Some(session_id)) = self.cgroup_manager.find_session_by_pid(pid) else { log::warn!(pid; "get_session_by_pid: no session found for pid"); session_core_session.send_destroyed(); return; };
if let Some(entity) = session::entity(&self.world, &self.session_entities, session_id) { session::add_session_object( &mut self.world, session_id, entity, session_core_session, ); return; }
log::warn!(session_id = session_id.as_raw(); "get_session_by_pid requested for unknown session"); session_core_session.send_destroyed(); } session_core_manager::Event::GetSessionByUid { session_core_session, uid, } => { session::sync_vt_session_states( &mut self.world, &self.seat_entities, &self.session_entities, );
if let Some(entity) = session::entity_by_uid(&self.world, &self.session_entities, uid) && let Some(session_id) = session::id(&self.world, entity) { session::add_session_object( &mut self.world, session_id, entity, session_core_session, ); return; }
log::warn!(uid; "get_session_by_uid requested for unknown session"); session_core_session.send_destroyed(); } session_core_manager::Event::Reboot => { let Some(client) = object.client() else { return; };
// let user reboot only if there are no other user sessions active let foreign_session_count = session::entities(&self.world, &self.session_entities) .filter_map(|entity| self.world.get::<ecs::Uid>(entity)) .filter(|session| session.0 != client.creds().uid.as_raw()) .count();
if !client.creds().uid.is_root() && foreign_session_count > 0 { log::warn!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "reboot denied: multiple sessions active"); object.send_error( session_core_v1::ErrorCode::ActiveSessions, "cannot reboot while multiple sessions are active (root privileges required)", ); return; }
log::info!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "reboot accepted");
for session_id in session::entities(&self.world, &self.session_entities) .filter_map(|entity| self.world.get::<ecs::Session>(entity)) { if let Err(err) = self.cgroup_manager.terminate_session(session_id.0) { log::error!(session_id = session_id.0.as_raw(), error = err.to_string().as_str(); "failed to terminate session before reboot"); object.send_error( session_core_v1::ErrorCode::Io, format!( "failed to terminate session {}: {err}", session_id.0.as_raw() ), ); return; } }
rustix::fs::sync();
if let Err(err) = system::reboot(system::RebootCommand::Restart) { log::error!(error = err.to_string().as_str(); "reboot syscall failed"); object.send_error( session_core_v1::ErrorCode::Io, format!("failed to reboot: {err}"), ); return; }
object.send_done(); } session_core_manager::Event::Poweroff => { let Some(client) = object.client() else { return; };
// let user poweroff only if there are no other user sessions active let foreign_session_count = session::entities(&self.world, &self.session_entities) .filter_map(|entity| self.world.get::<ecs::Uid>(entity)) .filter(|session| session.0 != client.creds().uid.as_raw()) .count();
if !client.creds().uid.is_root() && foreign_session_count > 0 { log::warn!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "poweroff denied: multiple sessions active"); object.send_error( session_core_v1::ErrorCode::ActiveSessions, "cannot power off while multiple sessions are active (root privileges required)", ); return; }
log::info!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "poweroff accepted");
for session_id in session::entities(&self.world, &self.session_entities) .filter_map(|entity| self.world.get::<ecs::Session>(entity)) { if let Err(err) = self.cgroup_manager.terminate_session(session_id.0) { log::error!(session_id = session_id.0.as_raw(), error = err.to_string().as_str(); "failed to terminate session before poweroff"); object.send_error( session_core_v1::ErrorCode::Io, format!( "failed to terminate session {}: {err}", session_id.0.as_raw() ), ); return; } }
rustix::fs::sync();
if let Err(err) = system::reboot(system::RebootCommand::PowerOff) { log::error!(error = err.to_string().as_str(); "poweroff syscall failed"); object.send_error( session_core_v1::ErrorCode::Io, format!("failed to power off: {err}"), ); return; }
object.send_done(); } session_core_manager::Event::Suspend => { let Some(client) = object.client() else { return; };
// let user suspend only if there are no other user sessions active let foreign_session_count = session::entities(&self.world, &self.session_entities) .filter_map(|entity| self.world.get::<ecs::Uid>(entity)) .filter(|session| session.0 != client.creds().uid.as_raw()) .count();
if !client.creds().uid.is_root() && foreign_session_count > 0 { log::warn!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "suspend denied: multiple sessions active"); object.send_error( session_core_v1::ErrorCode::ActiveSessions, "cannot suspend while multiple sessions are active (root privileges required)", ); return; }
log::info!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "suspend accepted");
rustix::fs::sync();
if let Err(err) = fs::write("/sys/power/state", "mem") { log::error!(error = err.to_string().as_str(); "suspend failed"); object.send_error( session_core_v1::ErrorCode::Io, format!("failed to suspend: {err}"), ); return; }
object.send_done(); } session_core_manager::Event::Hibernate => { let Some(client) = object.client() else { return; };
// let user hibernate only if there are no other user sessions active let foreign_session_count = session::entities(&self.world, &self.session_entities) .filter_map(|entity| self.world.get::<ecs::Uid>(entity)) .filter(|session| session.0 != client.creds().uid.as_raw()) .count();
if !client.creds().uid.is_root() && foreign_session_count > 0 { log::warn!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "hibernate denied: multiple sessions active"); object.send_error( session_core_v1::ErrorCode::ActiveSessions, "cannot hibernate while multiple sessions are active (root privileges required)", ); return; }
log::info!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "hibernate accepted");
rustix::fs::sync();
if let Err(err) = system::reboot(system::RebootCommand::SwSuspend) { log::error!(error = err.to_string().as_str(); "sw_suspend syscall failed"); object.send_error( session_core_v1::ErrorCode::Io, format!("failed to hibernate: {err}"), ); return; }
object.send_done(); } session_core_manager::Event::Destroy => { self.info_managers.retain(|m| m != object); } } }}
fn main() -> anyhow::Result<()> { let args = Args::parse();
let log_level = match env::var("LOG_LEVEL") { Ok(log) if log == "trace" => log::LevelFilter::Trace, Ok(log) if log == "debug" => log::LevelFilter::Debug, Ok(log) if log == "warn" => log::LevelFilter::Warn, Ok(log) if log == "error" => log::LevelFilter::Error, Ok(log) if log == "off" => log::LevelFilter::Off, _ => log::LevelFilter::Info, };
env_logger::Builder::new() .filter(Some("daemon"), log_level) .init();
log::info!("sessiond daemon starting");
let seat_access = SeatAccess::new( resolve_user(&args.seat_user)?, resolve_group(&args.seat_group)?, );
let mut event_loop = calloop::EventLoop::try_new()?; let loop_signal = event_loop.get_signal();
let cgroup_manager = cgroup::CgroupManager::new(event_loop.handle())?;
let vtbound = env::var("SESSIOND_VTBOUND") .ok() .is_none_or(|vtbound| vtbound == "1");
let mut sessiond = Sessiond { next_session_id: session::SessionId::from_raw(1), info_managers: Vec::new(), cgroup_manager, session_entities: Vec::new(), seat_entities: Vec::new(), client_entities: Vec::new(), seat_access, #[cfg(feature = "consolekit")] consolekit: consolekit::Server::start(), shutting_down: false, loop_signal, world: ecs::World::new(), };
let seat_entity = sessiond.world.insert_seat( "seat0".to_string(), if vtbound { ecs::SeatKind::VtBound } else { ecs::SeatKind::NonVtBound }, 0, ); sessiond.seat_entities.push(seat_entity);
let path = path::Path::new("/run/sessiond.sock"); let mut socket = server::Server::bind(&path)?; fs::set_permissions(path, fs::Permissions::from_mode(0o777))?;
socket .add_implementation::<session_management_v1::SessionManagementV1Impl, _>(&mut sessiond, 1); socket.add_implementation::<session_core_v1::SessionCoreV1Impl, _>(&mut sessiond, 1); socket.add_implementation::<seat_v1::SeatV1Impl, _>(&mut sessiond, 1);
let fd_wrapper = unsafe { generic::FdWrapper::new(socket.extract_loop_fd().as_raw_fd()) }; let source = generic::Generic::new( fd_wrapper, calloop::Interest { readable: true, writable: false, }, calloop::Mode::Level, ); event_loop .handle() .insert_source(source, move |_, _, state| { if state.shutting_down { return Ok(calloop::PostAction::Continue); }
_ = socket.dispatch_events(state, false); Ok(calloop::PostAction::Continue) })?;
let signals = signals::Signals::new(&[ signals::Signal::SIGUSR1, signals::Signal::SIGUSR2, signals::Signal::SIGTERM, signals::Signal::SIGINT, ])?; event_loop .handle() .insert_source(signals, move |event, (), state| { let session_updates = match event.signal() { signals::Signal::SIGUSR1 | signals::Signal::SIGUSR2 => { seat::handle_vt_signal(&mut state.world, &state.seat_entities, event.signal()) } signals::Signal::SIGTERM | signals::Signal::SIGINT => { if state.shutting_down { return; }
state.shutting_down = true;
for session_id in session::entities(&state.world, &state.session_entities) .filter_map(|entity| state.world.get::<ecs::Session>(entity)) { if let Err(err) = state.cgroup_manager.terminate_session(session_id.0) { log::error!(session_id = session_id.0.as_raw(), error = err.to_string().as_str(); "failed to terminate session during shutdown"); } }
if state.cgroup_manager.is_empty() { state.loop_signal.stop(); }
return; } _ => unreachable!(), };
for (vt, active_state) in session_updates { let mut state_updates = Vec::new();
if active_state == session_core_v1::ActiveState::Active { state_updates.extend( session::entities(&state.world, &state.session_entities).filter_map( |entity| { let session_vt = state.world.get::<ecs::Vt>(entity)?.0; let session_state = state.world.get::<ecs::ActiveState>(entity)?.0; (session_vt != vt && session_state == session_core_v1::ActiveState::Active) .then_some((entity, session_core_v1::ActiveState::Online)) }, ), ); }
state_updates.extend( session::entities(&state.world, &state.session_entities).filter_map(|entity| { let session_vt = state.world.get::<ecs::Vt>(entity)?.0; (session_vt == vt).then_some((entity, active_state)) }), );
for (entity, active_state) in state_updates { let Some(session_id) = session::id(&state.world, entity) else { continue; }; session::set_state(&mut state.world, session_id, entity, active_state); #[cfg(feature = "consolekit")] state.sync_consolekit_session(session_id, entity); } } })?;
event_loop.run(None, &mut sessiond, |_| {})?;
let _ = fs::remove_file("/run/sessiond.sock");
Ok(())}
fn resolve_user(user: &str) -> anyhow::Result<process::Uid> { if let Ok(uid) = user.parse::<process::RawUid>() { return Ok(process::Uid::from_raw(uid)); }
users::get_user_by_name(user) .map(|user: users::User| process::Uid::from_raw(user.uid())) .ok_or_else(|| anyhow::anyhow!("unknown seat user {user:?}"))}
fn resolve_group(group: &str) -> anyhow::Result<process::Gid> { if let Ok(gid) = group.parse::<process::RawGid>() { return Ok(process::Gid::from_raw(gid)); }
users::get_group_by_name(group) .map(|group: users::Group| process::Gid::from_raw(group.gid())) .ok_or_else(|| anyhow::anyhow!("unknown seat group {group:?}"))}