1package wayland
2
3import (
4 "context"
5 "image"
6 "strings"
7 "sync/atomic"
8 "testing"
9 "time"
10
11 source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
12 ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
13 extws "github.com/bnema/go-wayland-bindings/server/extworkspace"
14 "github.com/bnema/go-wayland-bindings/server/wayland"
15 "github.com/bnema/go-wayland-bindings/server/wlrlayershell"
16 wlr "github.com/bnema/go-wayland-bindings/server/wlrscreencopy"
17 "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
18 "github.com/bnema/neferwl/internal/logging"
19 "github.com/bnema/neferwl/internal/ports"
20 "github.com/bnema/purego-libwayland/server"
21 "github.com/bnema/wlturbo"
22 "golang.org/x/sys/unix"
23)
24
25// sessionMsg is one decoded event of the neferwl extension.
26type sessionMsg struct {
27 op uint16
28 token string
29 numbers []int64
30}
31
32// attachEvents decodes the events of a neferwl_capture_layer_v1.
33type attachEvents struct {
34 wlturbo.BaseProxy
35 msgs chan sessionMsg
36}
37
38func (p *attachEvents) Dispatch(e *wlturbo.Event) {
39 m := sessionMsg{op: e.Opcode}
40 if uint32(e.Opcode) != imagecapture.NeferwlCaptureLayerV1EventAttached {
41 m.numbers = append(m.numbers, int64(e.Uint32()))
42 }
43 p.msgs <- m
44}
45
46// exclusionEvents decodes the events of a neferwl_capture_exclusion_v1.
47type exclusionEvents struct {
48 wlturbo.BaseProxy
49 msgs chan sessionMsg
50}
51
52func (p *exclusionEvents) Dispatch(e *wlturbo.Event) {
53 m := sessionMsg{op: e.Opcode}
54 switch uint32(e.Opcode) {
55 case imagecapture.NeferwlCaptureExclusionV1EventToken:
56 m.token = e.String()
57 case imagecapture.NeferwlCaptureExclusionV1EventFailed:
58 m.numbers = append(m.numbers, int64(e.Uint32()))
59 }
60 p.msgs <- m
61}
62
63// captureHarness is a server with one 4x4 output and every capture channel.
64type captureHarness struct {
65 s *Server
66 dir string
67 events chan ports.ClientEvent
68 commands chan ports.ClientCommand
69 workspaces chan ports.Workspaces
70 captures chan ports.CaptureRequest
71 captured chan ports.CaptureDone
72}
73
74var captureOutput = ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 4, Height: 4}, Width: 4, Height: 4, Scale: 1}
75
76func newCaptureHarness(t *testing.T) *captureHarness {
77 t.Helper()
78 return newCaptureHarnessWith(t, nil)
79}
80
81// newCaptureHarnessWith lets a test adjust the options and channels (for
82// instance to wire a session-lock gate) before the server starts.
83func newCaptureHarnessWith(t *testing.T, tune func(*Options, *Channels)) *captureHarness {
84 t.Helper()
85 h := &captureHarness{
86 dir: t.TempDir(),
87 events: make(chan ports.ClientEvent, 64),
88 commands: make(chan ports.ClientCommand, 16),
89 workspaces: make(chan ports.Workspaces, 4),
90 captures: make(chan ports.CaptureRequest, 8),
91 captured: make(chan ports.CaptureDone, 8),
92 }
93 opts := Options{RuntimeDir: h.dir, Outputs: ports.Layout{captureOutput}, CaptureAllow: allowStore(t, "*\n")}
94 ch := Channels{Events: h.events, Commands: h.commands, Workspaces: h.workspaces, Captures: h.captures, Captured: h.captured}
95 if tune != nil {
96 tune(&opts, &ch)
97 }
98 s, err := New(opts, ch, logging.For(context.Background(), "wayland"))
99 if err != nil {
100 t.Fatal(err)
101 }
102 h.s = s
103 ctx, cancel := context.WithCancel(context.Background())
104 done := make(chan error, 1)
105 go func() { done <- s.Run(ctx) }()
106 t.Cleanup(func() {
107 cancel()
108 if err := <-done; err != nil {
109 t.Errorf("Run: %v", err)
110 }
111 for len(h.captures) > 0 {
112 _ = (<-h.captures).Dst.File.Close()
113 }
114 })
115 return h
116}
117
118// cclient is a client with every capture global bound.
119type cclient struct {
120 c *wlturbo.Display
121 output uint32
122 outSrc uint32 // ext_output_image_capture_source_manager_v1
123 extra uint32 // neferwl_image_capture_source_manager_v1
124 copyMgr uint32 // ext_image_copy_capture_manager_v1
125 excl uint32 // neferwl_capture_exclusion_manager_v1
126}
127
128func (h *captureHarness) client(t *testing.T) *cclient {
129 t.Helper()
130 c := protocolClient(t, h.s, h.dir)
131 out := bindVersion(t, c, "wl_output", 4)
132 registerProtocol(t, c, out)
133 return &cclient{c: c, output: out,
134 outSrc: bindProtocol(t, c, "ext_output_image_capture_source_manager_v1"),
135 extra: bindProtocol(t, c, "neferwl_image_capture_source_manager_v1"),
136 copyMgr: bindProtocol(t, c, "ext_image_copy_capture_manager_v1"),
137 excl: bindProtocol(t, c, "neferwl_capture_exclusion_manager_v1"),
138 }
139}
140
141func (cc *cclient) roundtrip(t *testing.T) {
142 t.Helper()
143 if err := cc.c.Roundtrip(); err != nil {
144 t.Fatal(err)
145 }
146}
147
148// outputSource, regionSource and workspaceSource make a capture source.
149func (cc *cclient) outputSource(t *testing.T) uint32 {
150 t.Helper()
151 src := cc.c.AllocateID()
152 registerProtocol(t, cc.c, src)
153 requestProtocol(t, cc.c, cc.outSrc, source.ExtOutputImageCaptureSourceManagerV1RequestCreateSource, src, cc.output)
154 return src
155}
156
157func (cc *cclient) regionSource(t *testing.T, x, y, w, h int32) uint32 {
158 t.Helper()
159 src := cc.c.AllocateID()
160 registerProtocol(t, cc.c, src)
161 requestProtocol(t, cc.c, cc.extra, imagecapture.NeferwlImageCaptureSourceManagerV1RequestCreateOutputRegionSource, src, cc.output, x, y, w, h)
162 return src
163}
164
165func (cc *cclient) workspaceSource(t *testing.T, handle uint32) uint32 {
166 t.Helper()
167 src := cc.c.AllocateID()
168 registerProtocol(t, cc.c, src)
169 requestProtocol(t, cc.c, cc.extra, imagecapture.NeferwlImageCaptureSourceManagerV1RequestCreateWorkspaceSource, src, handle)
170 return src
171}
172
173// session opens an ext-image-copy-capture session on a source and returns its
174// object and the decoded events (see captureDetails).
175func (cc *cclient) session(t *testing.T, src uint32) (uint32, chan []uint32) {
176 t.Helper()
177 id := cc.c.AllocateID()
178 p := &captureDetails{events: make(chan []uint32, 32)}
179 p.SetID(id)
180 registerWireProxy(cc.c, p)
181 requestProtocol(t, cc.c, cc.copyMgr, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, id, src, uint32(0))
182 cc.roundtrip(t)
183 return id, p.events
184}
185
186// Session event opcodes as captureDetails reports them.
187const (
188 evBufferSize = uint32(ext.ExtImageCopyCaptureSessionV1EventBufferSize)
189 evDone = uint32(ext.ExtImageCopyCaptureSessionV1EventDone)
190 evStopped = uint32(ext.ExtImageCopyCaptureSessionV1EventStopped)
191)
192
193// constraints reads one complete batch (buffer_size, two formats, done) and
194// returns the size.
195func constraints(t *testing.T, cc *cclient, ev chan []uint32) (w, h uint32) {
196 t.Helper()
197 got := nextSession(t, cc, ev)
198 if got[0] != evBufferSize || len(got) != 3 {
199 t.Fatalf("expected buffer_size, got %v", got)
200 }
201 w, h = got[1], got[2]
202 for _, want := range []uint32{uint32(ext.ExtImageCopyCaptureSessionV1EventShmFormat), uint32(ext.ExtImageCopyCaptureSessionV1EventShmFormat), evDone} {
203 if g := nextSession(t, cc, ev); g[0] != want {
204 t.Fatalf("batch event %v, want opcode %d", g, want)
205 }
206 }
207 return w, h
208}
209
210// nextSession waits for the next decoded session event, dispatching the client.
211func nextSession(t *testing.T, cc *cclient, ev chan []uint32) []uint32 {
212 t.Helper()
213 deadline := time.Now().Add(2 * time.Second)
214 for time.Now().Before(deadline) {
215 select {
216 case v := <-ev:
217 return v
218 default:
219 }
220 cc.roundtrip(t)
221 time.Sleep(time.Millisecond)
222 }
223 t.Fatal("timed out waiting for a session event")
224 return nil
225}
226
227func noSession(t *testing.T, cc *cclient, ev chan []uint32) {
228 t.Helper()
229 cc.roundtrip(t)
230 select {
231 case v := <-ev:
232 t.Fatalf("unexpected session event %v", v)
233 default:
234 }
235}
236
237// settle waits until the server applied the commands sent so far and the
238// client received what followed.
239func (h *captureHarness) settle(t *testing.T, cc *cclient) {
240 t.Helper()
241 // The command loop takes commands off the channel before it applies
242 // them on the display: an empty channel does not mean applied. Commands
243 // apply in order, so a marker (SlotsPending, a plain flag) seen on the
244 // display means every command sent before it was applied.
245 var was bool
246 if !h.s.display.Do(func() { was = h.s.slotsPending }) {
247 t.Fatal("display stopped")
248 }
249 h.applied(t, !was)
250 h.applied(t, was)
251 cc.roundtrip(t)
252}
253
254// applied sends SlotsPending{pending} and waits until the display applied it.
255func (h *captureHarness) applied(t *testing.T, pending bool) {
256 t.Helper()
257 h.commands <- ports.SlotsPending{Pending: pending}
258 deadline := time.Now().Add(2 * time.Second)
259 for {
260 var now bool
261 if !h.s.display.Do(func() { now = h.s.slotsPending }) {
262 t.Fatal("display stopped")
263 }
264 if now == pending {
265 return
266 }
267 if time.Now().After(deadline) {
268 t.Fatal("commands not applied")
269 }
270 time.Sleep(time.Millisecond)
271 }
272}
273
274func (h *captureHarness) send(cmd ports.ClientCommand) { h.commands <- cmd }
275
276// captureShm makes a w x h XRGB8888 wl_buffer.
277func captureShm(t *testing.T, c *wlturbo.Display, w, h int32) (buf uint32, fd int) {
278 t.Helper()
279 shm := bindProtocol(t, c, "wl_shm")
280 registerProtocol(t, c, shm)
281 fd, err := unix.MemfdCreate("ext-capture", 0)
282 if err != nil {
283 t.Fatal(err)
284 }
285 size := w * h * 4
286 if err := unix.Ftruncate(fd, int64(size)); err != nil {
287 t.Fatal(err)
288 }
289 pool := c.AllocateID()
290 buf = c.AllocateID()
291 registerProtocol(t, c, pool)
292 registerProtocol(t, c, buf)
293 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
294 t.Fatal(err)
295 }
296 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), w, h, w*4, uint32(wayland.ShmFormatXrgb8888))
297 t.Cleanup(func() { _ = unix.Close(fd) })
298 return buf, fd
299}
300
301// extFrame asks one frame of a session into a fresh w x h buffer and returns
302// its events; the frame object is destroyed so the next one may follow.
303func (cc *cclient) extFrame(t *testing.T, session uint32, w, h int32) *captureEvents {
304 t.Helper()
305 buf, _ := captureShm(t, cc.c, w, h)
306 frame := cc.c.AllocateID()
307 ev := &captureEvents{events: make(chan uint16, 16)}
308 ev.SetID(frame)
309 registerWireProxy(cc.c, ev)
310 requestProtocol(t, cc.c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame)
311 requestProtocol(t, cc.c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
312 requestProtocol(t, cc.c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
313 cc.roundtrip(t)
314 requestProtocol(t, cc.c, frame, ext.ExtImageCopyCaptureFrameV1RequestDestroy)
315 cc.roundtrip(t)
316 return ev
317}
318
319// wlrFrame asks a wlr-screencopy capture of the whole output into a buffer.
320func (cc *cclient) wlrFrame(t *testing.T) *captureEvents {
321 t.Helper()
322 buf, _ := captureShm(t, cc.c, 4, 4)
323 mgr := bindVersion(t, cc.c, "zwlr_screencopy_manager_v1", 3)
324 frame := cc.c.AllocateID()
325 ev := &captureEvents{events: make(chan uint16, 16)}
326 ev.SetID(frame)
327 registerWireProxy(cc.c, ev)
328 requestProtocol(t, cc.c, mgr, wlr.ZwlrScreencopyManagerV1RequestCaptureOutput, frame, int32(0), cc.output)
329 requestProtocol(t, cc.c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
330 cc.roundtrip(t)
331 return ev
332}
333
334func receiveCapture(t *testing.T, ch chan ports.CaptureRequest) ports.CaptureRequest {
335 t.Helper()
336 select {
337 case r := <-ch:
338 _ = r.Dst.File.Close()
339 return r
340 case <-time.After(2 * time.Second):
341 t.Fatal("no capture request")
342 return ports.CaptureRequest{}
343 }
344}
345
346func noCapture(t *testing.T, ch chan ports.CaptureRequest) {
347 t.Helper()
348 select {
349 case r := <-ch:
350 _ = r.Dst.File.Close()
351 t.Fatalf("a capture reached the renderer: %+v", r)
352 default:
353 }
354}
355
356// eventsInclude drains the frame events received so far and reports op among them.
357func eventsInclude(ev *captureEvents, op uint16) bool {
358 found := false
359 for len(ev.events) > 0 {
360 if <-ev.events == op {
361 found = true
362 }
363 }
364 return found
365}
366
367const (
368 frameReady = uint16(ext.ExtImageCopyCaptureFrameV1EventReady)
369 frameFailed = uint16(ext.ExtImageCopyCaptureFrameV1EventFailed)
370)
371
372func (h *captureHarness) open(t *testing.T) ports.CaptureSessionOpen {
373 t.Helper()
374 return nextEvent[ports.CaptureSessionOpen](t, h.events)
375}
376
377// active is the state core sends for a live session on the whole output.
378func active(id uint64) ports.CaptureSessionState {
379 return ports.CaptureSessionState{ID: id, Output: "HEADLESS-1", Rect: ports.Rect{W: 4, H: 4}, Active: true}
380}
381
382func TestTrustedPeer(t *testing.T) {
383 for _, tc := range []struct {
384 name string
385 peer server.Credentials
386 err error
387 want bool
388 }{
389 {"same user", server.Credentials{UID: 1000}, nil, true},
390 {"other user", server.Credentials{UID: 1001}, nil, false},
391 {"root is not the user", server.Credentials{UID: 0}, nil, false},
392 {"overflow id", server.Credentials{UID: 65534}, nil, false},
393 {"unreadable", server.Credentials{UID: 1000}, context.Canceled, false},
394 } {
395 if got := trustedPeer(1000, tc.peer, tc.err); got != tc.want {
396 t.Errorf("%s: %v", tc.name, got)
397 }
398 }
399}
400
401// A session on an output source registers with core and gets the output size.
402func TestOutputSessionRegistersAndClose(t *testing.T) {
403 h := newCaptureHarness(t)
404 cc := h.client(t)
405 session, ev := cc.session(t, cc.outputSource(t))
406 if w, hh := constraints(t, cc, ev); w != 4 || hh != 4 {
407 t.Fatalf("size %dx%d", w, hh)
408 }
409 open := h.open(t)
410 if open.ID == 0 || open.Output != "HEADLESS-1" || open.Workspace != 0 || open.Region != (ports.Rect{}) {
411 t.Fatalf("open %+v", open)
412 }
413 requestProtocol(t, cc.c, session, ext.ExtImageCopyCaptureSessionV1RequestDestroy)
414 cc.roundtrip(t)
415 if c := nextEvent[ports.CaptureSessionClose](t, h.events); c.ID != open.ID {
416 t.Fatalf("close %+v", c)
417 }
418}
419
420// A region source is clipped to the output, sized in physical pixels, and its
421// buffer size is sent again when the scale changes.
422func TestRegionSourceConstraintsFollowScale(t *testing.T) {
423 h := newCaptureHarness(t)
424 cc := h.client(t)
425 session, ev := cc.session(t, cc.regionSource(t, 1, 1, 2, 2))
426 if w, hh := constraints(t, cc, ev); w != 2 || hh != 2 {
427 t.Fatalf("size %dx%d", w, hh)
428 }
429 open := h.open(t)
430 if open.Output != "HEADLESS-1" || open.Region != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) {
431 t.Fatalf("open %+v", open)
432 }
433 // Clipped: a rectangle past the output edge.
434 _, ev2 := cc.session(t, cc.regionSource(t, 3, 3, 10, 10))
435 if w, hh := constraints(t, cc, ev2); w != 1 || hh != 1 {
436 t.Fatalf("clipped size %dx%d", w, hh)
437 }
438 h.open(t)
439 // Doubling the scale of a bigger mode doubles the buffer.
440 bigger := ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 8, Height: 8}, Width: 4, Height: 4, Scale: 2}
441 applyOutputs(t, h.s, h.commands, ports.SetOutputs{Outputs: ports.Layout{bigger}})
442 if w, hh := constraints(t, cc, ev); w != 4 || hh != 4 {
443 t.Fatalf("size after the scale change %dx%d", w, hh)
444 }
445 noSession(t, cc, ev) // one batch only
446 _ = session
447 // The frame asked with the new size is served for the new region.
448 r := func() ports.CaptureRequest {
449 cc.extFrame(t, session, 4, 4)
450 return receiveCapture(t, h.captures)
451 }()
452 if r.Region.Dx() != 4 || r.Region.Dy() != 4 || r.Workspace != 0 || r.Exclude {
453 t.Fatalf("request %+v", r)
454 }
455}
456
457// A frame whose buffer does not fit the geometry computed now, while the last
458// constraints the client got are stale (the refresh has not run), fails with
459// buffer_constraints after a new constraints batch: the client can retry.
460func TestStaleConstraintsAreResentBeforeBufferConstraintsFailure(t *testing.T) {
461 h := newCaptureHarness(t)
462 cc := h.client(t)
463 session, ev := cc.session(t, cc.outputSource(t))
464 constraints(t, cc, ev)
465 h.open(t)
466 // The client's last batch said 1x1 while the output is 4x4 now.
467 if !h.s.display.Do(func() {
468 for c := range h.s.captureSessions {
469 c.sent = image.Pt(1, 1)
470 }
471 }) {
472 t.Fatal("display stopped")
473 }
474 frame := cc.extFrame(t, session, 1, 1)
475 if !eventsInclude(frame, frameFailed) {
476 t.Fatal("a buffer of the stale size was served")
477 }
478 noCapture(t, h.captures)
479 if w, hh := constraints(t, cc, ev); w != 4 || hh != 4 {
480 t.Fatalf("resent constraints %dx%d", w, hh)
481 }
482 // Up to date: a wrong buffer fails without another batch.
483 frame = cc.extFrame(t, session, 1, 1)
484 if !eventsInclude(frame, frameFailed) {
485 t.Fatal("a buffer of the wrong size was served")
486 }
487 noSession(t, cc, ev)
488}
489
490func TestRegionSourceInvalidSize(t *testing.T) {
491 for _, size := range [][2]int32{{0, 4}, {4, 0}, {-1, 4}} {
492 h := newCaptureHarness(t)
493 cc := h.client(t)
494 cc.regionSource(t, 0, 0, size[0], size[1])
495 expectProtocolError(t, cc.c, cc.extra, uint32(imagecapture.NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion))
496 }
497}
498
499// A region with nothing left on the output stops at once and is not
500// registered with core; so does a session on an output that is gone.
501func TestRegionAndOutputSourcesThatAreEmptyStop(t *testing.T) {
502 h := newCaptureHarness(t)
503 cc := h.client(t)
504 _, ev := cc.session(t, cc.regionSource(t, 100, 100, 5, 5))
505 if g := nextSession(t, cc, ev); g[0] != evStopped {
506 t.Fatalf("event %v", g)
507 }
508 select {
509 case e := <-h.events:
510 t.Fatalf("event for a stopped session: %+v", e)
511 default:
512 }
513}
514
515// Removing the output stops its sessions at once and tells core.
516func TestOutputRemovalStopsRegionAndOutputSessions(t *testing.T) {
517 h := newCaptureHarness(t)
518 cc := h.client(t)
519 _, ev1 := cc.session(t, cc.outputSource(t))
520 _, ev2 := cc.session(t, cc.regionSource(t, 0, 0, 2, 2))
521 constraints(t, cc, ev1)
522 constraints(t, cc, ev2)
523 a, b := h.open(t), h.open(t)
524 applyOutputs(t, h.s, h.commands, ports.SetOutputs{})
525 for _, ev := range []chan []uint32{ev1, ev2} {
526 if g := nextSession(t, cc, ev); g[0] != evStopped {
527 t.Fatalf("event %v", g)
528 }
529 }
530 closed := map[uint64]bool{}
531 for len(closed) < 2 {
532 closed[nextEvent[ports.CaptureSessionClose](t, h.events).ID] = true
533 }
534 if !closed[a.ID] || !closed[b.ID] {
535 t.Fatalf("closed %v", closed)
536 }
537}
538
539// workspaceClient binds ext_workspace_manager_v1 on cc and returns the handle
540// of each workspace announced, by order.
541func (h *captureHarness) handles(t *testing.T, cc *cclient, want int) []uint32 {
542 t.Helper()
543 mgr := bindProtocol(t, cc.c, "ext_workspace_manager_v1")
544 p := &workspaceEvents{client: cc.c, events: make(chan [2]uint32, 128), idEvents: make(chan string, 16), order: &workspaceEventOrder{}}
545 p.SetID(mgr)
546 registerWireProxy(cc.c, p)
547 var out []uint32
548 for i := 0; i < 16 && len(out) < want; i++ {
549 cc.roundtrip(t)
550 for len(p.events) > 0 {
551 if v := <-p.events; v[0] == uint32(extws.ExtWorkspaceManagerV1EventWorkspace) {
552 out = append(out, v[1])
553 }
554 }
555 }
556 if len(out) != want {
557 t.Fatalf("%d workspace handles, want %d", len(out), want)
558 }
559 return out
560}
561
562func (h *captureHarness) snapshot(t *testing.T, ws ...ports.WorkspaceInfo) {
563 t.Helper()
564 h.workspaces <- ports.Workspaces{Outputs: []ports.WorkspaceOutput{{Name: "HEADLESS-1", Workspaces: ws}}}
565 deadline := time.Now().Add(2 * time.Second)
566 for time.Now().Before(deadline) {
567 got := 0
568 h.s.display.Do(func() {
569 for _, o := range h.s.workspaceSnapshot.Outputs {
570 got += len(o.Workspaces)
571 }
572 })
573 if got == len(ws) {
574 return
575 }
576 time.Sleep(time.Millisecond)
577 }
578 t.Fatal("snapshot not applied")
579}
580
581// A workspace source registers the workspace ID, sizes buffers from the
582// frame (off screen: the whole frame at the output scale), sends new
583// constraints when core moves or resizes it, and stops at once when the
584// handle is removed.
585func TestWorkspaceSourceSessionLifecycle(t *testing.T) {
586 h := newCaptureHarness(t)
587 h.snapshot(t,
588 ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}},
589 ports.WorkspaceInfo{ID: 43, Name: "2", Hidden: true, Index: 1, Frame: ports.Rect{W: 3, H: 2}},
590 )
591 cc := h.client(t)
592 handles := h.handles(t, cc, 2)
593 // On screen: its frame on the output.
594 _, evOn := cc.session(t, cc.workspaceSource(t, handles[0]))
595 if w, hh := constraints(t, cc, evOn); w != 4 || hh != 4 {
596 t.Fatalf("on-screen size %dx%d", w, hh)
597 }
598 if o := h.open(t); o.Workspace != 42 || o.Output != "" {
599 t.Fatalf("open %+v", o)
600 }
601 // Off screen: the whole frame.
602 sess, ev := cc.session(t, cc.workspaceSource(t, handles[1]))
603 if w, hh := constraints(t, cc, ev); w != 3 || hh != 2 {
604 t.Fatalf("off-screen size %dx%d", w, hh)
605 }
606 open := h.open(t)
607 if open.Workspace != 43 {
608 t.Fatalf("open %+v", open)
609 }
610 // Core resizes the frame: new constraints, once.
611 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 2, H: 2}, Workspace: 43, Hidden: true, Active: true})
612 if w, hh := constraints(t, cc, ev); w != 2 || hh != 2 {
613 t.Fatalf("resized %dx%d", w, hh)
614 }
615 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 2, H: 2}, Workspace: 43, Hidden: true, Active: true, Revision: 0})
616 h.settle(t, cc)
617 noSession(t, cc, ev)
618 // A frame asks for the off-screen workspace, whole.
619 cc.extFrame(t, sess, 2, 2)
620 r := receiveCapture(t, h.captures)
621 if r.Workspace != 43 || !r.OffScreen || r.Session != open.ID || r.Region.Dx() != 2 || r.Region.Dy() != 2 {
622 t.Fatalf("request %+v", r)
623 }
624 // Removing workspace 43 stops its session at once; the other lives on.
625 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
626 if g := nextSession(t, cc, ev); g[0] != evStopped {
627 t.Fatalf("event %v", g)
628 }
629 if c := nextEvent[ports.CaptureSessionClose](t, h.events); c.ID != open.ID {
630 t.Fatalf("close %+v", c)
631 }
632 noSession(t, cc, evOn)
633}
634
635// A handle that is already removed gives a source whose sessions stop at once.
636func TestWorkspaceSourceOfRemovedHandleStops(t *testing.T) {
637 h := newCaptureHarness(t)
638 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
639 cc := h.client(t)
640 handles := h.handles(t, cc, 1)
641 h.snapshot(t)
642 cc.roundtrip(t)
643 _, ev := cc.session(t, cc.workspaceSource(t, handles[0]))
644 if g := nextSession(t, cc, ev); g[0] != evStopped {
645 t.Fatalf("event %v", g)
646 }
647}
648
649// The workspace ID survives a configured-name change: same handle, same
650// decimal id, so a source made before stays valid.
651func TestWorkspaceSourceSurvivesConfiguredRename(t *testing.T) {
652 h := newCaptureHarness(t)
653 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
654 cc := h.client(t)
655 handles := h.handles(t, cc, 1)
656 _, ev := cc.session(t, cc.workspaceSource(t, handles[0]))
657 constraints(t, cc, ev)
658 h.open(t)
659 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "dev", Configured: "dev", Active: true, Frame: ports.Rect{W: 4, H: 4}})
660 h.settle(t, cc)
661 noSession(t, cc, ev)
662 select {
663 case e := <-h.events:
664 t.Fatalf("event %+v after a rename", e)
665 default:
666 }
667}
668
669// frameEvents decodes the events of a neferwl_workspace_frame_v1.
670type frameEvents struct {
671 wlturbo.BaseProxy
672 rects chan [4]int32
673}
674
675func (p *frameEvents) Dispatch(e *wlturbo.Event) {
676 p.rects <- [4]int32{e.Int32(), e.Int32(), e.Int32(), e.Int32()}
677}
678
679// workspaceFrame asks get_workspace_frame for a workspace handle.
680func (cc *cclient) workspaceFrame(t *testing.T, handle uint32) (uint32, chan [4]int32) {
681 t.Helper()
682 id := cc.c.AllocateID()
683 ev := &frameEvents{rects: make(chan [4]int32, 8)}
684 ev.SetID(id)
685 registerWireProxy(cc.c, ev)
686 requestProtocol(t, cc.c, cc.extra, imagecapture.NeferwlImageCaptureSourceManagerV1RequestGetWorkspaceFrame, id, handle)
687 cc.roundtrip(t)
688 return id, ev.rects
689}
690
691func wantFrame(t *testing.T, ch chan [4]int32, want [4]int32) {
692 t.Helper()
693 select {
694 case got := <-ch:
695 if got != want {
696 t.Fatalf("frame %v, want %v", got, want)
697 }
698 default:
699 t.Fatalf("no frame, want %v", want)
700 }
701}
702
703func noFrame(t *testing.T, ch chan [4]int32) {
704 t.Helper()
705 select {
706 case got := <-ch:
707 t.Fatalf("unexpected frame %v", got)
708 default:
709 }
710}
711
712// A workspace frame object reports the frame when it is created and each time
713// it changes, for every workspace, whether or not it is on screen; it reports
714// nothing for a removed handle and stops after a rename replaced the handle.
715func TestWorkspaceFrameEvents(t *testing.T) {
716 h := newCaptureHarness(t)
717 h.snapshot(t,
718 ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{X: 1, Y: 0, W: 2, H: 4}},
719 ports.WorkspaceInfo{ID: 43, Name: "2", Hidden: true, Index: 1, Frame: ports.Rect{W: 3, H: 2}},
720 )
721 cc := h.client(t)
722 handles := h.handles(t, cc, 2)
723 _, on := cc.workspaceFrame(t, handles[0])
724 wantFrame(t, on, [4]int32{1, 0, 2, 4})
725 _, off := cc.workspaceFrame(t, handles[1])
726 wantFrame(t, off, [4]int32{0, 0, 3, 2})
727 // Unchanged snapshot: nothing. A change: once, to the right object.
728 h.snapshot(t,
729 ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{X: 1, Y: 0, W: 2, H: 4}},
730 ports.WorkspaceInfo{ID: 43, Name: "2", Hidden: true, Index: 1, Frame: ports.Rect{X: 1, Y: 1, W: 2, H: 2}},
731 )
732 h.settle(t, cc)
733 noFrame(t, on)
734 wantFrame(t, off, [4]int32{1, 1, 2, 2})
735 noFrame(t, off)
736 // Removal sends nothing; a handle removed before the request sends nothing.
737 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{X: 1, Y: 0, W: 2, H: 4}})
738 h.settle(t, cc)
739 noFrame(t, off)
740 _, gone := cc.workspaceFrame(t, handles[1])
741 noFrame(t, gone)
742 // A configured rename replaces the handle: the old object goes quiet.
743 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "dev", Configured: "dev", Active: true, Frame: ports.Rect{W: 4, H: 4}})
744 h.settle(t, cc)
745 noFrame(t, on)
746}
747
748// While the session is locked a frame object sends nothing, whether it is
749// created before the lock or during it; the unlock sends the current frame
750// once, and only if it changed.
751func TestWorkspaceFrameSessionLock(t *testing.T) {
752 // unlock leaves protection the way the session-lock unlock does.
753 unlock := func(t *testing.T, h *captureHarness, state *atomic.Pointer[ports.SecurityState]) {
754 t.Helper()
755 protect(t, h.s, state, 2, false)
756 if !h.s.display.Do(func() { h.s.updateWorkspaceManagers(h.s.workspaceSnapshot) }) {
757 t.Fatal("display stopped")
758 }
759 }
760 info := func(r ports.Rect) ports.WorkspaceInfo {
761 return ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: r}
762 }
763 t.Run("change while locked", func(t *testing.T) {
764 h := newCaptureHarness(t)
765 h.snapshot(t, info(ports.Rect{W: 2, H: 2}))
766 cc := h.client(t)
767 handles := h.handles(t, cc, 1)
768 _, ev := cc.workspaceFrame(t, handles[0])
769 wantFrame(t, ev, [4]int32{0, 0, 2, 2})
770 state := installSecurity(t, h.s)
771 protect(t, h.s, state, 1, true)
772 h.snapshot(t, info(ports.Rect{X: 1, Y: 1, W: 3, H: 3}))
773 h.settle(t, cc)
774 noFrame(t, ev)
775 unlock(t, h, state)
776 cc.roundtrip(t)
777 wantFrame(t, ev, [4]int32{1, 1, 3, 3})
778 noFrame(t, ev)
779 })
780 t.Run("no change while locked", func(t *testing.T) {
781 h := newCaptureHarness(t)
782 h.snapshot(t, info(ports.Rect{W: 2, H: 2}))
783 cc := h.client(t)
784 handles := h.handles(t, cc, 1)
785 _, ev := cc.workspaceFrame(t, handles[0])
786 wantFrame(t, ev, [4]int32{0, 0, 2, 2})
787 state := installSecurity(t, h.s)
788 protect(t, h.s, state, 1, true)
789 h.snapshot(t, info(ports.Rect{X: 1, Y: 1, W: 3, H: 3}))
790 h.snapshot(t, info(ports.Rect{W: 2, H: 2}))
791 h.settle(t, cc)
792 unlock(t, h, state)
793 cc.roundtrip(t)
794 noFrame(t, ev)
795 })
796 t.Run("get while locked", func(t *testing.T) {
797 h := newCaptureHarness(t)
798 h.snapshot(t, info(ports.Rect{W: 2, H: 2}))
799 cc := h.client(t)
800 handles := h.handles(t, cc, 1)
801 state := installSecurity(t, h.s)
802 protect(t, h.s, state, 1, true)
803 _, ev := cc.workspaceFrame(t, handles[0])
804 h.settle(t, cc)
805 noFrame(t, ev)
806 unlock(t, h, state)
807 cc.roundtrip(t)
808 wantFrame(t, ev, [4]int32{0, 0, 2, 2})
809 noFrame(t, ev)
810 })
811}
812
813// Destroying a frame object drops it from the server's list, and nothing is
814// sent to it afterwards.
815func TestWorkspaceFrameDestroyRemovesObject(t *testing.T) {
816 h := newCaptureHarness(t)
817 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 2, H: 2}})
818 cc := h.client(t)
819 handles := h.handles(t, cc, 1)
820 count := func() (n int) {
821 h.s.display.Do(func() { n = len(h.s.workspaceFrames) })
822 return n
823 }
824 id, ev := cc.workspaceFrame(t, handles[0])
825 wantFrame(t, ev, [4]int32{0, 0, 2, 2})
826 if n := count(); n != 1 {
827 t.Fatalf("%d frame objects, want 1", n)
828 }
829 requestProtocol(t, cc.c, id, imagecapture.NeferwlWorkspaceFrameV1RequestDestroy)
830 cc.roundtrip(t)
831 if n := count(); n != 0 {
832 t.Fatalf("%d frame objects after destroy, want 0", n)
833 }
834 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 3, H: 3}})
835 h.settle(t, cc)
836 noFrame(t, ev)
837}
838
839// exclusion asks get_exclusion on a session and returns the event stream.
840func (cc *cclient) exclusion(t *testing.T, session uint32) (uint32, chan sessionMsg) {
841 t.Helper()
842 id := cc.c.AllocateID()
843 ev := &exclusionEvents{msgs: make(chan sessionMsg, 8)}
844 ev.SetID(id)
845 registerWireProxy(cc.c, ev)
846 requestProtocol(t, cc.c, cc.excl, imagecapture.NeferwlCaptureExclusionManagerV1RequestGetExclusion, id, session)
847 cc.roundtrip(t)
848 return id, ev.msgs
849}
850
851// exclusionOwner opens a session and its exclusion, confirmed by core.
852func (h *captureHarness) exclusionOwner(t *testing.T) (*cclient, uint32, ports.CaptureSessionOpen, chan sessionMsg, string) {
853 t.Helper()
854 cc := h.client(t)
855 session, sev := cc.session(t, cc.outputSource(t))
856 constraints(t, cc, sev)
857 open := h.open(t)
858 _, msgs := cc.exclusion(t, session)
859 m := waitMsg(t, cc.c, msgs)
860 if uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventToken || len(m.token) != 64 || strings.Trim(m.token, "0123456789abcdef") != "" {
861 t.Fatalf("token %+v", m)
862 }
863 if b := nextEvent[ports.CaptureExclusionBegin](t, h.events); b.Session != open.ID {
864 t.Fatalf("begin %+v", b)
865 }
866 return cc, session, open, msgs, m.token
867}
868
869// The exclusion answers with a token; a second one is busy, even from
870// another connection, and is not reported to core.
871func TestExclusionTokenAndBusy(t *testing.T) {
872 h := newCaptureHarness(t)
873 _, _, open, _, _ := h.exclusionOwner(t)
874 other := h.client(t)
875 session2, sev2 := other.session(t, other.outputSource(t))
876 constraints(t, other, sev2)
877 h.open(t)
878 _, msgs := other.exclusion(t, session2)
879 if m := waitMsg(t, other.c, msgs); uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventFailed || m.numbers[0] != int64(imagecapture.NeferwlCaptureExclusionV1FailureBusy) {
880 t.Fatalf("second exclusion %+v", m)
881 }
882 for len(h.events) > 0 {
883 if b, ok := (<-h.events).(ports.CaptureExclusionBegin); ok {
884 t.Fatalf("a refused exclusion reached core: %+v (owner %d)", b, open.ID)
885 }
886 }
887}
888
889// get_exclusion on a session that already stopped answers
890// failed(session_stopped), whatever the reason it stopped.
891func TestExclusionOnStoppedSession(t *testing.T) {
892 h := newCaptureHarness(t)
893 cc := h.client(t)
894 // Stopped at creation (a region with nothing on the output).
895 session, ev := cc.session(t, cc.regionSource(t, 100, 100, 5, 5))
896 if g := nextSession(t, cc, ev); g[0] != evStopped {
897 t.Fatalf("event %v", g)
898 }
899 _, msgs := cc.exclusion(t, session)
900 if m := waitMsg(t, cc.c, msgs); uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventFailed || m.numbers[0] != int64(imagecapture.NeferwlCaptureExclusionV1FailureSessionStopped) {
901 t.Fatalf("exclusion %+v", m)
902 }
903 // Stopped by core after it was registered.
904 session2, sev := cc.session(t, cc.outputSource(t))
905 constraints(t, cc, sev)
906 open := h.open(t)
907 h.send(ports.CaptureSessionState{ID: open.ID, Reason: ports.CaptureReasonOutputGone})
908 if g := nextSession(t, cc, sev); g[0] != evStopped {
909 t.Fatalf("event %v", g)
910 }
911 _, msgs = cc.exclusion(t, session2)
912 if m := waitMsg(t, cc.c, msgs); uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventFailed || m.numbers[0] != int64(imagecapture.NeferwlCaptureExclusionV1FailureSessionStopped) {
913 t.Fatalf("exclusion %+v", m)
914 }
915 for len(h.events) > 0 {
916 if b, ok := (<-h.events).(ports.CaptureExclusionBegin); ok {
917 t.Fatalf("a refused exclusion reached core: %+v", b)
918 }
919 }
920}
921
922// One session gets one live exclusion object.
923func TestExclusionAlreadyExcluded(t *testing.T) {
924 h := newCaptureHarness(t)
925 cc, session, _, _, _ := h.exclusionOwner(t)
926 requestProtocol(t, cc.c, cc.excl, imagecapture.NeferwlCaptureExclusionManagerV1RequestGetExclusion, cc.c.AllocateID(), session)
927 expectProtocolError(t, cc.c, cc.excl, uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorAlreadyExcluded))
928}
929
930// Destroying the exclusion ends it in core, detaches its layers and frees
931// the slot; the session lives on.
932func TestExclusionDestroyFreesTheSlot(t *testing.T) {
933 h := newCaptureHarness(t)
934 cc, session, open, _, _ := h.exclusionOwner(t)
935 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 4, H: 4}, Active: true, Exclusion: true, Revision: 1})
936 h.settle(t, cc)
937 var excl uint32
938 h.s.display.Do(func() { excl = h.s.excl.res.ID() })
939 requestProtocol(t, cc.c, excl, imagecapture.NeferwlCaptureExclusionV1RequestDestroy)
940 cc.roundtrip(t)
941 if e := nextEvent[ports.CaptureExclusionEnd](t, h.events); e.Session != open.ID {
942 t.Fatalf("end %+v", e)
943 }
944 _, msgs := cc.exclusion(t, session)
945 if m := waitMsg(t, cc.c, msgs); uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventToken {
946 t.Fatalf("new exclusion %+v", m)
947 }
948}
949
950// Frames of the session that owns the exclusion are fenced: refused until
951// core knows the exclusion, then tagged with its revision. Every other
952// capture, wlr-screencopy included, carries no tag.
953func TestExclusionFrameTagging(t *testing.T) {
954 h := newCaptureHarness(t)
955 owner, session, open, _, _ := h.exclusionOwner(t)
956 // Core has not confirmed the exclusion: the frame fails.
957 ev := owner.extFrame(t, session, 4, 4)
958 if !eventsInclude(ev, frameFailed) {
959 t.Fatal("a frame before core confirmed the exclusion was served")
960 }
961 noCapture(t, h.captures)
962 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 4, H: 4}, Active: true, Exclusion: true, Revision: 9})
963 h.settle(t, owner)
964 owner.extFrame(t, session, 4, 4)
965 r := receiveCapture(t, h.captures)
966 if !r.Exclude || r.Session != open.ID || r.CaptureRevision != 9 {
967 t.Fatalf("owner frame %+v", r)
968 }
969 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Time: time.Now()}
970 // A wlr-screencopy capture, even of the owner's connection, is untagged.
971 owner.wlrFrame(t)
972 r = receiveCapture(t, h.captures)
973 if r.Exclude || r.Session != 0 || r.CaptureRevision != 0 || r.Workspace != 0 {
974 t.Fatalf("wlr capture tagged: %+v", r)
975 }
976 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Time: time.Now()}
977 // So is a plain ext session of another client.
978 other := h.client(t)
979 session2, sev := other.session(t, other.outputSource(t))
980 constraints(t, other, sev)
981 h.open(t)
982 other.extFrame(t, session2, 4, 4)
983 r = receiveCapture(t, h.captures)
984 if r.Exclude || r.CaptureRevision != 0 {
985 t.Fatalf("another session's frame tagged: %+v", r)
986 }
987}
988
989// hud is a second connection: a layer surface it attaches with the token.
990type hud struct {
991 *cclient
992 surface, layer uint32
993}
994
995func (h *captureHarness) hud(t *testing.T, layer ports.Layer, keyboard uint32) *hud {
996 t.Helper()
997 return h.hudVersion(t, layer, keyboard, 1)
998}
999
1000func (h *captureHarness) hudVersion(t *testing.T, layer ports.Layer, keyboard, shellVersion uint32) *hud {
1001 t.Helper()
1002 pc := h.client(t)
1003 comp := bindProtocol(t, pc.c, "wl_compositor")
1004 shell := bindVersion(t, pc.c, "zwlr_layer_shell_v1", shellVersion)
1005 surf, l := pc.c.AllocateID(), pc.c.AllocateID()
1006 requestProtocol(t, pc.c, comp, wayland.CompositorRequestCreateSurface, surf)
1007 registerProtocol(t, pc.c, surf)
1008 requestProtocol(t, pc.c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, l, surf, pc.output, uint32(layer), "hud")
1009 registerProtocol(t, pc.c, l)
1010 if keyboard != 0 {
1011 requestProtocol(t, pc.c, l, wlrlayershell.ZwlrLayerSurfaceV1RequestSetKeyboardInteractivity, keyboard)
1012 }
1013 if err := pc.c.Roundtrip(); err != nil {
1014 t.Fatal(err)
1015 }
1016 return &hud{cclient: pc, surface: surf, layer: l}
1017}
1018
1019// attach sends attach_surface and returns the attachment's event stream.
1020func (hd *hud) attach(t *testing.T, token string) chan sessionMsg {
1021 t.Helper()
1022 return hd.attachSurface(t, token, hd.surface)
1023}
1024
1025func (hd *hud) attachSurface(t *testing.T, token string, surface uint32) chan sessionMsg {
1026 t.Helper()
1027 id := hd.c.AllocateID()
1028 ev := &attachEvents{msgs: make(chan sessionMsg, 8)}
1029 ev.SetID(id)
1030 registerWireProxy(hd.c, ev)
1031 requestProtocol(t, hd.c, hd.excl, imagecapture.NeferwlCaptureExclusionManagerV1RequestAttachSurface, id, token, surface)
1032 if err := hd.c.Roundtrip(); err != nil {
1033 t.Fatal(err)
1034 }
1035 return ev.msgs
1036}
1037
1038// expectAttachment waits for one attachment event of the given opcode and first argument.
1039func expectAttachment(t *testing.T, c *wlturbo.Display, ch chan sessionMsg, op uint32, arg ...int64) {
1040 t.Helper()
1041 m := waitMsg(t, c, ch)
1042 if uint32(m.op) != op || (len(arg) > 0 && m.numbers[0] != arg[0]) {
1043 t.Fatalf("attachment event %+v, want op %d %v", m, op, arg)
1044 }
1045}
1046
1047// waitMsg waits for a message on ch, dispatching the client meanwhile.
1048func waitMsg(t *testing.T, c *wlturbo.Display, ch chan sessionMsg) sessionMsg {
1049 t.Helper()
1050 deadline := time.Now().Add(2 * time.Second)
1051 for time.Now().Before(deadline) {
1052 select {
1053 case m := <-ch:
1054 return m
1055 default:
1056 }
1057 if err := c.Roundtrip(); err != nil {
1058 t.Fatal(err)
1059 }
1060 time.Sleep(time.Millisecond)
1061 }
1062 t.Fatal("timed out waiting for an event")
1063 return sessionMsg{}
1064}
1065
1066func noMessage(t *testing.T, c *wlturbo.Display, ch chan sessionMsg) {
1067 t.Helper()
1068 if err := c.Roundtrip(); err != nil {
1069 t.Fatal(err)
1070 }
1071 select {
1072 case m := <-ch:
1073 t.Fatalf("unexpected event %+v", m)
1074 default:
1075 }
1076}
1077
1078func nextEvent[T ports.ClientEvent](t *testing.T, ch chan ports.ClientEvent) T {
1079 t.Helper()
1080 for {
1081 select {
1082 case ev := <-ch:
1083 if v, ok := ev.(T); ok {
1084 return v
1085 }
1086 case <-time.After(2 * time.Second):
1087 t.Fatal("timed out waiting for a client event")
1088 }
1089 }
1090}
1091
1092// Attach works from another connection with the token, reaches core before
1093// any content, and is confirmed to the client only once core lists the layer.
1094func TestExclusionAttach(t *testing.T) {
1095 h := newCaptureHarness(t)
1096 _, _, open, _, token := h.exclusionOwner(t)
1097 hd := h.hud(t, ports.LayerOverlay, 2)
1098 // A wrong token fails that attachment without touching the exclusion.
1099 bad := hd.attach(t, strings.Repeat("0", 64))
1100 expectAttachment(t, hd.c, bad, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureUnknownToken))
1101 good := hd.attach(t, token)
1102 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1103 if layer.Session != open.ID || !layer.Attached || layer.Layer == 0 {
1104 t.Fatalf("layer %+v", layer)
1105 }
1106 noMessage(t, hd.c, good) // not confirmed yet
1107 st := active(open.ID)
1108 st.Exclusion, st.Revision = true, 2
1109 h.send(st)
1110 noMessage(t, hd.c, good) // a state that does not list the layer confirms nothing
1111 st.Revision, st.Layers = 3, []ports.WindowID{layer.Layer}
1112 h.send(st)
1113 expectAttachment(t, hd.c, good, imagecapture.NeferwlCaptureLayerV1EventAttached)
1114 // Attaching the same surface again fails on its own object.
1115 again := hd.attach(t, token)
1116 expectAttachment(t, hd.c, again, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureAlreadyAttached))
1117 // Destroying the layer detaches it: LayerChanged (the unmap) first, then
1118 // the detach, on the same ordered channel.
1119 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
1120 if err := hd.c.Roundtrip(); err != nil {
1121 t.Fatal(err)
1122 }
1123 expectAttachment(t, hd.c, good, imagecapture.NeferwlCaptureLayerV1EventDetached, int64(imagecapture.NeferwlCaptureLayerV1DetachReasonLayerDestroyed))
1124 if l := nextEvent[ports.CaptureExclusionLayer](t, h.events); l.Attached || l.Layer != layer.Layer {
1125 t.Fatalf("detach %+v", l)
1126 }
1127}
1128
1129// Once the exclusion ended the token is dead.
1130func TestExclusionTokenDeadAfterEnd(t *testing.T) {
1131 h := newCaptureHarness(t)
1132 owner, session, _, _, token := h.exclusionOwner(t)
1133 requestProtocol(t, owner.c, session, ext.ExtImageCopyCaptureSessionV1RequestDestroy)
1134 owner.roundtrip(t)
1135 hd := h.hud(t, ports.LayerOverlay, 0)
1136 att := hd.attach(t, token)
1137 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureUnknownToken))
1138}
1139
1140// Order on the event channel: a mapped attached layer that is destroyed
1141// queues its LayerChanged removal before the detach reaches core.
1142func TestExclusionDetachOrdering(t *testing.T) {
1143 h := newCaptureHarness(t)
1144 _, _, open, _, token := h.exclusionOwner(t)
1145 hd := h.hud(t, ports.LayerTop, 0)
1146 att := hd.attach(t, token)
1147 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1148 st := active(open.ID)
1149 st.Exclusion, st.Revision, st.Layers = true, 2, []ports.WindowID{layer.Layer}
1150 h.send(st)
1151 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventAttached)
1152 mapLayer(t, hd)
1153 if l := nextEvent[ports.LayerChanged](t, h.events); len(l.Layers) != 1 {
1154 t.Fatalf("mapped %+v", l)
1155 }
1156 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
1157 if err := hd.c.Roundtrip(); err != nil {
1158 t.Fatal(err)
1159 }
1160 var order []string
1161 for len(order) < 2 {
1162 switch v := (<-h.events).(type) {
1163 case ports.LayerChanged:
1164 if len(v.Layers) == 0 {
1165 order = append(order, "unmap")
1166 }
1167 case ports.CaptureExclusionLayer:
1168 if !v.Attached {
1169 order = append(order, "detach")
1170 }
1171 }
1172 }
1173 if order[0] != "unmap" || order[1] != "detach" {
1174 t.Fatalf("order %v: the exclusion must outlive the layer's last frame", order)
1175 }
1176}
1177
1178// mapLayer configures, acks and commits a buffer on the hud's layer.
1179func mapLayer(t *testing.T, hd *hud) {
1180 t.Helper()
1181 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1))
1182 requestProtocol(t, hd.c, hd.surface, wayland.SurfaceRequestCommit)
1183 cfg := &layerProxy{configured: make(chan [3]uint32, 4)}
1184 cfg.SetID(hd.layer)
1185 registerWireProxy(hd.c, cfg)
1186 if err := hd.c.Roundtrip(); err != nil {
1187 t.Fatal(err)
1188 }
1189 c := <-cfg.configured
1190 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, c[0])
1191 _, buf, fd := captureSmallBuffer(t, hd.c)
1192 t.Cleanup(func() { _ = unix.Close(fd) })
1193 requestProtocol(t, hd.c, hd.surface, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1194 requestProtocol(t, hd.c, hd.surface, wayland.SurfaceRequestCommit)
1195 if err := hd.c.Roundtrip(); err != nil {
1196 t.Fatal(err)
1197 }
1198}
1199
1200// Ending the exclusion (here with its session) detaches every attachment,
1201// closes the layer surfaces (so none lingers into the next exclusion) and
1202// removes them before core is told the session closed.
1203func TestExclusionEndClosesLayers(t *testing.T) {
1204 h := newCaptureHarness(t)
1205 owner, session, open, _, token := h.exclusionOwner(t)
1206 hd := h.hud(t, ports.LayerOverlay, 0)
1207 closed := make(chan sessionMsg, 4)
1208 lp := &layerClosedProxy{msgs: closed}
1209 lp.SetID(hd.layer)
1210 registerWireProxy(hd.c, lp)
1211 confirmed := hd.attach(t, token)
1212 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1213 st := active(open.ID)
1214 st.Exclusion, st.Revision, st.Layers = true, 2, []ports.WindowID{layer.Layer}
1215 h.send(st)
1216 expectAttachment(t, hd.c, confirmed, imagecapture.NeferwlCaptureLayerV1EventAttached)
1217 pending := hd.attachSurfaceNew(t, token)
1218 requestProtocol(t, owner.c, session, ext.ExtImageCopyCaptureSessionV1RequestDestroy)
1219 owner.roundtrip(t)
1220 expectAttachment(t, hd.c, confirmed, imagecapture.NeferwlCaptureLayerV1EventDetached, int64(imagecapture.NeferwlCaptureLayerV1DetachReasonExclusionEnded))
1221 expectAttachment(t, hd.c, pending, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureExclusionEnded))
1222 if m := waitMsg(t, hd.c, closed); uint32(m.op) != uint32(wlrlayershell.ZwlrLayerSurfaceV1EventClosed) {
1223 t.Fatalf("layer event %+v", m)
1224 }
1225 if e := nextEvent[ports.CaptureSessionClose](t, h.events); e.ID != open.ID {
1226 t.Fatalf("close %+v", e)
1227 }
1228}
1229
1230type layerClosedProxy struct {
1231 wlturbo.BaseProxy
1232 msgs chan sessionMsg
1233}
1234
1235func (p *layerClosedProxy) Dispatch(e *wlturbo.Event) {
1236 if e.Opcode == uint16(wlrlayershell.ZwlrLayerSurfaceV1EventClosed) {
1237 p.msgs <- sessionMsg{op: e.Opcode}
1238 }
1239}
1240
1241// attachSurfaceNew attaches a second, fresh layer surface of the hud and returns its stream.
1242func (hd *hud) attachSurfaceNew(t *testing.T, token string) chan sessionMsg {
1243 t.Helper()
1244 comp := bindProtocol(t, hd.c, "wl_compositor")
1245 shell := bindProtocol(t, hd.c, "zwlr_layer_shell_v1")
1246 surf, l := hd.c.AllocateID(), hd.c.AllocateID()
1247 requestProtocol(t, hd.c, comp, wayland.CompositorRequestCreateSurface, surf)
1248 registerProtocol(t, hd.c, surf)
1249 requestProtocol(t, hd.c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, l, surf, hd.output, uint32(ports.LayerTop), "hud2")
1250 registerProtocol(t, hd.c, l)
1251 return hd.attachSurface(t, token, surf)
1252}
1253
1254func TestExclusionAttachRefusals(t *testing.T) {
1255 for _, tc := range []struct {
1256 name string
1257 layer ports.Layer
1258 keyboard uint32
1259 code uint32
1260 }{
1261 {"background layer", ports.LayerBackground, 0, uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidLayer)},
1262 {"bottom layer", ports.LayerBottom, 0, uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidLayer)},
1263 {"exclusive keyboard", ports.LayerTop, 1, uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorExclusiveKeyboard)},
1264 } {
1265 t.Run(tc.name, func(t *testing.T) {
1266 h := newCaptureHarness(t)
1267 _, _, _, _, token := h.exclusionOwner(t)
1268 hd := h.hud(t, tc.layer, tc.keyboard)
1269 requestProtocol(t, hd.c, hd.excl, imagecapture.NeferwlCaptureExclusionManagerV1RequestAttachSurface, hd.c.AllocateID(), token, hd.surface)
1270 expectProtocolError(t, hd.c, hd.excl, tc.code)
1271 for len(h.events) > 0 {
1272 if l, ok := (<-h.events).(ports.CaptureExclusionLayer); ok {
1273 t.Fatalf("a refused attach reached core: %+v", l)
1274 }
1275 }
1276 })
1277 }
1278}
1279
1280func TestExclusionAttachLimit(t *testing.T) {
1281 h := newCaptureHarness(t)
1282 _, _, _, _, token := h.exclusionOwner(t)
1283 hd := h.hud(t, ports.LayerTop, 0)
1284 hd.attach(t, token)
1285 for i := 1; i < ports.MaxExclusionLayers; i++ {
1286 hd.attachSurfaceNew(t, token)
1287 }
1288 over := hd.attachSurfaceNew(t, token)
1289 expectAttachment(t, hd.c, over, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureTooManyLayers))
1290 // Events reach core through a forwarding goroutine: wait for each.
1291 for range ports.MaxExclusionLayers {
1292 if l := nextEvent[ports.CaptureExclusionLayer](t, h.events); !l.Attached {
1293 t.Fatalf("detach %+v", l)
1294 }
1295 }
1296 // The refused one never follows: it was refused before the failed event
1297 // the client already read, so a short wait covers the forwarder.
1298 deadline := time.After(50 * time.Millisecond)
1299 for {
1300 select {
1301 case ev := <-h.events:
1302 if l, ok := ev.(ports.CaptureExclusionLayer); ok && l.Attached {
1303 t.Fatalf("layer over the limit reached core: %+v", l)
1304 }
1305 case <-deadline:
1306 return
1307 }
1308 }
1309}
1310
1311// An attached layer that commits a layer below top is closed and detached
1312// once, and its unmap reaches core before the detach.
1313func TestExclusionLayerMovedBelowTop(t *testing.T) {
1314 h := newCaptureHarness(t)
1315 _, _, open, _, token := h.exclusionOwner(t)
1316 hd := h.hudVersion(t, ports.LayerTop, 0, 2)
1317 att := hd.attach(t, token)
1318 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1319 st := active(open.ID)
1320 st.Exclusion, st.Revision, st.Layers = true, 2, []ports.WindowID{layer.Layer}
1321 h.send(st)
1322 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventAttached)
1323 mapLayer(t, hd)
1324 if l := nextEvent[ports.LayerChanged](t, h.events); len(l.Layers) != 1 {
1325 t.Fatalf("mapped %+v", l)
1326 }
1327 closed := make(chan sessionMsg, 4)
1328 lp := &layerClosedProxy{msgs: closed}
1329 lp.SetID(hd.layer)
1330 registerWireProxy(hd.c, lp)
1331 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetLayer, uint32(ports.LayerBottom))
1332 requestProtocol(t, hd.c, hd.surface, wayland.SurfaceRequestCommit)
1333 if err := hd.c.Roundtrip(); err != nil {
1334 t.Fatal(err)
1335 }
1336 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventDetached, int64(imagecapture.NeferwlCaptureLayerV1DetachReasonLayerDestroyed))
1337 if m := waitMsg(t, hd.c, closed); uint32(m.op) != uint32(wlrlayershell.ZwlrLayerSurfaceV1EventClosed) {
1338 t.Fatalf("layer event %+v", m)
1339 }
1340 noMessage(t, hd.c, att) // detached exactly once
1341 var order []string
1342 for len(order) < 2 {
1343 switch v := (<-h.events).(type) {
1344 case ports.LayerChanged:
1345 if len(v.Layers) == 0 {
1346 order = append(order, "unmap")
1347 }
1348 case ports.CaptureExclusionLayer:
1349 if !v.Attached {
1350 order = append(order, "detach")
1351 }
1352 }
1353 }
1354 if order[0] != "unmap" || order[1] != "detach" {
1355 t.Fatalf("order %v", order)
1356 }
1357}
1358
1359// An attached layer asking for keyboard interactivity, before or after the
1360// attach, is never reported with it to core.
1361func TestExclusionLayerKeyboardNone(t *testing.T) {
1362 h := newCaptureHarness(t)
1363 _, _, open, _, token := h.exclusionOwner(t)
1364 hd := h.hud(t, ports.LayerOverlay, 2) // on demand before the attach
1365 att := hd.attach(t, token)
1366 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1367 st := active(open.ID)
1368 st.Exclusion, st.Revision, st.Layers = true, 2, []ports.WindowID{layer.Layer}
1369 h.send(st)
1370 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventAttached)
1371 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetKeyboardInteractivity, uint32(2)) // and after
1372 mapLayer(t, hd)
1373 l := nextEvent[ports.LayerChanged](t, h.events)
1374 if len(l.Layers) != 1 || l.Layers[0].Keyboard != 0 {
1375 t.Fatalf("layer reported with keyboard: %+v", l.Layers)
1376 }
1377}
1378
1379// frameTaken waits for the next CaptureFrameTaken core is told of.
1380func (h *captureHarness) frameTaken(t *testing.T) ports.CaptureFrameTaken {
1381 t.Helper()
1382 return nextEvent[ports.CaptureFrameTaken](t, h.events)
1383}
1384
1385// Every capture is reported to core, which shows the indicator for it: a
1386// wlr-screencopy frame (no session) and a frame of each kind of ext source,
1387// with the target the client asked for.
1388func TestEveryCaptureFrameIsReportedToCore(t *testing.T) {
1389 h := newCaptureHarness(t)
1390 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
1391 cc := h.client(t)
1392 // wlr-screencopy, whole output.
1393 cc.wlrFrame(t)
1394 r := receiveCapture(t, h.captures)
1395 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Output: "HEADLESS-1"}) {
1396 t.Fatalf("wlr frame reported %+v", got)
1397 }
1398 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Time: time.Now()}
1399 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Output: "HEADLESS-1"}) {
1400 t.Fatalf("wlr completion reported %+v", got)
1401 }
1402 // ext output source.
1403 session, ev := cc.session(t, cc.outputSource(t))
1404 constraints(t, cc, ev)
1405 open := h.open(t)
1406 cc.extFrame(t, session, 4, 4)
1407 receiveCapture(t, h.captures)
1408 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Session: open.ID, Output: "HEADLESS-1"}) {
1409 t.Fatalf("ext output frame reported %+v", got)
1410 }
1411 // ext region source: the logical rectangle as asked.
1412 session, ev = cc.session(t, cc.regionSource(t, 1, 1, 2, 2))
1413 constraints(t, cc, ev)
1414 open = h.open(t)
1415 cc.extFrame(t, session, 2, 2)
1416 receiveCapture(t, h.captures)
1417 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Session: open.ID, Output: "HEADLESS-1", Region: ports.Rect{X: 1, Y: 1, W: 2, H: 2}}) {
1418 t.Fatalf("ext region frame reported %+v", got)
1419 }
1420 // ext workspace source.
1421 handles := h.handles(t, cc, 1)
1422 session, ev = cc.session(t, cc.workspaceSource(t, handles[0]))
1423 constraints(t, cc, ev)
1424 open = h.open(t)
1425 cc.extFrame(t, session, 4, 4)
1426 receiveCapture(t, h.captures)
1427 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Session: open.ID, Workspace: 42}) {
1428 t.Fatalf("ext workspace frame reported %+v", got)
1429 }
1430}
1431
1432// Every capture request asks the output owner to hold it until the indicator
1433// is on screen, whatever protocol or source made it.
1434func TestEveryCaptureRequestAsksForItsIndicator(t *testing.T) {
1435 h := newCaptureHarness(t)
1436 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
1437 cc := h.client(t)
1438 cc.wlrFrame(t)
1439 if r := receiveCapture(t, h.captures); !r.Indicate {
1440 t.Fatalf("wlr request %+v", r)
1441 }
1442 session, ev := cc.session(t, cc.outputSource(t))
1443 constraints(t, cc, ev)
1444 h.open(t)
1445 cc.extFrame(t, session, 4, 4)
1446 if r := receiveCapture(t, h.captures); !r.Indicate {
1447 t.Fatalf("ext output request %+v", r)
1448 }
1449 handles := h.handles(t, cc, 1)
1450 session, ev = cc.session(t, cc.workspaceSource(t, handles[0]))
1451 constraints(t, cc, ev)
1452 h.open(t)
1453 cc.extFrame(t, session, 4, 4)
1454 if r := receiveCapture(t, h.captures); !r.Indicate || r.Workspace != 42 {
1455 t.Fatalf("ext workspace request %+v", r)
1456 }
1457}
1458
1459// A capture the compositor refuses is not reported: nothing was captured.
1460func TestRefusedCaptureIsNotReported(t *testing.T) {
1461 h := newCaptureHarness(t)
1462 cc := h.client(t)
1463 session, ev := cc.session(t, cc.outputSource(t))
1464 constraints(t, cc, ev)
1465 h.open(t)
1466 // A wrong-sized buffer is refused before it reaches the renderer.
1467 cc.extFrame(t, session, 2, 2)
1468 noCapture(t, h.captures)
1469 h.settle(t, cc)
1470 for len(h.events) > 0 {
1471 if v, ok := (<-h.events).(ports.CaptureFrameTaken); ok {
1472 t.Fatalf("refused capture reported: %+v", v)
1473 }
1474 }
1475}
1476
1477// logicalRegion gives core the logical rectangle of a physical region.
1478func TestLogicalRegion(t *testing.T) {
1479 o := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Width: 8, Height: 8}, Scale: 2}}
1480 for _, tc := range []struct {
1481 phys image.Rectangle
1482 want ports.Rect
1483 }{
1484 {image.Rect(0, 0, 8, 8), ports.Rect{}},
1485 {image.Rect(2, 2, 6, 4), ports.Rect{X: 1, Y: 1, W: 2, H: 1}},
1486 {image.Rect(1, 1, 5, 3), ports.Rect{X: 0, Y: 0, W: 3, H: 2}}, // rounded outwards
1487 } {
1488 if got := logicalRegion(o, tc.phys); got != tc.want {
1489 t.Errorf("%v: %+v, want %+v", tc.phys, got, tc.want)
1490 }
1491 }
1492}