1package app
2
3import (
4 "context"
5 "os"
6 "slices"
7
8 "github.com/bnema/neferwl/internal/ports"
9 "github.com/bnema/zerowrap"
10 "golang.org/x/sys/unix"
11)
12
13// leaseInventoryCard is what the lease publisher reads from a DRM card.
14type leaseInventoryCard interface {
15 Path() string
16 FinishedLeases() []uint32
17 Leasable() []ports.LeaseConnector
18 ClientFD() (*os.File, error)
19}
20
21// leasePublisher tells wayland which connectors of each card can be leased
22// (wp_drm_lease_device_v1), and which leases ended. It keeps one non-master
23// client fd per advertised card and resends an inventory only when it
24// changed. The DRM backend loop owns it.
25type leasePublisher struct {
26 ctx context.Context
27 events chan<- ports.LeaseMessage
28 log zerowrap.Logger
29 // seatActive and protected say whether leases may be offered: only on an
30 // active seat in an unprotected session.
31 seatActive, protected func() bool
32 clientFDs map[leaseInventoryCard]*os.File
33 last map[leaseInventoryCard][]ports.LeaseConnector
34}
35
36func newLeasePublisher(ctx context.Context, events chan<- ports.LeaseMessage, seatActive, protected func() bool, log zerowrap.Logger) *leasePublisher {
37 return &leasePublisher{ctx: ctx, events: events, seatActive: seatActive, protected: protected, log: log, clientFDs: map[leaseInventoryCard]*os.File{}, last: map[leaseInventoryCard][]ports.LeaseConnector{}}
38}
39
40// send delivers msg, closing its files when ctx ends first.
41func (p *leasePublisher) send(msg ports.LeaseMessage) {
42 select {
43 case p.events <- msg:
44 case <-p.ctx.Done():
45 ports.CloseLeaseFiles(msg)
46 }
47}
48
49// publish reports the finished leases of c and its leasable connectors. An
50// inactive seat or a protected session withdraws the inventory; protection
51// always tells wayland so.
52func (p *leasePublisher) publish(c leaseInventoryCard) {
53 for _, id := range c.FinishedLeases() {
54 p.send(ports.LeaseFinished{Card: c.Path(), LeaseID: id})
55 }
56 var next []ports.LeaseConnector
57 if p.seatActive() && !p.protected() {
58 next = c.Leasable()
59 }
60 if len(next) == 0 {
61 if p.protected() || p.clientFDs[c] != nil {
62 p.send(ports.LeaseConnectors{Card: c.Path()})
63 }
64 if p.clientFDs[c] != nil {
65 p.clientFDs[c].Close()
66 delete(p.clientFDs, c)
67 }
68 delete(p.last, c)
69 return
70 }
71 if slices.Equal(p.last[c], next) && p.clientFDs[c] != nil {
72 return
73 }
74 if p.clientFDs[c] == nil {
75 f, err := c.ClientFD()
76 if err != nil {
77 p.log.Warn().Err(err).Str("card", c.Path()).Msg("lease client fd")
78 return
79 }
80 p.clientFDs[c] = f
81 }
82 // Each message owns its fd: a queued inventory outlives clientFDs[c].
83 fd, err := unix.FcntlInt(p.clientFDs[c].Fd(), unix.F_DUPFD_CLOEXEC, 0)
84 if err != nil {
85 p.log.Warn().Err(err).Str("card", c.Path()).Msg("lease client fd")
86 return
87 }
88 p.last[c] = next
89 p.send(ports.LeaseConnectors{Card: c.Path(), Device: os.NewFile(uintptr(fd), c.Path()), Connectors: next})
90}
91
92// close releases the client fds.
93func (p *leasePublisher) close() {
94 for _, f := range p.clientFDs {
95 f.Close()
96 }
97 clear(p.clientFDs)
98}