Skip to main content

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}