1// Package wayland owns a standard-protocol control connection. It never owns
2// lock surfaces or grabs input; visual processes own ext-session-lock.
3package wayland
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "math"
10 "net"
11 "sort"
12 "time"
13
14 "github.com/bnema/neferafk/internal/ports"
15 "github.com/bnema/wlturbo"
16 "github.com/bnema/wlturbo/protocol/core"
17 "github.com/bnema/wlturbo/protocol/idlenotify"
18 "github.com/bnema/wlturbo/protocol/outputpower"
19 "github.com/bnema/zerowrap"
20)
21
22const queueSize = 64
23
24var ErrEventOverflow = errors.New("wayland: event queue overflow")
25
26type Options struct {
27 Requirements ports.WaylandRequirements
28 Deadlines []time.Duration
29 Generation uint64
30}
31type notice struct {
32 kind uint8
33 notification *notification
34 output *output
35 name string
36 mode uint32
37}
38type notification struct {
39 proxy *idlenotify.ExtIdleNotification
40 token, cycle, generation uint64
41 seat uint32
42 after time.Duration
43 active, wake, idled bool
44}
45type seat struct {
46 proxy *core.Seat
47 notifications map[time.Duration]*notification
48 wake *notification
49}
50type output struct {
51 proxy *core.Output
52 power *outputpower.OutputPower
53 lifetime uint64
54 global uint32
55 name string
56 off, failed bool
57 mode uint32
58 modeKnown bool
59}
60type Client struct {
61 d *wlturbo.Display
62 options Options
63 log zerowrap.Logger
64 notices chan notice
65 overflow chan struct{}
66 seats map[uint32]*seat
67 outputs map[uint32]*output
68 idle *idlenotify.ExtIdleNotifier
69 idleGlobal uint32
70 power *outputpower.OutputPowerManager
71 powerGlobal uint32
72 deadlines map[time.Duration]bool
73 generation, next uint64
74 dirty bool
75}
76
77// New takes ownership of conn. Run is called exactly once; no other reader or
78// caller may use the display. Supplying an already connected socket permits
79// real socketpair tests without a replacement transport.
80func New(conn net.Conn, options Options, log zerowrap.Logger) (*Client, error) {
81 deadlines, err := normalize(options.Deadlines)
82 if err != nil {
83 _ = conn.Close()
84 return nil, err
85 }
86 d, err := wlturbo.ConnectFromConn(conn)
87 if err != nil {
88 _ = conn.Close()
89 return nil, err
90 }
91 c := &Client{d: d, options: options, log: log.WithField("component", "wayland"), notices: make(chan notice, queueSize), overflow: make(chan struct{}, 1), seats: map[uint32]*seat{}, outputs: map[uint32]*output{}, deadlines: deadlines, generation: options.Generation}
92 d.Registry().AddHandler("*", func(*wlturbo.Registry, uint32, uint32) { c.enqueue(notice{kind: 1}) })
93 d.Registry().AddGlobalRemoveHandler(c)
94 return c, nil
95}
96func normalize(ds []time.Duration) (map[time.Duration]bool, error) {
97 out := map[time.Duration]bool{}
98 for _, d := range ds {
99 if d == 0 {
100 continue
101 }
102 if d < 0 || d%time.Millisecond != 0 || d/time.Millisecond > math.MaxUint32 {
103 return nil, fmt.Errorf("wayland: deadline must be positive whole milliseconds within uint32: %s", d)
104 }
105 out[d] = true
106 }
107 return out, nil
108}
109func (c *Client) HandleRegistryGlobalRemove(wlturbo.RegistryGlobalRemoveEvent) {
110 c.enqueue(notice{kind: 1})
111}
112func (c *Client) enqueue(n notice) {
113 select {
114 case c.notices <- n:
115 default:
116 select {
117 case c.overflow <- struct{}{}:
118 default:
119 }
120 _ = c.d.Close()
121 }
122}
123
124// Run uses one dispatch reader; callbacks only enqueue values. All policy,
125// notification, output and capability state belongs to the Run goroutine.
126func (c *Client) Run(ctx context.Context, commands <-chan ports.WaylandCommand, events chan<- ports.WaylandEvent) error {
127 defer c.d.Close()
128 stop := context.AfterFunc(ctx, func() { _ = c.d.Close() })
129 defer stop()
130 if err := c.d.Roundtrip(); err != nil {
131 if ctx.Err() != nil {
132 return nil
133 }
134 return err
135 }
136 // Bootstrap announcements are represented by the registry snapshot.
137 for len(c.notices) > 0 {
138 <-c.notices
139 }
140 if err := c.reconcile(); err != nil {
141 return err
142 }
143 // Initial seat/output metadata and output-power mode arrive before doctor.
144 if err := c.d.Roundtrip(); err != nil {
145 if ctx.Err() != nil {
146 return nil
147 }
148 return err
149 }
150 for len(c.notices) > 0 {
151 if err := c.handle(ctx, events, <-c.notices); err != nil {
152 return err
153 }
154 }
155 done := make(chan error, 1)
156 proceed := make(chan struct{})
157 readerStop := make(chan struct{})
158 readerExited := make(chan struct{})
159 go func() {
160 defer close(readerExited)
161 for {
162 err := c.d.Dispatch()
163 select {
164 case done <- err:
165 case <-readerStop:
166 return
167 }
168 if err != nil {
169 return
170 }
171 select {
172 case <-proceed:
173 case <-readerStop:
174 return
175 }
176 }
177 }()
178 defer func() {
179 close(readerStop)
180 _ = c.d.Close()
181 <-readerExited
182 }()
183 // Callbacks only enqueue immutable proxy references; state stays here.
184 var pendingCommands []ports.WaylandCommand
185 for {
186 if c.dirty {
187 if err := c.emit(ctx, events, ports.WaylandCapabilities{Capabilities: c.capabilities()}); err != nil {
188 return nil
189 }
190 c.dirty = false
191 }
192 select {
193 case <-ctx.Done():
194 return nil
195 case <-c.overflow:
196 return ErrEventOverflow
197 case err := <-done:
198 if err != nil {
199 if ctx.Err() != nil {
200 return nil
201 }
202 select {
203 case <-c.overflow:
204 return ErrEventOverflow
205 default:
206 }
207 return err
208 }
209 for len(c.notices) > 0 {
210 if err := c.handle(ctx, events, <-c.notices); err != nil {
211 return err
212 }
213 }
214 for _, cmd := range pendingCommands {
215 if err := c.command(cmd); err != nil {
216 return err
217 }
218 }
219 pendingCommands = nil
220 select {
221 case proceed <- struct{}{}:
222 case <-ctx.Done():
223 return nil
224 }
225 case cmd, ok := <-commands:
226 if !ok {
227 commands = nil
228 continue
229 }
230 // Process any completed wire frame before this command. This keeps
231 // queued old-generation idle and registry removals ahead of reload.
232 if len(c.notices) > 0 {
233 if len(pendingCommands) == queueSize {
234 return ErrEventOverflow
235 }
236 pendingCommands = append(pendingCommands, cmd)
237 } else if err := c.command(cmd); err != nil {
238 return err
239 }
240 }
241 }
242}
243func (c *Client) emit(ctx context.Context, out chan<- ports.WaylandEvent, event ports.WaylandEvent) error {
244 select {
245 case out <- event:
246 return nil
247 case <-ctx.Done():
248 return ctx.Err()
249 }
250}
251func (c *Client) handle(ctx context.Context, out chan<- ports.WaylandEvent, n notice) error {
252 switch n.kind {
253 case 1:
254 return c.reconcile()
255 case 2, 3:
256 p := n.notification
257 if !p.active {
258 return nil
259 }
260 if n.kind == 2 {
261 if p.idled {
262 return nil
263 }
264 p.idled = true
265 if !p.wake {
266 return c.emit(ctx, out, ports.WaylandIdle{Generation: p.generation, Proxy: p.token, Cycle: p.cycle, Seat: p.seat, After: p.after})
267 }
268 }
269 if n.kind == 3 {
270 if !p.idled {
271 return nil
272 }
273 p.idled = false
274 p.cycle++
275 if p.wake {
276 if err := c.setPower(true); err != nil {
277 return err
278 }
279 return c.emit(ctx, out, ports.WaylandActivity{Proxy: p.token, Cycle: p.cycle, Seat: p.seat})
280 }
281 }
282 case 4:
283 o := n.output
284 if c.outputs[o.global] != o {
285 return nil
286 }
287 o.name = n.name
288 c.dirty = true
289 case 6:
290 o := n.output
291 if c.outputs[o.global] != o || o.failed {
292 return nil
293 }
294 o.mode, o.modeKnown = n.mode, true
295 if n.mode == outputpower.MODE_ON {
296 o.off = false
297 }
298 case 5:
299 o := n.output
300 if c.outputs[o.global] != o || o.failed {
301 return nil
302 }
303 c.log.Warn().Uint64("output_lifetime", o.lifetime).Msg("output power control failed")
304 o.failed = true
305 o.off = false
306 if o.power != nil {
307 if err := o.power.Destroy(); err != nil {
308 return err
309 }
310 o.power = nil
311 }
312 c.dirty = true
313 return c.emit(ctx, out, ports.WaylandPowerFailed{Output: o.lifetime})
314 }
315 return nil
316}
317func (c *Client) bind(g wlturbo.Global, supported uint32, p wlturbo.Proxy) error {
318 return c.d.Registry().Bind(g.Name, g.Interface, min(g.Version, supported), p)
319}
320func (c *Client) reconcile() error {
321 globals := c.d.Registry().GetGlobals()
322 find := func(iface string, minVersion uint32) wlturbo.Global {
323 var selected wlturbo.Global
324 for _, g := range globals {
325 if g.Interface == iface && g.Version >= minVersion && (selected.Name == 0 || g.Name < selected.Name) {
326 selected = g
327 }
328 }
329 return selected
330 }
331 idle := find(idlenotify.ExtIdleNotifierInterface, 2)
332 wantIdle := c.options.Requirements.Idle || c.options.Requirements.InputWake || len(c.deadlines) > 0
333 if !wantIdle {
334 idle = wlturbo.Global{}
335 }
336 if c.idleGlobal != idle.Name {
337 for _, s := range c.seats {
338 for _, p := range s.notifications {
339 if err := c.destroyNotification(p); err != nil {
340 return err
341 }
342 }
343 s.notifications = map[time.Duration]*notification{}
344 if err := c.destroyNotification(s.wake); err != nil {
345 return err
346 }
347 s.wake = nil
348 }
349 if c.idle != nil {
350 if err := c.idle.Destroy(); err != nil {
351 return err
352 }
353 c.idle = nil
354 }
355 c.idleGlobal = idle.Name
356 if idle.Name != 0 {
357 c.idle = idlenotify.NewExtIdleNotifier(c.d.Context())
358 if err := c.bind(idle, 2, c.idle); err != nil {
359 return err
360 }
361 }
362 }
363 power := find(outputpower.OutputPowerManagerInterface, 1)
364 if !c.options.Requirements.OutputPower {
365 power = wlturbo.Global{}
366 }
367 if c.powerGlobal != power.Name {
368 for _, o := range c.outputs {
369 if o.power != nil {
370 if o.off {
371 if err := o.power.SetMode(outputpower.MODE_ON); err != nil {
372 return err
373 }
374 }
375 if err := o.power.Destroy(); err != nil {
376 return err
377 }
378 o.power = nil
379 }
380 o.off = false
381 o.failed = false
382 }
383 if c.power != nil {
384 if err := c.power.Destroy(); err != nil {
385 return err
386 }
387 c.power = nil
388 }
389 c.powerGlobal = power.Name
390 if power.Name != 0 {
391 c.power = outputpower.NewOutputPowerManager(c.d.Context())
392 if err := c.bind(power, 1, c.power); err != nil {
393 return err
394 }
395 }
396 }
397 for name, s := range c.seats {
398 if g, ok := globals[name]; !ok || g.Interface != core.SeatInterface {
399 for _, p := range s.notifications {
400 if err := c.destroyNotification(p); err != nil {
401 return err
402 }
403 }
404 if err := c.destroyNotification(s.wake); err != nil {
405 return err
406 }
407 if s.proxy.Version() >= 5 {
408 if err := s.proxy.Release(); err != nil {
409 return err
410 }
411 }
412 delete(c.seats, name)
413 }
414 }
415 for name, o := range c.outputs {
416 if g, ok := globals[name]; !ok || g.Interface != core.OutputInterface {
417 if o.power != nil {
418 if err := o.power.Destroy(); err != nil {
419 return err
420 }
421 }
422 if o.proxy.Version() >= 3 {
423 if err := o.proxy.Release(); err != nil {
424 return err
425 }
426 }
427 delete(c.outputs, name)
428 }
429 }
430 for name, g := range globals {
431 if g.Interface == core.SeatInterface && c.seats[name] == nil {
432 p := core.NewSeat(c.d.Context())
433 if err := c.bind(g, 5, p); err != nil {
434 return err
435 }
436 c.seats[name] = &seat{proxy: p, notifications: map[time.Duration]*notification{}}
437 }
438 if g.Interface == core.OutputInterface && c.outputs[name] == nil {
439 c.next++
440 o := &output{global: name, lifetime: c.next, proxy: core.NewOutput(c.d.Context())}
441 o.proxy.OnName(func(s string) { c.enqueue(notice{kind: 4, output: o, name: s}) })
442 if err := c.bind(g, 4, o.proxy); err != nil {
443 return err
444 }
445 c.outputs[name] = o
446 }
447 }
448 for name, s := range c.seats {
449 for d, p := range s.notifications {
450 if !c.deadlines[d] {
451 if err := c.destroyNotification(p); err != nil {
452 return err
453 }
454 delete(s.notifications, d)
455 } else {
456 p.generation = c.generation
457 }
458 }
459 if c.idle != nil {
460 for d := range c.deadlines {
461 if s.notifications[d] == nil {
462 p, err := c.newNotification(name, s, d, false)
463 if err != nil {
464 return err
465 }
466 s.notifications[d] = p
467 }
468 }
469 if c.options.Requirements.InputWake && s.wake == nil {
470 p, err := c.newNotification(name, s, 0, true)
471 if err != nil {
472 return err
473 }
474 s.wake = p
475 }
476 }
477 }
478 for _, o := range c.outputs {
479 if c.power != nil && o.power == nil && !o.failed {
480 p, err := c.power.GetOutputPower(o.proxy)
481 if err != nil {
482 return err
483 }
484 o.power = p
485 p.OnMode(func(mode uint32) { c.enqueue(notice{kind: 6, output: o, mode: mode}) })
486 p.OnFailed(func() { c.enqueue(notice{kind: 5, output: o}) })
487 }
488 }
489 c.dirty = true
490 return nil
491}
492func (c *Client) newNotification(name uint32, s *seat, d time.Duration, wake bool) (*notification, error) {
493 var proxy *idlenotify.ExtIdleNotification
494 var err error
495 if wake {
496 proxy, err = c.idle.GetInputIdleNotification(0, s.proxy)
497 } else {
498 proxy, err = c.idle.GetIdleNotification(uint32(d/time.Millisecond), s.proxy)
499 }
500 if err != nil {
501 return nil, err
502 }
503 c.next++
504 p := ¬ification{proxy: proxy, token: c.next, cycle: 1, generation: c.generation, seat: name, after: d, active: true, wake: wake}
505 proxy.OnIdled(func() { c.enqueue(notice{kind: 2, notification: p}) })
506 proxy.OnResumed(func() { c.enqueue(notice{kind: 3, notification: p}) })
507 return p, nil
508}
509func (c *Client) destroyNotification(p *notification) error {
510 if p == nil {
511 return nil
512 }
513 p.active = false
514 return p.proxy.Destroy()
515}
516func (c *Client) setPower(on bool) error {
517 for _, o := range c.outputs {
518 if o.power == nil || o.failed {
519 continue
520 }
521 if on && !o.off {
522 continue
523 }
524 if !on && (o.off || o.modeKnown && o.mode == outputpower.MODE_OFF) {
525 continue // never claim an output somebody else already turned off
526 }
527 mode := uint32(outputpower.MODE_OFF)
528 if on {
529 mode = outputpower.MODE_ON
530 }
531 if err := o.power.SetMode(mode); err != nil {
532 return err
533 }
534 o.off = !on
535 }
536 return nil
537}
538func (c *Client) capabilities() ports.Capabilities {
539 result := ports.Capabilities{Globals: map[string]uint32{}}
540 for _, g := range c.d.Registry().GetGlobals() {
541 result.Globals[g.Interface] = max(result.Globals[g.Interface], g.Version)
542 }
543 for name := range c.seats {
544 result.Seats = append(result.Seats, name)
545 }
546 sort.Slice(result.Seats, func(i, j int) bool { return result.Seats[i] < result.Seats[j] })
547 for _, o := range c.outputs {
548 result.Outputs = append(result.Outputs, ports.WaylandOutput{Lifetime: o.lifetime, Global: o.global, Name: o.name, PowerSupported: o.power != nil && !o.failed})
549 }
550 sort.Slice(result.Outputs, func(i, j int) bool { return result.Outputs[i].Global < result.Outputs[j].Global })
551 require := func(enabled bool, name string, version uint32) {
552 if enabled && result.Globals[name] < version {
553 result.Missing = append(result.Missing, fmt.Sprintf("%s >= %d", name, version))
554 }
555 }
556 r := c.options.Requirements
557 require(r.Idle || r.InputWake || len(c.deadlines) > 0, idlenotify.ExtIdleNotifierInterface, 2)
558 require(r.Idle || r.InputWake || len(c.deadlines) > 0, core.SeatInterface, 1)
559 require(r.OutputPower, outputpower.OutputPowerManagerInterface, 1)
560 require(r.OutputPower || r.Visual || r.Lock, core.OutputInterface, 1)
561 if r.OutputPower {
562 for _, o := range c.outputs {
563 if o.failed {
564 result.Missing = append(result.Missing, fmt.Sprintf("%s output lifetime %d", outputpower.OutputPowerInterface, o.lifetime))
565 }
566 }
567 }
568 require(r.Lock, "ext_session_lock_manager_v1", 1)
569 require(r.Visual, "wl_compositor", 1)
570 require(r.Visual, "zwp_linux_dmabuf_v1", 4)
571 require(r.Visual, "wp_linux_drm_syncobj_manager_v1", 1)
572 sort.Strings(result.Missing)
573 return result
574}
575
576func (c *Client) command(cmd ports.WaylandCommand) error {
577 switch v := cmd.(type) {
578 case ports.SetDeadlines:
579 ds, err := normalize(v.Deadlines)
580 if err != nil {
581 return err
582 }
583 c.deadlines, c.generation = ds, v.Generation
584 return c.reconcile()
585 case ports.OutputPower:
586 return c.setPower(v.On)
587 }
588 return nil
589}