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