1package wayland
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "testing"
8 "time"
9
10 dl "github.com/bnema/go-wayland-bindings/server/drmlease"
11 "github.com/bnema/neferwl/internal/logging"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/bnema/purego-libwayland/server"
14 "github.com/bnema/wlturbo"
15 "golang.org/x/sys/unix"
16)
17
18type leaseDeviceEvents struct {
19 wlturbo.BaseProxy
20 c *wlturbo.Display
21 connectors []uint32
22 fds []int
23}
24
25func (p *leaseDeviceEvents) Dispatch(e *wlturbo.Event) {
26 switch e.Opcode {
27 case uint16(dl.WpDrmLeaseDeviceV1EventDrmFd):
28 p.fds = append(p.fds, int(e.Fd()))
29 case uint16(dl.WpDrmLeaseDeviceV1EventConnector):
30 id := e.Uint32()
31 p.connectors = append(p.connectors, id)
32 child := &leaseConnectorEvents{}
33 child.SetID(id)
34 registerWireProxy(p.c, child)
35 }
36}
37
38type leaseConnectorEvents struct {
39 wlturbo.BaseProxy
40 name string
41}
42
43func (p *leaseConnectorEvents) Dispatch(e *wlturbo.Event) {
44 if e.Opcode == uint16(dl.WpDrmLeaseConnectorV1EventName) {
45 p.name = e.String()
46 }
47}
48
49type leaseResultEvents struct {
50 wlturbo.BaseProxy
51 fds []int
52 finished int
53}
54
55func (p *leaseResultEvents) Dispatch(e *wlturbo.Event) {
56 switch e.Opcode {
57 case uint16(dl.WpDrmLeaseV1EventLeaseFd):
58 p.fds = append(p.fds, int(e.Fd()))
59 case uint16(dl.WpDrmLeaseV1EventFinished):
60 p.finished++
61 }
62}
63
64// leaseFD opens a device fd for one LeaseConnectors message; the server
65// owns and closes it.
66func leaseFD(t *testing.T) *os.File {
67 t.Helper()
68 f, err := os.Open("/dev/null")
69 if err != nil {
70 t.Fatal(err)
71 }
72 return f
73}
74func leaseServer(t *testing.T) (*wlturbo.Display, chan ports.LeaseMessage, chan ports.LeaseMessage) {
75 c, requests, events, _ := leaseServerWithState(t)
76 return c, requests, events
77}
78func leaseServerWithState(t *testing.T) (*wlturbo.Display, chan ports.LeaseMessage, chan ports.LeaseMessage, *Server) {
79 t.Helper()
80 dir := t.TempDir()
81 requests := make(chan ports.LeaseMessage, 16)
82 events := make(chan ports.LeaseMessage, 16)
83 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{LeaseRequests: requests, LeaseEvents: events}, logging.For(context.Background(), "wayland"))
84 if err != nil {
85 t.Fatal(err)
86 }
87 ctx, cancel := context.WithCancel(context.Background())
88 done := make(chan error, 1)
89 go func() { done <- s.Run(ctx) }()
90 t.Cleanup(func() {
91 cancel()
92 select {
93 case err := <-done:
94 if err != nil {
95 t.Error(err)
96 }
97 case <-time.After(3 * time.Second):
98 t.Error("server hung")
99 }
100 })
101 c := protocolClient(t, s, dir)
102 events <- ports.LeaseConnectors{Card: "/dev/dri/card-test", Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: "/dev/dri/card-test", Name: "DP-1", Description: "VR headset", ConnectorID: 42}}}
103 for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
104 if err := c.Roundtrip(); err != nil {
105 t.Fatal(err)
106 }
107 if _, ok := c.Registry().FindGlobal("wp_drm_lease_device_v1"); ok {
108 return c, requests, events, s
109 }
110 time.Sleep(time.Millisecond)
111 }
112 t.Fatal("no lease global")
113 return nil, nil, nil, nil
114}
115func TestDRMLeaseFlow(t *testing.T) {
116 c, requests, events := leaseServer(t)
117 dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
118 p := &leaseDeviceEvents{c: c}
119 p.SetID(dev)
120 registerWireProxy(c, p)
121 if err := c.Roundtrip(); err != nil {
122 t.Fatal(err)
123 }
124 if len(p.fds) != 1 || len(p.connectors) != 1 {
125 t.Fatalf("device: fds=%v connectors=%v", p.fds, p.connectors)
126 }
127 defer unix.Close(p.fds[0])
128 conn := p.connectors[0]
129 req := c.AllocateID()
130 registerProtocol(t, c, req)
131 requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
132 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
133 lease := c.AllocateID()
134 l := &leaseResultEvents{}
135 l.SetID(lease)
136 registerWireProxy(c, l)
137 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
138 if err := c.Roundtrip(); err != nil {
139 t.Fatal(err)
140 }
141 var got ports.LeaseRequest
142 select {
143 case ev := <-requests:
144 var ok bool
145 got, ok = ev.(ports.LeaseRequest)
146 if !ok {
147 t.Fatalf("message %T", ev)
148 }
149 case <-time.After(2 * time.Second):
150 t.Fatal("no request")
151 }
152 if len(got.Connectors) != 1 || got.Connectors[0] != "DP-1" {
153 t.Fatalf("request %+v", got)
154 }
155 r, w, err := os.Pipe()
156 if err != nil {
157 t.Fatal(err)
158 }
159 defer r.Close()
160 events <- ports.LeaseReply{ID: got.ID, FD: w, LeaseID: 7}
161 if err := c.Roundtrip(); err != nil {
162 t.Fatal(err)
163 }
164 for deadline := time.Now().Add(2 * time.Second); len(l.fds) == 0 && time.Now().Before(deadline); {
165 if err := c.Roundtrip(); err != nil {
166 t.Fatal(err)
167 }
168 }
169 if len(l.fds) != 1 {
170 t.Fatalf("lease fd count %d", len(l.fds))
171 }
172 defer unix.Close(l.fds[0])
173 requestProtocol(t, c, lease, dl.WpDrmLeaseV1RequestDestroy)
174 if err := c.Roundtrip(); err != nil {
175 t.Fatal(err)
176 }
177 select {
178 case ev := <-requests:
179 if v, ok := ev.(ports.LeaseRevoke); !ok || v.LeaseID != 7 {
180 t.Fatalf("revoke %v", ev)
181 }
182 case <-time.After(2 * time.Second):
183 t.Fatal("no revoke")
184 }
185}
186func TestDRMLeaseErrors(t *testing.T) {
187 for _, tc := range []struct {
188 name string
189 duplicate bool
190 code uint32
191 }{{"empty", false, uint32(dl.WpDrmLeaseRequestV1ErrorEmptyLease)}, {"duplicate", true, uint32(dl.WpDrmLeaseRequestV1ErrorDuplicateConnector)}} {
192 t.Run(tc.name, func(t *testing.T) {
193 c, _, _ := leaseServer(t)
194 dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
195 p := &leaseDeviceEvents{c: c}
196 p.SetID(dev)
197 registerWireProxy(c, p)
198 if err := c.Roundtrip(); err != nil {
199 t.Fatal(err)
200 }
201 for _, fd := range p.fds {
202 defer unix.Close(fd)
203 }
204 req := c.AllocateID()
205 registerProtocol(t, c, req)
206 requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
207 if tc.duplicate {
208 conn := p.connectors[0]
209 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
210 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
211 } else {
212 lease := c.AllocateID()
213 registerProtocol(t, c, lease)
214 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
215 }
216 expectProtocolError(t, c, req, tc.code)
217 })
218 }
219}
220func TestDRMLeaseFinished(t *testing.T) {
221 c, requests, events := leaseServer(t)
222 dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
223 p := &leaseDeviceEvents{c: c}
224 p.SetID(dev)
225 registerWireProxy(c, p)
226 if err := c.Roundtrip(); err != nil {
227 t.Fatal(err)
228 }
229 defer unix.Close(p.fds[0])
230 req := c.AllocateID()
231 registerProtocol(t, c, req)
232 requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
233 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
234 lease := c.AllocateID()
235 l := &leaseResultEvents{}
236 l.SetID(lease)
237 registerWireProxy(c, l)
238 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
239 if err := c.Roundtrip(); err != nil {
240 t.Fatal(err)
241 }
242 got := (<-requests).(ports.LeaseRequest)
243 r, w, err := os.Pipe()
244 if err != nil {
245 t.Fatal(err)
246 }
247 defer r.Close()
248 events <- ports.LeaseReply{ID: got.ID, FD: w, LeaseID: 8}
249 if err := c.Roundtrip(); err != nil {
250 t.Fatal(err)
251 }
252 for deadline := time.Now().Add(2 * time.Second); len(l.fds) == 0 && time.Now().Before(deadline); {
253 if err := c.Roundtrip(); err != nil {
254 t.Fatal(err)
255 }
256 }
257 if len(l.fds) != 1 {
258 t.Fatal("missing lease fd")
259 }
260 defer unix.Close(l.fds[0])
261 events <- ports.LeaseFinished{Card: got.Card, LeaseID: 8}
262 if err := c.Roundtrip(); err != nil {
263 t.Fatal(err)
264 }
265 for deadline := time.Now().Add(2 * time.Second); l.finished == 0 && time.Now().Before(deadline); {
266 if err := c.Roundtrip(); err != nil {
267 t.Fatal(err)
268 }
269 }
270 if l.finished != 1 {
271 t.Fatalf("finished %d", l.finished)
272 }
273}
274
275func TestLeaseMessagesRemainOrderedWhenChannelFull(t *testing.T) {
276 requests := make(chan ports.LeaseMessage, 1)
277 ready := make(chan struct{}, 1)
278 display, err := server.NewDisplay()
279 if err != nil {
280 t.Fatal(err)
281 }
282 defer display.Close()
283 s := &Server{display: display, channels: Channels{LeaseRequests: requests}, leaseReady: ready}
284 requests <- ports.LeaseRequest{ID: 99}
285 ctx, cancel := context.WithCancel(context.Background())
286 done := make(chan struct{})
287 go func() { s.forwardLeaseRequests(ctx); close(done) }()
288 defer func() { cancel(); <-done }()
289 s.sendLeaseRequest(ports.LeaseRequest{ID: 1})
290 s.sendLeaseRequest(ports.LeaseRevoke{Card: "card", LeaseID: 1})
291 if got := <-requests; got.(ports.LeaseRequest).ID != 99 {
292 t.Fatal(got)
293 }
294 select {
295 case got := <-requests:
296 if v, ok := got.(ports.LeaseRequest); !ok || v.ID != 1 {
297 t.Fatalf("first %v", got)
298 }
299 case <-time.After(time.Second):
300 t.Fatal("request timeout")
301 }
302 select {
303 case got := <-requests:
304 if v, ok := got.(ports.LeaseRevoke); !ok || v.LeaseID != 1 {
305 t.Fatalf("second %v", got)
306 }
307 case <-time.After(time.Second):
308 t.Fatal("revoke timeout")
309 }
310}
311
312func TestLeaseIDsAreScopedToCard(t *testing.T) {
313 dir := t.TempDir()
314 requests := make(chan ports.LeaseMessage, 16)
315 events := make(chan ports.LeaseMessage, 16)
316 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{LeaseRequests: requests, LeaseEvents: events}, logging.For(context.Background(), "wayland"))
317 if err != nil {
318 t.Fatal(err)
319 }
320 ctx, cancel := context.WithCancel(context.Background())
321 done := make(chan error, 1)
322 go func() { done <- s.Run(ctx) }()
323 t.Cleanup(func() {
324 cancel()
325 if err := <-done; err != nil {
326 t.Error(err)
327 }
328 })
329 c := protocolClient(t, s, dir)
330 for i, card := range []string{"card-one", "card-two"} {
331 events <- ports.LeaseConnectors{Card: card, Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: card, Name: fmt.Sprintf("DP-%d", i+1), ConnectorID: uint32(i + 40)}}}
332 }
333 for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
334 if err := c.Roundtrip(); err != nil {
335 t.Fatal(err)
336 }
337 count := 0
338 for _, g := range c.Registry().GetGlobals() {
339 if g.Interface == "wp_drm_lease_device_v1" {
340 count++
341 }
342 }
343 if count == 2 {
344 break
345 }
346 }
347 var devices []uint32
348 for _, g := range c.Registry().GetGlobals() {
349 if g.Interface == "wp_drm_lease_device_v1" {
350 id, err := bindWireID(c, g.Name, g.Interface, 1)
351 if err != nil {
352 t.Fatal(err)
353 }
354 devices = append(devices, id)
355 }
356 }
357 if len(devices) != 2 {
358 t.Fatalf("device globals: %d", len(devices))
359 }
360 var bound []*leaseDeviceEvents
361 for _, dev := range devices {
362 p := &leaseDeviceEvents{c: c}
363 p.SetID(dev)
364 registerWireProxy(c, p)
365 bound = append(bound, p)
366 }
367 if err := c.Roundtrip(); err != nil {
368 t.Fatal(err)
369 }
370 for i, dev := range devices {
371 p := bound[i]
372 if len(p.connectors) != 1 || len(p.fds) != 1 {
373 t.Fatalf("device %d: %+v", dev, p)
374 }
375 defer unix.Close(p.fds[0])
376 req := c.AllocateID()
377 registerProtocol(t, c, req)
378 requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
379 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
380 id := c.AllocateID()
381 l := &leaseResultEvents{}
382 l.SetID(id)
383 registerWireProxy(c, l)
384 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, id)
385 }
386 if err := c.Roundtrip(); err != nil {
387 t.Fatal(err)
388 }
389 first := (<-requests).(ports.LeaseRequest)
390 second := (<-requests).(ports.LeaseRequest)
391 if first.Card == second.Card {
392 t.Fatalf("same card: %v %v", first, second)
393 }
394 for _, req := range []ports.LeaseRequest{first, second} {
395 r, w, err := os.Pipe()
396 if err != nil {
397 t.Fatal(err)
398 }
399 defer r.Close()
400 events <- ports.LeaseReply{ID: req.ID, FD: w, LeaseID: 7}
401 }
402 if err := c.Roundtrip(); err != nil {
403 t.Fatal(err)
404 }
405 for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
406 count := 0
407 s.display.Do(func() { count = len(s.activeLeases) })
408 if count == 2 {
409 break
410 }
411 if err := c.Roundtrip(); err != nil {
412 t.Fatal(err)
413 }
414 }
415 if !s.display.Do(func() {
416 if len(s.activeLeases) != 2 {
417 t.Errorf("active leases %v", s.activeLeases)
418 }
419 }) {
420 t.Fatal("display stopped")
421 }
422 events <- ports.LeaseFinished{Card: first.Card, LeaseID: 7}
423 if err := c.Roundtrip(); err != nil {
424 t.Fatal(err)
425 }
426 for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
427 count := 0
428 s.display.Do(func() { count = len(s.activeLeases) })
429 if count == 1 {
430 break
431 }
432 if err := c.Roundtrip(); err != nil {
433 t.Fatal(err)
434 }
435 }
436 if !s.display.Do(func() {
437 if len(s.activeLeases) != 1 || s.activeLeases[leaseKey{second.Card, 7}] == nil {
438 t.Errorf("wrong card finished: %v", s.activeLeases)
439 }
440 }) {
441 t.Fatal("display stopped")
442 }
443}
444
445func TestLeaseDestroyPendingRevokesLateReply(t *testing.T) {
446 c, requests, events, s := leaseServerWithState(t)
447 dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
448 p := &leaseDeviceEvents{c: c}
449 p.SetID(dev)
450 registerWireProxy(c, p)
451 if err := c.Roundtrip(); err != nil {
452 t.Fatal(err)
453 }
454 defer unix.Close(p.fds[0])
455 req := c.AllocateID()
456 registerProtocol(t, c, req)
457 requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
458 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
459 lease := c.AllocateID()
460 registerProtocol(t, c, lease)
461 requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
462 if err := c.Roundtrip(); err != nil {
463 t.Fatal(err)
464 }
465 var sent ports.LeaseRequest
466 select {
467 case msg := <-requests:
468 sent = msg.(ports.LeaseRequest)
469 case <-time.After(time.Second):
470 t.Fatal("no lease request")
471 }
472 requestProtocol(t, c, lease, dl.WpDrmLeaseV1RequestDestroy)
473 if err := c.Roundtrip(); err != nil {
474 t.Fatal(err)
475 }
476 if !s.display.Do(func() {
477 if len(s.pendingLeases) != 0 || s.pendingLeaseCards[sent.ID] != sent.Card {
478 t.Errorf("pending tombstone: %v %v", s.pendingLeases, s.pendingLeaseCards)
479 }
480 }) {
481 t.Fatal("display stopped")
482 }
483 if !safelyLeaseMaps(t, c, events, requests, sent) {
484 t.Fatal("pending reply not revoked")
485 }
486 if !s.display.Do(func() {
487 if len(s.pendingLeases) != 0 || len(s.pendingLeaseCards) != 0 {
488 t.Errorf("pending leaked: %v %v", s.pendingLeases, s.pendingLeaseCards)
489 }
490 }) {
491 t.Fatal("display stopped")
492 }
493}
494
495// A destroyed lease keeps only the card tombstone until the DRM reply arrives.
496func safelyLeaseMaps(t *testing.T, c *wlturbo.Display, events chan ports.LeaseMessage, requests chan ports.LeaseMessage, sent ports.LeaseRequest) bool {
497 t.Helper()
498 r, w, err := os.Pipe()
499 if err != nil {
500 t.Fatal(err)
501 }
502 defer r.Close()
503 events <- ports.LeaseReply{ID: sent.ID, FD: w, LeaseID: 51}
504 for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
505 if err := c.Roundtrip(); err != nil {
506 t.Fatal(err)
507 }
508 select {
509 case event := <-requests:
510 revoked, ok := event.(ports.LeaseRevoke)
511 return ok && revoked.Card == sent.Card && revoked.LeaseID == 51
512 default:
513 }
514 }
515 return false
516}
517
518func TestLeaseGlobalRemoveAndReadd(t *testing.T) {
519 c, _, events := leaseServer(t)
520 count := func() int {
521 n := 0
522 for _, g := range c.Registry().GetGlobals() {
523 if g.Interface == "wp_drm_lease_device_v1" {
524 n++
525 }
526 }
527 return n
528 }
529 if count() != 1 {
530 t.Fatalf("initial globals %d", count())
531 }
532 events <- ports.LeaseConnectors{Card: "/dev/dri/card-test"}
533 for deadline := time.Now().Add(2 * time.Second); count() != 0 && time.Now().Before(deadline); {
534 if err := c.Roundtrip(); err != nil {
535 t.Fatal(err)
536 }
537 }
538 if count() != 0 {
539 t.Fatal("lease global not removed")
540 }
541 events <- ports.LeaseConnectors{Card: "/dev/dri/card-test", Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: "/dev/dri/card-test", Name: "DP-1", ConnectorID: 42}}}
542 for deadline := time.Now().Add(2 * time.Second); count() != 1 && time.Now().Before(deadline); {
543 if err := c.Roundtrip(); err != nil {
544 t.Fatal(err)
545 }
546 }
547 if count() != 1 {
548 t.Fatal("lease global not re-added")
549 }
550}