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