drmkit_session/session.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The session state machine, over whatever seat is underneath.
5
6use std::collections::VecDeque;
7use std::os::fd::RawFd;
8use std::path::{Path, PathBuf};
9
10use crate::SessionError;
11
12/// A seat's handle for one open device.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[allow(
15 clippy::exhaustive_structs,
16 reason = "a value defined by its fields; there is nothing to add to it"
17)]
18pub struct DeviceId(pub i32);
19
20/// A device this session is holding.
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[non_exhaustive]
23pub struct TakenDevice {
24 /// Where it was opened from. Kept because a resume reopens by path.
25 pub path: PathBuf,
26 /// The seat's handle, for closing it.
27 pub id: DeviceId,
28 /// The descriptor as it stands now. Replaced by a resume.
29 ///
30 /// Borrowed, not owned: the seat closes it, and it does so at a moment of
31 /// its choosing. Closing it here leaves the seat holding a handle to a
32 /// descriptor number the process has since reused for something else.
33 pub fd: RawFd,
34}
35
36/// What a seat has to be able to do.
37///
38/// Deliberately small. Everything about *when* to call these -- and what state
39/// the session is in between them -- is [`Session`]'s, so it can be tested
40/// against a fake on a machine with no seat.
41pub trait Backend {
42 /// Open a device the seat controls.
43 ///
44 /// # Errors
45 ///
46 /// Whatever the seat says, as [`SessionError::Seat`].
47 fn open_device(&mut self, path: &Path) -> Result<(DeviceId, RawFd), SessionError>;
48
49 /// Close one this session opened.
50 ///
51 /// # Errors
52 ///
53 /// As [`Backend::open_device`].
54 fn close_device(&mut self, id: DeviceId) -> Result<(), SessionError>;
55
56 /// Tell the seat the pause has been acted on.
57 ///
58 /// Until this returns, the seat is waiting: the switch does not complete
59 /// and the user is looking at a display that has not changed.
60 ///
61 /// # Errors
62 ///
63 /// As [`Backend::open_device`].
64 fn acknowledge_pause(&mut self) -> Result<(), SessionError>;
65
66 /// Ask to switch to another session, by VT number.
67 ///
68 /// # Errors
69 ///
70 /// As [`Backend::open_device`].
71 fn switch_session(&mut self, session: i32) -> Result<(), SessionError>;
72
73 /// A descriptor that becomes readable when the seat has something to say.
74 fn poll_fd(&self) -> Option<RawFd>;
75
76 /// Let the seat deliver whatever it has, appending to `events`.
77 ///
78 /// # Errors
79 ///
80 /// As [`Backend::open_device`].
81 fn dispatch(&mut self, timeout_ms: i32, events: &mut Vec<RawEvent>)
82 -> Result<(), SessionError>;
83}
84
85/// What a seat says, before the session has made sense of it.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[non_exhaustive]
88pub enum RawEvent {
89 /// The session has become active.
90 Enable,
91 /// The session is about to become inactive.
92 Disable,
93}
94
95/// What a caller needs to act on.
96#[derive(Debug, Clone, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum SessionEvent {
99 /// Stop touching every device now.
100 ///
101 /// The descriptors are about to stop working. Anything mapped from them
102 /// has to be released by the caller -- closing a descriptor does not unmap
103 /// it, so a compositor that skips that leaks per switch.
104 Paused,
105 /// The devices are back, on new descriptors.
106 ///
107 /// Every per-descriptor kernel object is gone: framebuffers, property
108 /// blobs, client capabilities, GEM handles. All of it is rebuilt.
109 Resumed {
110 /// The devices as they now stand, in the order they were taken.
111 devices: Vec<TakenDevice>,
112 },
113}
114
115/// Devices held across VT switches.
116#[derive(Debug)]
117pub struct Session<B: Backend> {
118 backend: B,
119 devices: Vec<TakenDevice>,
120 active: bool,
121 pending: VecDeque<SessionEvent>,
122 raw: Vec<RawEvent>,
123}
124
125impl<B: Backend> Session<B> {
126 /// Wrap a seat that is currently active.
127 pub fn new(backend: B) -> Self {
128 Self {
129 backend,
130 devices: Vec::new(),
131 active: true,
132 pending: VecDeque::new(),
133 raw: Vec::new(),
134 }
135 }
136
137 /// Whether the session currently owns the display.
138 #[must_use]
139 pub const fn is_active(&self) -> bool {
140 self.active
141 }
142
143 /// The devices being held.
144 #[must_use]
145 pub fn devices(&self) -> &[TakenDevice] {
146 &self.devices
147 }
148
149 /// The seat underneath, for tests to drive.
150 #[cfg(test)]
151 pub(crate) const fn backend(&self) -> &B {
152 &self.backend
153 }
154
155 /// As [`Session::backend`].
156 #[cfg(test)]
157 pub(crate) const fn backend_mut(&mut self) -> &mut B {
158 &mut self.backend
159 }
160
161 /// A descriptor to poll for seat activity.
162 #[must_use]
163 pub fn poll_fd(&self) -> Option<RawFd> {
164 self.backend.poll_fd()
165 }
166
167 /// Open a device through the seat and hold it.
168 ///
169 /// Opening the same path twice returns what is already held rather than a
170 /// second descriptor: the seat would give one, and then a resume would
171 /// have two to reopen and the caller two to keep straight.
172 ///
173 /// # Errors
174 ///
175 /// - [`SessionError::Inactive`] while the session does not own the
176 /// display. The seat would refuse anyway; failing here says why.
177 /// - [`SessionError::Seat`] if the seat refuses.
178 pub fn take_device(&mut self, path: impl AsRef<Path>) -> Result<TakenDevice, SessionError> {
179 let path = path.as_ref();
180 if !self.active {
181 return Err(SessionError::Inactive);
182 }
183 if let Some(existing) = self.devices.iter().find(|device| device.path == path) {
184 return Ok(existing.clone());
185 }
186 let (id, fd) = self.backend.open_device(path)?;
187 let device = TakenDevice {
188 path: path.to_path_buf(),
189 id,
190 fd,
191 };
192 self.devices.push(device.clone());
193 Ok(device)
194 }
195
196 /// Give a device back to the seat.
197 ///
198 /// # Errors
199 ///
200 /// - [`SessionError::UnknownDevice`] if this session did not open it.
201 /// - [`SessionError::Seat`] if the seat refuses.
202 pub fn release_device(&mut self, path: impl AsRef<Path>) -> Result<(), SessionError> {
203 let path = path.as_ref();
204 let index = self
205 .devices
206 .iter()
207 .position(|device| device.path == path)
208 .ok_or(SessionError::UnknownDevice)?;
209 let device = self.devices.remove(index);
210 self.backend.close_device(device.id)
211 }
212
213 /// Ask to switch to another session, by VT number.
214 ///
215 /// Asynchronous: the seat pauses this session when it honors the request,
216 /// and resumes it when the user comes back. Useful where the process owns
217 /// the TTY in graphics mode and the kernel is no longer handling
218 /// `Ctrl+Alt+F<n>` itself.
219 ///
220 /// # Errors
221 ///
222 /// [`SessionError::Seat`] if the seat refuses.
223 pub fn switch_session(&mut self, session: i32) -> Result<(), SessionError> {
224 self.backend.switch_session(session)
225 }
226
227 /// Let the seat deliver what it has, turning it into events to act on.
228 ///
229 /// A negative timeout blocks. Call [`Session::next_event`] afterwards.
230 ///
231 /// The reference runs the caller's callbacks synchronously from inside
232 /// this call, which means the caller is re-entering the seat from inside
233 /// the seat's own dispatch. Queuing instead keeps that from being possible.
234 ///
235 /// # Errors
236 ///
237 /// [`SessionError::Seat`] if the seat refuses, or if reopening a device
238 /// on resume fails.
239 pub fn dispatch(&mut self, timeout_ms: i32) -> Result<(), SessionError> {
240 self.raw.clear();
241 let mut raw = std::mem::take(&mut self.raw);
242 let result = self.backend.dispatch(timeout_ms, &mut raw);
243 for event in raw.drain(..) {
244 match event {
245 RawEvent::Disable => self.on_disable()?,
246 RawEvent::Enable => self.on_enable()?,
247 }
248 }
249 self.raw = raw;
250 result
251 }
252
253 /// The next thing the caller has to act on.
254 pub fn next_event(&mut self) -> Option<SessionEvent> {
255 self.pending.pop_front()
256 }
257
258 /// Handle the seat going away.
259 fn on_disable(&mut self) -> Result<(), SessionError> {
260 if !self.active {
261 // A seat that says it twice is not a reason to tell the caller
262 // twice; the caller has already stopped.
263 return Ok(());
264 }
265 self.active = false;
266 drmkit_log::log_info!(
267 "session paused: {} device(s) are no longer ours",
268 self.devices.len()
269 );
270 self.pending.push_back(SessionEvent::Paused);
271 // Acknowledged immediately. The queued event is what the caller acts
272 // on, and holding the acknowledgement until they have would stall the
273 // switch on a display that has already stopped being ours.
274 self.backend.acknowledge_pause()
275 }
276
277 /// Handle the seat coming back.
278 fn on_enable(&mut self) -> Result<(), SessionError> {
279 if self.active {
280 return Ok(());
281 }
282 self.active = true;
283
284 // Every descriptor is replaced. The pessimistic reading, and the right
285 // one: the capability-revoking backends leave the descriptor valid but
286 // reset per-descriptor state anyway, so a caller that reused it would
287 // be working from objects the kernel has forgotten.
288 let mut refreshed = Vec::with_capacity(self.devices.len());
289 for device in std::mem::take(&mut self.devices) {
290 // Closing first: the seat tracks one open device per handle, and
291 // opening the same path again without closing leaks its handle for
292 // as long as the session runs.
293 self.backend.close_device(device.id)?;
294 let (id, fd) = self
295 .backend
296 .open_device(&device.path)
297 .inspect_err(|error| {
298 // The caller is told too, by the error out of `dispatch`. This
299 // says which device, which is the part that identifies why.
300 drmkit_log::log_error!("reopening {} failed: {error}", device.path.display());
301 })?;
302 refreshed.push(TakenDevice {
303 path: device.path,
304 id,
305 fd,
306 });
307 }
308 drmkit_log::log_info!("session resumed: {} device(s) reopened", refreshed.len());
309 self.devices.clone_from(&refreshed);
310 self.pending
311 .push_back(SessionEvent::Resumed { devices: refreshed });
312 Ok(())
313 }
314}