drmkit_session/lib.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Holding devices across a VT switch.
5//!
6//! A compositor that owns the display cannot simply open `/dev/dri/card0` and
7//! keep it: switching to another virtual terminal has to take the display away
8//! and give it back. A seat -- logind, seatd, or a bare-root fallback -- is
9//! what arbitrates that, handing out descriptors it can revoke.
10//!
11//! What arrives from the seat is two events. *Paused*: stop touching every
12//! descriptor immediately, because the ioctls will start failing. *Resumed*:
13//! here are fresh descriptors, and every per-descriptor kernel object you had
14//! -- framebuffers, property blobs, client caps, GEM handles -- is gone.
15//! `drmkit_cursor::Paused` is the shape that pairs with this on the other
16//! side.
17//!
18//! # Why the backend is a trait
19//!
20//! Because a seat cannot be opened on a machine that has no session. Over SSH,
21//! in a container, on a CI runner, logind says the session is busy, seatd is
22//! not running, and the builtin backend wants root and a TTY. That is most
23//! places this code will be built.
24//!
25//! So the sequencing -- which devices are held, what happens to them across a
26//! pause, when the pause is acknowledged, what a caller is allowed to do while
27//! inactive -- lives in [`Session`] over a [`Backend`], and is tested against
28//! a fake. The libseat binding is a thin adapter behind the `libseat` feature,
29//! and libseat's own diagnostics arrive through `drmkit-log`, tagged
30//! `[libseat]`, at error level.
31//!
32//! # Not here yet
33//!
34//! Two things the reference has and this does not.
35//!
36//! `TakeDeviceOpts::preserve_fd_across_resume` asks the seat to hand back the
37//! same descriptor rather than a fresh one, on the grounds that the
38//! capability-revoking backends leave it valid. Plausible, and it saves a
39//! reopen per device per switch, but it is an optimization over a path that is
40//! already correct -- and getting it wrong means scanning out through a
41//! descriptor the kernel has stopped honoring, on a machine none of the CI
42//! lanes resemble. It waits for a device to be proved on.
43//!
44//! `Seat::input_opener` routes libinput's privileged device opens through the
45//! same seat, so input descriptors get the revocable lifetime the DRM one has.
46//!
47//! Both halves it needs now exist: this crate opens devices through a seat,
48//! and `drmkit_input::Seat::open_with` takes any `LibinputInterface` the caller
49//! supplies. What is missing is the piece between them — an implementation of
50//! that trait backed by a [`Session`], so an input descriptor is revoked and
51//! reissued on a VT switch the way the DRM one is. Until it exists a caller
52//! passing their own opener gets input descriptors with an ordinary lifetime,
53//! which works and is not what the seat is for.
54
55#![cfg_attr(docsrs, feature(doc_cfg))]
56
57mod session;
58
59#[cfg(feature = "libseat")]
60mod seat;
61#[cfg(feature = "libseat")]
62mod seat_log;
63
64#[cfg(test)]
65mod tests;
66
67pub use session::{Backend, DeviceId, RawEvent, Session, SessionEvent, TakenDevice};
68
69#[cfg(feature = "libseat")]
70pub use seat::{DEFAULT_ENABLE_TIMEOUT, LibseatBackend};
71
72/// What can go wrong holding a session.
73#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
74#[non_exhaustive]
75pub enum SessionError {
76 /// No seat backend is available.
77 ///
78 /// No logind session, no seatd, and no permission for the builtin
79 /// fallback. A caller can still open devices directly; they simply will
80 /// not survive a VT switch, which is the right trade for a tool that is
81 /// not a compositor.
82 #[error("no seat backend is available")]
83 NoBackend,
84
85 /// The seat is not active, so devices cannot be opened.
86 ///
87 /// Between a pause and the resume that follows it. Not an error in the
88 /// usual sense -- it is the session working -- so a caller waits rather
89 /// than gives up.
90 #[error("the seat is not active")]
91 Inactive,
92
93 /// The device is not one this session opened.
94 #[error("no such device in this session")]
95 UnknownDevice,
96
97 /// The seat refused.
98 #[error("seat: {0}")]
99 Seat(String),
100}