session and seat management daemon
0

Configure Feed

Select the types of activity you want to include in your feed.

tty switching, seat management

authored by

Oskar Rochowiak and committed by
r0chd
(Jun 14, 2026, 12:25 PM +0200) f9f94b37 3237bdb7

+1997 -238
+21
Cargo.lock
··· 80 80 checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" 81 81 dependencies = [ 82 82 "bitflags", 83 + "nix", 83 84 "polling", 84 85 "rustix", 85 86 "slab", ··· 91 92 version = "1.0.4" 92 93 source = "registry+https://github.com/rust-lang/crates.io-index" 93 94 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 95 + 96 + [[package]] 97 + name = "cfg_aliases" 98 + version = "0.2.1" 99 + source = "registry+https://github.com/rust-lang/crates.io-index" 100 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 94 101 95 102 [[package]] 96 103 name = "clap" ··· 171 178 dependencies = [ 172 179 "anyhow", 173 180 "calloop", 181 + "clap", 174 182 "env_logger", 175 183 "hyprwire", 176 184 "hyprwire-scanner", 177 185 "log", 178 186 "rustix", 187 + "users", 179 188 ] 180 189 181 190 [[package]] ··· 308 317 version = "2.8.1" 309 318 source = "registry+https://github.com/rust-lang/crates.io-index" 310 319 checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" 320 + 321 + [[package]] 322 + name = "nix" 323 + version = "0.31.3" 324 + source = "registry+https://github.com/rust-lang/crates.io-index" 325 + checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" 326 + dependencies = [ 327 + "bitflags", 328 + "cfg-if", 329 + "cfg_aliases", 330 + "libc", 331 + ] 311 332 312 333 [[package]] 313 334 name = "once_cell_polyfill"
+12 -16
ctl/src/main.rs
··· 16 16 object: session_core_session::SessionCoreSession, 17 17 uid: u32, 18 18 username: String, 19 - tty: String, 19 + vt: u32, 20 20 seat: Option<String>, 21 21 leader: u32, 22 22 timestamp: u32, ··· 44 44 object: object.send_get_session::<State>(session_id).unwrap(), 45 45 uid: 0, 46 46 username: String::new(), 47 - tty: String::new(), 47 + vt: 0, 48 48 seat: None, 49 49 leader: 0, 50 50 timestamp: 0, ··· 60 60 object: object.send_get_session::<State>(session_id).unwrap(), 61 61 uid: 0, 62 62 username: String::new(), 63 - tty: String::new(), 63 + vt: 0, 64 64 seat: None, 65 65 leader: 0, 66 66 timestamp: 0, ··· 93 93 match event { 94 94 session_core_session::Event::Uid { uid } => session.uid = uid, 95 95 session_core_session::Event::Username { username } => session.username = username, 96 - session_core_session::Event::Tty { tty } => session.tty = tty, 96 + session_core_session::Event::Vt { vt } => session.vt = vt, 97 97 session_core_session::Event::Seat { seat } => session.seat = Some(seat), 98 98 session_core_session::Event::Leader { leader } => session.leader = leader, 99 99 session_core_session::Event::Timestamp { timestamp } => session.timestamp = timestamp, ··· 107 107 self.done = true; 108 108 } 109 109 session_core_session::Event::Active { state } => { 110 - session.active = match state { 111 - 0 => session_core_v1::ActiveState::Online, 112 - 1 => session_core_v1::ActiveState::Active, 113 - 2 => session_core_v1::ActiveState::Closing, 114 - _ => unimplemented!("unknown active state: {state}"), 115 - }; 110 + session.active = state; 116 111 } 117 112 _ => {} 118 113 } ··· 177 172 session_core_v1::ActiveState::Online => "online", 178 173 session_core_v1::ActiveState::Active => "active", 179 174 session_core_v1::ActiveState::Closing => "closing", 175 + session_core_v1::ActiveState::Opening => "opening", 180 176 } 181 177 }; 182 178 ··· 222 218 .max() 223 219 .unwrap_or(0) 224 220 .max(4); 225 - let tty_w = state 221 + let vt_w = state 226 222 .sessions 227 223 .iter() 228 - .map(|s| s.tty.len()) 224 + .map(|s| format!("tty{}", s.vt).len()) 229 225 .max() 230 226 .unwrap_or(0) 231 227 .max(3); ··· 259 255 .max(5); 260 256 261 257 println!( 262 - " {:<id_w$} {:<uid_w$} {:<user_w$} {:<tty_w$} {:<seat_w$} {:<leader_w$} {:<since_w$} {:<state_w$}", 258 + " {:<id_w$} {:<uid_w$} {:<user_w$} {:<vt_w$} {:<seat_w$} {:<leader_w$} {:<since_w$} {:<state_w$}", 263 259 "ID", "UID", "USER", "TTY", "SEAT", "LEADER", "SINCE", "STATE", 264 260 ); 265 261 println!( ··· 270 266 + 2 271 267 + user_w 272 268 + 2 273 - + tty_w 269 + + vt_w 274 270 + 2 275 271 + seat_w 276 272 + 2 ··· 283 279 ); 284 280 for session in state.sessions { 285 281 println!( 286 - " {:<id_w$} {:<uid_w$} {:<user_w$} {:<tty_w$} {:<seat_w$} {:<leader_w$} {:<since_w$} {:<state_w$}", 282 + " {:<id_w$} {:<uid_w$} {:<user_w$} {:<vt_w$} {:<seat_w$} {:<leader_w$} {:<since_w$} {:<state_w$}", 287 283 session.id, 288 284 session.uid, 289 285 session.username, 290 - session.tty, 286 + format!("tty{}", session.vt), 291 287 session.seat.as_deref().unwrap_or("-"), 292 288 session.leader, 293 289 fmt_ts(session.timestamp),
+3 -1
daemon/Cargo.toml
··· 11 11 12 12 [dependencies] 13 13 anyhow = "1.0.102" 14 + clap = { version = "4.6.1", features = ["derive"] } 14 15 env_logger = { version = "0.11.10", features = ["kv"] } 15 16 log = { version = "0.4.31", features = ["kv"] } 16 17 hyprwire = "0.4.2" 17 - calloop = "0.14.4" 18 + calloop = { version = "0.14.4", features = ["signals"] } 18 19 rustix = "1.1.4" 20 + users = "0.11.0" 19 21 20 22 [build-dependencies] 21 23 hyprwire-scanner = "0.4.2"
+1
daemon/build.rs
··· 4 4 .compile(&[ 5 5 "../protocols/session_management_v1.xml", 6 6 "../protocols/session_core_v1.xml", 7 + "../protocols/seat_v1.xml", 7 8 ]) 8 9 .unwrap(); 9 10 }
+13 -10
daemon/src/cgroup.rs
··· 1 + use crate::session::SessionId; 1 2 use anyhow::Context; 2 3 use calloop::generic; 3 4 use rustix::process; ··· 26 27 27 28 pub fn create_session_cgroup( 28 29 &mut self, 29 - session_id: u32, 30 + session_id: SessionId, 30 31 leader_pid: i32, 31 32 ) -> anyhow::Result<()> { 32 - let session_cgroup_path = self.root_path.join(format!("session-{session_id}")); 33 + let session_cgroup_path = self 34 + .root_path 35 + .join(format!("session-{}", session_id.as_raw())); 33 36 fs::create_dir(&session_cgroup_path)?; 34 37 fs::write( 35 38 session_cgroup_path.join("cgroup.procs"), 36 39 leader_pid.to_string(), 37 40 )?; 38 41 39 - log::info!(session_id = session_id, leader_pid = leader_pid, path = session_cgroup_path.to_str(); "cgroup created"); 42 + log::info!(session_id = session_id.as_raw(), leader_pid = leader_pid, path = session_cgroup_path.to_str(); "cgroup created"); 40 43 41 44 let cgroup = Cgroup { 42 45 session_id, ··· 48 51 Ok(()) 49 52 } 50 53 51 - pub fn poll_destroy(&self, session_id: u32) -> anyhow::Result<()> { 54 + pub fn poll_destroy(&self, session_id: SessionId) -> anyhow::Result<()> { 52 55 let cgroup = self 53 56 .cgroups 54 57 .iter() ··· 80 83 .context("session cgroup not found")?; 81 84 82 85 fs::remove_dir(&cgroup.path)?; 83 - log::info!(session_id = session_id, path = cgroup.path.to_str(); "cgroup destroyed"); 86 + log::info!(session_id = session_id.as_raw(), path = cgroup.path.to_str(); "cgroup destroyed"); 84 87 return Ok(calloop::PostAction::Remove); 85 88 } 86 89 ··· 90 93 match result { 91 94 Ok(action) => Ok(action), 92 95 Err(e) => { 93 - log::error!(session_id = session_id, error = e.to_string().as_str(); "cgroup poll error"); 96 + log::error!(session_id = session_id.as_raw(), error = e.to_string().as_str(); "cgroup poll error"); 94 97 Ok(calloop::PostAction::Remove) 95 98 } 96 99 } ··· 99 102 Ok(()) 100 103 } 101 104 102 - pub fn kill_session(&self, session_id: u32) -> anyhow::Result<()> { 105 + pub fn kill_session(&self, session_id: SessionId) -> anyhow::Result<()> { 103 106 let cgroup = self 104 107 .cgroups 105 108 .iter() ··· 112 115 Ok(()) 113 116 } 114 117 115 - pub fn terminate_session(&self, session_id: u32) -> anyhow::Result<()> { 118 + pub fn terminate_session(&self, session_id: SessionId) -> anyhow::Result<()> { 116 119 let cgroup = self 117 120 .cgroups 118 121 .iter() ··· 130 133 Ok(()) 131 134 } 132 135 133 - pub fn find_session_by_pid(&self, pid: u32) -> anyhow::Result<Option<u32>> { 136 + pub fn find_session_by_pid(&self, pid: u32) -> anyhow::Result<Option<SessionId>> { 134 137 for cgroup in self.cgroups.iter() { 135 138 let procs = fs::read_to_string(cgroup.path.join("cgroup.procs"))?; 136 139 ··· 148 151 } 149 152 150 153 struct Cgroup { 151 - session_id: u32, 154 + session_id: SessionId, 152 155 path: path::PathBuf, 153 156 }
+55
daemon/src/client.rs
··· 1 + use crate::device; 2 + use crate::seat_v1; 3 + use crate::seat_v1::seat; 4 + 5 + pub struct Client { 6 + pub(crate) seat_name: String, 7 + pub(crate) object: seat::Seat, 8 + pub(crate) devices: Vec<device::Device>, 9 + pub(crate) state: crate::seat::SeatState, 10 + pub(crate) session_id: u32, 11 + } 12 + 13 + impl PartialEq for Client { 14 + fn eq(&self, other: &Self) -> bool { 15 + self.object == other.object 16 + } 17 + } 18 + 19 + impl Eq for Client {} 20 + 21 + impl Client { 22 + pub(crate) fn close_device(&mut self, object: &seat_v1::device::Device) -> anyhow::Result<()> { 23 + let idx = self 24 + .devices 25 + .iter() 26 + .position(|device| device.contains_object(object)) 27 + .ok_or_else(|| anyhow::anyhow!("device not found"))?; 28 + 29 + let device = &mut self.devices[idx]; 30 + device.remove_object(object); 31 + log::debug!( 32 + "Closing device {} for client {} on {}", 33 + device.path().display(), 34 + self.session_id, 35 + self.seat_name 36 + ); 37 + 38 + if device.decrement_ref() > 0 { 39 + return Ok(()); 40 + } 41 + 42 + if let Err(err) = device.deactivate() { 43 + log::error!( 44 + "Could not deactivate device {} for client {} on {}: {err}", 45 + device.path().display(), 46 + self.session_id, 47 + self.seat_name 48 + ); 49 + } 50 + 51 + self.devices.remove(idx); 52 + 53 + Ok(()) 54 + } 55 + }
+268
daemon/src/device.rs
··· 1 + use crate::seat_v1::device; 2 + use rustix::{io, ioctl}; 3 + use std::ffi::c_int; 4 + use std::os::fd; 5 + use std::path; 6 + 7 + impl hyprwire::Dispatch<device::Device> for crate::Sessiond { 8 + fn event( 9 + &mut self, 10 + object: &device::Device, 11 + event: <device::Device as hyprwire::Object>::Event<'_>, 12 + ) { 13 + let device::Event::Destroy = event; 14 + 15 + let Some(client) = self.clients.iter().find(|client| { 16 + client 17 + .borrow() 18 + .devices 19 + .iter() 20 + .any(|device| device.contains_object(object)) 21 + }) else { 22 + return; 23 + }; 24 + 25 + if let Err(err) = client.borrow_mut().close_device(object) { 26 + log::error!("Could not close device: {err}"); 27 + } 28 + } 29 + } 30 + 31 + const DRM_SET_MASTER: ioctl::Opcode = ioctl::opcode::none(b'd', 0x1e); 32 + const DRM_DROP_MASTER: ioctl::Opcode = ioctl::opcode::none(b'd', 0x1f); 33 + const EVIOCREVOKE: ioctl::Opcode = ioctl::opcode::write::<c_int>(b'E', 0x91); 34 + const HIDIOCREVOKE: ioctl::Opcode = ioctl::opcode::write::<c_int>(b'H', 0x0d); 35 + 36 + fn drm_set_master<Fd: fd::AsFd>(fd: Fd) -> io::Result<()> { 37 + unsafe { ioctl::ioctl(fd, ioctl::NoArg::<DRM_SET_MASTER>::new()) } 38 + } 39 + 40 + fn drm_drop_master<Fd: fd::AsFd>(fd: Fd) -> io::Result<()> { 41 + unsafe { ioctl::ioctl(fd, ioctl::NoArg::<DRM_DROP_MASTER>::new()) } 42 + } 43 + 44 + fn evdev_revoke<Fd: fd::AsFd>(fd: Fd) -> io::Result<()> { 45 + unsafe { ioctl::ioctl(fd, ioctl::IntegerSetter::<EVIOCREVOKE>::new_usize(0)) } 46 + } 47 + 48 + fn hidraw_revoke<Fd: fd::AsFd>(fd: Fd) -> io::Result<()> { 49 + unsafe { ioctl::ioctl(fd, ioctl::IntegerSetter::<HIDIOCREVOKE>::new_usize(0)) } 50 + } 51 + 52 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 53 + pub enum DeviceKind { 54 + Evdev, 55 + Drm, 56 + Wscons, 57 + Hidraw, 58 + } 59 + 60 + impl DeviceKind { 61 + pub fn from_path(path: &path::Path) -> Option<Self> { 62 + if matches_named_device(path, "/dev/input", "event") { 63 + Some(Self::Evdev) 64 + } else if path.starts_with("/dev/dri/") || path.starts_with("/dev/drm/") { 65 + Some(Self::Drm) 66 + } else if path.starts_with("/dev/wskbd") 67 + || path.starts_with("/dev/wsmouse") 68 + || path.starts_with("/dev/wsmux") 69 + { 70 + Some(Self::Wscons) 71 + } else if path.starts_with("/dev/hidraw") { 72 + Some(Self::Hidraw) 73 + } else { 74 + None 75 + } 76 + } 77 + } 78 + 79 + fn matches_named_device(path: &path::Path, parent: &str, prefix: &str) -> bool { 80 + path.parent() == Some(path::Path::new(parent)) 81 + && path 82 + .file_name() 83 + .and_then(|name| name.to_str()) 84 + .is_some_and(|name| name.starts_with(prefix)) 85 + } 86 + 87 + pub struct Device { 88 + objects: Vec<device::Device>, 89 + path: path::PathBuf, 90 + ref_cnt: usize, 91 + r#type: DeviceKind, 92 + id: u32, 93 + active: bool, 94 + fd: fd::OwnedFd, 95 + } 96 + 97 + impl Device { 98 + pub(crate) fn new( 99 + object: device::Device, 100 + path: path::PathBuf, 101 + r#type: DeviceKind, 102 + id: u32, 103 + active: bool, 104 + fd: fd::OwnedFd, 105 + ) -> Self { 106 + Self { 107 + objects: vec![object], 108 + path, 109 + ref_cnt: 1, 110 + r#type, 111 + id, 112 + active, 113 + fd, 114 + } 115 + } 116 + 117 + pub(crate) fn activate(&mut self) -> anyhow::Result<()> { 118 + if self.active { 119 + return Ok(()); 120 + } 121 + 122 + match self.r#type { 123 + DeviceKind::Drm => { 124 + drm_set_master(&self.fd) 125 + .map_err(|err| anyhow::anyhow!("Could not make device fd drm master: {err}"))?; 126 + self.active = true; 127 + } 128 + DeviceKind::Evdev | DeviceKind::Hidraw => { 129 + return Err(anyhow::anyhow!("device type cannot be activated")); 130 + } 131 + DeviceKind::Wscons => { 132 + self.active = true; 133 + } 134 + } 135 + 136 + Ok(()) 137 + } 138 + 139 + pub(crate) fn deactivate(&mut self) -> anyhow::Result<()> { 140 + if !self.active { 141 + return Ok(()); 142 + } 143 + 144 + match self.r#type { 145 + DeviceKind::Drm => { 146 + if let Err(err) = drm_drop_master(&self.fd) { 147 + log::error!("Could not revoke drm master on device fd: {err}"); 148 + return Err(err.into()); 149 + } 150 + } 151 + DeviceKind::Evdev => { 152 + if let Err(err) = evdev_revoke(&self.fd) { 153 + log::error!("Could not revoke evdev on device fd: {err}"); 154 + return Err(err.into()); 155 + } 156 + } 157 + DeviceKind::Hidraw => { 158 + if let Err(err) = hidraw_revoke(&self.fd) { 159 + log::error!("Could not revoke hidraw on device fd: {err}"); 160 + return Err(err.into()); 161 + } 162 + } 163 + DeviceKind::Wscons => {} 164 + } 165 + 166 + self.active = false; 167 + Ok(()) 168 + } 169 + 170 + pub(crate) fn matches_path(&self, path: &path::Path) -> bool { 171 + self.path == path 172 + } 173 + 174 + pub(crate) fn add_object(&mut self, object: device::Device) { 175 + self.ref_cnt += 1; 176 + self.objects.push(object); 177 + } 178 + 179 + pub(crate) fn id(&self) -> u32 { 180 + self.id 181 + } 182 + 183 + pub(crate) fn kind(&self) -> DeviceKind { 184 + self.r#type 185 + } 186 + 187 + pub(crate) fn fd(&self) -> &fd::OwnedFd { 188 + &self.fd 189 + } 190 + 191 + pub(crate) fn path(&self) -> &path::Path { 192 + &self.path 193 + } 194 + 195 + pub(crate) fn absolute_path_string(&self) -> String { 196 + path::absolute(&self.path) 197 + .unwrap_or(self.path.clone()) 198 + .display() 199 + .to_string() 200 + } 201 + 202 + pub(crate) fn last_object(&self) -> Option<&device::Device> { 203 + self.objects.last() 204 + } 205 + 206 + pub(crate) fn contains_object(&self, object: &device::Device) -> bool { 207 + self.objects.iter().any(|candidate| candidate == object) 208 + } 209 + 210 + pub(crate) fn remove_object(&mut self, object: &device::Device) { 211 + self.objects.retain(|candidate| candidate != object); 212 + } 213 + 214 + pub(crate) fn send_revoked(&self) { 215 + for object in &self.objects { 216 + object.send_revoked(); 217 + } 218 + } 219 + 220 + pub(crate) fn decrement_ref(&mut self) -> usize { 221 + self.ref_cnt = self.ref_cnt.saturating_sub(1); 222 + self.ref_cnt 223 + } 224 + } 225 + 226 + impl Drop for Device { 227 + fn drop(&mut self) { 228 + if !self.active { 229 + return; 230 + } 231 + 232 + if let Err(err) = self.deactivate() { 233 + log::error!( 234 + "Could not deactivate {} during drop: {err}", 235 + self.path.display() 236 + ); 237 + } 238 + self.active = false; 239 + } 240 + } 241 + 242 + #[cfg(test)] 243 + mod tests { 244 + use super::{DeviceKind, matches_named_device}; 245 + use std::path::Path; 246 + 247 + #[test] 248 + fn recognizes_evdev_event_nodes() { 249 + assert_eq!( 250 + DeviceKind::from_path(Path::new("/dev/input/event0")), 251 + Some(DeviceKind::Evdev) 252 + ); 253 + assert_eq!( 254 + DeviceKind::from_path(Path::new("/dev/input/event12")), 255 + Some(DeviceKind::Evdev) 256 + ); 257 + } 258 + 259 + #[test] 260 + fn does_not_match_partial_parent_paths() { 261 + assert!(!matches_named_device( 262 + Path::new("/dev/input/by-path/event0"), 263 + "/dev/input", 264 + "event" 265 + )); 266 + assert_eq!(DeviceKind::from_path(Path::new("/dev/input")), None); 267 + } 268 + }
+252 -193
daemon/src/main.rs
··· 2 2 hyprwire::include_protocol!("session_management_v1"); 3 3 pub use server::*; 4 4 } 5 - 6 5 mod session_core_v1 { 7 6 hyprwire::include_protocol!("session_core_v1"); 8 7 pub use server::*; 9 8 pub use spec::*; 10 9 } 10 + mod seat_v1 { 11 + hyprwire::include_protocol!("seat_v1"); 12 + pub use server::*; 13 + pub use spec::*; 14 + } 11 15 12 16 mod cgroup; 17 + mod client; 18 + mod device; 19 + mod seat; 20 + mod session; 21 + mod terminal; 13 22 14 - use calloop::generic; 23 + use calloop::{generic, signals}; 24 + use clap::Parser; 15 25 use hyprwire::server; 26 + use rustix::{net, process}; 16 27 use session_core_v1::{session_core_manager, session_core_session}; 17 28 use session_management_v1::session_manager; 18 29 use std::os::fd::AsRawFd; 19 30 use std::os::unix::fs::PermissionsExt; 20 - use std::{env, fs, path, time}; 31 + use std::{cell, env, fs, path, rc}; 21 32 22 - struct Session { 23 - id: u32, 24 - uid: u32, 25 - username: String, 26 - tty: String, 27 - seat: Option<String>, 28 - timestamp: time::SystemTime, 29 - leader_id: i32, 30 - objects: Vec<session_core_session::SessionCoreSession>, 31 - state: session_core_v1::ActiveState, 33 + #[derive(Parser)] 34 + struct Args { 35 + #[arg(long, value_name = "USER", help = "Allow USER to bind seat_v1")] 36 + seat_user: Vec<String>, 37 + #[arg( 38 + long, 39 + value_name = "GROUP", 40 + help = "Allow members of GROUP to bind seat_v1" 41 + )] 42 + seat_group: Vec<String>, 32 43 } 33 44 34 - impl Session { 35 - fn set_state(&mut self, state: session_core_v1::ActiveState) { 36 - log::info!(session_id = self.id, tty = self.tty.as_str(); "session state changed"); 37 - self.state = state; 38 - for object in self.objects.iter() { 39 - object.send_active(self.state as u32); 45 + struct SeatAccess { 46 + users: Vec<process::Uid>, 47 + groups: Vec<process::Gid>, 48 + } 49 + 50 + impl SeatAccess { 51 + fn new(users: Vec<process::Uid>, groups: Vec<process::Gid>) -> Self { 52 + if users.is_empty() && groups.is_empty() { 53 + return Self { 54 + users: vec![process::Uid::ROOT], 55 + groups: vec![process::Gid::ROOT], 56 + }; 40 57 } 58 + 59 + Self { users, groups } 41 60 } 42 61 43 - fn add_object(&mut self, object: session_core_session::SessionCoreSession) { 44 - log::debug!(session_id = self.id, uid = object.client().map(|c| c.creds().uid.as_raw()).unwrap_or(0); "session info requested"); 45 - object.send_session_id(self.id); 46 - object.send_uid(self.uid); 47 - object.send_username(&self.username); 48 - object.send_tty(&self.tty); 49 - object.send_active(self.state as u32); 50 - if let Some(seat) = &self.seat { 51 - object.send_seat(seat); 62 + fn allows(&self, creds: &net::UCred) -> bool { 63 + if self.users.contains(&creds.uid) || self.groups.contains(&creds.gid) { 64 + return true; 52 65 } 53 - object.send_leader(self.leader_id as u32); 54 - object.send_timestamp( 55 - self.timestamp 56 - .duration_since(time::UNIX_EPOCH) 57 - .unwrap_or_default() 58 - .as_secs() as u32, 59 - ); 60 - object.send_done(); 61 66 62 - self.objects.push(object); 67 + self.process_groups(creds.pid.as_raw_pid() as u32) 68 + .is_ok_and(|groups| groups.iter().any(|gid| self.groups.contains(gid))) 69 + } 70 + 71 + fn process_groups(&self, pid: u32) -> anyhow::Result<Vec<process::Gid>> { 72 + let status = fs::read_to_string(format!("/proc/{pid}/status"))?; 73 + let Some(groups) = status.lines().find_map(|line| line.strip_prefix("Groups:")) else { 74 + return Ok(Vec::new()); 75 + }; 76 + 77 + groups 78 + .split_whitespace() 79 + .map(|group| { 80 + group 81 + .parse::<process::RawGid>() 82 + .map(process::Gid::from_raw) 83 + .map_err(Into::into) 84 + }) 85 + .collect() 63 86 } 64 87 } 65 88 66 89 struct Sessiond { 67 - active_tty: Option<String>, 68 - next_session_id: u32, 69 - sessions: Vec<Session>, 90 + next_session_id: session::SessionId, 91 + sessions: Vec<session::Session>, 70 92 info_managers: Vec<session_core_manager::SessionCoreManager>, 71 93 cgroup_manager: cgroup::CgroupManager, 94 + seat: Vec<seat::Seat>, 95 + clients: Vec<rc::Rc<cell::RefCell<client::Client>>>, 96 + seat_access: SeatAccess, 97 + } 98 + 99 + impl Sessiond { 100 + fn sync_vt_session_states(&mut self) { 101 + let active_vts = self 102 + .seat 103 + .iter_mut() 104 + .filter_map(|seat| { 105 + seat.active_vt() 106 + .inspect_err(|err| log::error!("Could not query active VT: {err}")) 107 + .ok() 108 + .flatten() 109 + }) 110 + .collect::<Vec<_>>(); 111 + 112 + if active_vts.is_empty() { 113 + return; 114 + } 115 + 116 + for session in &mut self.sessions { 117 + if matches!( 118 + session.state(), 119 + session_core_v1::ActiveState::Opening | session_core_v1::ActiveState::Closing 120 + ) { 121 + continue; 122 + } 123 + 124 + let state = if active_vts.contains(&session.vt()) { 125 + session_core_v1::ActiveState::Active 126 + } else { 127 + session_core_v1::ActiveState::Online 128 + }; 129 + 130 + if session.state() != state { 131 + session.set_state(state); 132 + } 133 + } 134 + } 72 135 } 73 136 74 137 impl session_management_v1::SessionManagementV1Handler for Sessiond {} 75 138 76 139 impl session_core_v1::SessionCoreV1Handler for Sessiond { 77 140 fn bind(&mut self, object: session_core_manager::SessionCoreManager) { 141 + self.sync_vt_session_states(); 142 + 78 143 let session_ids = self 79 144 .sessions 80 145 .iter() 81 - .map(|session| session.id) 146 + .map(|session| session.id().as_raw()) 82 147 .collect::<Vec<_>>(); 83 148 object.send_sessions(&session_ids); 84 149 self.info_managers.push(object); ··· 101 166 } 102 167 103 168 match event { 104 - session_manager::Event::CreateSession { uid, username, tty } => { 105 - log::info!(session_id = self.next_session_id, uid, username = username.as_str(), tty = tty.as_str(); "session created"); 169 + session_manager::Event::CreateSession { uid, username, vt } => { 170 + let session_id = self.next_session_id; 171 + log::info!(session_id = session_id.as_raw(), uid, username = username.as_str(), vt; "session created"); 106 172 107 173 let leader_id = client.creds().pid.as_raw_pid(); 108 174 109 175 if let Err(e) = self 110 176 .cgroup_manager 111 - .create_session_cgroup(self.next_session_id, leader_id) 177 + .create_session_cgroup(session_id, leader_id) 112 178 { 113 - log::error!(session_id = self.next_session_id, error = e.to_string().as_str(); "failed to create session cgroup"); 179 + log::error!(session_id = session_id.as_raw(), error = e.to_string().as_str(); "failed to create session cgroup"); 114 180 } 115 181 116 - let mut session = Session { 117 - id: self.next_session_id, 118 - uid, 119 - username, 120 - tty, 121 - seat: None, 122 - timestamp: time::SystemTime::now(), 123 - leader_id, 124 - objects: Vec::new(), 125 - state: session_core_v1::ActiveState::Online, 126 - }; 127 - if self 128 - .active_tty 129 - .as_ref() 130 - .is_some_and(|active_tty| *active_tty == session.tty) 131 - { 132 - session.set_state(session_core_v1::ActiveState::Active); 182 + let mut session = session::Session::new(session_id, uid, username, vt, leader_id); 183 + let session_seat = self 184 + .seat 185 + .first() 186 + .map(|seat| seat.name().to_string()) 187 + .unwrap_or_default(); 188 + if !session_seat.is_empty() { 189 + session.set_seat(session_seat.clone()); 133 190 } 134 191 135 192 let runtime_dir = format!("/run/user/{uid}"); ··· 147 204 log::error!(uid, error = e.to_string().as_str(); "failed to chmod runtime dir"); 148 205 } 149 206 150 - object.send_session_created(self.next_session_id); 207 + if let Some(seat) = self 208 + .seat 209 + .iter_mut() 210 + .filter(|seat| seat.is_vt_bound()) 211 + .find_map(|seat| match seat.is_vt_active(session.vt()) { 212 + Ok(true) => Some(seat.name().to_string()), 213 + Ok(false) => None, 214 + Err(err) => { 215 + log::error!( 216 + session_id = session.id().as_raw(), 217 + vt = session.vt(), 218 + error = err.to_string().as_str(); 219 + "failed to check active VT" 220 + ); 221 + None 222 + } 223 + }) 224 + { 225 + session.set_seat(seat); 226 + session.set_state(session_core_v1::ActiveState::Active); 227 + } else { 228 + session.set_state(session_core_v1::ActiveState::Online); 229 + } 230 + 231 + object.send_session_created(session_id.as_raw(), &session_seat); 151 232 self.sessions.push(session); 152 233 153 234 for manager in &self.info_managers { 154 - manager.send_session_added(self.next_session_id); 235 + manager.send_session_added(session_id.as_raw()); 155 236 } 156 237 157 - self.next_session_id = self.next_session_id.wrapping_add(1); 238 + self.next_session_id = self.next_session_id.next(); 158 239 } 159 240 session_manager::Event::DestroySession { session_id } => { 241 + let session_id = session::SessionId::from_raw(session_id); 160 242 let Some(mut session) = self 161 243 .sessions 162 - .extract_if(.., |session| session.id == session_id) 244 + .extract_if(.., |session| session.id() == session_id) 163 245 .next() 164 246 else { 165 - log::warn!(session_id; "destroy requested for unknown session"); 247 + log::warn!(session_id = session_id.as_raw(); "destroy requested for unknown session"); 166 248 return; 167 249 }; 168 - log::info!(session_id, uid = session.uid, username = session.username.as_str(); "session destroyed"); 250 + log::info!(session_id = session_id.as_raw(), uid = session.uid(), username = session.username(); "session destroyed"); 169 251 170 252 session.set_state(session_core_v1::ActiveState::Closing); 171 - for object in session.objects.iter() { 253 + for object in session.objects().iter() { 172 254 object.send_destroyed(); 173 255 } 174 256 175 257 for manager in &self.info_managers { 176 - manager.send_session_removed(session_id); 258 + manager.send_session_removed(session_id.as_raw()); 177 259 } 178 260 179 261 if let Err(e) = self.cgroup_manager.poll_destroy(session_id) { 180 - log::error!(session_id, error = e.to_string().as_str(); "failed to poll destroy session cgroup"); 262 + log::error!(session_id = session_id.as_raw(), error = e.to_string().as_str(); "failed to poll destroy session cgroup"); 181 263 } 182 264 183 - if !self.sessions.iter().any(|s| s.uid == session.uid) 184 - && let Err(e) = fs::remove_dir(format!("/run/user/{}", session.uid)) 265 + if !self.sessions.iter().any(|s| s.uid() == session.uid()) 266 + && let Err(e) = fs::remove_dir(format!("/run/user/{}", session.uid())) 185 267 { 186 - log::error!(session_id, uid = session.uid, error = e.to_string().as_str(); "failed to remove runtime dir"); 268 + log::error!(session_id = session_id.as_raw(), uid = session.uid(), error = e.to_string().as_str(); "failed to remove runtime dir"); 187 269 } 188 270 } 189 271 session_manager::Event::Destroy => {} ··· 202 284 session_core_session, 203 285 session_id, 204 286 } => { 287 + self.sync_vt_session_states(); 288 + 289 + let session_id = session::SessionId::from_raw(session_id); 205 290 if let Some(session) = self 206 291 .sessions 207 292 .iter_mut() 208 - .find(|session| session.id == session_id) 293 + .find(|session| session.id() == session_id) 209 294 { 210 295 session.add_object(session_core_session); 211 296 return; 212 297 } 213 298 214 - log::warn!(session_id; "get_session requested for unknown session"); 299 + log::warn!(session_id = session_id.as_raw(); "get_session requested for unknown session"); 215 300 session_core_session.send_destroyed(); 216 301 } 217 302 session_core_manager::Event::GetSessionByPid { 218 303 session_core_session, 219 304 pid, 220 305 } => { 306 + self.sync_vt_session_states(); 307 + 221 308 let Ok(Some(session_id)) = self.cgroup_manager.find_session_by_pid(pid) else { 222 309 log::warn!(pid; "get_session_by_pid: no session found for pid"); 223 310 session_core_session.send_destroyed(); ··· 227 314 if let Some(session) = self 228 315 .sessions 229 316 .iter_mut() 230 - .find(|session| session.id == session_id) 317 + .find(|session| session.id() == session_id) 231 318 { 232 319 session.add_object(session_core_session); 233 320 return; 234 321 } 235 322 236 - log::warn!(session_id; "get_session_by_pid requested for unknown session"); 323 + log::warn!(session_id = session_id.as_raw(); "get_session_by_pid requested for unknown session"); 237 324 session_core_session.send_destroyed(); 238 325 } 239 326 session_core_manager::Event::Destroy => { ··· 243 330 } 244 331 } 245 332 246 - impl hyprwire::Dispatch<session_core_session::SessionCoreSession> for Sessiond { 247 - fn event( 248 - &mut self, 249 - object: &session_core_session::SessionCoreSession, 250 - event: <session_core_session::SessionCoreSession as hyprwire::Object>::Event<'_>, 251 - ) { 252 - let Some(idx) = self.sessions.iter_mut().position(|session| { 253 - session 254 - .objects 255 - .iter() 256 - .any(|session_object| session_object == object) 257 - }) else { 258 - return; 259 - }; 260 - 261 - if let session_core_session::Event::Destroy = event { 262 - self.sessions[idx] 263 - .objects 264 - .retain(|session_object| session_object != object); 265 - return; 266 - } 267 - 268 - let Some(client) = object.client() else { 269 - return; 270 - }; 271 - if !client.creds().uid.is_root() && client.creds().uid.as_raw() != self.sessions[idx].uid { 272 - object.send_error( 273 - session_core_v1::ErrorCode::PermissionDenied as u32, 274 - "permission denied", 275 - ); 276 - return; 277 - } 333 + fn main() -> anyhow::Result<()> { 334 + let args = Args::parse(); 278 335 279 - match event { 280 - session_core_session::Event::Terminate => { 281 - let session = self.sessions.swap_remove(idx); 282 - 283 - log::info!(session_id = session.id; "terminate requested"); 284 - for obj in session.objects.iter() { 285 - obj.send_active(session_core_v1::ActiveState::Closing as u32); 286 - } 287 - if let Err(err) = self.cgroup_manager.terminate_session(session.id) { 288 - log::error!(session_id = session.id, err = err.to_string().as_str(); "terminate failed"); 289 - } 290 - } 291 - session_core_session::Event::Kill => { 292 - log::info!(session_id = self.sessions[idx].id; "kill requested"); 293 - for obj in self.sessions[idx].objects.iter() { 294 - obj.send_active(session_core_v1::ActiveState::Closing as u32); 295 - } 296 - if let Err(err) = self.cgroup_manager.kill_session(self.sessions[idx].id) { 297 - log::error!(session_id = self.sessions[idx].id, err = err.to_string().as_str(); "kill failed"); 298 - } 299 - self.sessions.swap_remove(idx); 300 - } 301 - session_core_session::Event::Lock => { 302 - log::info!(session_id = self.sessions[idx].id; "lock requested"); 303 - for object in self.sessions[idx].objects.iter() { 304 - object.send_locked(); 305 - object.send_ok(); 306 - } 307 - } 308 - session_core_session::Event::Unlock => { 309 - log::info!(session_id = self.sessions[idx].id; "unlock requested"); 310 - for object in self.sessions[idx].objects.iter() { 311 - object.send_unlocked(); 312 - object.send_ok(); 313 - } 314 - } 315 - _ => unreachable!(), 316 - } 317 - } 318 - } 319 - 320 - fn main() -> anyhow::Result<()> { 321 336 let log_level = match env::var("LOG_LEVEL") { 322 337 Ok(log) if log == "trace" => log::LevelFilter::Trace, 323 338 Ok(log) if log == "debug" => log::LevelFilter::Debug, ··· 333 348 334 349 log::info!("sessiond daemon starting"); 335 350 351 + let seat_access = SeatAccess::new( 352 + args.seat_user 353 + .iter() 354 + .map(|user| resolve_user(user)) 355 + .collect::<anyhow::Result<Vec<_>>>()?, 356 + args.seat_group 357 + .iter() 358 + .map(|group| resolve_group(group)) 359 + .collect::<anyhow::Result<Vec<_>>>()?, 360 + ); 361 + 336 362 let mut event_loop = calloop::EventLoop::try_new()?; 337 363 338 364 let cgroup_manager = cgroup::CgroupManager::new(event_loop.handle())?; 339 365 366 + let vtbound = env::var("SESSIOND_VTBOUND") 367 + .ok() 368 + .is_none_or(|vtbound| vtbound == "1"); 369 + 370 + let seat = if vtbound { 371 + seat::Seat::vt_bound("seat0".to_string()) 372 + } else { 373 + seat::Seat::non_vt_bound("seat0".to_string()) 374 + }; 375 + 340 376 let mut sessiond = Sessiond { 341 - active_tty: None, 342 - next_session_id: 1, 377 + next_session_id: session::SessionId::from_raw(1), 343 378 sessions: Vec::new(), 344 379 info_managers: Vec::new(), 345 380 cgroup_manager, 381 + seat: vec![seat], 382 + clients: Vec::new(), 383 + seat_access, 346 384 }; 347 385 348 386 let path = path::Path::new("/run/sessiond.sock"); ··· 352 390 socket 353 391 .add_implementation::<session_management_v1::SessionManagementV1Impl, _>(&mut sessiond, 1); 354 392 socket.add_implementation::<session_core_v1::SessionCoreV1Impl, _>(&mut sessiond, 1); 393 + socket.add_implementation::<seat_v1::SeatV1Impl, _>(&mut sessiond, 1); 355 394 356 395 let fd_wrapper = unsafe { generic::FdWrapper::new(socket.extract_loop_fd().as_raw_fd()) }; 357 396 let source = generic::Generic::new( ··· 370 409 }) 371 410 .unwrap(); 372 411 373 - let tty0_path = path::Path::new("/sys/class/tty/tty0/active"); 374 - let source = generic::Generic::new( 375 - fs::File::open(tty0_path)?, 376 - calloop::Interest { 377 - readable: true, 378 - writable: false, 379 - }, 380 - calloop::Mode::Level, 381 - ); 412 + let signals = signals::Signals::new(&[signals::Signal::SIGUSR1, signals::Signal::SIGUSR2])?; 382 413 event_loop 383 414 .handle() 384 - .insert_source(source, move |_, _, state| { 385 - let active = match fs::read_to_string(tty0_path) { 386 - Ok(active) => active, 387 - Err(e) => { 388 - log::error!(error = e.to_string().as_str(); "failed to read active tty"); 389 - return Ok(calloop::PostAction::Continue); 415 + .insert_source(signals, move |event, _, state| { 416 + let mut session_updates = Vec::new(); 417 + 418 + for seat in state.seat.iter_mut().filter(|seat| seat.is_vt_bound()) { 419 + match event.signal() { 420 + signals::Signal::SIGUSR1 => match seat.vt_release() { 421 + Ok(vt) => { 422 + session_updates.push((vt, session_core_v1::ActiveState::Online)); 423 + } 424 + Err(e) => { 425 + log::error!("Could not release VT: {e}"); 426 + } 427 + }, 428 + signals::Signal::SIGUSR2 => match seat.vt_activate() { 429 + Ok(vt) => { 430 + session_updates.push((vt, session_core_v1::ActiveState::Active)); 431 + } 432 + Err(e) => { 433 + log::error!("Could not activate VT: {e}"); 434 + } 435 + }, 436 + _ => unreachable!(), 390 437 } 391 - }; 392 - let active = active.trim(); 438 + } 393 439 394 - if let Some(session) = state.sessions.iter_mut().find(|session| { 395 - state 396 - .active_tty 397 - .as_ref() 398 - .is_some_and(|tty| *tty == session.tty) 399 - }) { 400 - if session.tty == active { 401 - return Ok(calloop::PostAction::Continue); 440 + for (vt, active_state) in session_updates { 441 + if active_state == session_core_v1::ActiveState::Active { 442 + for session in state.sessions.iter_mut().filter(|session| { 443 + session.vt() != vt 444 + && session.state() == session_core_v1::ActiveState::Active 445 + }) { 446 + session.set_state(session_core_v1::ActiveState::Online); 447 + } 402 448 } 403 449 404 - session.set_state(session_core_v1::ActiveState::Online); 450 + for session in state 451 + .sessions 452 + .iter_mut() 453 + .filter(|session| session.vt() == vt) 454 + { 455 + session.set_state(active_state); 456 + } 405 457 } 458 + })?; 459 + 460 + event_loop.run(None, &mut sessiond, |_| {})?; 406 461 407 - state.active_tty = Some(active.to_string()); 462 + Ok(()) 463 + } 408 464 409 - if let Some(session) = state 410 - .sessions 411 - .iter_mut() 412 - .find(|session| session.tty == active) 413 - { 414 - session.set_state(session_core_v1::ActiveState::Active); 415 - } 465 + fn resolve_user(user: &str) -> anyhow::Result<process::Uid> { 466 + if let Ok(uid) = user.parse::<process::RawUid>() { 467 + return Ok(process::Uid::from_raw(uid)); 468 + } 416 469 417 - Ok(calloop::PostAction::Continue) 418 - }) 419 - .unwrap(); 470 + users::get_user_by_name(user) 471 + .map(|user: users::User| process::Uid::from_raw(user.uid())) 472 + .ok_or_else(|| anyhow::anyhow!("unknown seat user {user:?}")) 473 + } 420 474 421 - event_loop.run(None, &mut sessiond, |_| {})?; 475 + fn resolve_group(group: &str) -> anyhow::Result<process::Gid> { 476 + if let Ok(gid) = group.parse::<process::RawGid>() { 477 + return Ok(process::Gid::from_raw(gid)); 478 + } 422 479 423 - Ok(()) 480 + users::get_group_by_name(group) 481 + .map(|group: users::Group| process::Gid::from_raw(group.gid())) 482 + .ok_or_else(|| anyhow::anyhow!("unknown seat group {group:?}")) 424 483 }
+827
daemon/src/seat.rs
··· 1 + use crate::seat_v1::{seat, seat_manager}; 2 + use crate::{client, device, seat_v1, terminal}; 3 + use rustix::io; 4 + use std::os::fd::AsFd; 5 + use std::{cell, fs, rc}; 6 + 7 + const MAX_SEAT_DEVICES: u8 = 128; 8 + 9 + impl seat_v1::SeatV1Handler for crate::Sessiond { 10 + fn bind(&mut self, object: seat_manager::SeatManager) { 11 + let Some(client) = object.client() else { 12 + object.error( 13 + io::Errno::PERM.raw_os_error() as u32, 14 + "seat access requires peer credentials", 15 + ); 16 + return; 17 + }; 18 + 19 + let creds = client.creds(); 20 + if self.seat_access.allows(creds) { 21 + return; 22 + } 23 + 24 + log::warn!( 25 + pid = creds.pid.as_raw_pid(), 26 + uid = creds.uid.as_raw(), 27 + gid = creds.gid.as_raw(); 28 + "rejected seat_v1 bind from unauthorized client" 29 + ); 30 + object.error(io::Errno::PERM.raw_os_error() as u32, "seat access denied"); 31 + } 32 + } 33 + 34 + impl hyprwire::Dispatch<seat_manager::SeatManager> for crate::Sessiond { 35 + fn event( 36 + &mut self, 37 + object: &seat_manager::SeatManager, 38 + event: <seat_manager::SeatManager as hyprwire::Object>::Event<'_>, 39 + ) { 40 + match event { 41 + seat_manager::Event::OpenSeat { seat } => { 42 + let Some(server_client) = object.client() else { 43 + seat.error( 44 + io::Errno::PERM.raw_os_error() as u32, 45 + "seat access requires peer credentials", 46 + ); 47 + return; 48 + }; 49 + 50 + let creds = server_client.creds(); 51 + if !self.seat_access.allows(creds) { 52 + log::warn!( 53 + pid = creds.pid.as_raw_pid(), 54 + uid = creds.uid.as_raw(), 55 + gid = creds.gid.as_raw(); 56 + "rejected seat open from unauthorized client" 57 + ); 58 + seat.error(io::Errno::PERM.raw_os_error() as u32, "seat access denied"); 59 + return; 60 + } 61 + 62 + let session_id = { 63 + let pid = server_client.creds().pid.as_raw_pid() as u32; 64 + match self.cgroup_manager.find_session_by_pid(pid) { 65 + Ok(session_id) => session_id, 66 + Err(err) => { 67 + log::error!("Could not find session for seat opener {pid}: {err}"); 68 + None 69 + } 70 + } 71 + }; 72 + 73 + let client = rc::Rc::new(cell::RefCell::new(client::Client { 74 + seat_name: String::new(), 75 + object: seat, 76 + devices: Vec::new(), 77 + state: crate::seat::SeatState::New, 78 + session_id: 0, 79 + })); 80 + 81 + let Some(seat) = self.seat.first_mut() else { 82 + log::error!("No seats configured"); 83 + client.borrow().object.send_disabled(); 84 + return; 85 + }; 86 + 87 + if let Err(err) = seat.add_client(rc::Rc::clone(&client)) { 88 + log::error!("Could not add client to seat: {err}"); 89 + client.borrow().object.send_disabled(); 90 + return; 91 + } 92 + if let Err(err) = seat.open_client(rc::Rc::clone(&client)) { 93 + log::error!("Could not open client on seat: {err}"); 94 + let _ = seat.remove_client(&client); 95 + client.borrow().object.send_disabled(); 96 + return; 97 + } 98 + 99 + if let Some(session_id) = session_id { 100 + let seat_name = client.borrow().seat_name.clone(); 101 + if let Some(session) = self 102 + .sessions 103 + .iter_mut() 104 + .find(|session| session.id() == session_id) 105 + { 106 + session.set_seat(seat_name); 107 + } else { 108 + log::warn!(session_id = session_id.as_raw(); "seat opened for unknown session"); 109 + } 110 + } 111 + 112 + self.clients.push(client); 113 + } 114 + seat_manager::Event::Destroy => {} 115 + } 116 + } 117 + } 118 + 119 + impl hyprwire::Dispatch<seat::Seat> for crate::Sessiond { 120 + fn event(&mut self, object: &seat::Seat, event: <seat::Seat as hyprwire::Object>::Event<'_>) { 121 + let Some(client) = self 122 + .clients 123 + .iter() 124 + .find(|client| client.borrow().object == *object) 125 + .cloned() 126 + else { 127 + return; 128 + }; 129 + 130 + let Some(seat) = self.seat.first_mut() else { 131 + log::error!("No seats configured"); 132 + return; 133 + }; 134 + 135 + match event { 136 + seat::Event::AckDisabled => { 137 + let state = client.borrow().state; 138 + if let Err(err) = seat.ack_disable_client(&client) { 139 + log::error!("Could not ack disable: {err}"); 140 + 141 + match state { 142 + SeatState::Enabled => object.send_seat_error( 143 + seat_v1::SeatError::InvalidAckDisabled, 144 + "ack_disabled was sent while enabled", 145 + ), 146 + SeatState::Disabled => object.send_seat_error( 147 + seat_v1::SeatError::AlreadyDisabled, 148 + "disabled state was already acknowledged", 149 + ), 150 + _ => {} 151 + } 152 + } 153 + } 154 + seat::Event::OpenDevice { device, path } => { 155 + let seat_name = client.borrow().seat_name.clone(); 156 + let Some(seat) = self.seat.iter().find(|seat| seat.name() == seat_name) else { 157 + log::error!("No shared seat found for {}", seat_name); 158 + return; 159 + }; 160 + 161 + if !seat.is_active_client(&client) { 162 + device.send_failed( 163 + seat_v1::DeviceError::SeatDisabled, 164 + io::Errno::PERM.raw_os_error() as u32, 165 + "open_device is only valid for the active seat client", 166 + ); 167 + return; 168 + } 169 + 170 + let mut client = client.borrow_mut(); 171 + 172 + let Ok(sanitized_path) = fs::canonicalize(path) else { 173 + device.send_failed( 174 + seat_v1::DeviceError::OpenFailed, 175 + io::Errno::NOENT.raw_os_error() as u32, 176 + "could not canonicalize device path", 177 + ); 178 + return; 179 + }; 180 + 181 + if client.state != crate::seat::SeatState::Enabled { 182 + device.send_failed( 183 + seat_v1::DeviceError::SeatDisabled, 184 + io::Errno::BUSY.raw_os_error() as u32, 185 + "open_device is only valid while the seat is enabled", 186 + ); 187 + return; 188 + } 189 + 190 + let Some(kind) = crate::device::DeviceKind::from_path(&sanitized_path) else { 191 + log::error!( 192 + "{} is not a supported device type ", 193 + sanitized_path.display() 194 + ); 195 + device.send_failed( 196 + seat_v1::DeviceError::OpenFailed, 197 + io::Errno::INVAL.raw_os_error() as u32, 198 + "unsupported device type", 199 + ); 200 + return; 201 + }; 202 + 203 + if let Some(old_device) = client 204 + .devices 205 + .iter_mut() 206 + .find(|device| device.matches_path(&sanitized_path)) 207 + { 208 + old_device.add_object(device); 209 + let object = old_device.last_object().unwrap(); 210 + object.send_opened( 211 + old_device.id(), 212 + old_device.fd().as_fd(), 213 + old_device.absolute_path_string(), 214 + ); 215 + return; 216 + } 217 + 218 + if client.devices.len() as u8 >= MAX_SEAT_DEVICES { 219 + log::error!("Client exceeded max seat devices"); 220 + device.send_failed( 221 + seat_v1::DeviceError::OpenFailed, 222 + io::Errno::MFILE.raw_os_error() as u32, 223 + "client exceeded max seat devices", 224 + ); 225 + return; 226 + } 227 + 228 + let device_id = client 229 + .devices 230 + .iter() 231 + .map(|device| device.id()) 232 + .max() 233 + .map_or(1, |id| id + 1); 234 + 235 + let fd = match rustix::fs::open( 236 + &sanitized_path, 237 + rustix::fs::OFlags::RDWR 238 + | rustix::fs::OFlags::NOCTTY 239 + | rustix::fs::OFlags::NOFOLLOW 240 + | rustix::fs::OFlags::CLOEXEC 241 + | rustix::fs::OFlags::NONBLOCK, 242 + rustix::fs::Mode::empty(), 243 + ) { 244 + Ok(fd) => fd, 245 + Err(e) => { 246 + log::error!("Could not open file: {e}"); 247 + device.send_failed( 248 + seat_v1::DeviceError::OpenFailed, 249 + e.raw_os_error() as u32, 250 + "could not open device", 251 + ); 252 + return; 253 + } 254 + }; 255 + 256 + let mut seat_device = crate::device::Device::new( 257 + device, 258 + sanitized_path.clone(), 259 + kind, 260 + device_id, 261 + matches!( 262 + kind, 263 + crate::device::DeviceKind::Evdev | crate::device::DeviceKind::Hidraw 264 + ), 265 + fd, 266 + ); 267 + 268 + if let Err(err) = seat_device.activate() { 269 + log::error!("Could not activate {}: {err}", seat_device.path().display()); 270 + let errno = err 271 + .root_cause() 272 + .downcast_ref::<io::Errno>() 273 + .copied() 274 + .unwrap_or(io::Errno::INVAL); 275 + seat_device.last_object().unwrap().send_failed( 276 + seat_v1::DeviceError::OpenFailed, 277 + errno.raw_os_error() as u32, 278 + "could not activate device", 279 + ); 280 + return; 281 + } 282 + 283 + seat_device.last_object().unwrap().send_opened( 284 + device_id, 285 + seat_device.fd().as_fd(), 286 + seat_device.absolute_path_string(), 287 + ); 288 + client.devices.push(seat_device); 289 + } 290 + seat::Event::SwitchSession { session } => { 291 + if !seat.is_vt_bound() { 292 + object.send_seat_error( 293 + seat_v1::SeatError::InvalidSwitchSession, 294 + "session switching is not supported", 295 + ); 296 + return; 297 + } 298 + 299 + if let Err(err) = seat.set_next_session(&client, session) { 300 + log::error!("Could not switch session: {err}"); 301 + } 302 + } 303 + seat::Event::Destroy => { 304 + if let Err(err) = seat.remove_client(&client) { 305 + log::error!("Could not remove client from seat: {err}"); 306 + } 307 + self.clients.retain(|c| !rc::Rc::ptr_eq(c, &client)); 308 + } 309 + } 310 + } 311 + } 312 + 313 + #[derive(Clone, Copy, PartialEq, Eq)] 314 + pub enum SeatState { 315 + New, 316 + Enabled, 317 + PendingDisabled, 318 + Disabled, 319 + Closed, 320 + } 321 + 322 + #[derive(Clone, Copy, PartialEq, Eq)] 323 + enum SeatKind { 324 + VtBound, 325 + NonVtBound, 326 + } 327 + 328 + pub struct Seat { 329 + name: String, 330 + clients: Vec<rc::Rc<cell::RefCell<client::Client>>>, 331 + active_client: Option<rc::Rc<cell::RefCell<client::Client>>>, 332 + next_client: Option<rc::Rc<cell::RefCell<client::Client>>>, 333 + kind: SeatKind, 334 + cur_vt: u32, 335 + } 336 + 337 + impl Default for Seat { 338 + fn default() -> Self { 339 + Seat { 340 + name: "seat0".to_string(), 341 + clients: Vec::new(), 342 + active_client: None, 343 + next_client: None, 344 + kind: SeatKind::NonVtBound, 345 + cur_vt: 0, 346 + } 347 + } 348 + } 349 + 350 + impl Seat { 351 + pub fn vt_bound(name: String) -> Self { 352 + log::info!("Created VT-bound seat {}", name); 353 + Self { 354 + name, 355 + kind: SeatKind::VtBound, 356 + ..Default::default() 357 + } 358 + } 359 + 360 + pub fn non_vt_bound(name: String) -> Self { 361 + Self { 362 + name, 363 + ..Default::default() 364 + } 365 + } 366 + 367 + pub fn is_vt_bound(&self) -> bool { 368 + self.kind == SeatKind::VtBound 369 + } 370 + 371 + pub fn name(&self) -> &str { 372 + &self.name 373 + } 374 + 375 + pub fn is_active_client(&self, client: &rc::Rc<cell::RefCell<client::Client>>) -> bool { 376 + self.active_client 377 + .as_ref() 378 + .is_some_and(|active_client| rc::Rc::ptr_eq(active_client, client)) 379 + } 380 + 381 + pub fn is_vt_active(&mut self, vt: u32) -> anyhow::Result<bool> { 382 + if !self.is_vt_bound() { 383 + return Ok(false); 384 + } 385 + 386 + self.update_vt()?; 387 + Ok(self.cur_vt == vt) 388 + } 389 + 390 + pub fn active_vt(&mut self) -> anyhow::Result<Option<u32>> { 391 + if !self.is_vt_bound() { 392 + return Ok(None); 393 + } 394 + 395 + self.update_vt()?; 396 + Ok(Some(self.cur_vt)) 397 + } 398 + 399 + pub fn update_vt(&mut self) -> anyhow::Result<()> { 400 + let tty0fd = terminal::Terminal::open(0)?; 401 + self.cur_vt = u32::from(tty0fd.current_vt()?); 402 + 403 + Ok(()) 404 + } 405 + 406 + pub fn vt_open(vt: u32) -> anyhow::Result<()> { 407 + let terminal = terminal::Terminal::open(vt)?; 408 + terminal.set_process_switching(true)?; 409 + terminal.set_keyboard(false)?; 410 + terminal.set_graphics(true)?; 411 + 412 + Ok(()) 413 + } 414 + 415 + pub fn vt_close(vt: u32) -> anyhow::Result<()> { 416 + let terminal = terminal::Terminal::open(vt)?; 417 + terminal.set_process_switching(false)?; 418 + terminal.set_keyboard(true)?; 419 + terminal.set_graphics(false)?; 420 + 421 + Ok(()) 422 + } 423 + 424 + pub fn vt_switch(&self, vt: u32) -> anyhow::Result<()> { 425 + let terminal = terminal::Terminal::open(self.cur_vt)?; 426 + terminal.set_process_switching(true)?; 427 + terminal.switch_vt(vt as i32)?; 428 + 429 + Ok(()) 430 + } 431 + 432 + pub fn vt_ack(&self, release: bool) -> anyhow::Result<()> { 433 + let terminal = terminal::Terminal::open(self.cur_vt)?; 434 + 435 + if release { 436 + terminal.ack_release()?; 437 + } else { 438 + terminal.ack_acquire()?; 439 + } 440 + 441 + Ok(()) 442 + } 443 + 444 + pub fn vt_activate(&mut self) -> anyhow::Result<u32> { 445 + if !self.is_vt_bound() { 446 + log::debug!("VT activation on non VT-bound seat, ignoring"); 447 + return Err(anyhow::anyhow!("seat is not VT-bound")); 448 + } 449 + 450 + self.update_vt()?; 451 + let acquired_vt = self.cur_vt; 452 + log::debug!("Activating VT"); 453 + self.vt_ack(false)?; 454 + 455 + if self.active_client.is_none() { 456 + self.activate()?; 457 + } 458 + 459 + Ok(acquired_vt) 460 + } 461 + 462 + pub fn vt_release(&mut self) -> anyhow::Result<u32> { 463 + if !self.is_vt_bound() { 464 + log::debug!("VT release request on non VT-bound seat, ignoring"); 465 + return Err(anyhow::anyhow!("seat is not VT-bound")); 466 + } 467 + 468 + self.update_vt()?; 469 + let released_vt = self 470 + .active_client 471 + .as_ref() 472 + .map(|client| client.borrow().session_id) 473 + .unwrap_or(self.cur_vt); 474 + log::debug!("Releasing VT"); 475 + 476 + if let Some(active_client) = self.active_client.clone() { 477 + self.disable_client(&active_client)?; 478 + } 479 + 480 + self.vt_ack(true)?; 481 + self.cur_vt = 0; 482 + 483 + Ok(released_vt) 484 + } 485 + 486 + pub fn activate(&mut self) -> anyhow::Result<()> { 487 + if self.active_client.is_some() { 488 + return Ok(()); 489 + } 490 + 491 + let next_client = if self.next_client.is_some() { 492 + log::debug!("Activating next queued client on {}", self.name()); 493 + self.next_client.take() 494 + } else if self.clients.is_empty() { 495 + log::info!("No clients on {} to activate", self.name()); 496 + return Err(anyhow::anyhow!("No clients on {} to activate", self.name())); 497 + } else if self.is_vt_bound() { 498 + if let Some(client) = self 499 + .clients 500 + .iter() 501 + .find(|client| client.borrow().session_id == self.cur_vt) 502 + { 503 + log::debug!("Activating client belonging to VT {}", self.cur_vt); 504 + Some(rc::Rc::clone(client)) 505 + } else { 506 + log::info!("No clients belonging to VT {} to activate", self.cur_vt); 507 + return Err(anyhow::anyhow!( 508 + "No clients belonging to VT {} to activate", 509 + self.cur_vt 510 + )); 511 + } 512 + } else { 513 + log::debug!("Activating first client on {}", self.name()); 514 + self.clients.first().cloned() 515 + }; 516 + 517 + if let Some(client) = next_client { 518 + self.open_client(client)?; 519 + } 520 + 521 + Ok(()) 522 + } 523 + 524 + pub fn open_client( 525 + &mut self, 526 + client: rc::Rc<cell::RefCell<client::Client>>, 527 + ) -> anyhow::Result<()> { 528 + debug_assert_eq!(client.borrow().seat_name, self.name); 529 + 530 + let state = client.borrow().state; 531 + if state != SeatState::New && state != SeatState::Disabled { 532 + log::error!("Could not enable client: client is not new or disabled"); 533 + return Err(anyhow::anyhow!("client is not new or disabled")); 534 + } 535 + 536 + if self.active_client.as_ref().is_some_and(|active_client| { 537 + !self.is_vt_bound() || active_client.borrow().state != SeatState::PendingDisabled 538 + }) { 539 + log::error!("Could not enable client: seat already has an active client"); 540 + return Err(anyhow::anyhow!("seat already has an active client")); 541 + } 542 + 543 + if self.is_vt_bound() 544 + && let Err(err) = Self::vt_open(client.borrow().session_id) 545 + { 546 + log::error!("Could not open VT for client: {err}"); 547 + return Err(err); 548 + } 549 + 550 + { 551 + let mut client_ref = client.borrow_mut(); 552 + for device in &mut client_ref.devices { 553 + if let Err(err) = device.activate() { 554 + log::error!("Could not activate {}: {err}", device.path().display()); 555 + } 556 + } 557 + } 558 + 559 + client.borrow().object.send_enabled(); 560 + 561 + client.borrow_mut().state = SeatState::Enabled; 562 + self.active_client = Some(rc::Rc::clone(&client)); 563 + log::info!( 564 + "Opened client {} on {}", 565 + client.borrow().session_id, 566 + self.name 567 + ); 568 + 569 + Ok(()) 570 + } 571 + 572 + pub fn add_client( 573 + &mut self, 574 + client: rc::Rc<cell::RefCell<client::Client>>, 575 + ) -> anyhow::Result<()> { 576 + if !client.borrow().seat_name.is_empty() { 577 + log::error!("Could not add client: client is already a member of a seat"); 578 + return Err(anyhow::anyhow!("client is already a member of a seat")); 579 + } 580 + 581 + if self.is_vt_bound() 582 + && let Some(active_client) = &self.active_client 583 + && active_client.borrow().state != SeatState::PendingDisabled 584 + { 585 + log::error!("Could not add client: seat is VT-bound and has an active client"); 586 + return Err(anyhow::anyhow!("seat is VT-bound and has an active client")); 587 + } 588 + 589 + if client.borrow().session_id != 0 { 590 + log::error!("Could not add client: client cannot be reused"); 591 + return Err(anyhow::anyhow!("client cannot be reused")); 592 + } 593 + 594 + if self.is_vt_bound() { 595 + self.update_vt()?; 596 + if self.cur_vt == 0 { 597 + log::error!("Could not determine VT for client"); 598 + return Err(anyhow::anyhow!("could not determine VT for client")); 599 + } 600 + 601 + if self.active_client.is_some() 602 + && self 603 + .clients 604 + .iter() 605 + .any(|existing| existing.borrow().session_id == self.cur_vt) 606 + { 607 + log::error!( 608 + "Could not add client: seat is VT-bound and already has pending client" 609 + ); 610 + return Err(anyhow::anyhow!( 611 + "seat is VT-bound and already has pending client" 612 + )); 613 + } 614 + 615 + client.borrow_mut().session_id = self.cur_vt; 616 + } else { 617 + let mut next_session = 1; 618 + while self 619 + .clients 620 + .iter() 621 + .any(|existing| existing.borrow().session_id == next_session) 622 + { 623 + next_session += 1; 624 + } 625 + 626 + client.borrow_mut().session_id = next_session; 627 + } 628 + 629 + client.borrow_mut().seat_name = self.name.clone(); 630 + log::info!( 631 + "Added client {} to {}", 632 + client.borrow().session_id, 633 + self.name 634 + ); 635 + self.clients.push(client); 636 + 637 + Ok(()) 638 + } 639 + 640 + pub fn disable_client( 641 + &mut self, 642 + client: &rc::Rc<cell::RefCell<client::Client>>, 643 + ) -> anyhow::Result<()> { 644 + if client.borrow().state != SeatState::Enabled { 645 + log::error!("Could not disable client: client is not active"); 646 + return Err(anyhow::anyhow!("client is not active")); 647 + } 648 + 649 + if self 650 + .active_client 651 + .as_ref() 652 + .is_none_or(|active_client| !rc::Rc::ptr_eq(active_client, client)) 653 + { 654 + log::error!("Could not disable client: client is not the active seat client"); 655 + return Err(anyhow::anyhow!("client is not the active seat client")); 656 + } 657 + 658 + { 659 + let mut client_ref = client.borrow_mut(); 660 + client_ref.devices.retain_mut(|device| { 661 + if let Err(err) = device.deactivate() { 662 + log::error!("Could not deactivate {}: {err}", device.path().display()); 663 + return true; 664 + } 665 + 666 + match device.kind() { 667 + device::DeviceKind::Drm | device::DeviceKind::Wscons => true, 668 + device::DeviceKind::Evdev | device::DeviceKind::Hidraw => { 669 + device.send_revoked(); 670 + false 671 + } 672 + } 673 + }); 674 + 675 + client_ref.state = SeatState::PendingDisabled; 676 + client_ref.object.send_disabled(); 677 + log::info!( 678 + "Disabling client {} on {}", 679 + client_ref.session_id, 680 + self.name 681 + ); 682 + } 683 + 684 + Ok(()) 685 + } 686 + 687 + pub fn ack_disable_client( 688 + &mut self, 689 + client: &rc::Rc<cell::RefCell<client::Client>>, 690 + ) -> anyhow::Result<()> { 691 + if client.borrow().state != SeatState::PendingDisabled { 692 + log::error!("Could not ack disable: client is not pending disable"); 693 + return Err(anyhow::anyhow!("client is not pending disable")); 694 + } 695 + 696 + client.borrow_mut().state = SeatState::Disabled; 697 + log::info!( 698 + "Disabled client {} on {}", 699 + client.borrow().session_id, 700 + self.name 701 + ); 702 + 703 + if self 704 + .active_client 705 + .as_ref() 706 + .is_none_or(|active_client| !rc::Rc::ptr_eq(active_client, client)) 707 + { 708 + return Ok(()); 709 + } 710 + 711 + self.active_client = None; 712 + let _ = self.activate(); 713 + 714 + Ok(()) 715 + } 716 + 717 + pub fn set_next_session( 718 + &mut self, 719 + client: &rc::Rc<cell::RefCell<client::Client>>, 720 + session: u32, 721 + ) -> anyhow::Result<()> { 722 + if client.borrow().state != SeatState::Enabled { 723 + log::error!("Could not set next session: client is not active"); 724 + return Err(anyhow::anyhow!("client is not active")); 725 + } 726 + 727 + if self 728 + .active_client 729 + .as_ref() 730 + .is_none_or(|active_client| !rc::Rc::ptr_eq(active_client, client)) 731 + { 732 + log::error!("Could not set next session: client is not the active seat client"); 733 + return Err(anyhow::anyhow!("client is not the active seat client")); 734 + } 735 + 736 + if session == 0 { 737 + log::error!("Could not set next session: invalid session value {session}"); 738 + return Err(anyhow::anyhow!("invalid session value")); 739 + } 740 + 741 + if session == client.borrow().session_id { 742 + log::info!("Could not set next session: requested session is already active"); 743 + return Ok(()); 744 + } 745 + 746 + if self.next_client.is_some() { 747 + log::info!("Could not set next session: switch is already queued"); 748 + return Ok(()); 749 + } 750 + 751 + if self.is_vt_bound() { 752 + log::info!("Switching from VT {} to VT {}", self.cur_vt, session); 753 + if let Err(err) = self.vt_switch(session) { 754 + log::error!("Could not switch VT: {err}"); 755 + return Err(err); 756 + } 757 + return Ok(()); 758 + } 759 + 760 + let Some(target) = self 761 + .clients 762 + .iter() 763 + .find(|candidate| candidate.borrow().session_id == session) 764 + .cloned() 765 + else { 766 + log::error!("Could not set next session: no such client"); 767 + return Err(anyhow::anyhow!("no such client")); 768 + }; 769 + 770 + log::info!("Queuing switch to client {} on {}", session, self.name); 771 + self.next_client = Some(target); 772 + self.disable_client(client)?; 773 + 774 + Ok(()) 775 + } 776 + 777 + pub fn remove_client( 778 + &mut self, 779 + client: &rc::Rc<cell::RefCell<client::Client>>, 780 + ) -> anyhow::Result<()> { 781 + if self 782 + .next_client 783 + .as_ref() 784 + .is_some_and(|next_client| rc::Rc::ptr_eq(next_client, client)) 785 + { 786 + self.next_client = None; 787 + } 788 + 789 + let (session, client_state) = { 790 + let client = client.borrow(); 791 + (client.session_id, client.state) 792 + }; 793 + 794 + self.clients 795 + .retain(|existing| !rc::Rc::ptr_eq(existing, client)); 796 + client.borrow_mut().devices.clear(); 797 + 798 + let was_current = self 799 + .active_client 800 + .as_ref() 801 + .is_some_and(|active_client| rc::Rc::ptr_eq(active_client, client)); 802 + if was_current { 803 + self.active_client = None; 804 + let _ = self.activate(); 805 + } 806 + 807 + if self.is_vt_bound() { 808 + if was_current && self.active_client.is_none() { 809 + log::debug!("Closing active VT"); 810 + Self::vt_close(self.cur_vt)?; 811 + } else if !was_current && client_state != SeatState::Closed { 812 + log::debug!("Closing inactive VT"); 813 + Self::vt_close(session)?; 814 + } 815 + } 816 + 817 + { 818 + let mut client = client.borrow_mut(); 819 + client.state = SeatState::Closed; 820 + client.seat_name.clear(); 821 + } 822 + 823 + log::info!("Removed client {} from {}", session, self.name); 824 + 825 + Ok(()) 826 + } 827 + }
+190
daemon/src/session.rs
··· 1 + use crate::{session_core_session, session_core_v1}; 2 + use std::time; 3 + 4 + #[derive(Clone, Copy, PartialEq, Eq)] 5 + pub struct SessionId(u32); 6 + 7 + impl SessionId { 8 + pub fn as_raw(self) -> u32 { 9 + self.0 10 + } 11 + 12 + pub fn from_raw(id: u32) -> Self { 13 + Self(id) 14 + } 15 + 16 + pub fn next(self) -> Self { 17 + Self(self.0.wrapping_add(1)) 18 + } 19 + } 20 + 21 + pub struct Session { 22 + id: SessionId, 23 + uid: u32, 24 + username: String, 25 + vt: u32, 26 + seat: Option<String>, 27 + timestamp: time::SystemTime, 28 + leader_id: i32, 29 + objects: Vec<session_core_session::SessionCoreSession>, 30 + state: session_core_v1::ActiveState, 31 + } 32 + 33 + impl Session { 34 + pub fn new(session_id: SessionId, uid: u32, username: String, vt: u32, leader_id: i32) -> Self { 35 + Self { 36 + id: session_id, 37 + uid, 38 + username, 39 + vt, 40 + seat: None, 41 + timestamp: time::SystemTime::now(), 42 + leader_id, 43 + objects: Vec::new(), 44 + state: session_core_v1::ActiveState::Opening, 45 + } 46 + } 47 + 48 + pub fn vt(&self) -> u32 { 49 + self.vt 50 + } 51 + 52 + pub fn username(&self) -> &str { 53 + &self.username 54 + } 55 + 56 + pub fn uid(&self) -> u32 { 57 + self.uid 58 + } 59 + 60 + pub fn id(&self) -> SessionId { 61 + self.id 62 + } 63 + 64 + pub fn objects(&self) -> &[session_core_session::SessionCoreSession] { 65 + &self.objects 66 + } 67 + 68 + pub fn state(&self) -> session_core_v1::ActiveState { 69 + self.state 70 + } 71 + 72 + pub fn set_state(&mut self, state: session_core_v1::ActiveState) { 73 + log::info!(session_id = self.id.as_raw(), vt = self.vt; "session state changed"); 74 + self.state = state; 75 + for object in self.objects.iter() { 76 + object.send_active(self.state); 77 + } 78 + } 79 + 80 + pub fn set_seat(&mut self, seat: String) { 81 + if self.seat.as_deref() == Some(seat.as_str()) { 82 + return; 83 + } 84 + 85 + log::info!(session_id = self.id.as_raw(), seat = seat.as_str(); "session seat changed"); 86 + self.seat = Some(seat); 87 + 88 + if let Some(seat) = self.seat.as_ref() { 89 + for object in self.objects.iter() { 90 + object.send_seat(seat); 91 + } 92 + } 93 + } 94 + 95 + pub fn add_object(&mut self, object: session_core_session::SessionCoreSession) { 96 + log::debug!(session_id = self.id.as_raw(), uid = object.client().map(|c| c.creds().uid.as_raw()).unwrap_or(0); "session info requested"); 97 + object.send_session_id(self.id.as_raw()); 98 + object.send_uid(self.uid); 99 + object.send_username(&self.username); 100 + object.send_vt(self.vt); 101 + object.send_active(self.state); 102 + if let Some(seat) = self.seat.as_ref() { 103 + object.send_seat(seat); 104 + } 105 + object.send_leader(self.leader_id as u32); 106 + object.send_timestamp( 107 + self.timestamp 108 + .duration_since(time::UNIX_EPOCH) 109 + .unwrap_or_default() 110 + .as_secs() as u32, 111 + ); 112 + object.send_done(); 113 + 114 + self.objects.push(object); 115 + } 116 + } 117 + 118 + impl hyprwire::Dispatch<session_core_session::SessionCoreSession> for crate::Sessiond { 119 + fn event( 120 + &mut self, 121 + object: &session_core_session::SessionCoreSession, 122 + event: <session_core_session::SessionCoreSession as hyprwire::Object>::Event<'_>, 123 + ) { 124 + let Some(idx) = self.sessions.iter_mut().position(|session| { 125 + session 126 + .objects 127 + .iter() 128 + .any(|session_object| session_object == object) 129 + }) else { 130 + return; 131 + }; 132 + 133 + if let session_core_session::Event::Destroy = event { 134 + self.sessions[idx] 135 + .objects 136 + .retain(|session_object| session_object != object); 137 + return; 138 + } 139 + 140 + let Some(client) = object.client() else { 141 + return; 142 + }; 143 + if !client.creds().uid.is_root() && client.creds().uid.as_raw() != self.sessions[idx].uid { 144 + object.send_error( 145 + session_core_v1::ErrorCode::PermissionDenied as u32, 146 + "permission denied", 147 + ); 148 + return; 149 + } 150 + 151 + match event { 152 + session_core_session::Event::Terminate => { 153 + let session = self.sessions.swap_remove(idx); 154 + 155 + log::info!(session_id = session.id.as_raw(); "terminate requested"); 156 + for obj in session.objects.iter() { 157 + obj.send_active(session_core_v1::ActiveState::Closing); 158 + } 159 + if let Err(err) = self.cgroup_manager.terminate_session(session.id) { 160 + log::error!(session_id = session.id.as_raw(), err = err.to_string().as_str(); "terminate failed"); 161 + } 162 + } 163 + session_core_session::Event::Kill => { 164 + log::info!(session_id = self.sessions[idx].id.as_raw(); "kill requested"); 165 + for obj in self.sessions[idx].objects.iter() { 166 + obj.send_active(session_core_v1::ActiveState::Closing); 167 + } 168 + if let Err(err) = self.cgroup_manager.kill_session(self.sessions[idx].id) { 169 + log::error!(session_id = self.sessions[idx].id.as_raw(), err = err.to_string().as_str(); "kill failed"); 170 + } 171 + self.sessions.swap_remove(idx); 172 + } 173 + session_core_session::Event::Lock => { 174 + log::info!(session_id = self.sessions[idx].id.as_raw(); "lock requested"); 175 + for object in self.sessions[idx].objects.iter() { 176 + object.send_locked(); 177 + object.send_ok(); 178 + } 179 + } 180 + session_core_session::Event::Unlock => { 181 + log::info!(session_id = self.sessions[idx].id.as_raw(); "unlock requested"); 182 + for object in self.sessions[idx].objects.iter() { 183 + object.send_unlocked(); 184 + object.send_ok(); 185 + } 186 + } 187 + _ => unreachable!(), 188 + } 189 + } 190 + }
+187
daemon/src/terminal.rs
··· 1 + use rustix::ioctl; 2 + use rustix::process; 3 + use std::{fs, path}; 4 + 5 + #[repr(C)] 6 + #[derive(Debug, Copy, Clone)] 7 + struct VtStat { 8 + v_active: u16, 9 + v_signal: u16, 10 + v_state: u16, 11 + } 12 + 13 + #[repr(C)] 14 + #[derive(Debug, Copy, Clone)] 15 + struct VtMode { 16 + mode: u8, 17 + waitv: u8, 18 + relsig: i16, 19 + acqsig: i16, 20 + frsig: i16, 21 + } 22 + 23 + const VT_GETSTATE: ioctl::Opcode = 0x5603; 24 + const VT_SETMODE: ioctl::Opcode = 0x5602; 25 + const VT_ACTIVATE: ioctl::Opcode = 0x5606; 26 + const VT_RELDISP: ioctl::Opcode = 0x5605; 27 + const KDSETMODE: ioctl::Opcode = 0x4B3A; 28 + const KDSKBMODE: ioctl::Opcode = 0x4B45; 29 + const VT_AUTO: u8 = 0x00; 30 + const VT_PROCESS: u8 = 0x01; 31 + const VT_ACKACQ: i32 = 0x02; 32 + const KD_TEXT: i32 = 0x00; 33 + const KD_GRAPHICS: i32 = 0x01; 34 + const K_UNICODE: i32 = 0x03; 35 + const K_OFF: i32 = 0x04; 36 + const FRSIG: i16 = 0; 37 + 38 + pub struct Terminal(fs::File); 39 + 40 + impl Terminal { 41 + fn get_tty_path(tty: u32) -> path::PathBuf { 42 + path::PathBuf::from(format!("/dev/tty{tty}")) 43 + } 44 + 45 + pub fn open(tty: u32) -> anyhow::Result<Self> { 46 + let path = Self::get_tty_path(tty); 47 + Ok(Self(fs::File::open(&path)?)) 48 + } 49 + 50 + pub fn current_vt(&self) -> anyhow::Result<u16> { 51 + let st = unsafe { ioctl::ioctl(&self.0, ioctl::Getter::<VT_GETSTATE, VtStat>::new())? }; 52 + 53 + Ok(st.v_active) 54 + } 55 + 56 + pub fn set_process_switching(&self, enable: bool) -> anyhow::Result<()> { 57 + log::debug!("Setting process switching to {enable}"); 58 + 59 + let mode = VtMode { 60 + mode: if enable { VT_PROCESS } else { VT_AUTO }, 61 + waitv: 0, 62 + relsig: if enable { 63 + process::Signal::USR1.as_raw() as i16 64 + } else { 65 + 0 66 + }, 67 + acqsig: if enable { 68 + process::Signal::USR2.as_raw() as i16 69 + } else { 70 + 0 71 + }, 72 + frsig: FRSIG, 73 + }; 74 + 75 + let result = 76 + unsafe { ioctl::ioctl(&self.0, ioctl::Setter::<VT_SETMODE, VtMode>::new(mode)) }; 77 + 78 + if let Err(err) = result { 79 + log::error!( 80 + "Could not set VT mode to {} process switching: {}", 81 + if enable { "enable" } else { "disable" }, 82 + err 83 + ); 84 + return Err(err.into()); 85 + } 86 + 87 + Ok(()) 88 + } 89 + 90 + pub fn switch_vt(&self, vt: i32) -> anyhow::Result<()> { 91 + log::debug!("Switching to VT {vt}"); 92 + 93 + let result = unsafe { 94 + ioctl::ioctl( 95 + &self.0, 96 + ioctl::IntegerSetter::<VT_ACTIVATE>::new_usize(vt as usize), 97 + ) 98 + }; 99 + 100 + if let Err(err) = result { 101 + log::error!("Could not activate VT {vt}: {err}"); 102 + return Err(err.into()); 103 + } 104 + 105 + Ok(()) 106 + } 107 + 108 + pub fn ack_release(&self) -> anyhow::Result<()> { 109 + log::debug!("Acking VT release"); 110 + 111 + let result = 112 + unsafe { ioctl::ioctl(&self.0, ioctl::IntegerSetter::<VT_RELDISP>::new_usize(1)) }; 113 + 114 + if let Err(err) = result { 115 + log::error!("Could not ack VT release: {err}"); 116 + return Err(err.into()); 117 + } 118 + 119 + Ok(()) 120 + } 121 + 122 + pub fn ack_acquire(&self) -> anyhow::Result<()> { 123 + log::debug!("Acking VT acquire"); 124 + 125 + let result = unsafe { 126 + ioctl::ioctl( 127 + &self.0, 128 + ioctl::IntegerSetter::<VT_RELDISP>::new_usize(VT_ACKACQ as usize), 129 + ) 130 + }; 131 + 132 + if let Err(err) = result { 133 + log::error!("Could not ack VT acquire: {err}"); 134 + return Err(err.into()); 135 + } 136 + 137 + Ok(()) 138 + } 139 + 140 + pub fn set_keyboard(&self, enable: bool) -> anyhow::Result<()> { 141 + log::debug!("Setting KD keyboard state to {enable}"); 142 + 143 + let result = unsafe { 144 + ioctl::ioctl( 145 + &self.0, 146 + ioctl::IntegerSetter::<KDSKBMODE>::new_usize( 147 + (if enable { K_UNICODE } else { K_OFF }) as usize, 148 + ), 149 + ) 150 + }; 151 + 152 + if let Err(err) = result { 153 + log::error!( 154 + "Could not set KD keyboard mode to {}: {}", 155 + if enable { "enabled" } else { "disabled" }, 156 + err 157 + ); 158 + return Err(err.into()); 159 + } 160 + 161 + Ok(()) 162 + } 163 + 164 + pub fn set_graphics(&self, enable: bool) -> anyhow::Result<()> { 165 + log::debug!("Setting KD graphics state to {enable}"); 166 + 167 + let result = unsafe { 168 + ioctl::ioctl( 169 + &self.0, 170 + ioctl::IntegerSetter::<KDSETMODE>::new_usize( 171 + (if enable { KD_GRAPHICS } else { KD_TEXT }) as usize, 172 + ), 173 + ) 174 + }; 175 + 176 + if let Err(err) = result { 177 + log::error!( 178 + "Could not set KD graphics mode to {}: {}", 179 + if enable { "graphics" } else { "text" }, 180 + err 181 + ); 182 + return Err(err.into()); 183 + } 184 + 185 + Ok(()) 186 + } 187 + }
+7 -1
nix/finix-vm.nix
··· 4 4 ... 5 5 }: 6 6 { 7 + environment.systemPackages = [ pkgs.busybox ]; 8 + 7 9 services = { 8 10 sysklogd.enable = true; 9 11 sessiond = { ··· 13 15 mdevd.enable = true; 14 16 getty = { 15 17 enable = true; 16 - ttys = [ "tty1" "tty2" "ttyS0" ]; 18 + ttys = [ 19 + "tty1" 20 + "tty2" 21 + "ttyS0" 22 + ]; 17 23 }; 18 24 }; 19 25
+16 -1
nix/finix.nix
··· 6 6 }: 7 7 let 8 8 cfg = config.services.sessiond; 9 + inherit (lib) types; 9 10 in 10 11 { 11 12 options.services.sessiond = { 12 13 enable = lib.mkEnableOption "sessiond"; 13 14 package = lib.mkPackageOption pkgs "sessiond" { }; 15 + seat = { 16 + user = lib.mkOption { 17 + type = types.str; 18 + default = "root"; 19 + description = "User to own the seatd socket"; 20 + }; 21 + group = lib.mkOption { 22 + type = types.str; 23 + default = "seat"; 24 + description = "Group to own the seatd socket"; 25 + }; 26 + }; 14 27 }; 15 28 16 29 config = lib.mkIf cfg.enable { 17 30 finit.services.sessiond = { 18 31 description = "session management daemon"; 19 32 conditions = [ ]; 20 - command = "${cfg.package}/bin/sessiond"; 33 + command = "${cfg.package}/bin/sessiond --seat-user ${cfg.seat.user} --seat-group ${cfg.seat.group}"; 21 34 log = true; 22 35 }; 23 36 ··· 26 39 ''; 27 40 28 41 environment.systemPackages = [ cfg.package ]; 42 + 43 + users.groups.${cfg.seat.group} = { }; 29 44 }; 30 45 }
+23 -12
pam/src/lib.rs
··· 6 6 use hyprwire::client; 7 7 use pamsm::{PamLibExt, PamServiceModule}; 8 8 use session_management_v1::session_manager; 9 - use std::path; 10 9 11 10 struct Session { 12 11 session_id: Option<u32>, 12 + seat: Option<String>, 13 13 } 14 14 15 15 impl hyprwire::Dispatch<session_manager::SessionManager> for Session { ··· 18 18 _: &session_manager::SessionManager, 19 19 event: <session_manager::SessionManager as hyprwire::Object>::Event<'_>, 20 20 ) { 21 - let session_manager::Event::SessionCreated { session_id } = event; 21 + let session_manager::Event::SessionCreated { session_id, seat } = event; 22 22 self.session_id = Some(session_id); 23 + self.seat = Some(seat.to_string()); 23 24 } 24 25 } 25 26 ··· 28 29 impl PamServiceModule for Pam { 29 30 fn open_session(pamh: pamsm::Pam, _: pamsm::PamFlags, _: Vec<String>) -> pamsm::PamError { 30 31 fn inner(pamh: pamsm::Pam) -> Result<(), pamsm::PamError> { 31 - let mut session = Session { session_id: None }; 32 + let mut session = Session { 33 + session_id: None, 34 + seat: None, 35 + }; 32 36 33 37 let mut socket = client::Client::connect("/run/sessiond.sock") 34 38 .map_err(|_| pamsm::PamError::SESSION_ERR)?; ··· 49 53 .ok_or(pamsm::PamError::USER_UNKNOWN)? 50 54 .to_string_lossy() 51 55 .to_string(); 52 - let tty: path::PathBuf = pamh 56 + let pam_tty = pamh 53 57 .get_tty()? 54 58 .ok_or(pamsm::PamError::SESSION_ERR)? 55 59 .to_string_lossy() 56 - .to_string() 57 - .into(); 60 + .to_string(); 58 61 59 62 let uid = users::get_user_by_name(&username) 60 63 .ok_or(pamsm::PamError::USER_UNKNOWN)? 61 64 .uid(); 62 - let tty_name = tty 63 - .file_name() 64 - .ok_or(pamsm::PamError::SESSION_ERR)? 65 - .to_string_lossy(); 66 - manager.send_create_session(uid, username, tty_name); 65 + let vt = pam_tty 66 + .rsplit('/') 67 + .next() 68 + .and_then(|name| name.strip_prefix("tty")) 69 + .and_then(|vt| vt.parse().ok()) 70 + .unwrap_or(0); 71 + manager.send_create_session(uid, username, vt); 67 72 68 73 while session.session_id.is_none() { 69 74 event_queue ··· 76 81 .map_err(|_| pamsm::PamError::SESSION_ERR)?; 77 82 78 83 pamh.putenv(&format!("XDG_SESSION_ID={session_id}"))?; 84 + if let Some(seat) = session.seat { 85 + pamh.putenv(&format!("XDG_SEAT={seat}"))?; 86 + } 79 87 pamh.putenv(&format!("XDG_RUNTIME_DIR=/run/user/{uid}"))?; 80 88 81 89 Ok(()) ··· 99 107 .map_err(|_| pamsm::PamError::SESSION_ERR) 100 108 })?; 101 109 102 - let mut session = Session { session_id: None }; 110 + let mut session = Session { 111 + session_id: None, 112 + seat: None, 113 + }; 103 114 104 115 let mut socket = client::Client::connect("/run/sessiond.sock") 105 116 .map_err(|_| pamsm::PamError::SESSION_ERR)?;
+116
protocols/seat_v1.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <protocol name="seat_v1" version="1"> 3 + <object name="seat_manager" version="1"> 4 + <description summary="seat manager"> 5 + Interface for creating client seat connections. 6 + </description> 7 + <c2s name="open_seat"> 8 + <description summary="open the caller's seat"> 9 + Creates a seat object for the client's current session. 10 + The daemon sends either enabled or disabled to indicate the initial seat 11 + state. 12 + </description> 13 + <returns iface="seat"/> 14 + </c2s> 15 + <c2s name="destroy" destructor="true"> 16 + <description summary="destroy this object"/> 17 + </c2s> 18 + </object> 19 + 20 + <object name="seat" version="1"> 21 + <description summary="client seat"> 22 + Seat control interface based on libseat. A client opens a seat, reacts to 23 + enabled and disabled events, opens device objects while enabled, and may 24 + request session switches when supported by the daemon. 25 + </description> 26 + <s2c name="enabled"> 27 + <description summary="seat is active for this client"> 28 + Sent when the client may open and use seat devices. 29 + </description> 30 + </s2c> 31 + <s2c name="disabled"> 32 + <description summary="seat is inactive for this client"> 33 + Sent when the client must stop using devices opened through this seat and 34 + acknowledge the state change with ack_disabled. 35 + </description> 36 + </s2c> 37 + <s2c name="session_switched"> 38 + <description summary="session switch became active"> 39 + Emitted when a session switch has taken effect. 40 + </description> 41 + <arg name="session" type="uint" summary="active session or VT number"/> 42 + </s2c> 43 + <s2c name="seat_error"> 44 + <description summary="fatal seat protocol error"> 45 + Sent before the seat object is terminated after a fatal seat-scoped 46 + protocol error. 47 + </description> 48 + <arg name="seat_error" type="enum" interface="seat_error" summary="error kind"/> 49 + <arg name="message" type="varchar" summary="human-readable error detail"/> 50 + </s2c> 51 + <c2s name="ack_disabled"> 52 + <description summary="acknowledge disabled state"> 53 + Acknowledges the most recent disabled event. 54 + </description> 55 + </c2s> 56 + <c2s name="open_device"> 57 + <description summary="open a device on this seat"> 58 + Opens path and returns a device object. The returned object resolves 59 + exactly once with either opened or failed. 60 + </description> 61 + <returns iface="device"/> 62 + <arg name="path" type="varchar" summary="device path"/> 63 + </c2s> 64 + <c2s name="switch_session"> 65 + <description summary="switch to another session or VT"> 66 + Requests switching to the given target session. The daemon reports a seat 67 + error if switching is not supported. 68 + </description> 69 + <arg name="session" type="uint" summary="target session or VT number"/> 70 + </c2s> 71 + <c2s name="destroy" destructor="true"> 72 + <description summary="close the seat"> 73 + Closes the seat and any devices still associated with it. 74 + </description> 75 + </c2s> 76 + </object> 77 + 78 + <object name="device" version="1"> 79 + <description summary="seat device"> 80 + Device object created by seat.open_device. It resolves exactly once with 81 + either opened or failed. If revoked is emitted, the device is permanently 82 + unusable and the client should destroy the object. 83 + </description> 84 + <s2c name="opened"> 85 + <description summary="device opened successfully"/> 86 + <arg name="device_id" type="uint" summary="device id"/> 87 + <arg name="fd" type="fd" summary="opened device file descriptor"/> 88 + <arg name="path" type="varchar" summary="device path"/> 89 + </s2c> 90 + <s2c name="failed"> 91 + <description summary="device open failed"/> 92 + <arg name="device_error" type="enum" interface="device_error" summary="error kind"/> 93 + <arg name="errno" type="uint" summary="system errno value"/> 94 + <arg name="message" type="varchar" summary="human-readable error detail"/> 95 + </s2c> 96 + <s2c name="revoked"> 97 + <description summary="device revoked"> 98 + The fd and object must not be used again after this event. 99 + </description> 100 + </s2c> 101 + <c2s name="destroy" destructor="true"> 102 + <description summary="close the device handle"/> 103 + </c2s> 104 + </object> 105 + 106 + <enum name="device_error"> 107 + <value idx="0" name="seat_disabled" summary="seat was disabled when opening the device"/> 108 + <value idx="1" name="open_failed" summary="device open failed"/> 109 + </enum> 110 + 111 + <enum name="seat_error"> 112 + <value idx="0" name="invalid_ack_disabled" summary="ack_disabled was sent while enabled"/> 113 + <value idx="1" name="already_disabled" summary="disabled state was already acknowledged"/> 114 + <value idx="2" name="invalid_switch_session" summary="session switching is not supported"/> 115 + </enum> 116 + </protocol>
+4 -3
protocols/session_core_v1.xml
··· 58 58 <s2c name="username"> 59 59 <arg name="username" type="varchar" summary="username"/> 60 60 </s2c> 61 - <s2c name="tty"> 62 - <arg name="tty" type="varchar" summary="TTY device"/> 61 + <s2c name="vt"> 62 + <arg name="vt" type="uint" summary="virtual terminal number"/> 63 63 </s2c> 64 64 <s2c name="seat"> 65 65 <arg name="seat" type="varchar" summary="seat name"/> ··· 72 72 </s2c> 73 73 <s2c name="active"> 74 74 <description summary="session active state"/> 75 - <arg name="state" type="uint" enum="active_state" summary="session active state"/> 75 + <arg name="state" type="enum" interface="active_state" summary="session active state"/> 76 76 </s2c> 77 77 <s2c name="locked"> 78 78 <description summary="session is locked"/> ··· 118 118 <value idx="0" name="online" summary="session is logged in but not in the foreground"/> 119 119 <value idx="1" name="active" summary="session is in the foreground"/> 120 120 <value idx="2" name="closing" summary="session is being destroyed"/> 121 + <value idx="3" name="opening" summary="session is being created"/> 121 122 </enum> 122 123 123 124 <enum name="error_code">
+2 -1
protocols/session_management_v1.xml
··· 12 12 </description> 13 13 <arg name="uid" type="uint" summary="user id"/> 14 14 <arg name="username" type="varchar" summary="username"/> 15 - <arg name="tty" type="varchar" summary="TTY device"/> 15 + <arg name="vt" type="uint" summary="virtual terminal number"/> 16 16 </c2s> 17 17 <s2c name="session_created"> 18 18 <description summary="session ID assigned to the new session"> 19 19 Sent only to the manager that called create_session. 20 20 </description> 21 21 <arg name="session_id" type="uint" summary="unique session identifier"/> 22 + <arg name="seat" type="varchar" summary="seat name assigned to the session"/> 22 23 </s2c> 23 24 <c2s name="destroy_session"> 24 25 <description summary="deregister a session">