1package wayland
2
3import (
4 "context"
5 "fmt"
6 "image"
7 "reflect"
8 "slices"
9 "strings"
10 "testing"
11 "time"
12
13 source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
14 ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
15
16 "github.com/bnema/go-wayland-bindings/server/wayland"
17 wlr "github.com/bnema/go-wayland-bindings/server/wlrscreencopy"
18 "github.com/bnema/neferwl/internal/logging"
19 "github.com/bnema/neferwl/internal/ports"
20 "github.com/bnema/wlturbo"
21 "golang.org/x/sys/unix"
22)
23
24type captureEvents struct {
25 wlturbo.BaseProxy
26 events chan uint16
27}
28
29func (p *captureEvents) Dispatch(e *wlturbo.Event) { p.events <- e.Opcode }
30func TestCaptureRegion(t *testing.T) {
31 o := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Width: 300, Height: 200}, Scale: 1.5}}
32 for _, tc := range []struct{ in, want image.Rectangle }{
33 {image.Rect(0, 0, 100, 100), image.Rect(0, 0, 150, 150)},
34 {image.Rect(-10, -10, 10, 10), image.Rect(0, 0, 15, 15)},
35 {image.Rect(190, 120, 240, 160), image.Rect(285, 180, 300, 200)},
36 } {
37 if got := captureRegion(o, tc.in); got != tc.want {
38 t.Errorf("%v: %v != %v", tc.in, got, tc.want)
39 }
40 }
41}
42func TestScreencopyProtocol(t *testing.T) {
43 dir := t.TempDir()
44 requests := make(chan ports.CaptureRequest, 1)
45 replies := make(chan ports.CaptureDone, 1)
46 s, err := New(Options{RuntimeDir: dir, CaptureAllow: allowStore(t, "*\n"), Outputs: ports.Layout{{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 4, Height: 4}, Width: 4, Height: 4, Scale: 1}}}, Channels{Captures: requests, Captured: replies}, logging.For(context.Background(), "wayland"))
47 if err != nil {
48 t.Fatal(err)
49 }
50 ctx, cancel := context.WithCancel(context.Background())
51 done := make(chan error, 1)
52 go func() { done <- s.Run(ctx) }()
53 defer func() {
54 cancel()
55 if err := <-done; err != nil {
56 t.Error(err)
57 }
58 }()
59 c := protocolClient(t, s, dir)
60 var out uint32
61 for name, g := range c.Registry().GetGlobals() {
62 if g.Interface == "wl_output" {
63 out = name
64 }
65 }
66 if out == 0 {
67 t.Fatal("output global missing")
68 }
69 outputID, err := bindWireID(c, out, "wl_output", 4)
70 if err != nil {
71 t.Fatal(err)
72 }
73 registerProtocol(t, c, outputID)
74 manager := bindVersion(t, c, "zwlr_screencopy_manager_v1", 3)
75 frame := c.AllocateID()
76 events := &captureEvents{events: make(chan uint16, 8)}
77 events.SetID(frame)
78 registerWireProxy(c, events)
79 requestProtocol(t, c, manager, wlr.ZwlrScreencopyManagerV1RequestCaptureOutputRegion, frame, int32(0), outputID, int32(1), int32(1), int32(2), int32(2))
80 if err := c.Roundtrip(); err != nil {
81 t.Fatal(err)
82 }
83 if op := <-events.events; op != uint16(wlr.ZwlrScreencopyFrameV1EventBuffer) {
84 t.Fatalf("buffer: %d", op)
85 }
86 if op := <-events.events; op != uint16(wlr.ZwlrScreencopyFrameV1EventBufferDone) {
87 t.Fatalf("done: %d", op)
88 }
89 shm := bindProtocol(t, c, "wl_shm")
90 registerProtocol(t, c, shm)
91 fd, err := unix.MemfdCreate("capture", 0)
92 if err != nil {
93 t.Fatal(err)
94 }
95 defer unix.Close(fd)
96 if err := unix.Ftruncate(fd, 16); err != nil {
97 t.Fatal(err)
98 }
99 pool, buf := c.AllocateID(), c.AllocateID()
100 registerProtocol(t, c, pool)
101 registerProtocol(t, c, buf)
102 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16)); err != nil {
103 t.Fatal(err)
104 }
105 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(2), int32(2), int32(8), uint32(wayland.ShmFormatXrgb8888))
106 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopyWithDamage, buf)
107 if err := c.Roundtrip(); err != nil {
108 t.Fatal(err)
109 }
110 select {
111 case req := <-requests:
112 if req.Region != image.Rect(1, 1, 3, 3) {
113 t.Errorf("region: %v", req.Region)
114 }
115 if _, err := req.Dst.File.WriteAt([]byte{1, 2, 3, 255}, 0); err != nil {
116 t.Fatal(err)
117 }
118 if err := req.Dst.File.Close(); err != nil {
119 t.Fatal(err)
120 }
121 deliverCapture(t, s, replies, ports.CaptureDone{ID: req.ID, Output: req.Output, Time: time.Now()})
122 case <-time.After(2 * time.Second):
123 t.Fatal("no request")
124 }
125 if err := c.Roundtrip(); err != nil {
126 t.Fatal(err)
127 }
128 for _, want := range []uint16{uint16(wlr.ZwlrScreencopyFrameV1EventFlags), uint16(wlr.ZwlrScreencopyFrameV1EventDamage), uint16(wlr.ZwlrScreencopyFrameV1EventReady)} {
129 var got uint16
130 select {
131 case got = <-events.events:
132 case <-time.After(2 * time.Second):
133 t.Fatalf("missing event %d", want)
134 }
135 if got != want {
136 t.Errorf("event %d, want %d", got, want)
137 }
138 }
139 data := make([]byte, 4)
140 if _, err := unix.Pread(fd, data, 0); err != nil {
141 t.Fatal(err)
142 }
143 if data[0] != 1 {
144 t.Fatal(data)
145 }
146}
147
148func captureTestClient(t *testing.T) (*Server, *wlturbo.Display, uint32) {
149 t.Helper()
150 s, _, _, dir := lifecycleServer(t)
151 c := protocolClient(t, s, dir)
152 g, ok := c.Registry().FindGlobal("wl_output")
153 if !ok {
154 t.Fatal("wl_output missing")
155 }
156 out, err := bindWireID(c, g.Name, g.Interface, 4)
157 if err != nil {
158 t.Fatal(err)
159 }
160 registerProtocol(t, c, out)
161 if err := c.Roundtrip(); err != nil {
162 t.Fatal(err)
163 }
164 return s, c, out
165}
166
167func captureTestExt(t *testing.T, c *wlturbo.Display, out uint32) (uint32, uint32, uint32) {
168 t.Helper()
169 sourceManager := bindProtocol(t, c, "ext_output_image_capture_source_manager_v1")
170 manager := bindProtocol(t, c, "ext_image_copy_capture_manager_v1")
171 src, session := c.AllocateID(), c.AllocateID()
172 registerProtocol(t, c, src)
173 requestProtocol(t, c, sourceManager, source.ExtOutputImageCaptureSourceManagerV1RequestCreateSource, src, out)
174 events := &captureEvents{events: make(chan uint16, 16)}
175 events.SetID(session)
176 registerWireProxy(c, events)
177 requestProtocol(t, c, manager, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, session, src, uint32(0))
178 if err := c.Roundtrip(); err != nil {
179 t.Fatal(err)
180 }
181 for _, want := range []uint16{0, 1, 1, 4} {
182 if got := <-events.events; got != want {
183 t.Fatalf("session event %d != %d", got, want)
184 }
185 }
186 return manager, session, src
187}
188
189func TestCaptureProtocolErrors(t *testing.T) {
190 for _, tc := range []struct {
191 name string
192 object string
193 code uint32
194 }{
195 {"wlr already used", "wlr", uint32(wlr.ZwlrScreencopyFrameV1ErrorAlreadyUsed)},
196 {"wlr invalid size", "wlr size", uint32(wlr.ZwlrScreencopyFrameV1ErrorInvalidBuffer)},
197 {"wlr invalid format", "wlr format", uint32(wlr.ZwlrScreencopyFrameV1ErrorInvalidBuffer)},
198 {"ext no buffer", "frame", uint32(ext.ExtImageCopyCaptureFrameV1ErrorNoBuffer)},
199 {"ext already captured", "frame twice", uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured)},
200 {"ext damage", "damage", uint32(ext.ExtImageCopyCaptureFrameV1ErrorInvalidBufferDamage)},
201 {"ext duplicate frame", "session", uint32(ext.ExtImageCopyCaptureSessionV1ErrorDuplicateFrame)},
202 {"ext invalid option", "manager", uint32(ext.ExtImageCopyCaptureManagerV1ErrorInvalidOption)},
203 {"ext duplicate cursor session", "cursor", uint32(ext.ExtImageCopyCaptureCursorSessionV1ErrorDuplicateSession)},
204 {"ext buffer constraints", "constraints", 0},
205 {"ext attach after capture", "attach after", uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured)},
206 {"ext damage after capture", "damage after", uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured)},
207 } {
208 t.Run(tc.name, func(t *testing.T) {
209 _, c, out := captureTestClient(t)
210 if strings.HasPrefix(tc.object, "wlr") {
211 manager := bindVersion(t, c, "zwlr_screencopy_manager_v1", 3)
212 frame := c.AllocateID()
213 registerProtocol(t, c, frame)
214 requestProtocol(t, c, manager, wlr.ZwlrScreencopyManagerV1RequestCaptureOutput, frame, int32(0), out)
215 if err := c.Roundtrip(); err != nil {
216 t.Fatal(err)
217 }
218 shm := bindProtocol(t, c, "wl_shm")
219 fd, err := unix.MemfdCreate("bad-capture", 0)
220 if err != nil {
221 t.Fatal(err)
222 }
223 defer unix.Close(fd)
224 if err := unix.Ftruncate(fd, int64(testOutputs[0].Info.Width*testOutputs[0].Info.Height*4)); err != nil {
225 t.Fatal(err)
226 }
227 pool, buf := c.AllocateID(), c.AllocateID()
228 registerProtocol(t, c, pool)
229 registerProtocol(t, c, buf)
230 registerProtocol(t, c, shm)
231 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(testOutputs[0].Info.Width*testOutputs[0].Info.Height*4)); err != nil {
232 t.Fatal(err)
233 }
234 w := int32(1)
235 format := uint32(wayland.ShmFormatXrgb8888)
236 if tc.object == "wlr format" {
237 format = uint32(wayland.ShmFormatArgb8888)
238 }
239 if tc.object != "wlr size" {
240 w = int32(testOutputs[0].Info.Width)
241 }
242 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), w, int32(testOutputs[0].Info.Height), w*4, format)
243 if tc.object == "wlr" {
244 if err := c.Roundtrip(); err != nil {
245 t.Fatal(err)
246 }
247 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
248 if err := c.Roundtrip(); err != nil {
249 t.Fatal(err)
250 }
251 }
252 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
253 expectProtocolError(t, c, frame, tc.code)
254 return
255 }
256 manager, session, src := captureTestExt(t, c, out)
257 switch tc.object {
258 case "manager":
259 requestProtocol(t, c, manager, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, c.AllocateID(), src, uint32(2))
260 expectProtocolError(t, c, manager, tc.code)
261 return
262 case "cursor":
263 seat := bindProtocol(t, c, "wl_seat")
264 pointer := c.AllocateID()
265 registerProtocol(t, c, pointer)
266 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
267 registerProtocol(t, c, seat)
268 cursor := c.AllocateID()
269 registerProtocol(t, c, cursor)
270 requestProtocol(t, c, manager, ext.ExtImageCopyCaptureManagerV1RequestCreatePointerCursorSession, cursor, src, pointer)
271 registerProtocol(t, c, manager)
272 sessionID := c.AllocateID()
273 registerProtocol(t, c, sessionID)
274 requestProtocol(t, c, cursor, ext.ExtImageCopyCaptureCursorSessionV1RequestGetCaptureSession, sessionID)
275 if err := c.Roundtrip(); err != nil {
276 t.Fatal(err)
277 }
278 requestProtocol(t, c, cursor, ext.ExtImageCopyCaptureCursorSessionV1RequestGetCaptureSession, c.AllocateID())
279 expectProtocolError(t, c, cursor, tc.code)
280 return
281 }
282 frame := c.AllocateID()
283 frameEvents := &captureDetails{events: make(chan []uint32, 4)}
284 frameEvents.SetID(frame)
285 registerWireProxy(c, frameEvents)
286 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame)
287 switch tc.object {
288 case "frame":
289 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
290 case "constraints", "attach after", "damage after", "frame twice":
291 shm := bindProtocol(t, c, "wl_shm")
292 registerProtocol(t, c, shm)
293 fd, err := unix.MemfdCreate("ext-capture", 0)
294 if err != nil {
295 t.Fatal(err)
296 }
297 defer unix.Close(fd)
298 if err := unix.Ftruncate(fd, 4); err != nil {
299 t.Fatal(err)
300 }
301 pool, buf := c.AllocateID(), c.AllocateID()
302 registerProtocol(t, c, pool)
303 registerProtocol(t, c, buf)
304 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
305 t.Fatal(err)
306 }
307 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(wayland.ShmFormatXrgb8888))
308 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
309 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
310 if tc.object == "constraints" {
311 if err := c.Roundtrip(); err != nil {
312 t.Fatal(err)
313 }
314 if got := <-frameEvents.events; !reflect.DeepEqual(got, []uint32{uint32(ext.ExtImageCopyCaptureFrameV1EventFailed), uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonBufferConstraints)}) {
315 t.Fatalf("constraints: event %v", got)
316 }
317 return
318 }
319 switch tc.object {
320 case "frame twice":
321 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
322 case "attach after":
323 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
324 case "damage after":
325 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestDamageBuffer, int32(0), int32(0), int32(1), int32(1))
326 }
327 case "damage":
328 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestDamageBuffer, int32(-1), int32(0), int32(1), int32(1))
329 case "session":
330 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, c.AllocateID())
331 }
332 object := frame
333 if tc.object == "session" {
334 object = session
335 }
336 expectProtocolError(t, c, object, tc.code)
337 })
338 }
339}
340
341// deliverCapture sends a capture reply and waits until the display
342// goroutine has run it, so a following Roundtrip sees its events.
343func deliverCapture(t *testing.T, s *Server, replies chan<- ports.CaptureDone, done ports.CaptureDone) {
344 t.Helper()
345 replies <- done
346 deadline := time.Now().Add(2 * time.Second)
347 for {
348 pending := false
349 if !s.display.Do(func() { _, pending = s.captureReplies[done.ID] }) {
350 t.Fatal("display stopped")
351 }
352 if !pending {
353 return
354 }
355 if time.Now().After(deadline) {
356 t.Fatal("capture reply not delivered")
357 }
358 time.Sleep(time.Millisecond)
359 }
360}
361
362// applyOutputs sends a layout and waits until the display goroutine has
363// applied it, so a following Roundtrip sees its events. A no-op Do alone can
364// run before the command goroutine has even received the command.
365func applyOutputs(t *testing.T, s *Server, commands chan<- ports.ClientCommand, c ports.SetOutputs) {
366 t.Helper()
367 commands <- c
368 deadline := time.Now().Add(2 * time.Second)
369 for {
370 applied := false
371 if !s.display.Do(func() {
372 applied = len(s.outputs) == len(c.Outputs) && slices.Equal(s.outputPlaces, c.Outputs)
373 }) {
374 t.Fatal("display stopped")
375 }
376 if applied {
377 return
378 }
379 if time.Now().After(deadline) {
380 t.Fatal("outputs not applied")
381 }
382 time.Sleep(time.Millisecond)
383 }
384}
385
386// captureDetails records protocol payloads, not just the event opcodes.
387type captureDetails struct {
388 wlturbo.BaseProxy
389 events chan []uint32
390}
391
392func (p *captureDetails) Dispatch(e *wlturbo.Event) {
393 n := 0
394 switch e.Opcode {
395 case 0:
396 n = 2
397 case 1:
398 n = 1
399 if len(e.Data()) >= 16 {
400 n = 4
401 }
402 case 2:
403 n = 3
404 case 4:
405 if len(e.Data()) >= 4 {
406 n = 1
407 }
408 }
409 values := []uint32{uint32(e.Opcode)}
410 for i := 0; i < n; i++ {
411 values = append(values, e.Uint32())
412 }
413 p.events <- values
414}
415func TestExtCaptureLifecycle(t *testing.T) {
416 dir := t.TempDir()
417 requests := make(chan ports.CaptureRequest, 1)
418 replies := make(chan ports.CaptureDone, 1)
419 commands := make(chan ports.ClientCommand, 4)
420 initial := ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 4, Height: 4}, Width: 4, Height: 4, Scale: 1}
421 s, err := New(Options{RuntimeDir: dir, CaptureAllow: allowStore(t, "*\n"), Outputs: ports.Layout{initial}}, Channels{Captures: requests, Captured: replies, Commands: commands}, logging.For(context.Background(), "wayland"))
422 if err != nil {
423 t.Fatal(err)
424 }
425 ctx, cancel := context.WithCancel(context.Background())
426 done := make(chan error, 1)
427 go func() { done <- s.Run(ctx) }()
428 defer func() {
429 cancel()
430 if err := <-done; err != nil {
431 t.Error(err)
432 }
433 }()
434 c := protocolClient(t, s, dir)
435 g, ok := c.Registry().FindGlobal("wl_output")
436 if !ok {
437 t.Fatal("output missing")
438 }
439 out, err := bindWireID(c, g.Name, g.Interface, 4)
440 if err != nil {
441 t.Fatal(err)
442 }
443 registerProtocol(t, c, out)
444 srcMgr := bindProtocol(t, c, "ext_output_image_capture_source_manager_v1")
445 mgr := bindProtocol(t, c, "ext_image_copy_capture_manager_v1")
446 src, session := c.AllocateID(), c.AllocateID()
447 registerProtocol(t, c, src)
448 p := &captureDetails{events: make(chan []uint32, 16)}
449 p.SetID(session)
450 registerWireProxy(c, p)
451 requestProtocol(t, c, srcMgr, source.ExtOutputImageCaptureSourceManagerV1RequestCreateSource, src, out)
452 requestProtocol(t, c, mgr, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, session, src, uint32(0))
453 if err := c.Roundtrip(); err != nil {
454 t.Fatal(err)
455 }
456 for _, want := range [][]uint32{{0, 4, 4}, {1, uint32(wayland.ShmFormatXrgb8888)}, {1, uint32(wayland.ShmFormatArgb8888)}, {4}} {
457 got := <-p.events
458 if !reflect.DeepEqual(got, want) {
459 t.Fatalf("session event %v != %v", got, want)
460 }
461 }
462 shm := bindProtocol(t, c, "wl_shm")
463 registerProtocol(t, c, shm)
464 fd, err := unix.MemfdCreate("ext-capture", 0)
465 if err != nil {
466 t.Fatal(err)
467 }
468 defer unix.Close(fd)
469 // Room for the 4x4 and the resized 5x4 buffers.
470 if err := unix.Ftruncate(fd, 80); err != nil {
471 t.Fatal(err)
472 }
473 pool, buf := c.AllocateID(), c.AllocateID()
474 registerProtocol(t, c, pool)
475 registerProtocol(t, c, buf)
476 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(80)); err != nil {
477 t.Fatal(err)
478 }
479 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(4), int32(4), int32(16), uint32(wayland.ShmFormatXrgb8888))
480 frame := c.AllocateID()
481 fp := &captureDetails{events: make(chan []uint32, 16)}
482 fp.SetID(frame)
483 registerWireProxy(c, fp)
484 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame)
485 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
486 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
487 if err := c.Roundtrip(); err != nil {
488 t.Fatal(err)
489 }
490 var req ports.CaptureRequest
491 select {
492 case req = <-requests:
493 case <-time.After(2 * time.Second):
494 t.Fatal("no capture request")
495 }
496 if req.Width != 4 || req.Height != 4 {
497 t.Fatalf("request %+v", req)
498 }
499 if err := req.Dst.File.Close(); err != nil {
500 t.Fatal(err)
501 }
502 var now unix.Timespec
503 if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
504 t.Fatal(err)
505 }
506 deliverCapture(t, s, replies, ports.CaptureDone{ID: req.ID, Time: time.Unix(now.Sec, now.Nsec)})
507 if err := c.Roundtrip(); err != nil {
508 t.Fatal(err)
509 }
510 for _, want := range [][]uint32{{0, 0, 0}, {1, 0, 0, 4, 4}, {2, 0, uint32(now.Sec), uint32(now.Nsec)}, {3}} {
511 got := <-fp.events
512 if !reflect.DeepEqual(got, want) {
513 t.Fatalf("frame event %v != %v", got, want)
514 }
515 }
516 resized := initial
517 resized.Info.Width = 5
518 resized.Width = 5
519 applyOutputs(t, s, commands, ports.SetOutputs{Outputs: ports.Layout{resized}})
520 if err := c.Roundtrip(); err != nil {
521 t.Fatal(err)
522 }
523 // A resize sends a complete constraint batch, formats included.
524 for _, want := range [][]uint32{{0, 5, 4}, {1, uint32(wayland.ShmFormatXrgb8888)}, {1, uint32(wayland.ShmFormatArgb8888)}, {4}} {
525 got := <-p.events
526 if !reflect.DeepEqual(got, want) {
527 t.Fatalf("resize event %v != %v", got, want)
528 }
529 }
530 // Destroying the session leaves its pending frame alive: it still gets ready.
531 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestDestroy) // one live frame per session
532 buf2, frame2 := c.AllocateID(), c.AllocateID()
533 registerProtocol(t, c, buf2)
534 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf2, int32(0), int32(5), int32(4), int32(20), uint32(wayland.ShmFormatXrgb8888))
535 fp2 := &captureDetails{events: make(chan []uint32, 16)}
536 fp2.SetID(frame2)
537 registerWireProxy(c, fp2)
538 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame2)
539 requestProtocol(t, c, frame2, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf2)
540 requestProtocol(t, c, frame2, ext.ExtImageCopyCaptureFrameV1RequestCapture)
541 if err := c.Roundtrip(); err != nil {
542 t.Fatal(err)
543 }
544 select {
545 case req = <-requests:
546 case <-time.After(2 * time.Second):
547 t.Fatal("no capture request after resize")
548 }
549 if err := req.Dst.File.Close(); err != nil {
550 t.Fatal(err)
551 }
552 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestDestroy)
553 if err := c.Roundtrip(); err != nil {
554 t.Fatal(err)
555 }
556 deliverCapture(t, s, replies, ports.CaptureDone{ID: req.ID, Time: time.Unix(now.Sec, now.Nsec)})
557 if err := c.Roundtrip(); err != nil {
558 t.Fatal(err)
559 }
560 for _, want := range [][]uint32{{0, 0, 0}, {1, 0, 0, 5, 4}, {2, 0, uint32(now.Sec), uint32(now.Nsec)}, {3}} {
561 select {
562 case got := <-fp2.events:
563 if !reflect.DeepEqual(got, want) {
564 t.Fatalf("frame after session destroy: %v != %v", got, want)
565 }
566 case <-time.After(2 * time.Second):
567 t.Fatalf("frame after session destroy: missing %v", want)
568 }
569 }
570}
571
572// The remaining stop check needs a live session.
573func TestExtCaptureStoppedOnOutputRemoval(t *testing.T) {
574 dir := t.TempDir()
575 commands := make(chan ports.ClientCommand, 4)
576 initial := ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 4, Height: 4}, Width: 4, Height: 4, Scale: 1}
577 s, err := New(Options{RuntimeDir: dir, CaptureAllow: allowStore(t, "*\n"), Outputs: ports.Layout{initial}}, Channels{Captures: make(chan ports.CaptureRequest, 1), Captured: make(chan ports.CaptureDone, 1), Commands: commands}, logging.For(context.Background(), "wayland"))
578 if err != nil {
579 t.Fatal(err)
580 }
581 ctx, cancel := context.WithCancel(context.Background())
582 done := make(chan error, 1)
583 go func() { done <- s.Run(ctx) }()
584 defer func() {
585 cancel()
586 if err := <-done; err != nil {
587 t.Error(err)
588 }
589 }()
590 c := protocolClient(t, s, dir)
591 g, ok := c.Registry().FindGlobal("wl_output")
592 if !ok {
593 t.Fatal("output missing")
594 }
595 out, err := bindWireID(c, g.Name, g.Interface, 4)
596 if err != nil {
597 t.Fatal(err)
598 }
599 registerProtocol(t, c, out)
600 srcMgr := bindProtocol(t, c, "ext_output_image_capture_source_manager_v1")
601 mgr := bindProtocol(t, c, "ext_image_copy_capture_manager_v1")
602 src, session := c.AllocateID(), c.AllocateID()
603 registerProtocol(t, c, src)
604 p := &captureDetails{events: make(chan []uint32, 16)}
605 p.SetID(session)
606 registerWireProxy(c, p)
607 requestProtocol(t, c, srcMgr, source.ExtOutputImageCaptureSourceManagerV1RequestCreateSource, src, out)
608 requestProtocol(t, c, mgr, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, session, src, uint32(0))
609 if err := c.Roundtrip(); err != nil {
610 t.Fatal(err)
611 }
612 for range 4 { // initial constraint batch
613 <-p.events
614 }
615 applyOutputs(t, s, commands, ports.SetOutputs{})
616 if err := c.Roundtrip(); err != nil {
617 t.Fatal(err)
618 }
619 if got := <-p.events; !reflect.DeepEqual(got, []uint32{5}) {
620 t.Fatalf("stopped: %v", got)
621 }
622}
623
624func TestWlrOutputGoneAndDestroyedBeforeReply(t *testing.T) {
625 for _, destroy := range []bool{false, true} {
626 t.Run(fmt.Sprint("destroy=", destroy), func(t *testing.T) {
627 dir := t.TempDir()
628 requests := make(chan ports.CaptureRequest, 1)
629 replies := make(chan ports.CaptureDone, 1)
630 commands := make(chan ports.ClientCommand, 2)
631 initial := ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 2, Height: 2}, Width: 2, Height: 2, Scale: 1}
632 s, err := New(Options{RuntimeDir: dir, CaptureAllow: allowStore(t, "*\n"), Outputs: ports.Layout{initial}}, Channels{Captures: requests, Captured: replies, Commands: commands}, logging.For(context.Background(), "wayland"))
633 if err != nil {
634 t.Fatal(err)
635 }
636 ctx, cancel := context.WithCancel(context.Background())
637 done := make(chan error, 1)
638 go func() { done <- s.Run(ctx) }()
639 defer func() {
640 cancel()
641 if err := <-done; err != nil {
642 t.Error(err)
643 }
644 }()
645 c := protocolClient(t, s, dir)
646 g, ok := c.Registry().FindGlobal("wl_output")
647 if !ok {
648 t.Fatal("output missing")
649 }
650 out, err := bindWireID(c, g.Name, g.Interface, 4)
651 if err != nil {
652 t.Fatal(err)
653 }
654 registerProtocol(t, c, out)
655 manager := bindVersion(t, c, "zwlr_screencopy_manager_v1", 3)
656 frame := c.AllocateID()
657 events := &captureEvents{events: make(chan uint16, 8)}
658 events.SetID(frame)
659 registerWireProxy(c, events)
660 requestProtocol(t, c, manager, wlr.ZwlrScreencopyManagerV1RequestCaptureOutput, frame, int32(0), out)
661 if err := c.Roundtrip(); err != nil {
662 t.Fatal(err)
663 }
664 if got := <-events.events; got != uint16(wlr.ZwlrScreencopyFrameV1EventBuffer) {
665 t.Fatal(got)
666 }
667 if got := <-events.events; got != uint16(wlr.ZwlrScreencopyFrameV1EventBufferDone) {
668 t.Fatal(got)
669 }
670 if !destroy {
671 applyOutputs(t, s, commands, ports.SetOutputs{})
672 shm, buf, fd := captureSmallBuffer(t, c)
673 defer unix.Close(fd)
674 _ = shm
675 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
676 if err := c.Roundtrip(); err != nil {
677 t.Fatal(err)
678 }
679 if got := <-events.events; got != uint16(wlr.ZwlrScreencopyFrameV1EventFailed) {
680 t.Fatal(got)
681 }
682 return
683 }
684 _, buf, fd := captureSmallBuffer(t, c)
685 defer unix.Close(fd)
686 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
687 if err := c.Roundtrip(); err != nil {
688 t.Fatal(err)
689 }
690 var req ports.CaptureRequest
691 select {
692 case req = <-requests:
693 case <-time.After(2 * time.Second):
694 t.Fatal("no request")
695 }
696 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestDestroy)
697 if err := c.Roundtrip(); err != nil {
698 t.Fatal(err)
699 }
700 if !s.display.Do(func() {
701 if _, ok := s.captureReplies[req.ID]; ok {
702 t.Error("reply retained after frame destroy")
703 }
704 }) {
705 t.Fatal("display stopped")
706 }
707 if err := req.Dst.File.Close(); err != nil {
708 t.Fatal(err)
709 }
710 if _, err := req.Dst.File.Stat(); err == nil {
711 t.Fatal("output descriptor open")
712 }
713 deliverCapture(t, s, replies, ports.CaptureDone{ID: req.ID})
714 if err := c.Roundtrip(); err != nil {
715 t.Fatal(err)
716 }
717 })
718 }
719}
720
721func captureSmallBuffer(t *testing.T, c *wlturbo.Display) (uint32, uint32, int) {
722 t.Helper()
723 shm := bindProtocol(t, c, "wl_shm")
724 registerProtocol(t, c, shm)
725 fd, err := unix.MemfdCreate("small-capture", 0)
726 if err != nil {
727 t.Fatal(err)
728 }
729 if err := unix.Ftruncate(fd, 16); err != nil {
730 t.Fatal(err)
731 }
732 pool, buf := c.AllocateID(), c.AllocateID()
733 registerProtocol(t, c, pool)
734 registerProtocol(t, c, buf)
735 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16)); err != nil {
736 t.Fatal(err)
737 }
738 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(2), int32(2), int32(8), uint32(wayland.ShmFormatXrgb8888))
739 return shm, buf, fd
740}