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}
75
76// Open connects to logind or seatd and waits for the first enable.
77func Open(ctx context.Context, log zerowrap.Logger) (*Seat, error) {
78 if err := load(); err != nil {
79 return nil, err
80 }
81 if current != nil {
82 return nil, errors.New("seat already open")
83 }
84 s := &Seat{devices: make(map[int32]int32), log: log}
85 current = s
86 s.mu.Lock()
87 defer s.mu.Unlock()
88 s.handle = openSeat(unsafe.Pointer(&listener), 0)
89 runtime.KeepAlive(&listener)
90 if s.handle == 0 {
91 current = nil
92 return nil, errors.New("libseat_open_seat failed: no logind/seatd session for this TTY (run from a text console you are logged into)")
93 }
94 log.Info().Str("seat", seatName(s.handle)).Msg("seat opened")
95 for !s.active {
96 if err := s.dispatchLocked(ctx, 100); err != nil {
97 s.closeLocked()
98 return nil, fmt.Errorf("waiting for seat enable: %w", err)
99 }
100 }
101 return s, nil
102}
103
104func (s *Seat) changed(active bool) {
105 s.active = active
106 s.log.Info().Bool("active", active).Msg("seat")
107 if !active {
108 // Devices are revoked by the seat manager; ack so the VT can switch.
109 disableSeat(s.handle)
110 }
111 for _, c := range s.subs {
112 select {
113 case c <- active:
114 default:
115 select {
116 case <-c:
117 default:
118 }
119 c <- active
120 }
121 }
122}
123
124// Name returns the seat name, e.g. seat0.
125func (s *Seat) Name() string {
126 s.mu.Lock()
127 defer s.mu.Unlock()
128 return seatName(s.handle)
129}
130
131// Subscribe returns a channel that receives the seat state after every enable/disable.
132// The current state is sent first.
133func (s *Seat) Subscribe() <-chan bool {
134 c := make(chan bool, 1)
135 s.mu.Lock()
136 c <- s.active
137 s.subs = append(s.subs, c)
138 s.mu.Unlock()
139 return c
140}
141
142// Unsubscribe stops sending to a channel from Subscribe.
143func (s *Seat) Unsubscribe(c <-chan bool) {
144 s.mu.Lock()
145 defer s.mu.Unlock()
146 s.subs = slices.DeleteFunc(s.subs, func(x chan bool) bool { return x == c })
147}
148
149func (s *Seat) dispatchLocked(ctx context.Context, timeoutMs int) error {
150 if ctx.Err() != nil {
151 return ctx.Err()
152 }
153 fds := []unix.PollFd{{Fd: getFD(s.handle), Events: unix.POLLIN}}
154 if _, err := unix.Poll(fds, timeoutMs); err != nil && !errors.Is(err, unix.EINTR) {
155 return err
156 }
157 if dispatch(s.handle, 0) < 0 {
158 return errors.New("libseat_dispatch failed")
159 }
160 return nil
161}
162
163// Run dispatches seat events until ctx ends. The owner calls Close afterwards,
164// once outputs have restored their CRTC state.
165func (s *Seat) Run(ctx context.Context) error {
166 for ctx.Err() == nil {
167 fds := []unix.PollFd{{Fd: getFD(s.handle), Events: unix.POLLIN}}
168 if _, err := unix.Poll(fds, 100); err != nil && !errors.Is(err, unix.EINTR) {
169 return fmt.Errorf("seat poll: %w", err)
170 }
171 // Dispatch every tick: libseat may queue events without the fd becoming readable.
172 s.mu.Lock()
173 r := dispatch(s.handle, 0)
174 s.mu.Unlock()
175 if r < 0 {
176 return errors.New("libseat_dispatch failed")
177 }
178 }
179 return nil
180}
181
182// OpenDevice opens path through the seat manager and returns its fd.
183func (s *Seat) OpenDevice(path string) (int, error) {
184 s.mu.Lock()
185 defer s.mu.Unlock()
186 var fd int32 = -1
187 id := openDevice(s.handle, path, &fd)
188 if id < 0 {
189 s.log.Warn().Str("path", path).Msg("open device failed")
190 return -1, fmt.Errorf("libseat_open_device %s failed", path)
191 }
192 s.devices[fd] = id
193 s.log.Info().Str("path", path).Int32("fd", fd).Msg("device opened")
194 return int(fd), nil
195}
196
197// CloseDevice releases a fd returned by OpenDevice.
198func (s *Seat) CloseDevice(fd int) {
199 s.mu.Lock()
200 defer s.mu.Unlock()
201 id, ok := s.devices[int32(fd)]
202 if !ok {
203 return
204 }
205 delete(s.devices, int32(fd))
206 closeDevice(s.handle, id)
207 _ = unix.Close(fd)
208 s.log.Info().Int("fd", fd).Msg("device closed")
209}
210
211// SwitchVT asks the seat manager to switch to virtual terminal vt.
212func (s *Seat) SwitchVT(vt int) {
213 s.mu.Lock()
214 defer s.mu.Unlock()
215 s.log.Info().Int("vt", vt).Msg("switch vt")
216 if switchSession(s.handle, int32(vt)) < 0 {
217 s.log.Warn().Int("vt", vt).Msg("switch vt failed")
218 }
219}
220
221// Close releases all devices and the seat; the VT returns to text mode. Safe to call twice.
222func (s *Seat) Close() {
223 s.mu.Lock()
224 defer s.mu.Unlock()
225 s.closeLocked()
226}
227
228func (s *Seat) closeLocked() {
229 if s.handle == 0 {
230 return
231 }
232 for fd, id := range s.devices {
233 closeDevice(s.handle, id)
234 _ = unix.Close(int(fd))
235 }
236 s.devices = map[int32]int32{}
237 closeSeat(s.handle)
238 s.handle = 0
239 current = nil
240 s.log.Info().Msg("seat closed")
241}