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