detached 5f793eb

Merge pull request #8 from bnema/feat/capture-generic

feat(server): add client fd, adoption and destroy listeners
commit
5f793ebfe5ef88252e7e24d4c4a7339d57e55b17
author
bnema · · 2026-10-01 00:38 UTC
committer
GitHub · · 2026-10-01 00:38 UTC
merge of
f00b1b1 + 5d81d73 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.8.0
4 files · +689 -3
A server/client.go +144 -0
@@ -0,0 +1,144 @@
1+package server
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "unsafe"
7+
8+ "github.com/bnema/purego"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// FD returns the client's connection socket. The descriptor is borrowed from
13+// libwayland: the caller must not close it, and it is valid only until the
14+// client is destroyed (see OnDestroy). Use it to identify or inspect the
15+// connection (for example fstat or /proc/self/fdinfo); reading or writing it
16+// would corrupt the protocol stream. It returns an error for an invalid
17+// (zero) Client. It does not allocate. Display goroutine only.
18+func (c Client) FD() (int, error) {
19+ if c.c == 0 {
20+ return -1, errInvalidClient
21+ }
22+ r, _, _ := purego.Syscall6(symClientGetFD, c.c, 0, 0, 0, 0, 0)
23+ return int(int32(r)), nil
24+}
25+
26+// CreateClient adopts fd, an already connected stream socket (for example one
27+// accepted on a listening socket the compositor owns), as a new client served
28+// by this display.
29+//
30+// Ownership of fd: on success libwayland owns it and closes it when the
31+// client is destroyed; the caller must not close it, nor use it for I/O. On
32+// 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 with
34+// SOCK_CLOEXEC); libwayland does not set the flag. fd must be a connected
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.
42+//
43+// The returned Client is valid until destroyed; register OnDestroy to learn
44+// when. Display goroutine only, or before Run starts.
45+func (d *Display) CreateClient(fd int) (Client, error) {
46+ if d == nil || d.c == 0 {
47+ return Client{}, errors.New("purego-libwayland: invalid display")
48+ }
49+ if fd < 0 {
50+ return Client{}, fmt.Errorf("purego-libwayland: wl_client_create: invalid fd %d", fd)
51+ }
52+ r, _, _ := purego.Syscall6(symClientCreate, d.c, uintptr(fd), 0, 0, 0, 0)
53+ if r == 0 {
54+ return Client{}, fmt.Errorf("purego-libwayland: wl_client_create(%d) failed, fd not adopted: %w", fd, lastErrno())
55+ }
56+ return Client{r}, nil
57+}
58+
59+// OnDestroy arranges for fn to run once when the client is destroyed, whether
60+// it disconnected, was disconnected after a protocol error, or the display was
61+// closed or stopped. Any number of functions may be registered per client;
62+// each runs once, in registration order. There is no way to unregister one;
63+// make fn a cheap no-op instead if it became irrelevant.
64+//
65+// fn runs on the display goroutine inside libwayland's client destruction. The
66+// Client is still valid during fn (FD and Credentials work, resources are not
67+// yet destroyed) but is dead afterwards: drop every copy of it, for example
68+// delete it from a map[Client]T, because the handle value may be reused by a
69+// later client.
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+//
76+// A Client is a comparable value (a handle to the C object) and is safe as a
77+// map key from its creation or bind until fn runs.
78+//
79+// OnDestroy does nothing for a zero Client or nil fn. Registering on an
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.
82+func (c Client) OnDestroy(fn func()) {
83+ if c.c == 0 || fn == nil {
84+ return
85+ }
86+ l := listeners.get()
87+ live.clientGone[l] = fn
88+ purego.Syscall6(symClientAddDestroyListener, c.c, l, 0, 0, 0, 0)
89+}
90+
91+// listenerSlots hands out wl_listener structs in C-visible memory that the Go
92+// GC cannot move or free. A slot is a struct wl_listener ({wl_list link;
93+// wl_notify_func_t notify}, 24 bytes on LP64), padded to 32. Slots are
94+// recycled through a free list, so only the peak number of concurrently
95+// registered listeners is ever mapped; the chunks themselves are never
96+// unmapped. Display goroutine only.
97+type listenerSlots struct {
98+ chunks [][]byte
99+ free []uintptr
100+}
101+
102+const (
103+ listenerSlot = 32
104+ listenerChunkSize = 4096
105+)
106+
107+var listeners listenerSlots
108+
109+func (s *listenerSlots) get() uintptr {
110+ if len(s.free) == 0 {
111+ mem, err := unix.Mmap(-1, 0, listenerChunkSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS)
112+ if err != nil {
113+ panic(fmt.Errorf("purego-libwayland: listener mmap: %w", err))
114+ }
115+ s.chunks = append(s.chunks, mem)
116+ base := uintptr(unsafe.Pointer(&mem[0]))
117+ for off := listenerChunkSize - listenerSlot; off >= 0; off -= listenerSlot {
118+ s.free = append(s.free, base+uintptr(off))
119+ }
120+ }
121+ l := s.free[len(s.free)-1]
122+ s.free = s.free[:len(s.free)-1]
123+ // notify lives at offset 16; wl_client_add_destroy_listener fills link.
124+ // l is an address inside an mmap chunk kept in s.chunks.
125+ *(*uintptr)(unsafe.Add(*(*unsafe.Pointer)(unsafe.Pointer(&l)), 16)) = cbClientGone
126+ return l
127+}
128+
129+func (s *listenerSlots) put(l uintptr) { s.free = append(s.free, l) }
130+
131+// clientGone is the wl_notify_func_t of every client destroy listener.
132+func clientGone(l uintptr) {
133+ fn := live.clientGone[l]
134+ delete(live.clientGone, l)
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.
139+ purego.Syscall6(symListRemove, l, 0, 0, 0, 0, 0)
140+ listeners.put(l)
141+ if fn != nil {
142+ fn()
143+ }
144+}
@@ -0,0 +1,144 @@
1+package server
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "unsafe"
7+
8+ "github.com/bnema/purego"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// FD returns the client's connection socket. The descriptor is borrowed from
13+// libwayland: the caller must not close it, and it is valid only until the
14+// client is destroyed (see OnDestroy). Use it to identify or inspect the
15+// connection (for example fstat or /proc/self/fdinfo); reading or writing it
16+// would corrupt the protocol stream. It returns an error for an invalid
17+// (zero) Client. It does not allocate. Display goroutine only.
18+func (c Client) FD() (int, error) {
19+ if c.c == 0 {
20+ return -1, errInvalidClient
21+ }
22+ r, _, _ := purego.Syscall6(symClientGetFD, c.c, 0, 0, 0, 0, 0)
23+ return int(int32(r)), nil
24+}
25+
26+// CreateClient adopts fd, an already connected stream socket (for example one
27+// accepted on a listening socket the compositor owns), as a new client served
28+// by this display.
29+//
30+// Ownership of fd: on success libwayland owns it and closes it when the
31+// client is destroyed; the caller must not close it, nor use it for I/O. On
32+// 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 with
34+// SOCK_CLOEXEC); libwayland does not set the flag. fd must be a connected
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.
42+//
43+// The returned Client is valid until destroyed; register OnDestroy to learn
44+// when. Display goroutine only, or before Run starts.
45+func (d *Display) CreateClient(fd int) (Client, error) {
46+ if d == nil || d.c == 0 {
47+ return Client{}, errors.New("purego-libwayland: invalid display")
48+ }
49+ if fd < 0 {
50+ return Client{}, fmt.Errorf("purego-libwayland: wl_client_create: invalid fd %d", fd)
51+ }
52+ r, _, _ := purego.Syscall6(symClientCreate, d.c, uintptr(fd), 0, 0, 0, 0)
53+ if r == 0 {
54+ return Client{}, fmt.Errorf("purego-libwayland: wl_client_create(%d) failed, fd not adopted: %w", fd, lastErrno())
55+ }
56+ return Client{r}, nil
57+}
58+
59+// OnDestroy arranges for fn to run once when the client is destroyed, whether
60+// it disconnected, was disconnected after a protocol error, or the display was
61+// closed or stopped. Any number of functions may be registered per client;
62+// each runs once, in registration order. There is no way to unregister one;
63+// make fn a cheap no-op instead if it became irrelevant.
64+//
65+// fn runs on the display goroutine inside libwayland's client destruction. The
66+// Client is still valid during fn (FD and Credentials work, resources are not
67+// yet destroyed) but is dead afterwards: drop every copy of it, for example
68+// delete it from a map[Client]T, because the handle value may be reused by a
69+// later client.
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+//
76+// A Client is a comparable value (a handle to the C object) and is safe as a
77+// map key from its creation or bind until fn runs.
78+//
79+// OnDestroy does nothing for a zero Client or nil fn. Registering on an
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.
82+func (c Client) OnDestroy(fn func()) {
83+ if c.c == 0 || fn == nil {
84+ return
85+ }
86+ l := listeners.get()
87+ live.clientGone[l] = fn
88+ purego.Syscall6(symClientAddDestroyListener, c.c, l, 0, 0, 0, 0)
89+}
90+
91+// listenerSlots hands out wl_listener structs in C-visible memory that the Go
92+// GC cannot move or free. A slot is a struct wl_listener ({wl_list link;
93+// wl_notify_func_t notify}, 24 bytes on LP64), padded to 32. Slots are
94+// recycled through a free list, so only the peak number of concurrently
95+// registered listeners is ever mapped; the chunks themselves are never
96+// unmapped. Display goroutine only.
97+type listenerSlots struct {
98+ chunks [][]byte
99+ free []uintptr
100+}
101+
102+const (
103+ listenerSlot = 32
104+ listenerChunkSize = 4096
105+)
106+
107+var listeners listenerSlots
108+
109+func (s *listenerSlots) get() uintptr {
110+ if len(s.free) == 0 {
111+ mem, err := unix.Mmap(-1, 0, listenerChunkSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS)
112+ if err != nil {
113+ panic(fmt.Errorf("purego-libwayland: listener mmap: %w", err))
114+ }
115+ s.chunks = append(s.chunks, mem)
116+ base := uintptr(unsafe.Pointer(&mem[0]))
117+ for off := listenerChunkSize - listenerSlot; off >= 0; off -= listenerSlot {
118+ s.free = append(s.free, base+uintptr(off))
119+ }
120+ }
121+ l := s.free[len(s.free)-1]
122+ s.free = s.free[:len(s.free)-1]
123+ // notify lives at offset 16; wl_client_add_destroy_listener fills link.
124+ // l is an address inside an mmap chunk kept in s.chunks.
125+ *(*uintptr)(unsafe.Add(*(*unsafe.Pointer)(unsafe.Pointer(&l)), 16)) = cbClientGone
126+ return l
127+}
128+
129+func (s *listenerSlots) put(l uintptr) { s.free = append(s.free, l) }
130+
131+// clientGone is the wl_notify_func_t of every client destroy listener.
132+func clientGone(l uintptr) {
133+ fn := live.clientGone[l]
134+ delete(live.clientGone, l)
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.
139+ purego.Syscall6(symListRemove, l, 0, 0, 0, 0, 0)
140+ listeners.put(l)
141+ if fn != nil {
142+ fn()
143+ }
144+}
A server/client_test.go +526 -0
@@ -0,0 +1,526 @@
1+package server
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "errors"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ "golang.org/x/sys/unix"
12+)
13+
14+// clientDisplay runs a real display and returns it with a stop function that
15+// waits for Run to return (and so for display destruction).
16+func clientDisplay(t *testing.T) (*Display, func()) {
17+ t.Helper()
18+ d, err := NewDisplay()
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ ctx, cancel := context.WithCancel(context.Background())
23+ done := make(chan error, 1)
24+ go func() { done <- d.Run(ctx) }()
25+ var once bool
26+ stop := func() {
27+ if once {
28+ return
29+ }
30+ once = true
31+ cancel()
32+ select {
33+ case err := <-done:
34+ if err != nil {
35+ t.Error(err)
36+ }
37+ case <-time.After(3 * time.Second):
38+ t.Fatal("Run did not return")
39+ }
40+ }
41+ t.Cleanup(stop)
42+ return d, stop
43+}
44+
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 {
53+ t.Helper()
54+ raw, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
55+ if err != nil {
56+ t.Fatal(err)
57+ }
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}
77+}
78+
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
86+}
87+
88+func adopt(t *testing.T, d *Display, fd int) Client {
89+ t.Helper()
90+ var c Client
91+ var err error
92+ if !d.Do(func() { c, err = d.CreateClient(fd) }) {
93+ t.Fatal("display stopped")
94+ }
95+ if err != nil {
96+ t.Fatal(err)
97+ }
98+ return c
99+}
100+
101+func TestClientFD(t *testing.T) {
102+ d, _ := clientDisplay(t)
103+ fds := socketPair(t)
104+ c := adopt(t, d, fds.srv)
105+ var (
106+ got, zero int
107+ err, zerr error
108+ allocs float64
109+ )
110+ if !d.Do(func() {
111+ got, err = c.FD()
112+ zero, zerr = Client{}.FD()
113+ allocs = testing.AllocsPerRun(100, func() { _, _ = c.FD() })
114+ }) {
115+ t.Fatal("display stopped")
116+ }
117+ if err != nil || got != fds.srv {
118+ t.Fatalf("FD = %d, %v; want %d", got, err, fds.srv)
119+ }
120+ if zerr == nil || zero != -1 {
121+ t.Fatalf("invalid client FD = %d, %v", zero, zerr)
122+ }
123+ if allocs != 0 {
124+ t.Fatalf("FD allocs = %g, want 0", allocs)
125+ }
126+}
127+
128+func TestCreateClientFailureKeepsFD(t *testing.T) {
129+ d, _ := clientDisplay(t)
130+ var p [2]int
131+ if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil {
132+ t.Fatal(err)
133+ }
134+ defer unix.Close(p[0])
135+ defer unix.Close(p[1])
136+ pipeID := identify(t, p[0])
137+ var err, errNeg error
138+ var c, cn Client
139+ if !d.Do(func() {
140+ c, err = d.CreateClient(p[0]) // not a socket: SO_PEERCRED fails
141+ cn, errNeg = d.CreateClient(-1)
142+ }) {
143+ t.Fatal("display stopped")
144+ }
145+ if !errors.Is(err, unix.ENOTSOCK) {
146+ t.Fatalf("error %v does not wrap ENOTSOCK", err)
147+ }
148+ if err == nil || c != (Client{}) || errNeg == nil || cn != (Client{}) {
149+ t.Fatalf("got %v %v, %v %v; want errors", c, err, cn, errNeg)
150+ }
151+ if !stillOpen(p[0], pipeID) {
152+ t.Fatal("failed CreateClient closed the fd")
153+ }
154+}
155+
156+func TestCreateClientOwnsFD(t *testing.T) {
157+ d, _ := clientDisplay(t)
158+ fds := socketPair(t)
159+ c := adopt(t, d, fds.srv)
160+ gone := make(chan struct{})
161+ d.Do(func() { c.OnDestroy(func() { close(gone) }) })
162+ id := identify(t, fds.srv)
163+ fds.closePeer()
164+ select {
165+ case <-gone:
166+ case <-time.After(3 * time.Second):
167+ t.Fatal("client not destroyed")
168+ }
169+ // The listener runs before libwayland closes the fd; Do returns only once
170+ // the destruction that ran it has finished.
171+ d.Do(func() {})
172+ if stillOpen(fds.srv, id) {
173+ t.Fatal("libwayland did not close the adopted fd")
174+ }
175+}
176+
177+// A raw client speaks the wire protocol over the other socketpair end.
178+func wireMsg(id uint32, opcode uint16, args ...[]byte) []byte {
179+ var body []byte
180+ for _, a := range args {
181+ body = append(body, a...)
182+ }
183+ b := make([]byte, 8, 8+len(body))
184+ binary.LittleEndian.PutUint32(b, id)
185+ binary.LittleEndian.PutUint32(b[4:], uint32(8+len(body))<<16|uint32(opcode))
186+ return append(b, body...)
187+}
188+
189+func u32(v uint32) []byte { return binary.LittleEndian.AppendUint32(nil, v) }
190+
191+func wireString(s string) []byte {
192+ b := u32(uint32(len(s) + 1))
193+ b = append(b, s...)
194+ b = append(b, 0)
195+ for len(b)%4 != 0 {
196+ b = append(b, 0)
197+ }
198+ return b
199+}
200+
201+func TestCreateClientBindsRegistry(t *testing.T) {
202+ iface := &Interface{Name: "adopted_client_test", Version: 1}
203+ if err := NewInterfaces(iface); err != nil {
204+ t.Fatal(err)
205+ }
206+ d, err := NewDisplay()
207+ if err != nil {
208+ t.Fatal(err)
209+ }
210+ bound := make(chan Client, 1)
211+ if err := d.CreateGlobal(iface, 1, func(c Client, version, id uint32) { bound <- c }); err != nil {
212+ t.Fatal(err)
213+ }
214+ ctx, cancel := context.WithCancel(context.Background())
215+ done := make(chan error, 1)
216+ go func() { done <- d.Run(ctx) }()
217+ defer func() { cancel(); <-done }()
218+
219+ fds := socketPair(t)
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 {
222+ t.Fatal(err)
223+ }
224+ // wl_display.get_registry(new_id 2), then wl_display.sync(new_id 3).
225+ req := append(wireMsg(1, 1, u32(2)), wireMsg(1, 0, u32(3))...)
226+ if _, err := unix.Write(fds.peer, req); err != nil {
227+ t.Fatal(err)
228+ }
229+ // Read until the sync callback's done event (object 3), collecting globals.
230+ var stream []byte
231+ var name uint32
232+ buf := make([]byte, 4096)
233+ for sawDone := false; !sawDone; {
234+ n, err := unix.Read(fds.peer, buf)
235+ if err != nil || n == 0 {
236+ t.Fatalf("read: %d, %v", n, err)
237+ }
238+ stream = append(stream, buf[:n]...)
239+ for len(stream) >= 8 {
240+ size := int(binary.LittleEndian.Uint32(stream[4:]) >> 16)
241+ if size < 8 || len(stream) < size {
242+ break
243+ }
244+ id, opcode := binary.LittleEndian.Uint32(stream), uint16(binary.LittleEndian.Uint32(stream[4:]))
245+ body := stream[8:size]
246+ if id == 2 && opcode == 0 { // wl_registry.global(name, interface, version)
247+ strLen := int(binary.LittleEndian.Uint32(body[4:]))
248+ if string(body[8:8+strLen-1]) == iface.Name {
249+ name = binary.LittleEndian.Uint32(body)
250+ }
251+ }
252+ if id == 3 {
253+ sawDone = true
254+ }
255+ stream = stream[size:]
256+ }
257+ }
258+ if name == 0 {
259+ t.Fatal("global not advertised to the adopted client")
260+ }
261+ // wl_registry.bind(name, interface, version, new_id 4)
262+ if _, err := unix.Write(fds.peer, wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil {
263+ t.Fatal(err)
264+ }
265+ select {
266+ case got := <-bound:
267+ if got != c {
268+ t.Fatalf("bind client %v, want adopted %v", got, c)
269+ }
270+ case <-time.After(3 * time.Second):
271+ t.Fatal("bind never reached the global")
272+ }
273+}
274+
275+func TestOnDestroyOnDisconnect(t *testing.T) {
276+ iface := &Interface{Name: "on_destroy_test", Version: 1}
277+ if err := NewInterfaces(iface); err != nil {
278+ t.Fatal(err)
279+ }
280+ d, _ := clientDisplay(t)
281+ fds := socketPair(t)
282+ c := adopt(t, d, fds.srv)
283+ var (
284+ order []int
285+ resAlive, fdOK bool
286+ other = map[Client]int{c: 1}
287+ freeBefore, free int
288+ fired = make(chan struct{})
289+ res *Resource
290+ createErr, fdError error
291+ )
292+ if !d.Do(func() {
293+ res, createErr = c.CreateResource(iface, 1, 2, nil)
294+ for i := 1; i <= 3; i++ {
295+ c.OnDestroy(func() {
296+ order = append(order, i)
297+ if i == 3 {
298+ var fd int
299+ fd, fdError = c.FD()
300+ fdOK = fd == fds.srv
301+ resAlive = res.Alive()
302+ delete(other, c)
303+ close(fired)
304+ }
305+ })
306+ }
307+ freeBefore = len(listeners.free) // after the slots are taken
308+ c.OnDestroy(nil)
309+ Client{}.OnDestroy(func() { t.Error("zero client listener ran") })
310+ }) {
311+ t.Fatal("display stopped")
312+ }
313+ if createErr != nil {
314+ t.Fatal(createErr)
315+ }
316+ select {
317+ case <-fired:
318+ t.Fatal("fired before disconnect")
319+ case <-time.After(50 * time.Millisecond):
320+ }
321+ fds.closePeer()
322+ select {
323+ case <-fired:
324+ case <-time.After(3 * time.Second):
325+ t.Fatal("OnDestroy did not fire on disconnect")
326+ }
327+ d.Do(func() { free = len(listeners.free) })
328+ if len(order) != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3 {
329+ t.Fatalf("order %v, want [1 2 3]", order)
330+ }
331+ if fdError != nil || !fdOK || !resAlive {
332+ t.Fatalf("during OnDestroy: fd ok=%v err=%v resource alive=%v", fdOK, fdError, resAlive)
333+ }
334+ if len(other) != 0 || res.Alive() {
335+ t.Fatalf("map %v, resource alive %v", other, res.Alive())
336+ }
337+ if free != freeBefore+3 || len(live.clientGone) != 0 {
338+ t.Fatalf("listener slots not recycled: free %d -> %d, live %d", freeBefore, free, len(live.clientGone))
339+ }
340+}
341+
342+func TestOnDestroyOnDisplayClose(t *testing.T) {
343+ d, err := NewDisplay()
344+ if err != nil {
345+ t.Fatal(err)
346+ }
347+ fds := socketPair(t)
348+ c, err := d.CreateClient(fds.srv)
349+ if err != nil {
350+ t.Fatal(err)
351+ }
352+ n := 0
353+ c.OnDestroy(func() { n++ })
354+ c.OnDestroy(func() { n += 10 })
355+ d.Close()
356+ d.Close()
357+ if n != 11 {
358+ t.Fatalf("listeners ran %d, want 11 (each once)", n)
359+ }
360+}
361+
362+func TestOnDestroyOnRunStop(t *testing.T) {
363+ d, stop := clientDisplay(t)
364+ fds := socketPair(t)
365+ c := adopt(t, d, fds.srv)
366+ n := 0
367+ d.Do(func() { c.OnDestroy(func() { n++ }) })
368+ stop()
369+ if n != 1 {
370+ t.Fatalf("listener ran %d times, want 1", n)
371+ }
372+}
373+
374+// A client destroyed at disconnect must not fire again when the display stops.
375+func TestOnDestroyOnce(t *testing.T) {
376+ d, stop := clientDisplay(t)
377+ fds := socketPair(t)
378+ c := adopt(t, d, fds.srv)
379+ fired := make(chan struct{}, 4)
380+ d.Do(func() { c.OnDestroy(func() { fired <- struct{}{} }) })
381+ fds.closePeer()
382+ select {
383+ case <-fired:
384+ case <-time.After(3 * time.Second):
385+ t.Fatal("OnDestroy did not fire")
386+ }
387+ stop()
388+ if len(fired) != 0 {
389+ t.Fatal("OnDestroy fired twice")
390+ }
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+}
@@ -0,0 +1,526 @@
1+package server
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "errors"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ "golang.org/x/sys/unix"
12+)
13+
14+// clientDisplay runs a real display and returns it with a stop function that
15+// waits for Run to return (and so for display destruction).
16+func clientDisplay(t *testing.T) (*Display, func()) {
17+ t.Helper()
18+ d, err := NewDisplay()
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ ctx, cancel := context.WithCancel(context.Background())
23+ done := make(chan error, 1)
24+ go func() { done <- d.Run(ctx) }()
25+ var once bool
26+ stop := func() {
27+ if once {
28+ return
29+ }
30+ once = true
31+ cancel()
32+ select {
33+ case err := <-done:
34+ if err != nil {
35+ t.Error(err)
36+ }
37+ case <-time.After(3 * time.Second):
38+ t.Fatal("Run did not return")
39+ }
40+ }
41+ t.Cleanup(stop)
42+ return d, stop
43+}
44+
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 {
53+ t.Helper()
54+ raw, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
55+ if err != nil {
56+ t.Fatal(err)
57+ }
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}
77+}
78+
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
86+}
87+
88+func adopt(t *testing.T, d *Display, fd int) Client {
89+ t.Helper()
90+ var c Client
91+ var err error
92+ if !d.Do(func() { c, err = d.CreateClient(fd) }) {
93+ t.Fatal("display stopped")
94+ }
95+ if err != nil {
96+ t.Fatal(err)
97+ }
98+ return c
99+}
100+
101+func TestClientFD(t *testing.T) {
102+ d, _ := clientDisplay(t)
103+ fds := socketPair(t)
104+ c := adopt(t, d, fds.srv)
105+ var (
106+ got, zero int
107+ err, zerr error
108+ allocs float64
109+ )
110+ if !d.Do(func() {
111+ got, err = c.FD()
112+ zero, zerr = Client{}.FD()
113+ allocs = testing.AllocsPerRun(100, func() { _, _ = c.FD() })
114+ }) {
115+ t.Fatal("display stopped")
116+ }
117+ if err != nil || got != fds.srv {
118+ t.Fatalf("FD = %d, %v; want %d", got, err, fds.srv)
119+ }
120+ if zerr == nil || zero != -1 {
121+ t.Fatalf("invalid client FD = %d, %v", zero, zerr)
122+ }
123+ if allocs != 0 {
124+ t.Fatalf("FD allocs = %g, want 0", allocs)
125+ }
126+}
127+
128+func TestCreateClientFailureKeepsFD(t *testing.T) {
129+ d, _ := clientDisplay(t)
130+ var p [2]int
131+ if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil {
132+ t.Fatal(err)
133+ }
134+ defer unix.Close(p[0])
135+ defer unix.Close(p[1])
136+ pipeID := identify(t, p[0])
137+ var err, errNeg error
138+ var c, cn Client
139+ if !d.Do(func() {
140+ c, err = d.CreateClient(p[0]) // not a socket: SO_PEERCRED fails
141+ cn, errNeg = d.CreateClient(-1)
142+ }) {
143+ t.Fatal("display stopped")
144+ }
145+ if !errors.Is(err, unix.ENOTSOCK) {
146+ t.Fatalf("error %v does not wrap ENOTSOCK", err)
147+ }
148+ if err == nil || c != (Client{}) || errNeg == nil || cn != (Client{}) {
149+ t.Fatalf("got %v %v, %v %v; want errors", c, err, cn, errNeg)
150+ }
151+ if !stillOpen(p[0], pipeID) {
152+ t.Fatal("failed CreateClient closed the fd")
153+ }
154+}
155+
156+func TestCreateClientOwnsFD(t *testing.T) {
157+ d, _ := clientDisplay(t)
158+ fds := socketPair(t)
159+ c := adopt(t, d, fds.srv)
160+ gone := make(chan struct{})
161+ d.Do(func() { c.OnDestroy(func() { close(gone) }) })
162+ id := identify(t, fds.srv)
163+ fds.closePeer()
164+ select {
165+ case <-gone:
166+ case <-time.After(3 * time.Second):
167+ t.Fatal("client not destroyed")
168+ }
169+ // The listener runs before libwayland closes the fd; Do returns only once
170+ // the destruction that ran it has finished.
171+ d.Do(func() {})
172+ if stillOpen(fds.srv, id) {
173+ t.Fatal("libwayland did not close the adopted fd")
174+ }
175+}
176+
177+// A raw client speaks the wire protocol over the other socketpair end.
178+func wireMsg(id uint32, opcode uint16, args ...[]byte) []byte {
179+ var body []byte
180+ for _, a := range args {
181+ body = append(body, a...)
182+ }
183+ b := make([]byte, 8, 8+len(body))
184+ binary.LittleEndian.PutUint32(b, id)
185+ binary.LittleEndian.PutUint32(b[4:], uint32(8+len(body))<<16|uint32(opcode))
186+ return append(b, body...)
187+}
188+
189+func u32(v uint32) []byte { return binary.LittleEndian.AppendUint32(nil, v) }
190+
191+func wireString(s string) []byte {
192+ b := u32(uint32(len(s) + 1))
193+ b = append(b, s...)
194+ b = append(b, 0)
195+ for len(b)%4 != 0 {
196+ b = append(b, 0)
197+ }
198+ return b
199+}
200+
201+func TestCreateClientBindsRegistry(t *testing.T) {
202+ iface := &Interface{Name: "adopted_client_test", Version: 1}
203+ if err := NewInterfaces(iface); err != nil {
204+ t.Fatal(err)
205+ }
206+ d, err := NewDisplay()
207+ if err != nil {
208+ t.Fatal(err)
209+ }
210+ bound := make(chan Client, 1)
211+ if err := d.CreateGlobal(iface, 1, func(c Client, version, id uint32) { bound <- c }); err != nil {
212+ t.Fatal(err)
213+ }
214+ ctx, cancel := context.WithCancel(context.Background())
215+ done := make(chan error, 1)
216+ go func() { done <- d.Run(ctx) }()
217+ defer func() { cancel(); <-done }()
218+
219+ fds := socketPair(t)
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 {
222+ t.Fatal(err)
223+ }
224+ // wl_display.get_registry(new_id 2), then wl_display.sync(new_id 3).
225+ req := append(wireMsg(1, 1, u32(2)), wireMsg(1, 0, u32(3))...)
226+ if _, err := unix.Write(fds.peer, req); err != nil {
227+ t.Fatal(err)
228+ }
229+ // Read until the sync callback's done event (object 3), collecting globals.
230+ var stream []byte
231+ var name uint32
232+ buf := make([]byte, 4096)
233+ for sawDone := false; !sawDone; {
234+ n, err := unix.Read(fds.peer, buf)
235+ if err != nil || n == 0 {
236+ t.Fatalf("read: %d, %v", n, err)
237+ }
238+ stream = append(stream, buf[:n]...)
239+ for len(stream) >= 8 {
240+ size := int(binary.LittleEndian.Uint32(stream[4:]) >> 16)
241+ if size < 8 || len(stream) < size {
242+ break
243+ }
244+ id, opcode := binary.LittleEndian.Uint32(stream), uint16(binary.LittleEndian.Uint32(stream[4:]))
245+ body := stream[8:size]
246+ if id == 2 && opcode == 0 { // wl_registry.global(name, interface, version)
247+ strLen := int(binary.LittleEndian.Uint32(body[4:]))
248+ if string(body[8:8+strLen-1]) == iface.Name {
249+ name = binary.LittleEndian.Uint32(body)
250+ }
251+ }
252+ if id == 3 {
253+ sawDone = true
254+ }
255+ stream = stream[size:]
256+ }
257+ }
258+ if name == 0 {
259+ t.Fatal("global not advertised to the adopted client")
260+ }
261+ // wl_registry.bind(name, interface, version, new_id 4)
262+ if _, err := unix.Write(fds.peer, wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil {
263+ t.Fatal(err)
264+ }
265+ select {
266+ case got := <-bound:
267+ if got != c {
268+ t.Fatalf("bind client %v, want adopted %v", got, c)
269+ }
270+ case <-time.After(3 * time.Second):
271+ t.Fatal("bind never reached the global")
272+ }
273+}
274+
275+func TestOnDestroyOnDisconnect(t *testing.T) {
276+ iface := &Interface{Name: "on_destroy_test", Version: 1}
277+ if err := NewInterfaces(iface); err != nil {
278+ t.Fatal(err)
279+ }
280+ d, _ := clientDisplay(t)
281+ fds := socketPair(t)
282+ c := adopt(t, d, fds.srv)
283+ var (
284+ order []int
285+ resAlive, fdOK bool
286+ other = map[Client]int{c: 1}
287+ freeBefore, free int
288+ fired = make(chan struct{})
289+ res *Resource
290+ createErr, fdError error
291+ )
292+ if !d.Do(func() {
293+ res, createErr = c.CreateResource(iface, 1, 2, nil)
294+ for i := 1; i <= 3; i++ {
295+ c.OnDestroy(func() {
296+ order = append(order, i)
297+ if i == 3 {
298+ var fd int
299+ fd, fdError = c.FD()
300+ fdOK = fd == fds.srv
301+ resAlive = res.Alive()
302+ delete(other, c)
303+ close(fired)
304+ }
305+ })
306+ }
307+ freeBefore = len(listeners.free) // after the slots are taken
308+ c.OnDestroy(nil)
309+ Client{}.OnDestroy(func() { t.Error("zero client listener ran") })
310+ }) {
311+ t.Fatal("display stopped")
312+ }
313+ if createErr != nil {
314+ t.Fatal(createErr)
315+ }
316+ select {
317+ case <-fired:
318+ t.Fatal("fired before disconnect")
319+ case <-time.After(50 * time.Millisecond):
320+ }
321+ fds.closePeer()
322+ select {
323+ case <-fired:
324+ case <-time.After(3 * time.Second):
325+ t.Fatal("OnDestroy did not fire on disconnect")
326+ }
327+ d.Do(func() { free = len(listeners.free) })
328+ if len(order) != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3 {
329+ t.Fatalf("order %v, want [1 2 3]", order)
330+ }
331+ if fdError != nil || !fdOK || !resAlive {
332+ t.Fatalf("during OnDestroy: fd ok=%v err=%v resource alive=%v", fdOK, fdError, resAlive)
333+ }
334+ if len(other) != 0 || res.Alive() {
335+ t.Fatalf("map %v, resource alive %v", other, res.Alive())
336+ }
337+ if free != freeBefore+3 || len(live.clientGone) != 0 {
338+ t.Fatalf("listener slots not recycled: free %d -> %d, live %d", freeBefore, free, len(live.clientGone))
339+ }
340+}
341+
342+func TestOnDestroyOnDisplayClose(t *testing.T) {
343+ d, err := NewDisplay()
344+ if err != nil {
345+ t.Fatal(err)
346+ }
347+ fds := socketPair(t)
348+ c, err := d.CreateClient(fds.srv)
349+ if err != nil {
350+ t.Fatal(err)
351+ }
352+ n := 0
353+ c.OnDestroy(func() { n++ })
354+ c.OnDestroy(func() { n += 10 })
355+ d.Close()
356+ d.Close()
357+ if n != 11 {
358+ t.Fatalf("listeners ran %d, want 11 (each once)", n)
359+ }
360+}
361+
362+func TestOnDestroyOnRunStop(t *testing.T) {
363+ d, stop := clientDisplay(t)
364+ fds := socketPair(t)
365+ c := adopt(t, d, fds.srv)
366+ n := 0
367+ d.Do(func() { c.OnDestroy(func() { n++ }) })
368+ stop()
369+ if n != 1 {
370+ t.Fatalf("listener ran %d times, want 1", n)
371+ }
372+}
373+
374+// A client destroyed at disconnect must not fire again when the display stops.
375+func TestOnDestroyOnce(t *testing.T) {
376+ d, stop := clientDisplay(t)
377+ fds := socketPair(t)
378+ c := adopt(t, d, fds.srv)
379+ fired := make(chan struct{}, 4)
380+ d.Do(func() { c.OnDestroy(func() { fired <- struct{}{} }) })
381+ fds.closePeer()
382+ select {
383+ case <-fired:
384+ case <-time.After(3 * time.Second):
385+ t.Fatal("OnDestroy did not fire")
386+ }
387+ stop()
388+ if len(fired) != 0 {
389+ t.Fatal("OnDestroy fired twice")
390+ }
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 +8 -2
@@ -20,7 +20,8 @@ type Handler func(r *Resource, opcode uint32, args []Arg)
2020 // BindFunc runs when a client binds a global.
2121 type BindFunc func(c Client, version, id uint32)
2222
23-// Client is a connected wl_client.
23+// Client is a connected wl_client. It is a comparable handle to the C object,
24+// usable as a map key until the client is destroyed (see Client.OnDestroy).
2425 type Client struct{ c uintptr }
2526
2627 // Resource is a server-side wl_resource.
@@ -45,10 +46,15 @@ var live = struct {
4546 resources map[uintptr]*Resource
4647 binds map[uintptr]BindFunc
4748 nextBind uintptr
48-}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}}
49+ // clientGone maps wl_listener addresses to OnDestroy functions.
50+ clientGone map[uintptr]func()
51+}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}}
4952
5053 // Display owns a wl_display. All methods except Do must run on the goroutine
5154 // 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.
5258 // Do wakes the loop through an eventfd so calls run at once instead of after
5359 // the dispatch timeout.
5460 type Display struct {
@@ -20,7 +20,8 @@ type Handler func(r *Resource, opcode uint32, args []Arg)
20 // BindFunc runs when a client binds a global.20 // BindFunc runs when a client binds a global.
21 type BindFunc func(c Client, version, id uint32)21 type BindFunc func(c Client, version, id uint32)
22 22
23-// Client is a connected wl_client.23+// Client is a connected wl_client. It is a comparable handle to the C object,
24+// usable as a map key until the client is destroyed (see Client.OnDestroy).
24 type Client struct{ c uintptr }25 type Client struct{ c uintptr }
25 26
26 // Resource is a server-side wl_resource.27 // Resource is a server-side wl_resource.
@@ -45,10 +46,15 @@ var live = struct {
45 resources map[uintptr]*Resource46 resources map[uintptr]*Resource
46 binds map[uintptr]BindFunc47 binds map[uintptr]BindFunc
47 nextBind uintptr48 nextBind uintptr
48-}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}}49+ // clientGone maps wl_listener addresses to OnDestroy functions.
50+ clientGone map[uintptr]func()
51+}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}}
49 52
50 // 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
51 // 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.
52 // 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
53 // the dispatch timeout.59 // the dispatch timeout.
54 type Display struct {60 type Display struct {
M server/lib.go +11 -1
@@ -30,7 +30,8 @@ var (
3030
3131 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr
3232 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr
33- symClientGetCredentials uintptr
33+ symClientGetCredentials, symClientGetFD, symClientCreate uintptr
34+ symClientAddDestroyListener, symListRemove uintptr
3435 symErrnoLocation uintptr
3536 // wl_fixes support (libwayland 1.26+); zero on older libraries.
3637 symFixesAckGlobalRemove uintptr
@@ -39,6 +40,7 @@ var (
3940 // of each for the whole process.
4041 cbDispatcher uintptr
4142 cbDestroy uintptr
43+ cbClientGone uintptr
4244 cbBind uintptr
4345 cbWake uintptr
4446 )
@@ -76,6 +78,10 @@ func load() error {
7678 {"wl_resource_create", &symResourceCreate},
7779 {"wl_resource_get_id", &symResourceGetID},
7880 {"wl_client_get_credentials", &symClientGetCredentials},
81+ {"wl_client_get_fd", &symClientGetFD},
82+ {"wl_client_create", &symClientCreate},
83+ {"wl_client_add_destroy_listener", &symClientAddDestroyListener},
84+ {"wl_list_remove", &symListRemove},
7985 {"wl_resource_set_dispatcher", &symResourceSetDispatcher},
8086 {"wl_resource_post_event_array", &symResourcePostEventArr},
8187 {"wl_resource_destroy", &symResourceDestroy},
@@ -110,6 +116,10 @@ func load() error {
110116 destroyed(a.Int(0))
111117 return 0
112118 })
119+ cbClientGone = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
120+ clientGone(a.Int(0))
121+ return 0
122+ })
113123 cbBind = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
114124 bind(a.Int(0), a.Int(1), uint32(a.Int(2)), uint32(a.Int(3)))
115125 return 0
@@ -30,7 +30,8 @@ var (
30 30
31 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr31 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr
32 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr32 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr
33- symClientGetCredentials uintptr33+ symClientGetCredentials, symClientGetFD, symClientCreate uintptr
34+ symClientAddDestroyListener, symListRemove uintptr
34 symErrnoLocation uintptr35 symErrnoLocation uintptr
35 // wl_fixes support (libwayland 1.26+); zero on older libraries.36 // wl_fixes support (libwayland 1.26+); zero on older libraries.
36 symFixesAckGlobalRemove uintptr37 symFixesAckGlobalRemove uintptr
@@ -39,6 +40,7 @@ var (
39 // of each for the whole process.40 // of each for the whole process.
40 cbDispatcher uintptr41 cbDispatcher uintptr
41 cbDestroy uintptr42 cbDestroy uintptr
43+ cbClientGone uintptr
42 cbBind uintptr44 cbBind uintptr
43 cbWake uintptr45 cbWake uintptr
44 )46 )
@@ -76,6 +78,10 @@ func load() error {
76 {"wl_resource_create", &symResourceCreate},78 {"wl_resource_create", &symResourceCreate},
77 {"wl_resource_get_id", &symResourceGetID},79 {"wl_resource_get_id", &symResourceGetID},
78 {"wl_client_get_credentials", &symClientGetCredentials},80 {"wl_client_get_credentials", &symClientGetCredentials},
81+ {"wl_client_get_fd", &symClientGetFD},
82+ {"wl_client_create", &symClientCreate},
83+ {"wl_client_add_destroy_listener", &symClientAddDestroyListener},
84+ {"wl_list_remove", &symListRemove},
79 {"wl_resource_set_dispatcher", &symResourceSetDispatcher},85 {"wl_resource_set_dispatcher", &symResourceSetDispatcher},
80 {"wl_resource_post_event_array", &symResourcePostEventArr},86 {"wl_resource_post_event_array", &symResourcePostEventArr},
81 {"wl_resource_destroy", &symResourceDestroy},87 {"wl_resource_destroy", &symResourceDestroy},
@@ -110,6 +116,10 @@ func load() error {
110 destroyed(a.Int(0))116 destroyed(a.Int(0))
111 return 0117 return 0
112 })118 })
119+ cbClientGone = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
120+ clientGone(a.Int(0))
121+ return 0
122+ })
113 cbBind = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {123 cbBind = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
114 bind(a.Int(0), a.Int(1), uint32(a.Int(2)), uint32(a.Int(3)))124 bind(a.Int(0), a.Int(1), uint32(a.Int(2)), uint32(a.Int(3)))
115 return 0125 return 0

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