1// Package seat opens a libseat session (logind or seatd) and hands out device fds.
2package seat
3
4import (
5 "context"
6 "errors"
7 "fmt"
8 "runtime"
9 "slices"
10 "sync"
11 "unsafe"
12
13 "github.com/bnema/purego"
14 "github.com/bnema/zerowrap"
15 "golang.org/x/sys/unix"
16)
17
18var (
19 loadOnce sync.Once
20 loadErr error
21
22 openSeat func(listener unsafe.Pointer, data uintptr) uintptr
23 closeSeat func(seat uintptr) int32
24 disableSeat func(seat uintptr) int32
25 openDevice func(seat uintptr, path string, fd *int32) int32
26 closeDevice func(seat uintptr, id int32) int32
27 seatName func(seat uintptr) string
28 switchSession func(seat uintptr, session int32) int32
29 getFD func(seat uintptr) int32
30 dispatch func(seat uintptr, timeout int32) int32
31
32 listener [2]uintptr
33 // libseat allows one seat per process; callbacks find it here.
34 current *Seat
35)
36
37func load() error {
38 loadOnce.Do(func() {
39 lib, err := purego.Dlopen("libseat.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
40 if err != nil {
41 loadErr = fmt.Errorf("open libseat.so.1: %w", err)
42 return
43 }
44 defer func() {
45 if r := recover(); r != nil {
46 loadErr = fmt.Errorf("libseat: %v", r)
47 }
48 }()
49 reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, "libseat_"+name) }
50 reg(&openSeat, "open_seat")
51 reg(&closeSeat, "close_seat")
52 reg(&disableSeat, "disable_seat")
53 reg(&openDevice, "open_device")
54 reg(&closeDevice, "close_device")
55 reg(&seatName, "seat_name")
56 reg(&switchSession, "switch_session")
57 reg(&getFD, "get_fd")
58 reg(&dispatch, "dispatch")
59 listener[0] = purego.NewCallback(func(_, _ uintptr) { current.changed(true) })
60 listener[1] = purego.NewCallback(func(_, _ uintptr) { current.changed(false) })
61 })
62 return loadErr
63}
64
65// Seat owns the libseat handle. libseat is not thread-safe, so every call goes
66// through mu; callbacks only run inside dispatch, which already holds it.
67type Seat struct {
68 mu sync.Mutex
69 handle uintptr
70 active bool
71 devices map[int32]int32 // fd → libseat device id
72 subs []chan bool
73 log zerowrap.Logger
74 // beforeDisable runs on a seat disable, before it is acked to the seat
75 // manager: the last moment this process is still DRM master.
76 beforeDisable func()
77 disabler disabler
78}
79
80// disabler acks a seat disable to the seat manager (libseat_disable_seat),
81// which revokes the devices.
82type disabler interface{ disable() }
83
84type libseatDisabler struct{ s *Seat }
85
86func (d libseatDisabler) disable() { disableSeat(d.s.handle) }
87
88// SetBeforeDisable registers f, run on every seat disable before it is
89// acked. f runs inside libseat's dispatch, with the seat locked: it must be
90// short and must not call the Seat. Call it before Run.
91func (s *Seat) SetBeforeDisable(f func()) {
92 s.mu.Lock()
93 defer s.mu.Unlock()
94 s.beforeDisable = f
95}
96
97// Open connects to logind or seatd and waits for the first enable.
98func Open(ctx context.Context, log zerowrap.Logger) (*Seat, error) {
99 if err := load(); err != nil {
100 return nil, err
101 }
102 if current != nil {
103 return nil, errors.New("seat already open")
104 }
105 s := &Seat{devices: make(map[int32]int32), log: log}
106 s.disabler = libseatDisabler{s}
107 current = s
108 s.mu.Lock()
109 defer s.mu.Unlock()
110 s.handle = openSeat(unsafe.Pointer(&listener), 0)
111 runtime.KeepAlive(&listener)
112 if s.handle == 0 {
113 current = nil
114 return nil, errors.New("libseat_open_seat failed: no logind/seatd session for this TTY (run from a text console you are logged into)")
115 }
116 log.Info().Str("seat", seatName(s.handle)).Msg("seat opened")
117 for !s.active {
118 if err := s.dispatchLocked(ctx, 100); err != nil {
119 s.closeLocked()
120 return nil, fmt.Errorf("waiting for seat enable: %w", err)
121 }
122 }
123 return s, nil
124}
125
126func (s *Seat) changed(active bool) {
127 s.active = active
128 s.log.Info().Bool("active", active).Msg("seat")
129 if !active {
130 // Once acked the devices are revoked and the next master inherits
131 // the planes as they are: let the owner of the outputs reset them.
132 if s.beforeDisable != nil {
133 s.beforeDisable()
134 }
135 // Devices are revoked by the seat manager; ack so the VT can switch.
136 s.disabler.disable()
137 }
138 for _, c := range s.subs {
139 select {
140 case c <- active:
141 default:
142 select {
143 case <-c:
144 default:
145 }
146 c <- active
147 }
148 }
149}
150
151// Name returns the seat name, e.g. seat0.
152func (s *Seat) Name() string {
153 s.mu.Lock()
154 defer s.mu.Unlock()
155 return seatName(s.handle)
156}
157
158// Subscribe returns a channel that receives the seat state after every enable/disable.
159// The current state is sent first.
160func (s *Seat) Subscribe() <-chan bool {
161 c := make(chan bool, 1)
162 s.mu.Lock()
163 c <- s.active
164 s.subs = append(s.subs, c)
165 s.mu.Unlock()
166 return c
167}
168
169// Unsubscribe stops sending to a channel from Subscribe.
170func (s *Seat) Unsubscribe(c <-chan bool) {
171 s.mu.Lock()
172 defer s.mu.Unlock()
173 s.subs = slices.DeleteFunc(s.subs, func(x chan bool) bool { return x == c })
174}
175
176func (s *Seat) dispatchLocked(ctx context.Context, timeoutMs int) error {
177 if ctx.Err() != nil {
178 return ctx.Err()
179 }
180 fds := []unix.PollFd{{Fd: getFD(s.handle), Events: unix.POLLIN}}
181 if _, err := unix.Poll(fds, timeoutMs); err != nil && !errors.Is(err, unix.EINTR) {
182 return err
183 }
184 if dispatch(s.handle, 0) < 0 {
185 return errors.New("libseat_dispatch failed")
186 }
187 return nil
188}
189
190// Run dispatches seat events until ctx ends. The owner calls Close afterwards,
191// once outputs have restored their CRTC state.
192func (s *Seat) Run(ctx context.Context) error {
193 for ctx.Err() == nil {
194 fds := []unix.PollFd{{Fd: getFD(s.handle), Events: unix.POLLIN}}
195 if _, err := unix.Poll(fds, 100); err != nil && !errors.Is(err, unix.EINTR) {
196 return fmt.Errorf("seat poll: %w", err)
197 }
198 // Dispatch every tick: libseat may queue events without the fd becoming readable.
199 s.mu.Lock()
200 r := dispatch(s.handle, 0)
201 s.mu.Unlock()
202 if r < 0 {
203 return errors.New("libseat_dispatch failed")
204 }
205 }
206 return nil
207}
208
209// OpenDevice opens path through the seat manager and returns its fd.
210func (s *Seat) OpenDevice(path string) (int, error) {
211 s.mu.Lock()
212 defer s.mu.Unlock()
213 var fd int32 = -1
214 id := openDevice(s.handle, path, &fd)
215 if id < 0 {
216 s.log.Warn().Str("path", path).Msg("open device failed")
217 return -1, fmt.Errorf("libseat_open_device %s failed", path)
218 }
219 s.devices[fd] = id
220 s.log.Info().Str("path", path).Int32("fd", fd).Msg("device opened")
221 return int(fd), nil
222}
223
224// CloseDevice releases a fd returned by OpenDevice.
225func (s *Seat) CloseDevice(fd int) {
226 s.mu.Lock()
227 defer s.mu.Unlock()
228 id, ok := s.devices[int32(fd)]
229 if !ok {
230 return
231 }
232 delete(s.devices, int32(fd))
233 closeDevice(s.handle, id)
234 _ = unix.Close(fd)
235 s.log.Info().Int("fd", fd).Msg("device closed")
236}
237
238// SwitchVT asks the seat manager to switch to virtual terminal vt.
239func (s *Seat) SwitchVT(vt int) {
240 s.mu.Lock()
241 defer s.mu.Unlock()
242 s.log.Info().Int("vt", vt).Msg("switch vt")
243 if switchSession(s.handle, int32(vt)) < 0 {
244 s.log.Warn().Int("vt", vt).Msg("switch vt failed")
245 }
246}
247
248// Close releases all devices and the seat; the VT returns to text mode. Safe to call twice.
249func (s *Seat) Close() {
250 s.mu.Lock()
251 defer s.mu.Unlock()
252 s.closeLocked()
253}
254
255func (s *Seat) closeLocked() {
256 if s.handle == 0 {
257 return
258 }
259 for fd, id := range s.devices {
260 closeDevice(s.handle, id)
261 _ = unix.Close(int(fd))
262 }
263 s.devices = map[int32]int32{}
264 closeSeat(s.handle)
265 s.handle = 0
266 current = nil
267 s.log.Info().Msg("seat closed")
268}