Merge pull request #8 from bnema/feat/capture-generic
feat(server): add client fd, adoption and destroy listeners
4 files · +689 -3
A server/client.go +144 -0
| @@ -0,0 +1,144 @@ | |||
| 1 | + | package server | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "fmt" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/purego" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // FD returns the client's connection socket. The descriptor is borrowed from | |
| 13 | + | // libwayland: the caller must not close it, and it is valid only until the | |
| 14 | + | // client is destroyed (see OnDestroy). Use it to identify or inspect the | |
| 15 | + | // connection (for example fstat or /proc/self/fdinfo); reading or writing it | |
| 16 | + | // would corrupt the protocol stream. It returns an error for an invalid | |
| 17 | + | // (zero) Client. It does not allocate. Display goroutine only. | |
| 18 | + | func (c Client) FD() (int, error) { | |
| 19 | + | if c.c == 0 { | |
| 20 | + | return -1, errInvalidClient | |
| 21 | + | } | |
| 22 | + | r, _, _ := purego.Syscall6(symClientGetFD, c.c, 0, 0, 0, 0, 0) | |
| 23 | + | return int(int32(r)), nil | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // CreateClient adopts fd, an already connected stream socket (for example one | |
| 27 | + | // accepted on a listening socket the compositor owns), as a new client served | |
| 28 | + | // by this display. | |
| 29 | + | // | |
| 30 | + | // Ownership of fd: on success libwayland owns it and closes it when the | |
| 31 | + | // client is destroyed; the caller must not close it, nor use it for I/O. On | |
| 32 | + | // failure libwayland does not take it (it is left open) and the caller keeps | |
| 33 | + | // ownership and must close it. Sockets should be close-on-exec (accept with | |
| 34 | + | // SOCK_CLOEXEC); libwayland does not set the flag. fd must be a connected | |
| 35 | + | // unix socket: libwayland reads its peer credentials with SO_PEERCRED. A | |
| 36 | + | // socket whose peer has already closed is still adopted; the client is then | |
| 37 | + | // destroyed on the next dispatch. | |
| 38 | + | // | |
| 39 | + | // On failure the error wraps the errno libwayland left (for example ENOTSOCK | |
| 40 | + | // or ENOMEM), read right after the call on the same thread; it is reliable | |
| 41 | + | // when called on the locked display goroutine. | |
| 42 | + | // | |
| 43 | + | // The returned Client is valid until destroyed; register OnDestroy to learn | |
| 44 | + | // when. Display goroutine only, or before Run starts. | |
| 45 | + | func (d *Display) CreateClient(fd int) (Client, error) { | |
| 46 | + | if d == nil || d.c == 0 { | |
| 47 | + | return Client{}, errors.New("purego-libwayland: invalid display") | |
| 48 | + | } | |
| 49 | + | if fd < 0 { | |
| 50 | + | return Client{}, fmt.Errorf("purego-libwayland: wl_client_create: invalid fd %d", fd) | |
| 51 | + | } | |
| 52 | + | r, _, _ := purego.Syscall6(symClientCreate, d.c, uintptr(fd), 0, 0, 0, 0) | |
| 53 | + | if r == 0 { | |
| 54 | + | return Client{}, fmt.Errorf("purego-libwayland: wl_client_create(%d) failed, fd not adopted: %w", fd, lastErrno()) | |
| 55 | + | } | |
| 56 | + | return Client{r}, nil | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // OnDestroy arranges for fn to run once when the client is destroyed, whether | |
| 60 | + | // it disconnected, was disconnected after a protocol error, or the display was | |
| 61 | + | // closed or stopped. Any number of functions may be registered per client; | |
| 62 | + | // each runs once, in registration order. There is no way to unregister one; | |
| 63 | + | // make fn a cheap no-op instead if it became irrelevant. | |
| 64 | + | // | |
| 65 | + | // fn runs on the display goroutine inside libwayland's client destruction. The | |
| 66 | + | // Client is still valid during fn (FD and Credentials work, resources are not | |
| 67 | + | // yet destroyed) but is dead afterwards: drop every copy of it, for example | |
| 68 | + | // delete it from a map[Client]T, because the handle value may be reused by a | |
| 69 | + | // later client. | |
| 70 | + | // | |
| 71 | + | // Registering another listener on the same client from inside fn is not | |
| 72 | + | // supported: whether it would run depends on how libwayland's final emit walks | |
| 73 | + | // the listener list (it differs between versions). Registering on a different, | |
| 74 | + | // live client from inside fn is fine. | |
| 75 | + | // | |
| 76 | + | // A Client is a comparable value (a handle to the C object) and is safe as a | |
| 77 | + | // map key from its creation or bind until fn runs. | |
| 78 | + | // | |
| 79 | + | // OnDestroy does nothing for a zero Client or nil fn. Registering on an | |
| 80 | + | // already destroyed client is invalid. It panics if the memory for the | |
| 81 | + | // listener cannot be mapped. Display goroutine only, or before Run starts. | |
| 82 | + | func (c Client) OnDestroy(fn func()) { | |
| 83 | + | if c.c == 0 || fn == nil { | |
| 84 | + | return | |
| 85 | + | } | |
| 86 | + | l := listeners.get() | |
| 87 | + | live.clientGone[l] = fn | |
| 88 | + | purego.Syscall6(symClientAddDestroyListener, c.c, l, 0, 0, 0, 0) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // listenerSlots hands out wl_listener structs in C-visible memory that the Go | |
| 92 | + | // GC cannot move or free. A slot is a struct wl_listener ({wl_list link; | |
| 93 | + | // wl_notify_func_t notify}, 24 bytes on LP64), padded to 32. Slots are | |
| 94 | + | // recycled through a free list, so only the peak number of concurrently | |
| 95 | + | // registered listeners is ever mapped; the chunks themselves are never | |
| 96 | + | // unmapped. Display goroutine only. | |
| 97 | + | type listenerSlots struct { | |
| 98 | + | chunks [][]byte | |
| 99 | + | free []uintptr | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | const ( | |
| 103 | + | listenerSlot = 32 | |
| 104 | + | listenerChunkSize = 4096 | |
| 105 | + | ) | |
| 106 | + | ||
| 107 | + | var listeners listenerSlots | |
| 108 | + | ||
| 109 | + | func (s *listenerSlots) get() uintptr { | |
| 110 | + | if len(s.free) == 0 { | |
| 111 | + | mem, err := unix.Mmap(-1, 0, listenerChunkSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS) | |
| 112 | + | if err != nil { | |
| 113 | + | panic(fmt.Errorf("purego-libwayland: listener mmap: %w", err)) | |
| 114 | + | } | |
| 115 | + | s.chunks = append(s.chunks, mem) | |
| 116 | + | base := uintptr(unsafe.Pointer(&mem[0])) | |
| 117 | + | for off := listenerChunkSize - listenerSlot; off >= 0; off -= listenerSlot { | |
| 118 | + | s.free = append(s.free, base+uintptr(off)) | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | l := s.free[len(s.free)-1] | |
| 122 | + | s.free = s.free[:len(s.free)-1] | |
| 123 | + | // notify lives at offset 16; wl_client_add_destroy_listener fills link. | |
| 124 | + | // l is an address inside an mmap chunk kept in s.chunks. | |
| 125 | + | *(*uintptr)(unsafe.Add(*(*unsafe.Pointer)(unsafe.Pointer(&l)), 16)) = cbClientGone | |
| 126 | + | return l | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func (s *listenerSlots) put(l uintptr) { s.free = append(s.free, l) } | |
| 130 | + | ||
| 131 | + | // clientGone is the wl_notify_func_t of every client destroy listener. | |
| 132 | + | func clientGone(l uintptr) { | |
| 133 | + | fn := live.clientGone[l] | |
| 134 | + | delete(live.clientGone, l) | |
| 135 | + | // libwayland's emit loop first unlinks each node and, on current versions, | |
| 136 | + | // resets it with wl_list_init, so this is a no-op on a self-linked node; | |
| 137 | + | // on older versions the node may still be linked and must be unlinked | |
| 138 | + | // before the slot can be reused. | |
| 139 | + | purego.Syscall6(symListRemove, l, 0, 0, 0, 0, 0) | |
| 140 | + | listeners.put(l) | |
| 141 | + | if fn != nil { | |
| 142 | + | fn() | |
| 143 | + | } | |
| 144 | + | } | |
| @@ -0,0 +1,144 @@ | |||||
| 1 | + | package server | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "fmt" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/purego" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // FD returns the client's connection socket. The descriptor is borrowed from | |||
| 13 | + | // libwayland: the caller must not close it, and it is valid only until the | |||
| 14 | + | // client is destroyed (see OnDestroy). Use it to identify or inspect the | |||
| 15 | + | // connection (for example fstat or /proc/self/fdinfo); reading or writing it | |||
| 16 | + | // would corrupt the protocol stream. It returns an error for an invalid | |||
| 17 | + | // (zero) Client. It does not allocate. Display goroutine only. | |||
| 18 | + | func (c Client) FD() (int, error) { | |||
| 19 | + | if c.c == 0 { | |||
| 20 | + | return -1, errInvalidClient | |||
| 21 | + | } | |||
| 22 | + | r, _, _ := purego.Syscall6(symClientGetFD, c.c, 0, 0, 0, 0, 0) | |||
| 23 | + | return int(int32(r)), nil | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // CreateClient adopts fd, an already connected stream socket (for example one | |||
| 27 | + | // accepted on a listening socket the compositor owns), as a new client served | |||
| 28 | + | // by this display. | |||
| 29 | + | // | |||
| 30 | + | // Ownership of fd: on success libwayland owns it and closes it when the | |||
| 31 | + | // client is destroyed; the caller must not close it, nor use it for I/O. On | |||
| 32 | + | // failure libwayland does not take it (it is left open) and the caller keeps | |||
| 33 | + | // ownership and must close it. Sockets should be close-on-exec (accept with | |||
| 34 | + | // SOCK_CLOEXEC); libwayland does not set the flag. fd must be a connected | |||
| 35 | + | // unix socket: libwayland reads its peer credentials with SO_PEERCRED. A | |||
| 36 | + | // socket whose peer has already closed is still adopted; the client is then | |||
| 37 | + | // destroyed on the next dispatch. | |||
| 38 | + | // | |||
| 39 | + | // On failure the error wraps the errno libwayland left (for example ENOTSOCK | |||
| 40 | + | // or ENOMEM), read right after the call on the same thread; it is reliable | |||
| 41 | + | // when called on the locked display goroutine. | |||
| 42 | + | // | |||
| 43 | + | // The returned Client is valid until destroyed; register OnDestroy to learn | |||
| 44 | + | // when. Display goroutine only, or before Run starts. | |||
| 45 | + | func (d *Display) CreateClient(fd int) (Client, error) { | |||
| 46 | + | if d == nil || d.c == 0 { | |||
| 47 | + | return Client{}, errors.New("purego-libwayland: invalid display") | |||
| 48 | + | } | |||
| 49 | + | if fd < 0 { | |||
| 50 | + | return Client{}, fmt.Errorf("purego-libwayland: wl_client_create: invalid fd %d", fd) | |||
| 51 | + | } | |||
| 52 | + | r, _, _ := purego.Syscall6(symClientCreate, d.c, uintptr(fd), 0, 0, 0, 0) | |||
| 53 | + | if r == 0 { | |||
| 54 | + | return Client{}, fmt.Errorf("purego-libwayland: wl_client_create(%d) failed, fd not adopted: %w", fd, lastErrno()) | |||
| 55 | + | } | |||
| 56 | + | return Client{r}, nil | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // OnDestroy arranges for fn to run once when the client is destroyed, whether | |||
| 60 | + | // it disconnected, was disconnected after a protocol error, or the display was | |||
| 61 | + | // closed or stopped. Any number of functions may be registered per client; | |||
| 62 | + | // each runs once, in registration order. There is no way to unregister one; | |||
| 63 | + | // make fn a cheap no-op instead if it became irrelevant. | |||
| 64 | + | // | |||
| 65 | + | // fn runs on the display goroutine inside libwayland's client destruction. The | |||
| 66 | + | // Client is still valid during fn (FD and Credentials work, resources are not | |||
| 67 | + | // yet destroyed) but is dead afterwards: drop every copy of it, for example | |||
| 68 | + | // delete it from a map[Client]T, because the handle value may be reused by a | |||
| 69 | + | // later client. | |||
| 70 | + | // | |||
| 71 | + | // Registering another listener on the same client from inside fn is not | |||
| 72 | + | // supported: whether it would run depends on how libwayland's final emit walks | |||
| 73 | + | // the listener list (it differs between versions). Registering on a different, | |||
| 74 | + | // live client from inside fn is fine. | |||
| 75 | + | // | |||
| 76 | + | // A Client is a comparable value (a handle to the C object) and is safe as a | |||
| 77 | + | // map key from its creation or bind until fn runs. | |||
| 78 | + | // | |||
| 79 | + | // OnDestroy does nothing for a zero Client or nil fn. Registering on an | |||
| 80 | + | // already destroyed client is invalid. It panics if the memory for the | |||
| 81 | + | // listener cannot be mapped. Display goroutine only, or before Run starts. | |||
| 82 | + | func (c Client) OnDestroy(fn func()) { | |||
| 83 | + | if c.c == 0 || fn == nil { | |||
| 84 | + | return | |||
| 85 | + | } | |||
| 86 | + | l := listeners.get() | |||
| 87 | + | live.clientGone[l] = fn | |||
| 88 | + | purego.Syscall6(symClientAddDestroyListener, c.c, l, 0, 0, 0, 0) | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // listenerSlots hands out wl_listener structs in C-visible memory that the Go | |||
| 92 | + | // GC cannot move or free. A slot is a struct wl_listener ({wl_list link; | |||
| 93 | + | // wl_notify_func_t notify}, 24 bytes on LP64), padded to 32. Slots are | |||
| 94 | + | // recycled through a free list, so only the peak number of concurrently | |||
| 95 | + | // registered listeners is ever mapped; the chunks themselves are never | |||
| 96 | + | // unmapped. Display goroutine only. | |||
| 97 | + | type listenerSlots struct { | |||
| 98 | + | chunks [][]byte | |||
| 99 | + | free []uintptr | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | const ( | |||
| 103 | + | listenerSlot = 32 | |||
| 104 | + | listenerChunkSize = 4096 | |||
| 105 | + | ) | |||
| 106 | + | ||||
| 107 | + | var listeners listenerSlots | |||
| 108 | + | ||||
| 109 | + | func (s *listenerSlots) get() uintptr { | |||
| 110 | + | if len(s.free) == 0 { | |||
| 111 | + | mem, err := unix.Mmap(-1, 0, listenerChunkSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS) | |||
| 112 | + | if err != nil { | |||
| 113 | + | panic(fmt.Errorf("purego-libwayland: listener mmap: %w", err)) | |||
| 114 | + | } | |||
| 115 | + | s.chunks = append(s.chunks, mem) | |||
| 116 | + | base := uintptr(unsafe.Pointer(&mem[0])) | |||
| 117 | + | for off := listenerChunkSize - listenerSlot; off >= 0; off -= listenerSlot { | |||
| 118 | + | s.free = append(s.free, base+uintptr(off)) | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | l := s.free[len(s.free)-1] | |||
| 122 | + | s.free = s.free[:len(s.free)-1] | |||
| 123 | + | // notify lives at offset 16; wl_client_add_destroy_listener fills link. | |||
| 124 | + | // l is an address inside an mmap chunk kept in s.chunks. | |||
| 125 | + | *(*uintptr)(unsafe.Add(*(*unsafe.Pointer)(unsafe.Pointer(&l)), 16)) = cbClientGone | |||
| 126 | + | return l | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func (s *listenerSlots) put(l uintptr) { s.free = append(s.free, l) } | |||
| 130 | + | ||||
| 131 | + | // clientGone is the wl_notify_func_t of every client destroy listener. | |||
| 132 | + | func clientGone(l uintptr) { | |||
| 133 | + | fn := live.clientGone[l] | |||
| 134 | + | delete(live.clientGone, l) | |||
| 135 | + | // libwayland's emit loop first unlinks each node and, on current versions, | |||
| 136 | + | // resets it with wl_list_init, so this is a no-op on a self-linked node; | |||
| 137 | + | // on older versions the node may still be linked and must be unlinked | |||
| 138 | + | // before the slot can be reused. | |||
| 139 | + | purego.Syscall6(symListRemove, l, 0, 0, 0, 0, 0) | |||
| 140 | + | listeners.put(l) | |||
| 141 | + | if fn != nil { | |||
| 142 | + | fn() | |||
| 143 | + | } | |||
| 144 | + | } | |||
A server/client_test.go +526 -0
| @@ -0,0 +1,526 @@ | |||
| 1 | + | package server | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "errors" | |
| 7 | + | "sync" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "golang.org/x/sys/unix" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // clientDisplay runs a real display and returns it with a stop function that | |
| 15 | + | // waits for Run to return (and so for display destruction). | |
| 16 | + | func clientDisplay(t *testing.T) (*Display, func()) { | |
| 17 | + | t.Helper() | |
| 18 | + | d, err := NewDisplay() | |
| 19 | + | if err != nil { | |
| 20 | + | t.Fatal(err) | |
| 21 | + | } | |
| 22 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 23 | + | done := make(chan error, 1) | |
| 24 | + | go func() { done <- d.Run(ctx) }() | |
| 25 | + | var once bool | |
| 26 | + | stop := func() { | |
| 27 | + | if once { | |
| 28 | + | return | |
| 29 | + | } | |
| 30 | + | once = true | |
| 31 | + | cancel() | |
| 32 | + | select { | |
| 33 | + | case err := <-done: | |
| 34 | + | if err != nil { | |
| 35 | + | t.Error(err) | |
| 36 | + | } | |
| 37 | + | case <-time.After(3 * time.Second): | |
| 38 | + | t.Fatal("Run did not return") | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | t.Cleanup(stop) | |
| 42 | + | return d, stop | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // sockPair is a connected socketpair. srv is meant to be adopted by the | |
| 46 | + | // display; peer is the test's end, closed at most once. | |
| 47 | + | type sockPair struct { | |
| 48 | + | srv, peer int | |
| 49 | + | once sync.Once | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func socketPair(t *testing.T) *sockPair { | |
| 53 | + | t.Helper() | |
| 54 | + | raw, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 55 | + | if err != nil { | |
| 56 | + | t.Fatal(err) | |
| 57 | + | } | |
| 58 | + | p := &sockPair{srv: raw[0], peer: raw[1]} | |
| 59 | + | t.Cleanup(p.closePeer) | |
| 60 | + | return p | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // closePeer closes the peer end; it is idempotent, so a test may call it and | |
| 64 | + | // the cleanup still will not close a reused descriptor number. | |
| 65 | + | func (p *sockPair) closePeer() { p.once.Do(func() { _ = unix.Close(p.peer) }) } | |
| 66 | + | ||
| 67 | + | // fileID identifies the open file behind fd, immune to fd number reuse. | |
| 68 | + | type fileID struct{ dev, ino uint64 } | |
| 69 | + | ||
| 70 | + | func identify(t *testing.T, fd int) fileID { | |
| 71 | + | t.Helper() | |
| 72 | + | var st unix.Stat_t | |
| 73 | + | if err := unix.Fstat(fd, &st); err != nil { | |
| 74 | + | t.Fatal(err) | |
| 75 | + | } | |
| 76 | + | return fileID{uint64(st.Dev), st.Ino} | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // stillOpen reports whether fd still refers to the file id. | |
| 80 | + | func stillOpen(fd int, id fileID) bool { | |
| 81 | + | var st unix.Stat_t | |
| 82 | + | if unix.Fstat(fd, &st) != nil { | |
| 83 | + | return false | |
| 84 | + | } | |
| 85 | + | return fileID{uint64(st.Dev), st.Ino} == id | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func adopt(t *testing.T, d *Display, fd int) Client { | |
| 89 | + | t.Helper() | |
| 90 | + | var c Client | |
| 91 | + | var err error | |
| 92 | + | if !d.Do(func() { c, err = d.CreateClient(fd) }) { | |
| 93 | + | t.Fatal("display stopped") | |
| 94 | + | } | |
| 95 | + | if err != nil { | |
| 96 | + | t.Fatal(err) | |
| 97 | + | } | |
| 98 | + | return c | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func TestClientFD(t *testing.T) { | |
| 102 | + | d, _ := clientDisplay(t) | |
| 103 | + | fds := socketPair(t) | |
| 104 | + | c := adopt(t, d, fds.srv) | |
| 105 | + | var ( | |
| 106 | + | got, zero int | |
| 107 | + | err, zerr error | |
| 108 | + | allocs float64 | |
| 109 | + | ) | |
| 110 | + | if !d.Do(func() { | |
| 111 | + | got, err = c.FD() | |
| 112 | + | zero, zerr = Client{}.FD() | |
| 113 | + | allocs = testing.AllocsPerRun(100, func() { _, _ = c.FD() }) | |
| 114 | + | }) { | |
| 115 | + | t.Fatal("display stopped") | |
| 116 | + | } | |
| 117 | + | if err != nil || got != fds.srv { | |
| 118 | + | t.Fatalf("FD = %d, %v; want %d", got, err, fds.srv) | |
| 119 | + | } | |
| 120 | + | if zerr == nil || zero != -1 { | |
| 121 | + | t.Fatalf("invalid client FD = %d, %v", zero, zerr) | |
| 122 | + | } | |
| 123 | + | if allocs != 0 { | |
| 124 | + | t.Fatalf("FD allocs = %g, want 0", allocs) | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func TestCreateClientFailureKeepsFD(t *testing.T) { | |
| 129 | + | d, _ := clientDisplay(t) | |
| 130 | + | var p [2]int | |
| 131 | + | if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil { | |
| 132 | + | t.Fatal(err) | |
| 133 | + | } | |
| 134 | + | defer unix.Close(p[0]) | |
| 135 | + | defer unix.Close(p[1]) | |
| 136 | + | pipeID := identify(t, p[0]) | |
| 137 | + | var err, errNeg error | |
| 138 | + | var c, cn Client | |
| 139 | + | if !d.Do(func() { | |
| 140 | + | c, err = d.CreateClient(p[0]) // not a socket: SO_PEERCRED fails | |
| 141 | + | cn, errNeg = d.CreateClient(-1) | |
| 142 | + | }) { | |
| 143 | + | t.Fatal("display stopped") | |
| 144 | + | } | |
| 145 | + | if !errors.Is(err, unix.ENOTSOCK) { | |
| 146 | + | t.Fatalf("error %v does not wrap ENOTSOCK", err) | |
| 147 | + | } | |
| 148 | + | if err == nil || c != (Client{}) || errNeg == nil || cn != (Client{}) { | |
| 149 | + | t.Fatalf("got %v %v, %v %v; want errors", c, err, cn, errNeg) | |
| 150 | + | } | |
| 151 | + | if !stillOpen(p[0], pipeID) { | |
| 152 | + | t.Fatal("failed CreateClient closed the fd") | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func TestCreateClientOwnsFD(t *testing.T) { | |
| 157 | + | d, _ := clientDisplay(t) | |
| 158 | + | fds := socketPair(t) | |
| 159 | + | c := adopt(t, d, fds.srv) | |
| 160 | + | gone := make(chan struct{}) | |
| 161 | + | d.Do(func() { c.OnDestroy(func() { close(gone) }) }) | |
| 162 | + | id := identify(t, fds.srv) | |
| 163 | + | fds.closePeer() | |
| 164 | + | select { | |
| 165 | + | case <-gone: | |
| 166 | + | case <-time.After(3 * time.Second): | |
| 167 | + | t.Fatal("client not destroyed") | |
| 168 | + | } | |
| 169 | + | // The listener runs before libwayland closes the fd; Do returns only once | |
| 170 | + | // the destruction that ran it has finished. | |
| 171 | + | d.Do(func() {}) | |
| 172 | + | if stillOpen(fds.srv, id) { | |
| 173 | + | t.Fatal("libwayland did not close the adopted fd") | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // A raw client speaks the wire protocol over the other socketpair end. | |
| 178 | + | func wireMsg(id uint32, opcode uint16, args ...[]byte) []byte { | |
| 179 | + | var body []byte | |
| 180 | + | for _, a := range args { | |
| 181 | + | body = append(body, a...) | |
| 182 | + | } | |
| 183 | + | b := make([]byte, 8, 8+len(body)) | |
| 184 | + | binary.LittleEndian.PutUint32(b, id) | |
| 185 | + | binary.LittleEndian.PutUint32(b[4:], uint32(8+len(body))<<16|uint32(opcode)) | |
| 186 | + | return append(b, body...) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | func u32(v uint32) []byte { return binary.LittleEndian.AppendUint32(nil, v) } | |
| 190 | + | ||
| 191 | + | func wireString(s string) []byte { | |
| 192 | + | b := u32(uint32(len(s) + 1)) | |
| 193 | + | b = append(b, s...) | |
| 194 | + | b = append(b, 0) | |
| 195 | + | for len(b)%4 != 0 { | |
| 196 | + | b = append(b, 0) | |
| 197 | + | } | |
| 198 | + | return b | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | func TestCreateClientBindsRegistry(t *testing.T) { | |
| 202 | + | iface := &Interface{Name: "adopted_client_test", Version: 1} | |
| 203 | + | if err := NewInterfaces(iface); err != nil { | |
| 204 | + | t.Fatal(err) | |
| 205 | + | } | |
| 206 | + | d, err := NewDisplay() | |
| 207 | + | if err != nil { | |
| 208 | + | t.Fatal(err) | |
| 209 | + | } | |
| 210 | + | bound := make(chan Client, 1) | |
| 211 | + | if err := d.CreateGlobal(iface, 1, func(c Client, version, id uint32) { bound <- c }); err != nil { | |
| 212 | + | t.Fatal(err) | |
| 213 | + | } | |
| 214 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 215 | + | done := make(chan error, 1) | |
| 216 | + | go func() { done <- d.Run(ctx) }() | |
| 217 | + | defer func() { cancel(); <-done }() | |
| 218 | + | ||
| 219 | + | fds := socketPair(t) | |
| 220 | + | c := adopt(t, d, fds.srv) | |
| 221 | + | if err := unix.SetsockoptTimeval(fds.peer, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil { | |
| 222 | + | t.Fatal(err) | |
| 223 | + | } | |
| 224 | + | // wl_display.get_registry(new_id 2), then wl_display.sync(new_id 3). | |
| 225 | + | req := append(wireMsg(1, 1, u32(2)), wireMsg(1, 0, u32(3))...) | |
| 226 | + | if _, err := unix.Write(fds.peer, req); err != nil { | |
| 227 | + | t.Fatal(err) | |
| 228 | + | } | |
| 229 | + | // Read until the sync callback's done event (object 3), collecting globals. | |
| 230 | + | var stream []byte | |
| 231 | + | var name uint32 | |
| 232 | + | buf := make([]byte, 4096) | |
| 233 | + | for sawDone := false; !sawDone; { | |
| 234 | + | n, err := unix.Read(fds.peer, buf) | |
| 235 | + | if err != nil || n == 0 { | |
| 236 | + | t.Fatalf("read: %d, %v", n, err) | |
| 237 | + | } | |
| 238 | + | stream = append(stream, buf[:n]...) | |
| 239 | + | for len(stream) >= 8 { | |
| 240 | + | size := int(binary.LittleEndian.Uint32(stream[4:]) >> 16) | |
| 241 | + | if size < 8 || len(stream) < size { | |
| 242 | + | break | |
| 243 | + | } | |
| 244 | + | id, opcode := binary.LittleEndian.Uint32(stream), uint16(binary.LittleEndian.Uint32(stream[4:])) | |
| 245 | + | body := stream[8:size] | |
| 246 | + | if id == 2 && opcode == 0 { // wl_registry.global(name, interface, version) | |
| 247 | + | strLen := int(binary.LittleEndian.Uint32(body[4:])) | |
| 248 | + | if string(body[8:8+strLen-1]) == iface.Name { | |
| 249 | + | name = binary.LittleEndian.Uint32(body) | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | if id == 3 { | |
| 253 | + | sawDone = true | |
| 254 | + | } | |
| 255 | + | stream = stream[size:] | |
| 256 | + | } | |
| 257 | + | } | |
| 258 | + | if name == 0 { | |
| 259 | + | t.Fatal("global not advertised to the adopted client") | |
| 260 | + | } | |
| 261 | + | // wl_registry.bind(name, interface, version, new_id 4) | |
| 262 | + | if _, err := unix.Write(fds.peer, wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil { | |
| 263 | + | t.Fatal(err) | |
| 264 | + | } | |
| 265 | + | select { | |
| 266 | + | case got := <-bound: | |
| 267 | + | if got != c { | |
| 268 | + | t.Fatalf("bind client %v, want adopted %v", got, c) | |
| 269 | + | } | |
| 270 | + | case <-time.After(3 * time.Second): | |
| 271 | + | t.Fatal("bind never reached the global") | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | func TestOnDestroyOnDisconnect(t *testing.T) { | |
| 276 | + | iface := &Interface{Name: "on_destroy_test", Version: 1} | |
| 277 | + | if err := NewInterfaces(iface); err != nil { | |
| 278 | + | t.Fatal(err) | |
| 279 | + | } | |
| 280 | + | d, _ := clientDisplay(t) | |
| 281 | + | fds := socketPair(t) | |
| 282 | + | c := adopt(t, d, fds.srv) | |
| 283 | + | var ( | |
| 284 | + | order []int | |
| 285 | + | resAlive, fdOK bool | |
| 286 | + | other = map[Client]int{c: 1} | |
| 287 | + | freeBefore, free int | |
| 288 | + | fired = make(chan struct{}) | |
| 289 | + | res *Resource | |
| 290 | + | createErr, fdError error | |
| 291 | + | ) | |
| 292 | + | if !d.Do(func() { | |
| 293 | + | res, createErr = c.CreateResource(iface, 1, 2, nil) | |
| 294 | + | for i := 1; i <= 3; i++ { | |
| 295 | + | c.OnDestroy(func() { | |
| 296 | + | order = append(order, i) | |
| 297 | + | if i == 3 { | |
| 298 | + | var fd int | |
| 299 | + | fd, fdError = c.FD() | |
| 300 | + | fdOK = fd == fds.srv | |
| 301 | + | resAlive = res.Alive() | |
| 302 | + | delete(other, c) | |
| 303 | + | close(fired) | |
| 304 | + | } | |
| 305 | + | }) | |
| 306 | + | } | |
| 307 | + | freeBefore = len(listeners.free) // after the slots are taken | |
| 308 | + | c.OnDestroy(nil) | |
| 309 | + | Client{}.OnDestroy(func() { t.Error("zero client listener ran") }) | |
| 310 | + | }) { | |
| 311 | + | t.Fatal("display stopped") | |
| 312 | + | } | |
| 313 | + | if createErr != nil { | |
| 314 | + | t.Fatal(createErr) | |
| 315 | + | } | |
| 316 | + | select { | |
| 317 | + | case <-fired: | |
| 318 | + | t.Fatal("fired before disconnect") | |
| 319 | + | case <-time.After(50 * time.Millisecond): | |
| 320 | + | } | |
| 321 | + | fds.closePeer() | |
| 322 | + | select { | |
| 323 | + | case <-fired: | |
| 324 | + | case <-time.After(3 * time.Second): | |
| 325 | + | t.Fatal("OnDestroy did not fire on disconnect") | |
| 326 | + | } | |
| 327 | + | d.Do(func() { free = len(listeners.free) }) | |
| 328 | + | if len(order) != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3 { | |
| 329 | + | t.Fatalf("order %v, want [1 2 3]", order) | |
| 330 | + | } | |
| 331 | + | if fdError != nil || !fdOK || !resAlive { | |
| 332 | + | t.Fatalf("during OnDestroy: fd ok=%v err=%v resource alive=%v", fdOK, fdError, resAlive) | |
| 333 | + | } | |
| 334 | + | if len(other) != 0 || res.Alive() { | |
| 335 | + | t.Fatalf("map %v, resource alive %v", other, res.Alive()) | |
| 336 | + | } | |
| 337 | + | if free != freeBefore+3 || len(live.clientGone) != 0 { | |
| 338 | + | t.Fatalf("listener slots not recycled: free %d -> %d, live %d", freeBefore, free, len(live.clientGone)) | |
| 339 | + | } | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | func TestOnDestroyOnDisplayClose(t *testing.T) { | |
| 343 | + | d, err := NewDisplay() | |
| 344 | + | if err != nil { | |
| 345 | + | t.Fatal(err) | |
| 346 | + | } | |
| 347 | + | fds := socketPair(t) | |
| 348 | + | c, err := d.CreateClient(fds.srv) | |
| 349 | + | if err != nil { | |
| 350 | + | t.Fatal(err) | |
| 351 | + | } | |
| 352 | + | n := 0 | |
| 353 | + | c.OnDestroy(func() { n++ }) | |
| 354 | + | c.OnDestroy(func() { n += 10 }) | |
| 355 | + | d.Close() | |
| 356 | + | d.Close() | |
| 357 | + | if n != 11 { | |
| 358 | + | t.Fatalf("listeners ran %d, want 11 (each once)", n) | |
| 359 | + | } | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | func TestOnDestroyOnRunStop(t *testing.T) { | |
| 363 | + | d, stop := clientDisplay(t) | |
| 364 | + | fds := socketPair(t) | |
| 365 | + | c := adopt(t, d, fds.srv) | |
| 366 | + | n := 0 | |
| 367 | + | d.Do(func() { c.OnDestroy(func() { n++ }) }) | |
| 368 | + | stop() | |
| 369 | + | if n != 1 { | |
| 370 | + | t.Fatalf("listener ran %d times, want 1", n) | |
| 371 | + | } | |
| 372 | + | } | |
| 373 | + | ||
| 374 | + | // A client destroyed at disconnect must not fire again when the display stops. | |
| 375 | + | func TestOnDestroyOnce(t *testing.T) { | |
| 376 | + | d, stop := clientDisplay(t) | |
| 377 | + | fds := socketPair(t) | |
| 378 | + | c := adopt(t, d, fds.srv) | |
| 379 | + | fired := make(chan struct{}, 4) | |
| 380 | + | d.Do(func() { c.OnDestroy(func() { fired <- struct{}{} }) }) | |
| 381 | + | fds.closePeer() | |
| 382 | + | select { | |
| 383 | + | case <-fired: | |
| 384 | + | case <-time.After(3 * time.Second): | |
| 385 | + | t.Fatal("OnDestroy did not fire") | |
| 386 | + | } | |
| 387 | + | stop() | |
| 388 | + | if len(fired) != 0 { | |
| 389 | + | t.Fatal("OnDestroy fired twice") | |
| 390 | + | } | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | // A peer that already hung up does not stop adoption; the client is then | |
| 394 | + | // destroyed and its listener runs. | |
| 395 | + | func TestCreateClientPeerAlreadyClosed(t *testing.T) { | |
| 396 | + | d, _ := clientDisplay(t) | |
| 397 | + | fds := socketPair(t) | |
| 398 | + | fds.closePeer() | |
| 399 | + | id := identify(t, fds.srv) // before adoption: the client may die at once | |
| 400 | + | gone := make(chan struct{}) | |
| 401 | + | var err error | |
| 402 | + | // Adopt and register in one call, before the loop can destroy the client. | |
| 403 | + | if !d.Do(func() { | |
| 404 | + | var c Client | |
| 405 | + | if c, err = d.CreateClient(fds.srv); err == nil { | |
| 406 | + | c.OnDestroy(func() { close(gone) }) | |
| 407 | + | } | |
| 408 | + | }) { | |
| 409 | + | t.Fatal("display stopped") | |
| 410 | + | } | |
| 411 | + | if err != nil { | |
| 412 | + | t.Fatal(err) | |
| 413 | + | } | |
| 414 | + | select { | |
| 415 | + | case <-gone: | |
| 416 | + | case <-time.After(3 * time.Second): | |
| 417 | + | t.Fatal("client with a closed peer was not destroyed") | |
| 418 | + | } | |
| 419 | + | d.Do(func() {}) | |
| 420 | + | if stillOpen(fds.srv, id) { | |
| 421 | + | t.Fatal("adopted fd not closed") | |
| 422 | + | } | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | // A listener may register on another live client from inside its callback, | |
| 426 | + | // even though it is handed the slot that is firing. | |
| 427 | + | func TestOnDestroyRegistersOnOtherClient(t *testing.T) { | |
| 428 | + | d, _ := clientDisplay(t) | |
| 429 | + | a, b := socketPair(t), socketPair(t) | |
| 430 | + | ca, cb := adopt(t, d, a.srv), adopt(t, d, b.srv) | |
| 431 | + | aGone := make(chan struct{}) | |
| 432 | + | bGone := make(chan struct{}, 2) | |
| 433 | + | var slotA, slotB uintptr | |
| 434 | + | d.Do(func() { | |
| 435 | + | for l := range live.clientGone { | |
| 436 | + | t.Errorf("unexpected live listener %#x", l) | |
| 437 | + | } | |
| 438 | + | ca.OnDestroy(func() { | |
| 439 | + | cb.OnDestroy(func() { bGone <- struct{}{} }) | |
| 440 | + | for l := range live.clientGone { | |
| 441 | + | slotB = l | |
| 442 | + | } | |
| 443 | + | close(aGone) | |
| 444 | + | }) | |
| 445 | + | for l := range live.clientGone { | |
| 446 | + | slotA = l | |
| 447 | + | } | |
| 448 | + | }) | |
| 449 | + | a.closePeer() | |
| 450 | + | select { | |
| 451 | + | case <-aGone: | |
| 452 | + | case <-time.After(3 * time.Second): | |
| 453 | + | t.Fatal("first client's listener did not fire") | |
| 454 | + | } | |
| 455 | + | if slotA == 0 || slotA != slotB { | |
| 456 | + | t.Fatalf("freed slot not reused: %#x then %#x", slotA, slotB) | |
| 457 | + | } | |
| 458 | + | select { | |
| 459 | + | case <-bGone: | |
| 460 | + | t.Fatal("second client's listener fired early") | |
| 461 | + | case <-time.After(50 * time.Millisecond): | |
| 462 | + | } | |
| 463 | + | b.closePeer() | |
| 464 | + | select { | |
| 465 | + | case <-bGone: | |
| 466 | + | case <-time.After(3 * time.Second): | |
| 467 | + | t.Fatal("listener registered from a callback did not fire") | |
| 468 | + | } | |
| 469 | + | time.Sleep(50 * time.Millisecond) | |
| 470 | + | if len(bGone) != 0 { | |
| 471 | + | t.Fatal("listener fired twice") | |
| 472 | + | } | |
| 473 | + | } | |
| 474 | + | ||
| 475 | + | // More listeners than one mapped chunk holds: all fire once, every slot comes | |
| 476 | + | // back, and a later registration reuses a recycled slot. | |
| 477 | + | func TestOnDestroyManyListeners(t *testing.T) { | |
| 478 | + | d, _ := clientDisplay(t) | |
| 479 | + | fds := socketPair(t) | |
| 480 | + | fds2 := socketPair(t) | |
| 481 | + | c := adopt(t, d, fds.srv) | |
| 482 | + | const n = 3*listenerChunkSize/listenerSlot + 5 | |
| 483 | + | var fired, chunksBefore int | |
| 484 | + | done := make(chan struct{}) | |
| 485 | + | var before, after int | |
| 486 | + | d.Do(func() { | |
| 487 | + | for i := 0; i < n; i++ { | |
| 488 | + | c.OnDestroy(func() { | |
| 489 | + | fired++ | |
| 490 | + | if fired == n { | |
| 491 | + | close(done) | |
| 492 | + | } | |
| 493 | + | }) | |
| 494 | + | } | |
| 495 | + | before = len(listeners.free) | |
| 496 | + | chunksBefore = len(listeners.chunks) | |
| 497 | + | }) | |
| 498 | + | if chunksBefore < 3 { | |
| 499 | + | t.Fatalf("test did not cross a chunk boundary: %d chunks", chunksBefore) | |
| 500 | + | } | |
| 501 | + | fds.closePeer() | |
| 502 | + | select { | |
| 503 | + | case <-done: | |
| 504 | + | case <-time.After(3 * time.Second): | |
| 505 | + | t.Fatal("not every listener fired") | |
| 506 | + | } | |
| 507 | + | var reused bool | |
| 508 | + | var firedNow int | |
| 509 | + | d.Do(func() { | |
| 510 | + | firedNow = fired | |
| 511 | + | after = len(listeners.free) | |
| 512 | + | c2, err := d.CreateClient(fds2.srv) | |
| 513 | + | if err != nil { | |
| 514 | + | t.Error(err) | |
| 515 | + | return | |
| 516 | + | } | |
| 517 | + | c2.OnDestroy(func() {}) | |
| 518 | + | reused = len(listeners.chunks) == chunksBefore && len(listeners.free) == after-1 | |
| 519 | + | }) | |
| 520 | + | if after != before+n { | |
| 521 | + | t.Fatalf("free slots %d -> %d, want +%d", before, after, n) | |
| 522 | + | } | |
| 523 | + | if firedNow != n || !reused { | |
| 524 | + | t.Fatalf("fired %d, slot reused %v", firedNow, reused) | |
| 525 | + | } | |
| 526 | + | } | |
| @@ -0,0 +1,526 @@ | |||||
| 1 | + | package server | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "errors" | |||
| 7 | + | "sync" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // clientDisplay runs a real display and returns it with a stop function that | |||
| 15 | + | // waits for Run to return (and so for display destruction). | |||
| 16 | + | func clientDisplay(t *testing.T) (*Display, func()) { | |||
| 17 | + | t.Helper() | |||
| 18 | + | d, err := NewDisplay() | |||
| 19 | + | if err != nil { | |||
| 20 | + | t.Fatal(err) | |||
| 21 | + | } | |||
| 22 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 23 | + | done := make(chan error, 1) | |||
| 24 | + | go func() { done <- d.Run(ctx) }() | |||
| 25 | + | var once bool | |||
| 26 | + | stop := func() { | |||
| 27 | + | if once { | |||
| 28 | + | return | |||
| 29 | + | } | |||
| 30 | + | once = true | |||
| 31 | + | cancel() | |||
| 32 | + | select { | |||
| 33 | + | case err := <-done: | |||
| 34 | + | if err != nil { | |||
| 35 | + | t.Error(err) | |||
| 36 | + | } | |||
| 37 | + | case <-time.After(3 * time.Second): | |||
| 38 | + | t.Fatal("Run did not return") | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | t.Cleanup(stop) | |||
| 42 | + | return d, stop | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // sockPair is a connected socketpair. srv is meant to be adopted by the | |||
| 46 | + | // display; peer is the test's end, closed at most once. | |||
| 47 | + | type sockPair struct { | |||
| 48 | + | srv, peer int | |||
| 49 | + | once sync.Once | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func socketPair(t *testing.T) *sockPair { | |||
| 53 | + | t.Helper() | |||
| 54 | + | raw, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 55 | + | if err != nil { | |||
| 56 | + | t.Fatal(err) | |||
| 57 | + | } | |||
| 58 | + | p := &sockPair{srv: raw[0], peer: raw[1]} | |||
| 59 | + | t.Cleanup(p.closePeer) | |||
| 60 | + | return p | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // closePeer closes the peer end; it is idempotent, so a test may call it and | |||
| 64 | + | // the cleanup still will not close a reused descriptor number. | |||
| 65 | + | func (p *sockPair) closePeer() { p.once.Do(func() { _ = unix.Close(p.peer) }) } | |||
| 66 | + | ||||
| 67 | + | // fileID identifies the open file behind fd, immune to fd number reuse. | |||
| 68 | + | type fileID struct{ dev, ino uint64 } | |||
| 69 | + | ||||
| 70 | + | func identify(t *testing.T, fd int) fileID { | |||
| 71 | + | t.Helper() | |||
| 72 | + | var st unix.Stat_t | |||
| 73 | + | if err := unix.Fstat(fd, &st); err != nil { | |||
| 74 | + | t.Fatal(err) | |||
| 75 | + | } | |||
| 76 | + | return fileID{uint64(st.Dev), st.Ino} | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // stillOpen reports whether fd still refers to the file id. | |||
| 80 | + | func stillOpen(fd int, id fileID) bool { | |||
| 81 | + | var st unix.Stat_t | |||
| 82 | + | if unix.Fstat(fd, &st) != nil { | |||
| 83 | + | return false | |||
| 84 | + | } | |||
| 85 | + | return fileID{uint64(st.Dev), st.Ino} == id | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func adopt(t *testing.T, d *Display, fd int) Client { | |||
| 89 | + | t.Helper() | |||
| 90 | + | var c Client | |||
| 91 | + | var err error | |||
| 92 | + | if !d.Do(func() { c, err = d.CreateClient(fd) }) { | |||
| 93 | + | t.Fatal("display stopped") | |||
| 94 | + | } | |||
| 95 | + | if err != nil { | |||
| 96 | + | t.Fatal(err) | |||
| 97 | + | } | |||
| 98 | + | return c | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | func TestClientFD(t *testing.T) { | |||
| 102 | + | d, _ := clientDisplay(t) | |||
| 103 | + | fds := socketPair(t) | |||
| 104 | + | c := adopt(t, d, fds.srv) | |||
| 105 | + | var ( | |||
| 106 | + | got, zero int | |||
| 107 | + | err, zerr error | |||
| 108 | + | allocs float64 | |||
| 109 | + | ) | |||
| 110 | + | if !d.Do(func() { | |||
| 111 | + | got, err = c.FD() | |||
| 112 | + | zero, zerr = Client{}.FD() | |||
| 113 | + | allocs = testing.AllocsPerRun(100, func() { _, _ = c.FD() }) | |||
| 114 | + | }) { | |||
| 115 | + | t.Fatal("display stopped") | |||
| 116 | + | } | |||
| 117 | + | if err != nil || got != fds.srv { | |||
| 118 | + | t.Fatalf("FD = %d, %v; want %d", got, err, fds.srv) | |||
| 119 | + | } | |||
| 120 | + | if zerr == nil || zero != -1 { | |||
| 121 | + | t.Fatalf("invalid client FD = %d, %v", zero, zerr) | |||
| 122 | + | } | |||
| 123 | + | if allocs != 0 { | |||
| 124 | + | t.Fatalf("FD allocs = %g, want 0", allocs) | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func TestCreateClientFailureKeepsFD(t *testing.T) { | |||
| 129 | + | d, _ := clientDisplay(t) | |||
| 130 | + | var p [2]int | |||
| 131 | + | if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil { | |||
| 132 | + | t.Fatal(err) | |||
| 133 | + | } | |||
| 134 | + | defer unix.Close(p[0]) | |||
| 135 | + | defer unix.Close(p[1]) | |||
| 136 | + | pipeID := identify(t, p[0]) | |||
| 137 | + | var err, errNeg error | |||
| 138 | + | var c, cn Client | |||
| 139 | + | if !d.Do(func() { | |||
| 140 | + | c, err = d.CreateClient(p[0]) // not a socket: SO_PEERCRED fails | |||
| 141 | + | cn, errNeg = d.CreateClient(-1) | |||
| 142 | + | }) { | |||
| 143 | + | t.Fatal("display stopped") | |||
| 144 | + | } | |||
| 145 | + | if !errors.Is(err, unix.ENOTSOCK) { | |||
| 146 | + | t.Fatalf("error %v does not wrap ENOTSOCK", err) | |||
| 147 | + | } | |||
| 148 | + | if err == nil || c != (Client{}) || errNeg == nil || cn != (Client{}) { | |||
| 149 | + | t.Fatalf("got %v %v, %v %v; want errors", c, err, cn, errNeg) | |||
| 150 | + | } | |||
| 151 | + | if !stillOpen(p[0], pipeID) { | |||
| 152 | + | t.Fatal("failed CreateClient closed the fd") | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func TestCreateClientOwnsFD(t *testing.T) { | |||
| 157 | + | d, _ := clientDisplay(t) | |||
| 158 | + | fds := socketPair(t) | |||
| 159 | + | c := adopt(t, d, fds.srv) | |||
| 160 | + | gone := make(chan struct{}) | |||
| 161 | + | d.Do(func() { c.OnDestroy(func() { close(gone) }) }) | |||
| 162 | + | id := identify(t, fds.srv) | |||
| 163 | + | fds.closePeer() | |||
| 164 | + | select { | |||
| 165 | + | case <-gone: | |||
| 166 | + | case <-time.After(3 * time.Second): | |||
| 167 | + | t.Fatal("client not destroyed") | |||
| 168 | + | } | |||
| 169 | + | // The listener runs before libwayland closes the fd; Do returns only once | |||
| 170 | + | // the destruction that ran it has finished. | |||
| 171 | + | d.Do(func() {}) | |||
| 172 | + | if stillOpen(fds.srv, id) { | |||
| 173 | + | t.Fatal("libwayland did not close the adopted fd") | |||
| 174 | + | } | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | // A raw client speaks the wire protocol over the other socketpair end. | |||
| 178 | + | func wireMsg(id uint32, opcode uint16, args ...[]byte) []byte { | |||
| 179 | + | var body []byte | |||
| 180 | + | for _, a := range args { | |||
| 181 | + | body = append(body, a...) | |||
| 182 | + | } | |||
| 183 | + | b := make([]byte, 8, 8+len(body)) | |||
| 184 | + | binary.LittleEndian.PutUint32(b, id) | |||
| 185 | + | binary.LittleEndian.PutUint32(b[4:], uint32(8+len(body))<<16|uint32(opcode)) | |||
| 186 | + | return append(b, body...) | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | func u32(v uint32) []byte { return binary.LittleEndian.AppendUint32(nil, v) } | |||
| 190 | + | ||||
| 191 | + | func wireString(s string) []byte { | |||
| 192 | + | b := u32(uint32(len(s) + 1)) | |||
| 193 | + | b = append(b, s...) | |||
| 194 | + | b = append(b, 0) | |||
| 195 | + | for len(b)%4 != 0 { | |||
| 196 | + | b = append(b, 0) | |||
| 197 | + | } | |||
| 198 | + | return b | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | func TestCreateClientBindsRegistry(t *testing.T) { | |||
| 202 | + | iface := &Interface{Name: "adopted_client_test", Version: 1} | |||
| 203 | + | if err := NewInterfaces(iface); err != nil { | |||
| 204 | + | t.Fatal(err) | |||
| 205 | + | } | |||
| 206 | + | d, err := NewDisplay() | |||
| 207 | + | if err != nil { | |||
| 208 | + | t.Fatal(err) | |||
| 209 | + | } | |||
| 210 | + | bound := make(chan Client, 1) | |||
| 211 | + | if err := d.CreateGlobal(iface, 1, func(c Client, version, id uint32) { bound <- c }); err != nil { | |||
| 212 | + | t.Fatal(err) | |||
| 213 | + | } | |||
| 214 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 215 | + | done := make(chan error, 1) | |||
| 216 | + | go func() { done <- d.Run(ctx) }() | |||
| 217 | + | defer func() { cancel(); <-done }() | |||
| 218 | + | ||||
| 219 | + | fds := socketPair(t) | |||
| 220 | + | c := adopt(t, d, fds.srv) | |||
| 221 | + | if err := unix.SetsockoptTimeval(fds.peer, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil { | |||
| 222 | + | t.Fatal(err) | |||
| 223 | + | } | |||
| 224 | + | // wl_display.get_registry(new_id 2), then wl_display.sync(new_id 3). | |||
| 225 | + | req := append(wireMsg(1, 1, u32(2)), wireMsg(1, 0, u32(3))...) | |||
| 226 | + | if _, err := unix.Write(fds.peer, req); err != nil { | |||
| 227 | + | t.Fatal(err) | |||
| 228 | + | } | |||
| 229 | + | // Read until the sync callback's done event (object 3), collecting globals. | |||
| 230 | + | var stream []byte | |||
| 231 | + | var name uint32 | |||
| 232 | + | buf := make([]byte, 4096) | |||
| 233 | + | for sawDone := false; !sawDone; { | |||
| 234 | + | n, err := unix.Read(fds.peer, buf) | |||
| 235 | + | if err != nil || n == 0 { | |||
| 236 | + | t.Fatalf("read: %d, %v", n, err) | |||
| 237 | + | } | |||
| 238 | + | stream = append(stream, buf[:n]...) | |||
| 239 | + | for len(stream) >= 8 { | |||
| 240 | + | size := int(binary.LittleEndian.Uint32(stream[4:]) >> 16) | |||
| 241 | + | if size < 8 || len(stream) < size { | |||
| 242 | + | break | |||
| 243 | + | } | |||
| 244 | + | id, opcode := binary.LittleEndian.Uint32(stream), uint16(binary.LittleEndian.Uint32(stream[4:])) | |||
| 245 | + | body := stream[8:size] | |||
| 246 | + | if id == 2 && opcode == 0 { // wl_registry.global(name, interface, version) | |||
| 247 | + | strLen := int(binary.LittleEndian.Uint32(body[4:])) | |||
| 248 | + | if string(body[8:8+strLen-1]) == iface.Name { | |||
| 249 | + | name = binary.LittleEndian.Uint32(body) | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | if id == 3 { | |||
| 253 | + | sawDone = true | |||
| 254 | + | } | |||
| 255 | + | stream = stream[size:] | |||
| 256 | + | } | |||
| 257 | + | } | |||
| 258 | + | if name == 0 { | |||
| 259 | + | t.Fatal("global not advertised to the adopted client") | |||
| 260 | + | } | |||
| 261 | + | // wl_registry.bind(name, interface, version, new_id 4) | |||
| 262 | + | if _, err := unix.Write(fds.peer, wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil { | |||
| 263 | + | t.Fatal(err) | |||
| 264 | + | } | |||
| 265 | + | select { | |||
| 266 | + | case got := <-bound: | |||
| 267 | + | if got != c { | |||
| 268 | + | t.Fatalf("bind client %v, want adopted %v", got, c) | |||
| 269 | + | } | |||
| 270 | + | case <-time.After(3 * time.Second): | |||
| 271 | + | t.Fatal("bind never reached the global") | |||
| 272 | + | } | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | func TestOnDestroyOnDisconnect(t *testing.T) { | |||
| 276 | + | iface := &Interface{Name: "on_destroy_test", Version: 1} | |||
| 277 | + | if err := NewInterfaces(iface); err != nil { | |||
| 278 | + | t.Fatal(err) | |||
| 279 | + | } | |||
| 280 | + | d, _ := clientDisplay(t) | |||
| 281 | + | fds := socketPair(t) | |||
| 282 | + | c := adopt(t, d, fds.srv) | |||
| 283 | + | var ( | |||
| 284 | + | order []int | |||
| 285 | + | resAlive, fdOK bool | |||
| 286 | + | other = map[Client]int{c: 1} | |||
| 287 | + | freeBefore, free int | |||
| 288 | + | fired = make(chan struct{}) | |||
| 289 | + | res *Resource | |||
| 290 | + | createErr, fdError error | |||
| 291 | + | ) | |||
| 292 | + | if !d.Do(func() { | |||
| 293 | + | res, createErr = c.CreateResource(iface, 1, 2, nil) | |||
| 294 | + | for i := 1; i <= 3; i++ { | |||
| 295 | + | c.OnDestroy(func() { | |||
| 296 | + | order = append(order, i) | |||
| 297 | + | if i == 3 { | |||
| 298 | + | var fd int | |||
| 299 | + | fd, fdError = c.FD() | |||
| 300 | + | fdOK = fd == fds.srv | |||
| 301 | + | resAlive = res.Alive() | |||
| 302 | + | delete(other, c) | |||
| 303 | + | close(fired) | |||
| 304 | + | } | |||
| 305 | + | }) | |||
| 306 | + | } | |||
| 307 | + | freeBefore = len(listeners.free) // after the slots are taken | |||
| 308 | + | c.OnDestroy(nil) | |||
| 309 | + | Client{}.OnDestroy(func() { t.Error("zero client listener ran") }) | |||
| 310 | + | }) { | |||
| 311 | + | t.Fatal("display stopped") | |||
| 312 | + | } | |||
| 313 | + | if createErr != nil { | |||
| 314 | + | t.Fatal(createErr) | |||
| 315 | + | } | |||
| 316 | + | select { | |||
| 317 | + | case <-fired: | |||
| 318 | + | t.Fatal("fired before disconnect") | |||
| 319 | + | case <-time.After(50 * time.Millisecond): | |||
| 320 | + | } | |||
| 321 | + | fds.closePeer() | |||
| 322 | + | select { | |||
| 323 | + | case <-fired: | |||
| 324 | + | case <-time.After(3 * time.Second): | |||
| 325 | + | t.Fatal("OnDestroy did not fire on disconnect") | |||
| 326 | + | } | |||
| 327 | + | d.Do(func() { free = len(listeners.free) }) | |||
| 328 | + | if len(order) != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3 { | |||
| 329 | + | t.Fatalf("order %v, want [1 2 3]", order) | |||
| 330 | + | } | |||
| 331 | + | if fdError != nil || !fdOK || !resAlive { | |||
| 332 | + | t.Fatalf("during OnDestroy: fd ok=%v err=%v resource alive=%v", fdOK, fdError, resAlive) | |||
| 333 | + | } | |||
| 334 | + | if len(other) != 0 || res.Alive() { | |||
| 335 | + | t.Fatalf("map %v, resource alive %v", other, res.Alive()) | |||
| 336 | + | } | |||
| 337 | + | if free != freeBefore+3 || len(live.clientGone) != 0 { | |||
| 338 | + | t.Fatalf("listener slots not recycled: free %d -> %d, live %d", freeBefore, free, len(live.clientGone)) | |||
| 339 | + | } | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | func TestOnDestroyOnDisplayClose(t *testing.T) { | |||
| 343 | + | d, err := NewDisplay() | |||
| 344 | + | if err != nil { | |||
| 345 | + | t.Fatal(err) | |||
| 346 | + | } | |||
| 347 | + | fds := socketPair(t) | |||
| 348 | + | c, err := d.CreateClient(fds.srv) | |||
| 349 | + | if err != nil { | |||
| 350 | + | t.Fatal(err) | |||
| 351 | + | } | |||
| 352 | + | n := 0 | |||
| 353 | + | c.OnDestroy(func() { n++ }) | |||
| 354 | + | c.OnDestroy(func() { n += 10 }) | |||
| 355 | + | d.Close() | |||
| 356 | + | d.Close() | |||
| 357 | + | if n != 11 { | |||
| 358 | + | t.Fatalf("listeners ran %d, want 11 (each once)", n) | |||
| 359 | + | } | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | func TestOnDestroyOnRunStop(t *testing.T) { | |||
| 363 | + | d, stop := clientDisplay(t) | |||
| 364 | + | fds := socketPair(t) | |||
| 365 | + | c := adopt(t, d, fds.srv) | |||
| 366 | + | n := 0 | |||
| 367 | + | d.Do(func() { c.OnDestroy(func() { n++ }) }) | |||
| 368 | + | stop() | |||
| 369 | + | if n != 1 { | |||
| 370 | + | t.Fatalf("listener ran %d times, want 1", n) | |||
| 371 | + | } | |||
| 372 | + | } | |||
| 373 | + | ||||
| 374 | + | // A client destroyed at disconnect must not fire again when the display stops. | |||
| 375 | + | func TestOnDestroyOnce(t *testing.T) { | |||
| 376 | + | d, stop := clientDisplay(t) | |||
| 377 | + | fds := socketPair(t) | |||
| 378 | + | c := adopt(t, d, fds.srv) | |||
| 379 | + | fired := make(chan struct{}, 4) | |||
| 380 | + | d.Do(func() { c.OnDestroy(func() { fired <- struct{}{} }) }) | |||
| 381 | + | fds.closePeer() | |||
| 382 | + | select { | |||
| 383 | + | case <-fired: | |||
| 384 | + | case <-time.After(3 * time.Second): | |||
| 385 | + | t.Fatal("OnDestroy did not fire") | |||
| 386 | + | } | |||
| 387 | + | stop() | |||
| 388 | + | if len(fired) != 0 { | |||
| 389 | + | t.Fatal("OnDestroy fired twice") | |||
| 390 | + | } | |||
| 391 | + | } | |||
| 392 | + | ||||
| 393 | + | // A peer that already hung up does not stop adoption; the client is then | |||
| 394 | + | // destroyed and its listener runs. | |||
| 395 | + | func TestCreateClientPeerAlreadyClosed(t *testing.T) { | |||
| 396 | + | d, _ := clientDisplay(t) | |||
| 397 | + | fds := socketPair(t) | |||
| 398 | + | fds.closePeer() | |||
| 399 | + | id := identify(t, fds.srv) // before adoption: the client may die at once | |||
| 400 | + | gone := make(chan struct{}) | |||
| 401 | + | var err error | |||
| 402 | + | // Adopt and register in one call, before the loop can destroy the client. | |||
| 403 | + | if !d.Do(func() { | |||
| 404 | + | var c Client | |||
| 405 | + | if c, err = d.CreateClient(fds.srv); err == nil { | |||
| 406 | + | c.OnDestroy(func() { close(gone) }) | |||
| 407 | + | } | |||
| 408 | + | }) { | |||
| 409 | + | t.Fatal("display stopped") | |||
| 410 | + | } | |||
| 411 | + | if err != nil { | |||
| 412 | + | t.Fatal(err) | |||
| 413 | + | } | |||
| 414 | + | select { | |||
| 415 | + | case <-gone: | |||
| 416 | + | case <-time.After(3 * time.Second): | |||
| 417 | + | t.Fatal("client with a closed peer was not destroyed") | |||
| 418 | + | } | |||
| 419 | + | d.Do(func() {}) | |||
| 420 | + | if stillOpen(fds.srv, id) { | |||
| 421 | + | t.Fatal("adopted fd not closed") | |||
| 422 | + | } | |||
| 423 | + | } | |||
| 424 | + | ||||
| 425 | + | // A listener may register on another live client from inside its callback, | |||
| 426 | + | // even though it is handed the slot that is firing. | |||
| 427 | + | func TestOnDestroyRegistersOnOtherClient(t *testing.T) { | |||
| 428 | + | d, _ := clientDisplay(t) | |||
| 429 | + | a, b := socketPair(t), socketPair(t) | |||
| 430 | + | ca, cb := adopt(t, d, a.srv), adopt(t, d, b.srv) | |||
| 431 | + | aGone := make(chan struct{}) | |||
| 432 | + | bGone := make(chan struct{}, 2) | |||
| 433 | + | var slotA, slotB uintptr | |||
| 434 | + | d.Do(func() { | |||
| 435 | + | for l := range live.clientGone { | |||
| 436 | + | t.Errorf("unexpected live listener %#x", l) | |||
| 437 | + | } | |||
| 438 | + | ca.OnDestroy(func() { | |||
| 439 | + | cb.OnDestroy(func() { bGone <- struct{}{} }) | |||
| 440 | + | for l := range live.clientGone { | |||
| 441 | + | slotB = l | |||
| 442 | + | } | |||
| 443 | + | close(aGone) | |||
| 444 | + | }) | |||
| 445 | + | for l := range live.clientGone { | |||
| 446 | + | slotA = l | |||
| 447 | + | } | |||
| 448 | + | }) | |||
| 449 | + | a.closePeer() | |||
| 450 | + | select { | |||
| 451 | + | case <-aGone: | |||
| 452 | + | case <-time.After(3 * time.Second): | |||
| 453 | + | t.Fatal("first client's listener did not fire") | |||
| 454 | + | } | |||
| 455 | + | if slotA == 0 || slotA != slotB { | |||
| 456 | + | t.Fatalf("freed slot not reused: %#x then %#x", slotA, slotB) | |||
| 457 | + | } | |||
| 458 | + | select { | |||
| 459 | + | case <-bGone: | |||
| 460 | + | t.Fatal("second client's listener fired early") | |||
| 461 | + | case <-time.After(50 * time.Millisecond): | |||
| 462 | + | } | |||
| 463 | + | b.closePeer() | |||
| 464 | + | select { | |||
| 465 | + | case <-bGone: | |||
| 466 | + | case <-time.After(3 * time.Second): | |||
| 467 | + | t.Fatal("listener registered from a callback did not fire") | |||
| 468 | + | } | |||
| 469 | + | time.Sleep(50 * time.Millisecond) | |||
| 470 | + | if len(bGone) != 0 { | |||
| 471 | + | t.Fatal("listener fired twice") | |||
| 472 | + | } | |||
| 473 | + | } | |||
| 474 | + | ||||
| 475 | + | // More listeners than one mapped chunk holds: all fire once, every slot comes | |||
| 476 | + | // back, and a later registration reuses a recycled slot. | |||
| 477 | + | func TestOnDestroyManyListeners(t *testing.T) { | |||
| 478 | + | d, _ := clientDisplay(t) | |||
| 479 | + | fds := socketPair(t) | |||
| 480 | + | fds2 := socketPair(t) | |||
| 481 | + | c := adopt(t, d, fds.srv) | |||
| 482 | + | const n = 3*listenerChunkSize/listenerSlot + 5 | |||
| 483 | + | var fired, chunksBefore int | |||
| 484 | + | done := make(chan struct{}) | |||
| 485 | + | var before, after int | |||
| 486 | + | d.Do(func() { | |||
| 487 | + | for i := 0; i < n; i++ { | |||
| 488 | + | c.OnDestroy(func() { | |||
| 489 | + | fired++ | |||
| 490 | + | if fired == n { | |||
| 491 | + | close(done) | |||
| 492 | + | } | |||
| 493 | + | }) | |||
| 494 | + | } | |||
| 495 | + | before = len(listeners.free) | |||
| 496 | + | chunksBefore = len(listeners.chunks) | |||
| 497 | + | }) | |||
| 498 | + | if chunksBefore < 3 { | |||
| 499 | + | t.Fatalf("test did not cross a chunk boundary: %d chunks", chunksBefore) | |||
| 500 | + | } | |||
| 501 | + | fds.closePeer() | |||
| 502 | + | select { | |||
| 503 | + | case <-done: | |||
| 504 | + | case <-time.After(3 * time.Second): | |||
| 505 | + | t.Fatal("not every listener fired") | |||
| 506 | + | } | |||
| 507 | + | var reused bool | |||
| 508 | + | var firedNow int | |||
| 509 | + | d.Do(func() { | |||
| 510 | + | firedNow = fired | |||
| 511 | + | after = len(listeners.free) | |||
| 512 | + | c2, err := d.CreateClient(fds2.srv) | |||
| 513 | + | if err != nil { | |||
| 514 | + | t.Error(err) | |||
| 515 | + | return | |||
| 516 | + | } | |||
| 517 | + | c2.OnDestroy(func() {}) | |||
| 518 | + | reused = len(listeners.chunks) == chunksBefore && len(listeners.free) == after-1 | |||
| 519 | + | }) | |||
| 520 | + | if after != before+n { | |||
| 521 | + | t.Fatalf("free slots %d -> %d, want +%d", before, after, n) | |||
| 522 | + | } | |||
| 523 | + | if firedNow != n || !reused { | |||
| 524 | + | t.Fatalf("fired %d, slot reused %v", firedNow, reused) | |||
| 525 | + | } | |||
| 526 | + | } | |||
M server/display.go +8 -2
| @@ -20,7 +20,8 @@ type Handler func(r *Resource, opcode uint32, args []Arg) | |||
| 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,10 +46,15 @@ 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. | |
| 55 | + | // | |
| 56 | + | // Resource, bind and client-listener state is shared by the whole process, so | |
| 57 | + | // only one Display may be running at a time per process. | |
| 52 | 58 | // Do wakes the loop through an eventfd so calls run at once instead of after | |
| 53 | 59 | // the dispatch timeout. | |
| 54 | 60 | type Display struct { | |
| @@ -20,7 +20,8 @@ type Handler func(r *Resource, opcode uint32, args []Arg) | |||||
| 20 | // BindFunc runs when a client binds a global. | 20 | // BindFunc runs when a client binds a global. | ||
| 21 | type BindFunc func(c Client, version, id uint32) | 21 | type BindFunc func(c Client, version, id uint32) | ||
| 22 | 22 | ||||
| 23 | - | // Client is a connected wl_client. | 23 | + | // Client is a connected wl_client. It is a comparable handle to the C object, |
| 24 | + | // usable as a map key until the client is destroyed (see Client.OnDestroy). | |||
| 24 | type Client struct{ c uintptr } | 25 | type Client struct{ c uintptr } | ||
| 25 | 26 | ||||
| 26 | // Resource is a server-side wl_resource. | 27 | // Resource is a server-side wl_resource. | ||
| @@ -45,10 +46,15 @@ var live = struct { | |||||
| 45 | resources map[uintptr]*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. | ||
| 55 | + | // | |||
| 56 | + | // Resource, bind and client-listener state is shared by the whole process, so | |||
| 57 | + | // only one Display may be running at a time per process. | |||
| 52 | // Do wakes the loop through an eventfd so calls run at once instead of after | 58 | // Do wakes the loop through an eventfd so calls run at once instead of after | ||
| 53 | // the dispatch timeout. | 59 | // the dispatch timeout. | ||
| 54 | type Display struct { | 60 | type Display struct { | ||
M server/lib.go +11 -1
| @@ -30,7 +30,8 @@ var ( | |||
| 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 | ||