modular session management daemon
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726use crate::{ecs, session, session_core_v1};use bevy_ecs::{entity, world};use calloop::channel;use rustix::process;use std::collections::HashMap;use std::fmt::Write as _;use std::io::Write as _;use std::sync::mpsc;use std::{fs, io, sync, thread, time};use zbus::blocking::connection;use zbus::blocking::fdo::DBusProxy;use zbus::{blocking, fdo, interface, message, names, zvariant};
const BUS_NAME: &str = "org.freedesktop.ConsoleKit";const MANAGER_PATH: &str = "/org/freedesktop/ConsoleKit/Manager";const DATABASE_DIR: &str = "/var/run/ConsoleKit";const DATABASE_PATH: &str = "/var/run/ConsoleKit/database";const DATABASE_TMP_PATH: &str = "/var/run/ConsoleKit/database~";
#[derive(Clone)]pub struct SessionInfo { pub id: session::SessionId, pub uid: u32, pub seat: String, pub timestamp: time::SystemTime, pub is_local: bool, pub active: session_core_v1::ActiveState,}
impl SessionInfo { pub fn from_world( world: &world::World, entity: entity::Entity, id: session::SessionId, ) -> Option<Self> { Some(Self { id, uid: world.get::<ecs::Uid>(entity)?.0, seat: world .get::<ecs::SeatName>(entity) .map_or_else(|| "seat0".to_string(), |seat| seat.0.clone()), timestamp: world.get::<ecs::Timestamp>(entity)?.0, is_local: world.get::<ecs::IsLocal>(entity).is_some(), active: world.get::<ecs::ActiveState>(entity)?.0, }) }}
pub struct Server { sender: mpsc::Sender<Command>,}
impl Server { pub fn start(power_sender: channel::Sender<PowerRequest>) -> Self { let (sender, receiver) = mpsc::channel(); if let Err(err) = thread::Builder::new() .name("sessiond-consolekit".to_string()) .spawn(move || run(&receiver, &power_sender)) { log::error!(error = err.to_string().as_str(); "failed to spawn ConsoleKit thread"); }
Self { sender } }
pub fn upsert_session(&self, session: SessionInfo) { if let Err(err) = self.sender.send(Command::Upsert(session)) { log::debug!(error = err.to_string().as_str(); "failed to update ConsoleKit session"); } }
pub fn remove_session(&self, session_id: session::SessionId) { if let Err(err) = self.sender.send(Command::Remove(session_id.as_raw())) { log::debug!(error = err.to_string().as_str(); "failed to remove ConsoleKit session"); } }}
enum Command { Upsert(SessionInfo), Remove(u32),}
pub struct PowerRequest { pub kind: PowerRequestKind, pub creds: hyprwire::Creds, pub reply: mpsc::Sender<PowerResponse>,}
pub enum PowerRequestKind { Execute(session::PowerAction), Can(session::PowerAction),}
pub enum PowerResponse { Done(Result<(), session::PowerError>), Can(bool),}
#[derive(Default)]struct State { sessions: HashMap<u32, SessionInfo>,}
type SharedState = sync::Arc<sync::RwLock<State>>;
struct Manager { state: SharedState, power_sender: channel::Sender<PowerRequest>,}
#[allow(clippy::needless_pass_by_value)]#[interface(name = "org.freedesktop.ConsoleKit.Manager")]impl Manager { #[zbus(name = "Restart")] fn restart(&self, #[zbus(header)] header: message::Header<'_>) -> fdo::Result<()> { self.execute_power(session::PowerAction::Reboot, &header) }
#[zbus(name = "Stop")] fn stop(&self, #[zbus(header)] header: message::Header<'_>) -> fdo::Result<()> { self.execute_power(session::PowerAction::Poweroff, &header) }
#[zbus(name = "Suspend")] fn suspend( &self, #[allow(unused_variables)] policykit_interactivity: bool, #[zbus(header)] header: message::Header<'_>, ) -> fdo::Result<()> { self.execute_power(session::PowerAction::Suspend, &header) }
#[zbus(name = "Hibernate")] fn hibernate( &self, #[allow(unused_variables)] policykit_interactivity: bool, #[zbus(header)] header: message::Header<'_>, ) -> fdo::Result<()> { self.execute_power(session::PowerAction::Hibernate, &header) }
#[zbus(name = "CanRestart")] fn can_restart(&self, #[zbus(header)] header: message::Header<'_>) -> fdo::Result<bool> { self.can_power(session::PowerAction::Reboot, &header) }
#[zbus(name = "CanStop")] fn can_stop(&self, #[zbus(header)] header: message::Header<'_>) -> fdo::Result<bool> { self.can_power(session::PowerAction::Poweroff, &header) }
#[zbus(name = "CanSuspend")] fn can_suspend(&self, #[zbus(header)] header: message::Header<'_>) -> fdo::Result<String> { self.can_power_string(session::PowerAction::Suspend, &header) }
#[zbus(name = "CanHibernate")] fn can_hibernate(&self, #[zbus(header)] header: message::Header<'_>) -> fdo::Result<String> { self.can_power_string(session::PowerAction::Hibernate, &header) }
#[zbus(name = "GetSessionForUnixProcess")] fn get_session_for_unix_process(&self, pid: u32) -> fdo::Result<zvariant::OwnedObjectPath> { let session_id = session_id_for_pid(pid)?; self.with_session(session_id, |session| session_path(session.id)) }
#[zbus(name = "GetCurrentSession")] #[allow(clippy::needless_pass_by_value)] fn get_current_session( &self, #[zbus(header)] header: message::Header<'_>, ) -> fdo::Result<zvariant::OwnedObjectPath> { let sender = header .sender() .ok_or_else(|| fdo::Error::Failed("caller has no D-Bus sender".to_string()))?; let connection = blocking::Connection::system() .map_err(|err| fdo::Error::Failed(format!("failed to connect to system bus: {err}")))?; let proxy = DBusProxy::new(&connection) .map_err(|err| fdo::Error::Failed(format!("failed to create D-Bus proxy: {err}")))?; let pid = proxy .get_connection_unix_process_id(names::BusName::from(sender.clone())) .map_err(|err| fdo::Error::Failed(format!("failed to resolve caller pid: {err}")))?; let session_id = session_id_for_pid(pid)?; self.with_session(session_id, |session| session_path(session.id)) }
#[zbus(name = "GetSessions")] fn get_sessions(&self) -> fdo::Result<Vec<zvariant::OwnedObjectPath>> { let state = self .state .read() .map_err(|_| fdo::Error::Failed("ConsoleKit state lock poisoned".to_string()))?; Ok(state .sessions .values() .map(|session| session_path(session.id)) .collect()) }}
impl Manager { fn execute_power( &self, action: session::PowerAction, header: &message::Header<'_>, ) -> fdo::Result<()> { match self.send_power_request(PowerRequestKind::Execute(action), header)? { PowerResponse::Done(Ok(())) => Ok(()), PowerResponse::Done(Err(session::PowerError::ActiveSessions(message))) => { Err(fdo::Error::AccessDenied(message)) } PowerResponse::Done(Err(session::PowerError::Io(message))) => { Err(fdo::Error::Failed(message)) } PowerResponse::Can(_) => Err(fdo::Error::Failed( "unexpected ConsoleKit power response".to_string(), )), } }
fn can_power( &self, action: session::PowerAction, header: &message::Header<'_>, ) -> fdo::Result<bool> { match self.send_power_request(PowerRequestKind::Can(action), header)? { PowerResponse::Can(can) => Ok(can), PowerResponse::Done(_) => Err(fdo::Error::Failed( "unexpected ConsoleKit power response".to_string(), )), } }
fn can_power_string( &self, action: session::PowerAction, header: &message::Header<'_>, ) -> fdo::Result<String> { self.can_power(action, header) .map(|can| if can { "yes" } else { "no" }.to_string()) }
fn send_power_request( &self, kind: PowerRequestKind, header: &message::Header<'_>, ) -> fdo::Result<PowerResponse> { let creds = caller_creds(header)?; let (reply, receiver) = mpsc::channel(); self.power_sender .send(PowerRequest { kind, creds, reply }) .map_err(|_| fdo::Error::Failed("ConsoleKit power handler unavailable".to_string()))?; receiver .recv() .map_err(|_| fdo::Error::Failed("ConsoleKit power handler closed".to_string())) }
fn with_session<T>( &self, session_id: u32, f: impl FnOnce(&SessionInfo) -> T, ) -> fdo::Result<T> { let session = { let state = self .state .read() .map_err(|_| fdo::Error::Failed("ConsoleKit state lock poisoned".to_string()))?; state .sessions .get(&session_id) .cloned() .ok_or_else(|| fdo::Error::Failed("unknown ConsoleKit session".to_string()))? };
Ok(f(&session)) }}
struct Session { id: u32, state: SharedState,}
#[interface(name = "org.freedesktop.ConsoleKit.Session")]impl Session { #[zbus(name = "GetUnixUser")] fn get_unix_user(&self) -> fdo::Result<u32> { self.with_session(|session| session.uid) }
#[zbus(name = "GetUser")] fn get_user(&self) -> fdo::Result<u32> { self.get_unix_user() }
#[zbus(name = "IsActive")] fn is_active(&self) -> fdo::Result<bool> { self.with_session(is_active) }
#[zbus(name = "IsLocal")] fn is_local(&self) -> fdo::Result<bool> { self.with_session(|session| session.is_local) }
#[zbus(name = "GetSeatId")] fn get_seat_id(&self) -> fdo::Result<zvariant::OwnedObjectPath> { self.with_session(|session| { let seat = if session.seat.is_empty() { "seat0" } else { session.seat.as_str() }; object_path(&format!("/org/freedesktop/ConsoleKit/Seat/{seat}")) })? }
#[zbus(name = "GetId")] fn get_id(&self) -> fdo::Result<String> { self.with_session(|session| session.id.as_raw().to_string()) }
#[zbus(name = "GetLoginSessionId")] fn get_login_session_id(&self) -> fdo::Result<String> { self.with_session(|session| session.id.as_raw().to_string()) }
#[zbus(name = "GetCreationTime")] fn get_creation_time(&self) -> fdo::Result<String> { self.with_session(|session| timestamp_string(session.timestamp)) }
#[zbus(property, name = "unix-user")] fn unix_user(&self) -> fdo::Result<u32> { self.get_unix_user() }
#[zbus(property, name = "is-active")] fn active_property(&self) -> fdo::Result<bool> { self.is_active() }
#[zbus(property, name = "is-local")] fn local_property(&self) -> fdo::Result<bool> { self.is_local() }}
impl Session { fn with_session<T>(&self, f: impl FnOnce(&SessionInfo) -> T) -> fdo::Result<T> { let session = { let state = self .state .read() .map_err(|_| fdo::Error::Failed("ConsoleKit state lock poisoned".to_string()))?; state .sessions .get(&self.id) .cloned() .ok_or_else(|| fdo::Error::Failed("unknown ConsoleKit session".to_string()))? };
Ok(f(&session)) }}
fn run(receiver: &mpsc::Receiver<Command>, power_sender: &channel::Sender<PowerRequest>) { let state = SharedState::default(); let mut connection = None; let mut warned_unavailable = false;
if let Err(err) = write_database(&state) { log::warn!(error = err.to_string().as_str(); "failed to write ConsoleKit database"); }
loop { if connection.is_none() { match start_connection(&state, power_sender.clone()) { Ok(new_connection) => { register_existing_sessions(&new_connection, &state); connection = Some(new_connection); warned_unavailable = false; log::info!("ConsoleKit compatibility layer started"); } Err(err) => { if !warned_unavailable { log::warn!(error = err.to_string().as_str(); "ConsoleKit compatibility waiting for system bus"); warned_unavailable = true; } } } }
match receiver.recv_timeout(time::Duration::from_secs(1)) { Ok(command) => apply_command(connection.as_ref(), &state, command), Err(mpsc::RecvTimeoutError::Timeout) => {} Err(mpsc::RecvTimeoutError::Disconnected) => return, } }}
fn start_connection( state: &SharedState, power_sender: channel::Sender<PowerRequest>,) -> zbus::Result<blocking::Connection> { connection::Builder::system()? .serve_at( MANAGER_PATH, Manager { state: state.clone(), power_sender, }, )? .name(BUS_NAME)? .build()}
fn register_existing_sessions(connection: &blocking::Connection, state: &SharedState) { let sessions = state .read() .map(|state| state.sessions.keys().copied().collect::<Vec<_>>()) .unwrap_or_default();
for id in sessions { register_session(connection, state, id); }}
fn apply_command(connection: Option<&blocking::Connection>, state: &SharedState, command: Command) { match command { Command::Upsert(session) => { let id = session.id.as_raw(); let mut should_register = false; if let Ok(mut state) = state.write() { should_register = !state.sessions.contains_key(&id); state.sessions.insert(id, session); }
if should_register && let Some(connection) = connection { register_session(connection, state, id); }
if let Err(err) = write_database(state) { log::warn!(error = err.to_string().as_str(); "failed to write ConsoleKit database"); } } Command::Remove(id) => { if let Ok(mut state) = state.write() { state.sessions.remove(&id); }
if let Some(connection) = connection { let path = session_path(session::SessionId::from_raw(id)); if let Err(err) = connection.object_server().remove::<Session, _>(path) { log::debug!(session_id = id, error = err.to_string().as_str(); "failed to unregister ConsoleKit session object"); } }
if let Err(err) = write_database(state) { log::warn!(error = err.to_string().as_str(); "failed to write ConsoleKit database"); } } }}
fn register_session(connection: &blocking::Connection, state: &SharedState, id: u32) { if let Err(err) = connection.object_server().at( session_path(session::SessionId::from_raw(id)), Session { id, state: state.clone(), }, ) { log::debug!(session_id = id, error = err.to_string().as_str(); "failed to register ConsoleKit session object"); }}
fn session_path(session_id: session::SessionId) -> zvariant::OwnedObjectPath { object_path(&session_path_string(session_id)).unwrap_or_else(|_| { zvariant::ObjectPath::from_static_str_unchecked("/org/freedesktop/ConsoleKit/Session/0") .into() })}
fn session_path_string(session_id: session::SessionId) -> String { format!( "/org/freedesktop/ConsoleKit/Session/{}", session_id.as_raw() )}
fn object_path(path: &str) -> fdo::Result<zvariant::OwnedObjectPath> { zvariant::ObjectPath::try_from(path) .map(Into::into) .map_err(|err| fdo::Error::Failed(format!("invalid object path: {err}")))}
fn caller_creds(header: &message::Header<'_>) -> fdo::Result<hyprwire::Creds> { let sender = header .sender() .ok_or_else(|| fdo::Error::Failed("caller has no D-Bus sender".to_string()))?; let connection = blocking::Connection::system() .map_err(|err| fdo::Error::Failed(format!("failed to connect to system bus: {err}")))?; let proxy = DBusProxy::new(&connection) .map_err(|err| fdo::Error::Failed(format!("failed to create D-Bus proxy: {err}")))?; let credentials = proxy .get_connection_credentials(names::BusName::from(sender.clone())) .map_err(|err| { fdo::Error::Failed(format!("failed to resolve caller credentials: {err}")) })?;
let raw_pid = credentials .process_id() .ok_or_else(|| fdo::Error::Failed("caller credentials have no pid".to_string()))?; let raw_uid = credentials .unix_user_id() .ok_or_else(|| fdo::Error::Failed("caller credentials have no uid".to_string()))?; let raw_gid = process_gid(raw_pid).or_else(|| { credentials .unix_group_ids() .and_then(|groups| groups.first().copied()) });
let pid = i32::try_from(raw_pid) .ok() .and_then(process::Pid::from_raw) .ok_or_else(|| fdo::Error::Failed(format!("invalid caller pid: {raw_pid}")))?; let gid = raw_gid .map(process::Gid::from_raw) .ok_or_else(|| fdo::Error::Failed("caller credentials have no gid".to_string()))?;
Ok(hyprwire::Creds { pid, uid: process::Uid::from_raw(raw_uid), gid, })}
fn process_gid(pid: u32) -> Option<u32> { let status = fs::read_to_string(format!("/proc/{pid}/status")).ok()?; status.lines().find_map(|line| { let gids = line.strip_prefix("Gid:")?; gids.split_whitespace().nth(1)?.parse().ok() })}
fn session_id_for_pid(pid: u32) -> fdo::Result<u32> { let cgroup = fs::read_to_string(format!("/proc/{pid}/cgroup")) .map_err(|err| fdo::Error::Failed(format!("could not read process cgroup: {err}")))?;
cgroup .lines() .filter_map(|line| line.rsplit_once(':').map(|(_, path)| path)) .find_map(|path| { path.split('/').find_map(|component| { component .strip_prefix("session-") .and_then(|id| id.parse::<u32>().ok()) }) }) .ok_or_else(|| fdo::Error::Failed("no ConsoleKit session for pid".to_string()))}
pub fn timestamp_string(timestamp: time::SystemTime) -> String { timestamp .duration_since(time::UNIX_EPOCH) .unwrap_or_default() .as_secs() .to_string()}
fn write_database(state: &SharedState) -> io::Result<()> { let sessions = state .read() .map(|state| state.sessions.values().cloned().collect::<Vec<_>>()) .unwrap_or_default();
fs::create_dir_all(DATABASE_DIR)?;
let mut database = String::new(); append_database(&mut database, &sessions);
let mut file = fs::File::create(DATABASE_TMP_PATH)?; file.write_all(database.as_bytes())?; file.sync_all()?; drop(file);
fs::rename(DATABASE_TMP_PATH, DATABASE_PATH)?;
Ok(())}
pub fn append_database(database: &mut String, sessions: &[SessionInfo]) { let seats = sessions .iter() .map(|session| seat_name(session).to_string()) .fold(Vec::<String>::new(), |mut seats, seat| { if !seats.contains(&seat) { seats.push(seat); } seats });
let _ = writeln!(database, "[Seats]"); let _ = writeln!(database, "seats={}", key_file_list(seats.iter())); let _ = writeln!(database);
for session in sessions { append_session_group(database, session, &session_path_string(session.id)); }
for seat in &seats { append_seat_group(database, seat, sessions); }
append_user_groups(database, sessions);}
fn append_session_group(database: &mut String, session: &SessionInfo, id: &str) { let _ = writeln!(database, "[Session {id}]"); let _ = writeln!(database, "uid={}", session.uid); let _ = writeln!(database, "seat={}", key_file_escape(seat_name(session))); let _ = writeln!(database, "type=unspecified"); let _ = writeln!(database, "display_device="); let _ = writeln!(database, "remote_host_name="); let _ = writeln!(database, "is_active={}", key_file_bool(is_active(session))); let _ = writeln!(database, "is_local={}", key_file_bool(session.is_local)); let _ = writeln!(database, "XDG_RUNTIME_DIR=/run/user/{}", session.uid); let _ = writeln!( database, "creation_time={}", key_file_escape(×tamp_string(session.timestamp)) ); let _ = writeln!(database);}
fn append_seat_group(database: &mut String, seat: &str, sessions: &[SessionInfo]) { let session_paths = sessions .iter() .filter(|session| seat_name(session) == seat) .map(|session| session_path_string(session.id)) .collect::<Vec<_>>();
let _ = writeln!(database, "[Seat {}]", key_file_escape(seat)); let _ = writeln!(database, "sessions={}", key_file_list(session_paths.iter())); if let Some(active) = sessions .iter() .find(|session| { seat_name(session) == seat && session.active == session_core_v1::ActiveState::Active }) .map(|session| session_path_string(session.id)) { let _ = writeln!(database, "active_session={}", key_file_escape(&active)); } let _ = writeln!(database);}
fn append_user_groups(database: &mut String, sessions: &[SessionInfo]) { let mut uids = sessions .iter() .map(|session| session.uid) .collect::<Vec<_>>(); uids.sort_unstable(); uids.dedup();
for uid in uids { let user_sessions = sessions .iter() .filter(|session| session.uid == uid) .collect::<Vec<_>>(); let session_paths = user_sessions .iter() .map(|session| session_path_string(session.id)) .collect::<Vec<_>>(); let is_local = user_sessions.iter().any(|session| session.is_local);
let _ = writeln!(database, "[User {uid}]"); let _ = writeln!(database, "is_local={}", key_file_bool(is_local)); let _ = writeln!(database, "sessions={}", key_file_list(session_paths.iter())); let _ = writeln!(database); }}
pub const fn seat_name(session: &SessionInfo) -> &str { if session.seat.is_empty() { "seat0" } else { session.seat.as_str() }}
pub const fn is_active(session: &SessionInfo) -> bool { matches!( session.active, session_core_v1::ActiveState::Active | session_core_v1::ActiveState::Online )}
pub const fn key_file_bool(value: bool) -> &'static str { if value { "true" } else { "false" }}
pub fn key_file_list<'a>(values: impl Iterator<Item = &'a String>) -> String { values .map(|value| key_file_escape(value)) .collect::<Vec<_>>() .join(";")}
pub fn key_file_escape(value: &str) -> String { let mut escaped = String::with_capacity(value.len()); for ch in value.chars() { match ch { '\\' => escaped.push_str("\\\\"), '\n' => escaped.push_str("\\n"), '\r' => escaped.push_str("\\r"), '\t' => escaped.push_str("\\t"), ';' => escaped.push_str("\\;"), _ => escaped.push(ch), } } escaped}