detached 5d81d73

fix(server): report errno from CreateClient, harden client tests and docs

CreateClient failures wrap the libwayland errno. Clarify OnDestroy (same-client registration unsupported, panics if listener memory cannot be mapped) and the single running Display per process. Tests: idempotent peer close, fd identity via fstat, peer-already-closed adoption, registering from a callback on another client, and free-list reuse across chunk boundaries.
commit
5d81d73861c6294c73d06ddbca36aa853c5ef829
author
brice · · 2026-09-30 18:25 UTC
parent
b9309cb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +218 -35
M server/client.go +19 -6
@@ -32,7 +32,13 @@ func (c Client) FD() (int, error) {
3232 // failure libwayland does not take it (it is left open) and the caller keeps
3333 // ownership and must close it. Sockets should be close-on-exec (accept with
3434 // SOCK_CLOEXEC); libwayland does not set the flag. fd must be a connected
35-// unix socket: libwayland reads its peer credentials with SO_PEERCRED.
35+// unix socket: libwayland reads its peer credentials with SO_PEERCRED. A
36+// socket whose peer has already closed is still adopted; the client is then
37+// destroyed on the next dispatch.
38+//
39+// On failure the error wraps the errno libwayland left (for example ENOTSOCK
40+// or ENOMEM), read right after the call on the same thread; it is reliable
41+// when called on the locked display goroutine.
3642 //
3743 // The returned Client is valid until destroyed; register OnDestroy to learn
3844 // when. Display goroutine only, or before Run starts.
@@ -45,7 +51,7 @@ func (d *Display) CreateClient(fd int) (Client, error) {
4551 }
4652 r, _, _ := purego.Syscall6(symClientCreate, d.c, uintptr(fd), 0, 0, 0, 0)
4753 if r == 0 {
48- return Client{}, fmt.Errorf("purego-libwayland: wl_client_create(%d) failed; fd not adopted", fd)
54+ return Client{}, fmt.Errorf("purego-libwayland: wl_client_create(%d) failed, fd not adopted: %w", fd, lastErrno())
4955 }
5056 return Client{r}, nil
5157 }
@@ -62,12 +68,17 @@ func (d *Display) CreateClient(fd int) (Client, error) {
6268 // delete it from a map[Client]T, because the handle value may be reused by a
6369 // later client.
6470 //
71+// Registering another listener on the same client from inside fn is not
72+// supported: whether it would run depends on how libwayland's final emit walks
73+// the listener list (it differs between versions). Registering on a different,
74+// live client from inside fn is fine.
75+//
6576 // A Client is a comparable value (a handle to the C object) and is safe as a
6677 // map key from its creation or bind until fn runs.
6778 //
6879 // OnDestroy does nothing for a zero Client or nil fn. Registering on an
69-// already destroyed client is invalid. Display goroutine only, or before Run
70-// starts.
80+// already destroyed client is invalid. It panics if the memory for the
81+// listener cannot be mapped. Display goroutine only, or before Run starts.
7182 func (c Client) OnDestroy(fn func()) {
7283 if c.c == 0 || fn == nil {
7384 return
@@ -121,8 +132,10 @@ func (s *listenerSlots) put(l uintptr) { s.free = append(s.free, l) }
121132 func clientGone(l uintptr) {
122133 fn := live.clientGone[l]
123134 delete(live.clientGone, l)
124- // libwayland may leave the node linked while notifying; unlink it
125- // (harmless if it already is) before the slot can be reused.
135+ // libwayland's emit loop first unlinks each node and, on current versions,
136+ // resets it with wl_list_init, so this is a no-op on a self-linked node;
137+ // on older versions the node may still be linked and must be unlinked
138+ // before the slot can be reused.
126139 purego.Syscall6(symListRemove, l, 0, 0, 0, 0, 0)
127140 listeners.put(l)
128141 if fn != nil {
@@ -32,7 +32,13 @@ func (c Client) FD() (int, error) {
32 // failure libwayland does not take it (it is left open) and the caller keeps32 // failure libwayland does not take it (it is left open) and the caller keeps
33 // ownership and must close it. Sockets should be close-on-exec (accept with33 // ownership and must close it. Sockets should be close-on-exec (accept with
34 // SOCK_CLOEXEC); libwayland does not set the flag. fd must be a connected34 // SOCK_CLOEXEC); libwayland does not set the flag. fd must be a connected
35-// unix socket: libwayland reads its peer credentials with SO_PEERCRED.35+// unix socket: libwayland reads its peer credentials with SO_PEERCRED. A
36+// socket whose peer has already closed is still adopted; the client is then
37+// destroyed on the next dispatch.
38+//
39+// On failure the error wraps the errno libwayland left (for example ENOTSOCK
40+// or ENOMEM), read right after the call on the same thread; it is reliable
41+// when called on the locked display goroutine.
36 //42 //
37 // The returned Client is valid until destroyed; register OnDestroy to learn43 // The returned Client is valid until destroyed; register OnDestroy to learn
38 // when. Display goroutine only, or before Run starts.44 // when. Display goroutine only, or before Run starts.
@@ -45,7 +51,7 @@ func (d *Display) CreateClient(fd int) (Client, error) {
45 }51 }
46 r, _, _ := purego.Syscall6(symClientCreate, d.c, uintptr(fd), 0, 0, 0, 0)52 r, _, _ := purego.Syscall6(symClientCreate, d.c, uintptr(fd), 0, 0, 0, 0)
47 if r == 0 {53 if r == 0 {
48- return Client{}, fmt.Errorf("purego-libwayland: wl_client_create(%d) failed; fd not adopted", fd)54+ return Client{}, fmt.Errorf("purego-libwayland: wl_client_create(%d) failed, fd not adopted: %w", fd, lastErrno())
49 }55 }
50 return Client{r}, nil56 return Client{r}, nil
51 }57 }
@@ -62,12 +68,17 @@ func (d *Display) CreateClient(fd int) (Client, error) {
62 // delete it from a map[Client]T, because the handle value may be reused by a68 // delete it from a map[Client]T, because the handle value may be reused by a
63 // later client.69 // later client.
64 //70 //
71+// Registering another listener on the same client from inside fn is not
72+// supported: whether it would run depends on how libwayland's final emit walks
73+// the listener list (it differs between versions). Registering on a different,
74+// live client from inside fn is fine.
75+//
65 // A Client is a comparable value (a handle to the C object) and is safe as a76 // A Client is a comparable value (a handle to the C object) and is safe as a
66 // map key from its creation or bind until fn runs.77 // map key from its creation or bind until fn runs.
67 //78 //
68 // OnDestroy does nothing for a zero Client or nil fn. Registering on an79 // OnDestroy does nothing for a zero Client or nil fn. Registering on an
69-// already destroyed client is invalid. Display goroutine only, or before Run80+// already destroyed client is invalid. It panics if the memory for the
70-// starts.81+// listener cannot be mapped. Display goroutine only, or before Run starts.
71 func (c Client) OnDestroy(fn func()) {82 func (c Client) OnDestroy(fn func()) {
72 if c.c == 0 || fn == nil {83 if c.c == 0 || fn == nil {
73 return84 return
@@ -121,8 +132,10 @@ func (s *listenerSlots) put(l uintptr) { s.free = append(s.free, l) }
121 func clientGone(l uintptr) {132 func clientGone(l uintptr) {
122 fn := live.clientGone[l]133 fn := live.clientGone[l]
123 delete(live.clientGone, l)134 delete(live.clientGone, l)
124- // libwayland may leave the node linked while notifying; unlink it135+ // libwayland's emit loop first unlinks each node and, on current versions,
125- // (harmless if it already is) before the slot can be reused.136+ // resets it with wl_list_init, so this is a no-op on a self-linked node;
137+ // on older versions the node may still be linked and must be unlinked
138+ // before the slot can be reused.
126 purego.Syscall6(symListRemove, l, 0, 0, 0, 0, 0)139 purego.Syscall6(symListRemove, l, 0, 0, 0, 0, 0)
127 listeners.put(l)140 listeners.put(l)
128 if fn != nil {141 if fn != nil {
M server/client_test.go +196 -29
@@ -3,6 +3,8 @@ package server
33 import (
44 "context"
55 "encoding/binary"
6+ "errors"
7+ "sync"
68 "testing"
79 "time"
810
@@ -40,19 +42,47 @@ func clientDisplay(t *testing.T) (*Display, func()) {
4042 return d, stop
4143 }
4244
43-func socketPair(t *testing.T) [2]int {
45+// sockPair is a connected socketpair. srv is meant to be adopted by the
46+// display; peer is the test's end, closed at most once.
47+type sockPair struct {
48+ srv, peer int
49+ once sync.Once
50+}
51+
52+func socketPair(t *testing.T) *sockPair {
4453 t.Helper()
45- fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
54+ raw, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
4655 if err != nil {
4756 t.Fatal(err)
4857 }
49- t.Cleanup(func() { _ = unix.Close(fds[1]) })
50- return [2]int{fds[0], fds[1]}
58+ p := &sockPair{srv: raw[0], peer: raw[1]}
59+ t.Cleanup(p.closePeer)
60+ return p
61+}
62+
63+// closePeer closes the peer end; it is idempotent, so a test may call it and
64+// the cleanup still will not close a reused descriptor number.
65+func (p *sockPair) closePeer() { p.once.Do(func() { _ = unix.Close(p.peer) }) }
66+
67+// fileID identifies the open file behind fd, immune to fd number reuse.
68+type fileID struct{ dev, ino uint64 }
69+
70+func identify(t *testing.T, fd int) fileID {
71+ t.Helper()
72+ var st unix.Stat_t
73+ if err := unix.Fstat(fd, &st); err != nil {
74+ t.Fatal(err)
75+ }
76+ return fileID{uint64(st.Dev), st.Ino}
5177 }
5278
53-func fdOpen(fd int) bool {
54- _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
55- return err == nil
79+// stillOpen reports whether fd still refers to the file id.
80+func stillOpen(fd int, id fileID) bool {
81+ var st unix.Stat_t
82+ if unix.Fstat(fd, &st) != nil {
83+ return false
84+ }
85+ return fileID{uint64(st.Dev), st.Ino} == id
5686 }
5787
5888 func adopt(t *testing.T, d *Display, fd int) Client {
@@ -71,7 +101,7 @@ func adopt(t *testing.T, d *Display, fd int) Client {
71101 func TestClientFD(t *testing.T) {
72102 d, _ := clientDisplay(t)
73103 fds := socketPair(t)
74- c := adopt(t, d, fds[0])
104+ c := adopt(t, d, fds.srv)
75105 var (
76106 got, zero int
77107 err, zerr error
@@ -84,8 +114,8 @@ func TestClientFD(t *testing.T) {
84114 }) {
85115 t.Fatal("display stopped")
86116 }
87- if err != nil || got != fds[0] {
88- t.Fatalf("FD = %d, %v; want %d", got, err, fds[0])
117+ if err != nil || got != fds.srv {
118+ t.Fatalf("FD = %d, %v; want %d", got, err, fds.srv)
89119 }
90120 if zerr == nil || zero != -1 {
91121 t.Fatalf("invalid client FD = %d, %v", zero, zerr)
@@ -103,6 +133,7 @@ func TestCreateClientFailureKeepsFD(t *testing.T) {
103133 }
104134 defer unix.Close(p[0])
105135 defer unix.Close(p[1])
136+ pipeID := identify(t, p[0])
106137 var err, errNeg error
107138 var c, cn Client
108139 if !d.Do(func() {
@@ -111,10 +142,13 @@ func TestCreateClientFailureKeepsFD(t *testing.T) {
111142 }) {
112143 t.Fatal("display stopped")
113144 }
145+ if !errors.Is(err, unix.ENOTSOCK) {
146+ t.Fatalf("error %v does not wrap ENOTSOCK", err)
147+ }
114148 if err == nil || c != (Client{}) || errNeg == nil || cn != (Client{}) {
115149 t.Fatalf("got %v %v, %v %v; want errors", c, err, cn, errNeg)
116150 }
117- if !fdOpen(p[0]) {
151+ if !stillOpen(p[0], pipeID) {
118152 t.Fatal("failed CreateClient closed the fd")
119153 }
120154 }
@@ -122,13 +156,11 @@ func TestCreateClientFailureKeepsFD(t *testing.T) {
122156 func TestCreateClientOwnsFD(t *testing.T) {
123157 d, _ := clientDisplay(t)
124158 fds := socketPair(t)
125- c := adopt(t, d, fds[0])
159+ c := adopt(t, d, fds.srv)
126160 gone := make(chan struct{})
127161 d.Do(func() { c.OnDestroy(func() { close(gone) }) })
128- if !fdOpen(fds[0]) {
129- t.Fatal("adopted fd closed early")
130- }
131- _ = unix.Close(fds[1])
162+ id := identify(t, fds.srv)
163+ fds.closePeer()
132164 select {
133165 case <-gone:
134166 case <-time.After(3 * time.Second):
@@ -137,7 +169,7 @@ func TestCreateClientOwnsFD(t *testing.T) {
137169 // The listener runs before libwayland closes the fd; Do returns only once
138170 // the destruction that ran it has finished.
139171 d.Do(func() {})
140- if fdOpen(fds[0]) {
172+ if stillOpen(fds.srv, id) {
141173 t.Fatal("libwayland did not close the adopted fd")
142174 }
143175 }
@@ -185,13 +217,13 @@ func TestCreateClientBindsRegistry(t *testing.T) {
185217 defer func() { cancel(); <-done }()
186218
187219 fds := socketPair(t)
188- c := adopt(t, d, fds[0])
189- if err := unix.SetsockoptTimeval(fds[1], unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil {
220+ c := adopt(t, d, fds.srv)
221+ if err := unix.SetsockoptTimeval(fds.peer, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil {
190222 t.Fatal(err)
191223 }
192224 // wl_display.get_registry(new_id 2), then wl_display.sync(new_id 3).
193225 req := append(wireMsg(1, 1, u32(2)), wireMsg(1, 0, u32(3))...)
194- if _, err := unix.Write(fds[1], req); err != nil {
226+ if _, err := unix.Write(fds.peer, req); err != nil {
195227 t.Fatal(err)
196228 }
197229 // Read until the sync callback's done event (object 3), collecting globals.
@@ -199,7 +231,7 @@ func TestCreateClientBindsRegistry(t *testing.T) {
199231 var name uint32
200232 buf := make([]byte, 4096)
201233 for sawDone := false; !sawDone; {
202- n, err := unix.Read(fds[1], buf)
234+ n, err := unix.Read(fds.peer, buf)
203235 if err != nil || n == 0 {
204236 t.Fatalf("read: %d, %v", n, err)
205237 }
@@ -227,7 +259,7 @@ func TestCreateClientBindsRegistry(t *testing.T) {
227259 t.Fatal("global not advertised to the adopted client")
228260 }
229261 // wl_registry.bind(name, interface, version, new_id 4)
230- if _, err := unix.Write(fds[1], wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil {
262+ if _, err := unix.Write(fds.peer, wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil {
231263 t.Fatal(err)
232264 }
233265 select {
@@ -247,7 +279,7 @@ func TestOnDestroyOnDisconnect(t *testing.T) {
247279 }
248280 d, _ := clientDisplay(t)
249281 fds := socketPair(t)
250- c := adopt(t, d, fds[0])
282+ c := adopt(t, d, fds.srv)
251283 var (
252284 order []int
253285 resAlive, fdOK bool
@@ -265,7 +297,7 @@ func TestOnDestroyOnDisconnect(t *testing.T) {
265297 if i == 3 {
266298 var fd int
267299 fd, fdError = c.FD()
268- fdOK = fd == fds[0]
300+ fdOK = fd == fds.srv
269301 resAlive = res.Alive()
270302 delete(other, c)
271303 close(fired)
@@ -286,7 +318,7 @@ func TestOnDestroyOnDisconnect(t *testing.T) {
286318 t.Fatal("fired before disconnect")
287319 case <-time.After(50 * time.Millisecond):
288320 }
289- _ = unix.Close(fds[1])
321+ fds.closePeer()
290322 select {
291323 case <-fired:
292324 case <-time.After(3 * time.Second):
@@ -313,7 +345,7 @@ func TestOnDestroyOnDisplayClose(t *testing.T) {
313345 t.Fatal(err)
314346 }
315347 fds := socketPair(t)
316- c, err := d.CreateClient(fds[0])
348+ c, err := d.CreateClient(fds.srv)
317349 if err != nil {
318350 t.Fatal(err)
319351 }
@@ -330,7 +362,7 @@ func TestOnDestroyOnDisplayClose(t *testing.T) {
330362 func TestOnDestroyOnRunStop(t *testing.T) {
331363 d, stop := clientDisplay(t)
332364 fds := socketPair(t)
333- c := adopt(t, d, fds[0])
365+ c := adopt(t, d, fds.srv)
334366 n := 0
335367 d.Do(func() { c.OnDestroy(func() { n++ }) })
336368 stop()
@@ -343,10 +375,10 @@ func TestOnDestroyOnRunStop(t *testing.T) {
343375 func TestOnDestroyOnce(t *testing.T) {
344376 d, stop := clientDisplay(t)
345377 fds := socketPair(t)
346- c := adopt(t, d, fds[0])
378+ c := adopt(t, d, fds.srv)
347379 fired := make(chan struct{}, 4)
348380 d.Do(func() { c.OnDestroy(func() { fired <- struct{}{} }) })
349- _ = unix.Close(fds[1])
381+ fds.closePeer()
350382 select {
351383 case <-fired:
352384 case <-time.After(3 * time.Second):
@@ -357,3 +389,138 @@ func TestOnDestroyOnce(t *testing.T) {
357389 t.Fatal("OnDestroy fired twice")
358390 }
359391 }
392+
393+// A peer that already hung up does not stop adoption; the client is then
394+// destroyed and its listener runs.
395+func TestCreateClientPeerAlreadyClosed(t *testing.T) {
396+ d, _ := clientDisplay(t)
397+ fds := socketPair(t)
398+ fds.closePeer()
399+ id := identify(t, fds.srv) // before adoption: the client may die at once
400+ gone := make(chan struct{})
401+ var err error
402+ // Adopt and register in one call, before the loop can destroy the client.
403+ if !d.Do(func() {
404+ var c Client
405+ if c, err = d.CreateClient(fds.srv); err == nil {
406+ c.OnDestroy(func() { close(gone) })
407+ }
408+ }) {
409+ t.Fatal("display stopped")
410+ }
411+ if err != nil {
412+ t.Fatal(err)
413+ }
414+ select {
415+ case <-gone:
416+ case <-time.After(3 * time.Second):
417+ t.Fatal("client with a closed peer was not destroyed")
418+ }
419+ d.Do(func() {})
420+ if stillOpen(fds.srv, id) {
421+ t.Fatal("adopted fd not closed")
422+ }
423+}
424+
425+// A listener may register on another live client from inside its callback,
426+// even though it is handed the slot that is firing.
427+func TestOnDestroyRegistersOnOtherClient(t *testing.T) {
428+ d, _ := clientDisplay(t)
429+ a, b := socketPair(t), socketPair(t)
430+ ca, cb := adopt(t, d, a.srv), adopt(t, d, b.srv)
431+ aGone := make(chan struct{})
432+ bGone := make(chan struct{}, 2)
433+ var slotA, slotB uintptr
434+ d.Do(func() {
435+ for l := range live.clientGone {
436+ t.Errorf("unexpected live listener %#x", l)
437+ }
438+ ca.OnDestroy(func() {
439+ cb.OnDestroy(func() { bGone <- struct{}{} })
440+ for l := range live.clientGone {
441+ slotB = l
442+ }
443+ close(aGone)
444+ })
445+ for l := range live.clientGone {
446+ slotA = l
447+ }
448+ })
449+ a.closePeer()
450+ select {
451+ case <-aGone:
452+ case <-time.After(3 * time.Second):
453+ t.Fatal("first client's listener did not fire")
454+ }
455+ if slotA == 0 || slotA != slotB {
456+ t.Fatalf("freed slot not reused: %#x then %#x", slotA, slotB)
457+ }
458+ select {
459+ case <-bGone:
460+ t.Fatal("second client's listener fired early")
461+ case <-time.After(50 * time.Millisecond):
462+ }
463+ b.closePeer()
464+ select {
465+ case <-bGone:
466+ case <-time.After(3 * time.Second):
467+ t.Fatal("listener registered from a callback did not fire")
468+ }
469+ time.Sleep(50 * time.Millisecond)
470+ if len(bGone) != 0 {
471+ t.Fatal("listener fired twice")
472+ }
473+}
474+
475+// More listeners than one mapped chunk holds: all fire once, every slot comes
476+// back, and a later registration reuses a recycled slot.
477+func TestOnDestroyManyListeners(t *testing.T) {
478+ d, _ := clientDisplay(t)
479+ fds := socketPair(t)
480+ fds2 := socketPair(t)
481+ c := adopt(t, d, fds.srv)
482+ const n = 3*listenerChunkSize/listenerSlot + 5
483+ var fired, chunksBefore int
484+ done := make(chan struct{})
485+ var before, after int
486+ d.Do(func() {
487+ for i := 0; i < n; i++ {
488+ c.OnDestroy(func() {
489+ fired++
490+ if fired == n {
491+ close(done)
492+ }
493+ })
494+ }
495+ before = len(listeners.free)
496+ chunksBefore = len(listeners.chunks)
497+ })
498+ if chunksBefore < 3 {
499+ t.Fatalf("test did not cross a chunk boundary: %d chunks", chunksBefore)
500+ }
501+ fds.closePeer()
502+ select {
503+ case <-done:
504+ case <-time.After(3 * time.Second):
505+ t.Fatal("not every listener fired")
506+ }
507+ var reused bool
508+ var firedNow int
509+ d.Do(func() {
510+ firedNow = fired
511+ after = len(listeners.free)
512+ c2, err := d.CreateClient(fds2.srv)
513+ if err != nil {
514+ t.Error(err)
515+ return
516+ }
517+ c2.OnDestroy(func() {})
518+ reused = len(listeners.chunks) == chunksBefore && len(listeners.free) == after-1
519+ })
520+ if after != before+n {
521+ t.Fatalf("free slots %d -> %d, want +%d", before, after, n)
522+ }
523+ if firedNow != n || !reused {
524+ t.Fatalf("fired %d, slot reused %v", firedNow, reused)
525+ }
526+}
@@ -3,6 +3,8 @@ package server
3 import (3 import (
4 "context"4 "context"
5 "encoding/binary"5 "encoding/binary"
6+ "errors"
7+ "sync"
6 "testing"8 "testing"
7 "time"9 "time"
8 10
@@ -40,19 +42,47 @@ func clientDisplay(t *testing.T) (*Display, func()) {
40 return d, stop42 return d, stop
41 }43 }
42 44
43-func socketPair(t *testing.T) [2]int {45+// sockPair is a connected socketpair. srv is meant to be adopted by the
46+// display; peer is the test's end, closed at most once.
47+type sockPair struct {
48+ srv, peer int
49+ once sync.Once
50+}
51+
52+func socketPair(t *testing.T) *sockPair {
44 t.Helper()53 t.Helper()
45- fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)54+ raw, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
46 if err != nil {55 if err != nil {
47 t.Fatal(err)56 t.Fatal(err)
48 }57 }
49- t.Cleanup(func() { _ = unix.Close(fds[1]) })58+ p := &sockPair{srv: raw[0], peer: raw[1]}
50- return [2]int{fds[0], fds[1]}59+ t.Cleanup(p.closePeer)
60+ return p
61+}
62+
63+// closePeer closes the peer end; it is idempotent, so a test may call it and
64+// the cleanup still will not close a reused descriptor number.
65+func (p *sockPair) closePeer() { p.once.Do(func() { _ = unix.Close(p.peer) }) }
66+
67+// fileID identifies the open file behind fd, immune to fd number reuse.
68+type fileID struct{ dev, ino uint64 }
69+
70+func identify(t *testing.T, fd int) fileID {
71+ t.Helper()
72+ var st unix.Stat_t
73+ if err := unix.Fstat(fd, &st); err != nil {
74+ t.Fatal(err)
75+ }
76+ return fileID{uint64(st.Dev), st.Ino}
51 }77 }
52 78
53-func fdOpen(fd int) bool {79+// stillOpen reports whether fd still refers to the file id.
54- _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)80+func stillOpen(fd int, id fileID) bool {
55- return err == nil81+ var st unix.Stat_t
82+ if unix.Fstat(fd, &st) != nil {
83+ return false
84+ }
85+ return fileID{uint64(st.Dev), st.Ino} == id
56 }86 }
57 87
58 func adopt(t *testing.T, d *Display, fd int) Client {88 func adopt(t *testing.T, d *Display, fd int) Client {
@@ -71,7 +101,7 @@ func adopt(t *testing.T, d *Display, fd int) Client {
71 func TestClientFD(t *testing.T) {101 func TestClientFD(t *testing.T) {
72 d, _ := clientDisplay(t)102 d, _ := clientDisplay(t)
73 fds := socketPair(t)103 fds := socketPair(t)
74- c := adopt(t, d, fds[0])104+ c := adopt(t, d, fds.srv)
75 var (105 var (
76 got, zero int106 got, zero int
77 err, zerr error107 err, zerr error
@@ -84,8 +114,8 @@ func TestClientFD(t *testing.T) {
84 }) {114 }) {
85 t.Fatal("display stopped")115 t.Fatal("display stopped")
86 }116 }
87- if err != nil || got != fds[0] {117+ if err != nil || got != fds.srv {
88- t.Fatalf("FD = %d, %v; want %d", got, err, fds[0])118+ t.Fatalf("FD = %d, %v; want %d", got, err, fds.srv)
89 }119 }
90 if zerr == nil || zero != -1 {120 if zerr == nil || zero != -1 {
91 t.Fatalf("invalid client FD = %d, %v", zero, zerr)121 t.Fatalf("invalid client FD = %d, %v", zero, zerr)
@@ -103,6 +133,7 @@ func TestCreateClientFailureKeepsFD(t *testing.T) {
103 }133 }
104 defer unix.Close(p[0])134 defer unix.Close(p[0])
105 defer unix.Close(p[1])135 defer unix.Close(p[1])
136+ pipeID := identify(t, p[0])
106 var err, errNeg error137 var err, errNeg error
107 var c, cn Client138 var c, cn Client
108 if !d.Do(func() {139 if !d.Do(func() {
@@ -111,10 +142,13 @@ func TestCreateClientFailureKeepsFD(t *testing.T) {
111 }) {142 }) {
112 t.Fatal("display stopped")143 t.Fatal("display stopped")
113 }144 }
145+ if !errors.Is(err, unix.ENOTSOCK) {
146+ t.Fatalf("error %v does not wrap ENOTSOCK", err)
147+ }
114 if err == nil || c != (Client{}) || errNeg == nil || cn != (Client{}) {148 if err == nil || c != (Client{}) || errNeg == nil || cn != (Client{}) {
115 t.Fatalf("got %v %v, %v %v; want errors", c, err, cn, errNeg)149 t.Fatalf("got %v %v, %v %v; want errors", c, err, cn, errNeg)
116 }150 }
117- if !fdOpen(p[0]) {151+ if !stillOpen(p[0], pipeID) {
118 t.Fatal("failed CreateClient closed the fd")152 t.Fatal("failed CreateClient closed the fd")
119 }153 }
120 }154 }
@@ -122,13 +156,11 @@ func TestCreateClientFailureKeepsFD(t *testing.T) {
122 func TestCreateClientOwnsFD(t *testing.T) {156 func TestCreateClientOwnsFD(t *testing.T) {
123 d, _ := clientDisplay(t)157 d, _ := clientDisplay(t)
124 fds := socketPair(t)158 fds := socketPair(t)
125- c := adopt(t, d, fds[0])159+ c := adopt(t, d, fds.srv)
126 gone := make(chan struct{})160 gone := make(chan struct{})
127 d.Do(func() { c.OnDestroy(func() { close(gone) }) })161 d.Do(func() { c.OnDestroy(func() { close(gone) }) })
128- if !fdOpen(fds[0]) {162+ id := identify(t, fds.srv)
129- t.Fatal("adopted fd closed early")163+ fds.closePeer()
130- }
131- _ = unix.Close(fds[1])
132 select {164 select {
133 case <-gone:165 case <-gone:
134 case <-time.After(3 * time.Second):166 case <-time.After(3 * time.Second):
@@ -137,7 +169,7 @@ func TestCreateClientOwnsFD(t *testing.T) {
137 // The listener runs before libwayland closes the fd; Do returns only once169 // The listener runs before libwayland closes the fd; Do returns only once
138 // the destruction that ran it has finished.170 // the destruction that ran it has finished.
139 d.Do(func() {})171 d.Do(func() {})
140- if fdOpen(fds[0]) {172+ if stillOpen(fds.srv, id) {
141 t.Fatal("libwayland did not close the adopted fd")173 t.Fatal("libwayland did not close the adopted fd")
142 }174 }
143 }175 }
@@ -185,13 +217,13 @@ func TestCreateClientBindsRegistry(t *testing.T) {
185 defer func() { cancel(); <-done }()217 defer func() { cancel(); <-done }()
186 218
187 fds := socketPair(t)219 fds := socketPair(t)
188- c := adopt(t, d, fds[0])220+ c := adopt(t, d, fds.srv)
189- if err := unix.SetsockoptTimeval(fds[1], unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil {221+ if err := unix.SetsockoptTimeval(fds.peer, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil {
190 t.Fatal(err)222 t.Fatal(err)
191 }223 }
192 // wl_display.get_registry(new_id 2), then wl_display.sync(new_id 3).224 // wl_display.get_registry(new_id 2), then wl_display.sync(new_id 3).
193 req := append(wireMsg(1, 1, u32(2)), wireMsg(1, 0, u32(3))...)225 req := append(wireMsg(1, 1, u32(2)), wireMsg(1, 0, u32(3))...)
194- if _, err := unix.Write(fds[1], req); err != nil {226+ if _, err := unix.Write(fds.peer, req); err != nil {
195 t.Fatal(err)227 t.Fatal(err)
196 }228 }
197 // Read until the sync callback's done event (object 3), collecting globals.229 // Read until the sync callback's done event (object 3), collecting globals.
@@ -199,7 +231,7 @@ func TestCreateClientBindsRegistry(t *testing.T) {
199 var name uint32231 var name uint32
200 buf := make([]byte, 4096)232 buf := make([]byte, 4096)
201 for sawDone := false; !sawDone; {233 for sawDone := false; !sawDone; {
202- n, err := unix.Read(fds[1], buf)234+ n, err := unix.Read(fds.peer, buf)
203 if err != nil || n == 0 {235 if err != nil || n == 0 {
204 t.Fatalf("read: %d, %v", n, err)236 t.Fatalf("read: %d, %v", n, err)
205 }237 }
@@ -227,7 +259,7 @@ func TestCreateClientBindsRegistry(t *testing.T) {
227 t.Fatal("global not advertised to the adopted client")259 t.Fatal("global not advertised to the adopted client")
228 }260 }
229 // wl_registry.bind(name, interface, version, new_id 4)261 // wl_registry.bind(name, interface, version, new_id 4)
230- if _, err := unix.Write(fds[1], wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil {262+ if _, err := unix.Write(fds.peer, wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil {
231 t.Fatal(err)263 t.Fatal(err)
232 }264 }
233 select {265 select {
@@ -247,7 +279,7 @@ func TestOnDestroyOnDisconnect(t *testing.T) {
247 }279 }
248 d, _ := clientDisplay(t)280 d, _ := clientDisplay(t)
249 fds := socketPair(t)281 fds := socketPair(t)
250- c := adopt(t, d, fds[0])282+ c := adopt(t, d, fds.srv)
251 var (283 var (
252 order []int284 order []int
253 resAlive, fdOK bool285 resAlive, fdOK bool
@@ -265,7 +297,7 @@ func TestOnDestroyOnDisconnect(t *testing.T) {
265 if i == 3 {297 if i == 3 {
266 var fd int298 var fd int
267 fd, fdError = c.FD()299 fd, fdError = c.FD()
268- fdOK = fd == fds[0]300+ fdOK = fd == fds.srv
269 resAlive = res.Alive()301 resAlive = res.Alive()
270 delete(other, c)302 delete(other, c)
271 close(fired)303 close(fired)
@@ -286,7 +318,7 @@ func TestOnDestroyOnDisconnect(t *testing.T) {
286 t.Fatal("fired before disconnect")318 t.Fatal("fired before disconnect")
287 case <-time.After(50 * time.Millisecond):319 case <-time.After(50 * time.Millisecond):
288 }320 }
289- _ = unix.Close(fds[1])321+ fds.closePeer()
290 select {322 select {
291 case <-fired:323 case <-fired:
292 case <-time.After(3 * time.Second):324 case <-time.After(3 * time.Second):
@@ -313,7 +345,7 @@ func TestOnDestroyOnDisplayClose(t *testing.T) {
313 t.Fatal(err)345 t.Fatal(err)
314 }346 }
315 fds := socketPair(t)347 fds := socketPair(t)
316- c, err := d.CreateClient(fds[0])348+ c, err := d.CreateClient(fds.srv)
317 if err != nil {349 if err != nil {
318 t.Fatal(err)350 t.Fatal(err)
319 }351 }
@@ -330,7 +362,7 @@ func TestOnDestroyOnDisplayClose(t *testing.T) {
330 func TestOnDestroyOnRunStop(t *testing.T) {362 func TestOnDestroyOnRunStop(t *testing.T) {
331 d, stop := clientDisplay(t)363 d, stop := clientDisplay(t)
332 fds := socketPair(t)364 fds := socketPair(t)
333- c := adopt(t, d, fds[0])365+ c := adopt(t, d, fds.srv)
334 n := 0366 n := 0
335 d.Do(func() { c.OnDestroy(func() { n++ }) })367 d.Do(func() { c.OnDestroy(func() { n++ }) })
336 stop()368 stop()
@@ -343,10 +375,10 @@ func TestOnDestroyOnRunStop(t *testing.T) {
343 func TestOnDestroyOnce(t *testing.T) {375 func TestOnDestroyOnce(t *testing.T) {
344 d, stop := clientDisplay(t)376 d, stop := clientDisplay(t)
345 fds := socketPair(t)377 fds := socketPair(t)
346- c := adopt(t, d, fds[0])378+ c := adopt(t, d, fds.srv)
347 fired := make(chan struct{}, 4)379 fired := make(chan struct{}, 4)
348 d.Do(func() { c.OnDestroy(func() { fired <- struct{}{} }) })380 d.Do(func() { c.OnDestroy(func() { fired <- struct{}{} }) })
349- _ = unix.Close(fds[1])381+ fds.closePeer()
350 select {382 select {
351 case <-fired:383 case <-fired:
352 case <-time.After(3 * time.Second):384 case <-time.After(3 * time.Second):
@@ -357,3 +389,138 @@ func TestOnDestroyOnce(t *testing.T) {
357 t.Fatal("OnDestroy fired twice")389 t.Fatal("OnDestroy fired twice")
358 }390 }
359 }391 }
392+
393+// A peer that already hung up does not stop adoption; the client is then
394+// destroyed and its listener runs.
395+func TestCreateClientPeerAlreadyClosed(t *testing.T) {
396+ d, _ := clientDisplay(t)
397+ fds := socketPair(t)
398+ fds.closePeer()
399+ id := identify(t, fds.srv) // before adoption: the client may die at once
400+ gone := make(chan struct{})
401+ var err error
402+ // Adopt and register in one call, before the loop can destroy the client.
403+ if !d.Do(func() {
404+ var c Client
405+ if c, err = d.CreateClient(fds.srv); err == nil {
406+ c.OnDestroy(func() { close(gone) })
407+ }
408+ }) {
409+ t.Fatal("display stopped")
410+ }
411+ if err != nil {
412+ t.Fatal(err)
413+ }
414+ select {
415+ case <-gone:
416+ case <-time.After(3 * time.Second):
417+ t.Fatal("client with a closed peer was not destroyed")
418+ }
419+ d.Do(func() {})
420+ if stillOpen(fds.srv, id) {
421+ t.Fatal("adopted fd not closed")
422+ }
423+}
424+
425+// A listener may register on another live client from inside its callback,
426+// even though it is handed the slot that is firing.
427+func TestOnDestroyRegistersOnOtherClient(t *testing.T) {
428+ d, _ := clientDisplay(t)
429+ a, b := socketPair(t), socketPair(t)
430+ ca, cb := adopt(t, d, a.srv), adopt(t, d, b.srv)
431+ aGone := make(chan struct{})
432+ bGone := make(chan struct{}, 2)
433+ var slotA, slotB uintptr
434+ d.Do(func() {
435+ for l := range live.clientGone {
436+ t.Errorf("unexpected live listener %#x", l)
437+ }
438+ ca.OnDestroy(func() {
439+ cb.OnDestroy(func() { bGone <- struct{}{} })
440+ for l := range live.clientGone {
441+ slotB = l
442+ }
443+ close(aGone)
444+ })
445+ for l := range live.clientGone {
446+ slotA = l
447+ }
448+ })
449+ a.closePeer()
450+ select {
451+ case <-aGone:
452+ case <-time.After(3 * time.Second):
453+ t.Fatal("first client's listener did not fire")
454+ }
455+ if slotA == 0 || slotA != slotB {
456+ t.Fatalf("freed slot not reused: %#x then %#x", slotA, slotB)
457+ }
458+ select {
459+ case <-bGone:
460+ t.Fatal("second client's listener fired early")
461+ case <-time.After(50 * time.Millisecond):
462+ }
463+ b.closePeer()
464+ select {
465+ case <-bGone:
466+ case <-time.After(3 * time.Second):
467+ t.Fatal("listener registered from a callback did not fire")
468+ }
469+ time.Sleep(50 * time.Millisecond)
470+ if len(bGone) != 0 {
471+ t.Fatal("listener fired twice")
472+ }
473+}
474+
475+// More listeners than one mapped chunk holds: all fire once, every slot comes
476+// back, and a later registration reuses a recycled slot.
477+func TestOnDestroyManyListeners(t *testing.T) {
478+ d, _ := clientDisplay(t)
479+ fds := socketPair(t)
480+ fds2 := socketPair(t)
481+ c := adopt(t, d, fds.srv)
482+ const n = 3*listenerChunkSize/listenerSlot + 5
483+ var fired, chunksBefore int
484+ done := make(chan struct{})
485+ var before, after int
486+ d.Do(func() {
487+ for i := 0; i < n; i++ {
488+ c.OnDestroy(func() {
489+ fired++
490+ if fired == n {
491+ close(done)
492+ }
493+ })
494+ }
495+ before = len(listeners.free)
496+ chunksBefore = len(listeners.chunks)
497+ })
498+ if chunksBefore < 3 {
499+ t.Fatalf("test did not cross a chunk boundary: %d chunks", chunksBefore)
500+ }
501+ fds.closePeer()
502+ select {
503+ case <-done:
504+ case <-time.After(3 * time.Second):
505+ t.Fatal("not every listener fired")
506+ }
507+ var reused bool
508+ var firedNow int
509+ d.Do(func() {
510+ firedNow = fired
511+ after = len(listeners.free)
512+ c2, err := d.CreateClient(fds2.srv)
513+ if err != nil {
514+ t.Error(err)
515+ return
516+ }
517+ c2.OnDestroy(func() {})
518+ reused = len(listeners.chunks) == chunksBefore && len(listeners.free) == after-1
519+ })
520+ if after != before+n {
521+ t.Fatalf("free slots %d -> %d, want +%d", before, after, n)
522+ }
523+ if firedNow != n || !reused {
524+ t.Fatalf("fired %d, slot reused %v", firedNow, reused)
525+ }
526+}
M server/display.go +3 -0
@@ -52,6 +52,9 @@ var live = struct {
5252
5353 // Display owns a wl_display. All methods except Do must run on the goroutine
5454 // that calls Run, or before Run starts.
55+//
56+// Resource, bind and client-listener state is shared by the whole process, so
57+// only one Display may be running at a time per process.
5558 // Do wakes the loop through an eventfd so calls run at once instead of after
5659 // the dispatch timeout.
5760 type Display struct {
@@ -52,6 +52,9 @@ var live = struct {
52 52
53 // Display owns a wl_display. All methods except Do must run on the goroutine53 // Display owns a wl_display. All methods except Do must run on the goroutine
54 // that calls Run, or before Run starts.54 // that calls Run, or before Run starts.
55+//
56+// Resource, bind and client-listener state is shared by the whole process, so
57+// only one Display may be running at a time per process.
55 // Do wakes the loop through an eventfd so calls run at once instead of after58 // Do wakes the loop through an eventfd so calls run at once instead of after
56 // the dispatch timeout.59 // the dispatch timeout.
57 type Display struct {60 type Display struct {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 14

↑↓ moveenter openesc close