1package server
2
3import (
4 "encoding/binary"
5 "runtime"
6 "testing"
7
8 "github.com/bnema/purego"
9 "golang.org/x/sys/unix"
10)
11
12func xwmDisplay(t testing.TB) *Display {
13 t.Helper()
14 runtime.LockOSThread()
15 t.Cleanup(runtime.UnlockOSThread)
16 d, err := NewDisplay()
17 if err != nil {
18 t.Fatal(err)
19 }
20 t.Cleanup(d.Close)
21 return d
22}
23
24func xwmClient(t testing.TB, d *Display) (Client, int) {
25 t.Helper()
26 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
27 if err != nil {
28 t.Fatal(err)
29 }
30 t.Cleanup(func() { unix.Close(fds[1]) })
31 c, err := d.CreateClient(fds[0])
32 if err != nil {
33 unix.Close(fds[0])
34 t.Fatal(err)
35 }
36 return c, fds[1]
37}
38
39// registry requests exercise libwayland's real advertisement/filter path.
40func registry(t testing.TB, d *Display, fd int) int {
41 t.Helper()
42 request := [12]byte{}
43 binary.NativeEndian.PutUint32(request[:4], 1)
44 binary.NativeEndian.PutUint32(request[4:8], 12<<16|1)
45 binary.NativeEndian.PutUint32(request[8:], 2)
46 if _, err := unix.Write(fd, request[:]); err != nil {
47 t.Fatal(err)
48 }
49 if xwmDispatch(d) < 0 {
50 t.Fatalf("dispatch failed: errno=%v", lastErrno())
51 }
52 wlDisplayFlushClients(d.c)
53 var buf [4096]byte
54 n, err := unix.Read(fd, buf[:])
55 if err == unix.EAGAIN {
56 return 0
57 }
58 if err != nil {
59 t.Fatal(err)
60 }
61 return n
62}
63
64func TestGlobalFilter(t *testing.T) {
65 d := xwmDisplay(t)
66 iface := &Interface{Name: "xwm_filter_test", Version: 1}
67 if err := NewInterfaces(iface); err != nil {
68 t.Fatal(err)
69 }
70 g, err := d.AddGlobal(iface, 1, nil)
71 if err != nil {
72 t.Fatal(err)
73 }
74 allowed, a := xwmClient(t, d)
75 _, b := xwmClient(t, d)
76 calls := 0
77 d.SetGlobalFilter(func(c Client, got *Global) bool {
78 calls++
79 if got != g {
80 t.Error("wrong global handle")
81 }
82 return c == allowed
83 })
84 if registry(t, d, a) == 0 {
85 t.Fatal("allowed client saw no global")
86 }
87 if registry(t, d, b) != 0 {
88 t.Fatal("denied client saw global")
89 }
90 if calls != 2 {
91 t.Fatalf("filter calls = %d", calls)
92 }
93 allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) })
94 if allocs != 0 {
95 t.Fatalf("filter allocs = %g", allocs)
96 }
97 // Creating a global with registries already present invokes the filter
98 // before AddGlobal returns, but still passes the eventual handle.
99 var during *Global
100 d.SetGlobalFilter(func(_ Client, got *Global) bool { during = got; return false })
101 second, err := d.AddGlobal(iface, 1, nil)
102 if err != nil {
103 t.Fatal(err)
104 }
105 if during != second {
106 t.Fatal("creation-time filter handle differs")
107 }
108 d.SetGlobalFilter(nil)
109 _, c := xwmClient(t, d)
110 if registry(t, d, c) == 0 {
111 t.Fatal("nil did not remove filter")
112 }
113 g.Remove()
114 g.Remove()
115}
116
117func TestWritableInterest(t *testing.T) {
118 d := xwmDisplay(t)
119 fds := xwmSocketpair(t)
120 fd := fds[0]
121 reads, writes := 0, 0
122 s, err := d.AddFDRW(fd, func() { reads++ }, func() { writes++ })
123 if err != nil {
124 t.Fatal(err)
125 }
126 dispatch := func() {
127 if xwmDispatch(d) < 0 {
128 t.Fatalf("dispatch failed: errno=%v", lastErrno())
129 }
130 }
131 dispatch()
132 if writes != 0 {
133 t.Fatal("writable starts enabled")
134 }
135 if err := s.SetWritable(true); err != nil {
136 t.Fatal(err)
137 }
138 dispatch()
139 if writes != 1 || reads != 0 {
140 t.Fatalf("reads=%d writes=%d", reads, writes)
141 }
142 allocs := testing.AllocsPerRun(1000, dispatch)
143 if allocs != 0 {
144 t.Fatalf("fd dispatch allocs = %g", allocs)
145 }
146 before := writes
147 if err := s.SetWritable(false); err != nil {
148 t.Fatal(err)
149 }
150 dispatch()
151 if writes != before {
152 t.Fatal("writable not disabled")
153 }
154 // Readable callbacks also stay allocation-free through the real C loop.
155 serverFD := fds[1]
156 var payload = [1]byte{1}
157 if _, err := unix.Write(serverFD, payload[:]); err != nil {
158 t.Fatal(err)
159 }
160 allocs = testing.AllocsPerRun(1000, dispatch)
161 if allocs != 0 {
162 t.Fatalf("readable dispatch allocs = %g", allocs)
163 }
164 if reads == 0 {
165 t.Fatal("readable interest lost")
166 }
167 if _, err := unix.Read(fd, payload[:]); err != nil {
168 t.Fatal(err)
169 }
170 reads = 0
171 // Combined masks, hangup routing and removal during readable delivery.
172 fdReadable(0, 1|2, s.token)
173 if reads != 1 || writes != before+1 {
174 t.Fatal("combined mask not delivered")
175 }
176 s.onReadable = func() { reads++; s.Remove() }
177 fdReadable(0, 4|8|2, s.token)
178 if reads != 2 || writes != before+1 {
179 t.Fatal("removal did not suppress writable")
180 }
181 if err := s.SetWritable(true); err != nil {
182 t.Fatal(err)
183 }
184 plain, err := d.AddFD(fd, func() {})
185 if err != nil {
186 t.Fatal(err)
187 }
188 if plain.SetWritable(true) == nil {
189 t.Fatal("enabled without writable callback")
190 }
191}
192
193func BenchmarkGlobalFilter(b *testing.B) {
194 d := xwmDisplay(b)
195 iface := &Interface{Name: "xwm_filter_bench", Version: 1}
196 if err := NewInterfaces(iface); err != nil {
197 b.Fatal(err)
198 }
199 g, err := d.AddGlobal(iface, 1, nil)
200 if err != nil {
201 b.Fatal(err)
202 }
203 c, _ := xwmClient(b, d)
204 d.SetGlobalFilter(func(Client, *Global) bool { return true })
205 b.ReportAllocs()
206 b.ResetTimer()
207 for b.Loop() {
208 purego.Syscall6(cbGlobalFilter, c.c, g.c, d.c, 0, 0, 0)
209 }
210}
211
212func BenchmarkFDCallback(b *testing.B) {
213 d := xwmDisplay(b)
214 fds := xwmSocketpair(b)
215 fd := fds[0]
216 s, err := d.AddFDRW(fd, func() {}, func() {})
217 if err != nil {
218 b.Fatal(err)
219 }
220 if err := s.SetWritable(true); err != nil {
221 b.Fatal(err)
222 }
223 b.ReportAllocs()
224 b.ResetTimer()
225 for b.Loop() {
226 xwmDispatch(d)
227 }
228}
229
230func TestLateRestrictedGlobal(t *testing.T) {
231 d := xwmDisplay(t)
232 allowed, a := xwmClient(t, d)
233 _, denied := xwmClient(t, d)
234 if registry(t, d, a) != 0 || registry(t, d, denied) != 0 {
235 t.Fatal("unexpected globals")
236 }
237 iface := &Interface{Name: "xwm_late_restricted", Version: 2}
238 if err := NewInterfaces(iface); err != nil {
239 t.Fatal(err)
240 }
241 calls, binds := 0, 0
242 policy := func(c Client, g *Global) bool {
243 calls++
244 if g.Version() != 2 {
245 t.Error("creation-time version unavailable")
246 }
247 return g.Interface() != iface.Name || c == allowed
248 }
249 d.SetGlobalFilter(policy)
250 g, err := d.AddGlobal(iface, 2, func(Client, uint32, uint32) { binds++ })
251 if err != nil {
252 t.Fatal(err)
253 }
254 if calls != 2 {
255 t.Fatalf("creation-time calls = %d", calls)
256 }
257 wlDisplayFlushClients(d.c)
258 var buf [4096]byte
259 n, err := unix.Read(a, buf[:])
260 if err != nil || n < 16 {
261 t.Fatalf("allowed announcement: n=%d err=%v", n, err)
262 }
263 name := binary.NativeEndian.Uint32(buf[8:12])
264 if n, err := unix.Read(denied, buf[:]); err != unix.EAGAIN || n > 0 {
265 t.Fatalf("denied announcement: n=%d err=%v", n, err)
266 }
267 if allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) }); allocs != 0 {
268 t.Fatalf("metadata policy allocs = %g", allocs)
269 }
270 // Guessing a hidden global's numeric name must not bypass the filter.
271 strLen := len(iface.Name) + 1
272 padded := (strLen + 3) &^ 3
273 req := make([]byte, 24+padded)
274 binary.NativeEndian.PutUint32(req[0:4], 2) // wl_registry
275 binary.NativeEndian.PutUint32(req[4:8], uint32(len(req))<<16) // bind
276 binary.NativeEndian.PutUint32(req[8:12], name)
277 binary.NativeEndian.PutUint32(req[12:16], uint32(strLen))
278 copy(req[16:], iface.Name)
279 binary.NativeEndian.PutUint32(req[16+padded:], 2)
280 binary.NativeEndian.PutUint32(req[20+padded:], 3)
281 if _, err := unix.Write(denied, req); err != nil {
282 t.Fatal(err)
283 }
284 if xwmDispatch(d) < 0 {
285 t.Fatal("dispatch")
286 }
287 wlDisplayFlushClients(d.c)
288 if binds != 0 {
289 t.Fatal("denied bind reached handler")
290 }
291 n, err = unix.Read(denied, buf[:])
292 if err != nil || n < 12 || binary.NativeEndian.Uint32(buf[:4]) != 1 || binary.NativeEndian.Uint32(buf[4:8])&0xffff != 0 {
293 t.Fatalf("expected wl_display.error: n=%d err=%v", n, err)
294 }
295}
296
297func TestWritableBackpressureAndRemoval(t *testing.T) {
298 d := xwmDisplay(t)
299 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
300 if err != nil {
301 t.Fatal(err)
302 }
303 defer unix.Close(fds[0])
304 defer unix.Close(fds[1])
305 if err := unix.SetsockoptInt(fds[0], unix.SOL_SOCKET, unix.SO_SNDBUF, 4096); err != nil {
306 t.Fatal(err)
307 }
308 var payload [4096]byte
309 total := 0
310 for attempts := 0; attempts < 1024; attempts++ {
311 n, err := unix.Write(fds[0], payload[:])
312 if err == unix.EAGAIN {
313 break
314 }
315 if err != nil {
316 t.Fatal(err)
317 }
318 total += n
319 }
320 if _, err := unix.Write(fds[0], payload[:]); err != unix.EAGAIN {
321 t.Fatalf("not backpressured: %v", err)
322 }
323 writes := 0
324 var s *Source
325 s, err = d.AddFDRW(fds[0], func() { t.Error("unexpected readable") }, func() {
326 writes++
327 if n, err := unix.Write(fds[0], payload[:1]); err != nil || n != 1 {
328 t.Errorf("resume: n=%d err=%v", n, err)
329 }
330 if err := s.SetWritable(false); err != nil {
331 t.Error(err)
332 }
333 s.Remove() // Removal from a real C-dispatched callback.
334 })
335 if err != nil {
336 t.Fatal(err)
337 }
338 if err := s.SetWritable(true); err != nil {
339 t.Fatal(err)
340 }
341 xwmDispatch(d)
342 if writes != 0 {
343 t.Fatal("writable fired before peer drained")
344 }
345 for remaining := total; remaining > 0; {
346 n, err := unix.Read(fds[1], payload[:])
347 if err != nil || n == 0 {
348 t.Fatalf("drain: n=%d err=%v", n, err)
349 }
350 remaining -= n
351 }
352 xwmDispatch(d)
353 if writes != 1 || s.c != 0 {
354 t.Fatalf("writes=%d source=%x", writes, s.c)
355 }
356 xwmDispatch(d)
357 if writes != 1 {
358 t.Fatal("removed source dispatched again")
359 }
360}
361
362// The Go runtime can interrupt epoll_wait with its preemption signal.
363func xwmDispatch(d *Display) int32 {
364 for attempt := 0; attempt < 16; attempt++ {
365 result := wlEventLoopDispatch(d.loop, 0)
366 if result >= 0 || lastErrno() != unix.EINTR {
367 return result
368 }
369 }
370 return -1
371}
372
373// FD sources use raw sockets, not Wayland clients: arbitrary payloads must
374// never enter libwayland's protocol transport or its flush/error handling.
375func xwmSocketpair(t testing.TB) [2]int {
376 t.Helper()
377 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
378 if err != nil {
379 t.Fatal(err)
380 }
381 t.Cleanup(func() { unix.Close(fds[0]); unix.Close(fds[1]) })
382 return fds
383}