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