Skip to main content

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//! `libseat-log`, also on by default, routes libseat's own diagnostics into
30//! the `log` crate.
31//!
32//! # Not here yet
33//!
34//! Three 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 optimisation 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` 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//! The reference installs its own handler for libseat's log, at error level,
56//! and sends what arrives to `drm::log`. Here the binding's handler sends it
57//! at debug level to the `log` crate, which `drmkit-log` does not read: a
58//! caller with no `log` logger never sees libseat's reasons for refusing a
59//! seat. Taking it over means a C shim, since libseat hands a handler a
60//! `va_list`, and the binding already builds one.
61
62#![cfg_attr(docsrs, feature(doc_cfg))]
63
64mod session;
65
66#[cfg(feature = "libseat")]
67mod seat;
68
69#[cfg(test)]
70mod tests;
71
72pub use session::{Backend, DeviceId, RawEvent, Session, SessionEvent, TakenDevice};
73
74#[cfg(feature = "libseat")]
75pub use seat::{DEFAULT_ENABLE_TIMEOUT, LibseatBackend};
76
77/// What can go wrong holding a session.
78#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
79#[non_exhaustive]
80pub enum SessionError {
81    /// No seat backend is available.
82    ///
83    /// No logind session, no seatd, and no permission for the builtin
84    /// fallback. A caller can still open devices directly; they simply will
85    /// not survive a VT switch, which is the right trade for a tool that is
86    /// not a compositor.
87    #[error("no seat backend is available")]
88    NoBackend,
89
90    /// The seat is not active, so devices cannot be opened.
91    ///
92    /// Between a pause and the resume that follows it. Not an error in the
93    /// usual sense -- it is the session working -- so a caller waits rather
94    /// than gives up.
95    #[error("the seat is not active")]
96    Inactive,
97
98    /// The device is not one this session opened.
99    #[error("no such device in this session")]
100    UnknownDevice,
101
102    /// The seat refused.
103    #[error("seat: {0}")]
104    Seat(String),
105}