1package neferclient_test
2
3import (
4 "context"
5 "encoding/binary"
6 "errors"
7 "io"
8 "net"
9 "os"
10 "sync"
11 "testing"
12 "time"
13
14 "github.com/bnema/go-wayland-bindings/client/wayland"
15 "github.com/bnema/neferclient"
16 "golang.org/x/sys/unix"
17)
18
19// wireServer speaks just enough of the compositor side of the Wayland wire
20// protocol over a socketpair to drive Conn: registry globals, sync callbacks,
21// wl_output binds and caller-scripted events. It is the protocol peer, not a
22// double of one of our own interfaces.
23type wireServer struct {
24 t *testing.T
25 conn *net.UnixConn
26
27 mu sync.Mutex
28 cond *sync.Cond
29 registry uint32
30 globals []wireGlobal
31 bound map[uint32]boundObject // global name -> bound object
32 outputs map[uint32]outputSpec
33 hook func(obj uint32, op uint16, body []byte) // other requests, if set
34}
35
36type wireGlobal struct {
37 name uint32
38 iface string
39 version uint32
40}
41
42type boundObject struct {
43 id uint32
44 version uint32
45}
46
47// outputSpec is what the server sends right after an output bind.
48type outputSpec struct {
49 name string
50 scale int32
51 width, height int32
52}
53
54func appendU32(b []byte, v uint32) []byte { return binary.LittleEndian.AppendUint32(b, v) }
55
56func appendStr(b []byte, s string) []byte {
57 n := len(s) + 1
58 b = appendU32(b, uint32(n))
59 b = append(b, s...)
60 b = append(b, 0)
61 for len(b)%4 != 0 {
62 b = append(b, 0)
63 }
64 return b
65}
66
67func frame(obj uint32, op uint16, body []byte) []byte {
68 b := appendU32(nil, obj)
69 b = appendU32(b, uint32(8+len(body))<<16|uint32(op))
70 return append(b, body...)
71}
72
73// socketPair returns the client end as a net.Conn and the server end.
74func socketPair(t *testing.T) (net.Conn, *net.UnixConn) {
75 t.Helper()
76 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
77 if err != nil {
78 t.Fatal(err)
79 }
80 mk := func(fd int) net.Conn {
81 f := os.NewFile(uintptr(fd), "socketpair")
82 c, err := net.FileConn(f)
83 _ = f.Close()
84 if err != nil {
85 t.Fatal(err)
86 }
87 return c
88 }
89 client, server := mk(fds[0]), mk(fds[1])
90 t.Cleanup(func() { _ = server.Close() })
91 return client, server.(*net.UnixConn)
92}
93
94// startWireServer serves a socketpair. outputs maps an announced wl_output
95// global to the description sent when it is bound.
96func startWireServer(t *testing.T, globals []wireGlobal, outputs map[uint32]outputSpec) (net.Conn, *wireServer) {
97 t.Helper()
98 client, srvConn := socketPair(t)
99 s := &wireServer{t: t, conn: srvConn, globals: globals, outputs: outputs,
100 bound: map[uint32]boundObject{}}
101 s.cond = sync.NewCond(&s.mu)
102 go s.serve()
103 return client, s
104}
105
106func (s *wireServer) write(b []byte) {
107 if _, err := s.conn.Write(b); err != nil && !isClosed(err) {
108 s.t.Errorf("wire server write: %v", err)
109 }
110}
111
112func isClosed(err error) bool {
113 return errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) || errors.Is(err, os.ErrDeadlineExceeded) ||
114 errors.Is(err, unix.EPIPE) || errors.Is(err, unix.ECONNRESET)
115}
116
117func (s *wireServer) serve() {
118 var hdr [8]byte
119 var buf []byte // reused: the peer must not allocate while draining
120 for {
121 if _, err := io.ReadFull(s.conn, hdr[:]); err != nil {
122 return
123 }
124 obj := binary.LittleEndian.Uint32(hdr[0:4])
125 w := binary.LittleEndian.Uint32(hdr[4:8])
126 size, op := int(w>>16), uint16(w)
127 if cap(buf) < size-8 {
128 buf = make([]byte, size-8)
129 }
130 body := buf[:size-8]
131 if _, err := io.ReadFull(s.conn, body); err != nil {
132 return
133 }
134 s.handle(obj, op, body)
135 }
136}
137
138func (s *wireServer) handle(obj uint32, op uint16, body []byte) {
139 s.mu.Lock()
140 reg := s.registry
141 hook := s.hook
142 s.mu.Unlock()
143 if hook != nil && obj != 1 && obj != reg {
144 hook(obj, op, body) // every request on a client-created or bound object
145 }
146 switch {
147 case obj == 1 && op == 1: // get_registry
148 id := binary.LittleEndian.Uint32(body)
149 s.mu.Lock()
150 s.registry = id
151 globals := s.globals
152 s.mu.Unlock()
153 for _, g := range globals {
154 s.announce(g)
155 }
156 case obj == 1 && op == 0: // sync
157 id := binary.LittleEndian.Uint32(body)
158 s.write(frame(id, 0, appendU32(nil, 0)))
159 s.write(frame(1, 1, appendU32(nil, id)))
160 s.mu.Lock()
161 s.cond.Broadcast()
162 s.mu.Unlock()
163 case obj == reg && op == 0: // bind
164 name := binary.LittleEndian.Uint32(body)
165 n := int(binary.LittleEndian.Uint32(body[4:]))
166 off := 8 + (n+3)&^3
167 version := binary.LittleEndian.Uint32(body[off:])
168 id := binary.LittleEndian.Uint32(body[off+4:])
169 s.mu.Lock()
170 s.bound[name] = boundObject{id: id, version: version}
171 spec, isOutput := s.outputs[name]
172 s.cond.Broadcast()
173 s.mu.Unlock()
174 if isOutput {
175 s.describe(id, version, spec)
176 }
177 case op == 0 && len(body) == 0: // wl_output.release (destructor, no args)
178 s.write(frame(1, 1, appendU32(nil, obj)))
179 }
180}
181
182func (s *wireServer) announce(g wireGlobal) {
183 b := appendU32(nil, g.name)
184 b = appendStr(b, g.iface)
185 b = appendU32(b, g.version)
186 s.mu.Lock()
187 reg := s.registry
188 s.mu.Unlock()
189 s.write(frame(reg, 0, b))
190}
191
192func (s *wireServer) remove(name uint32) {
193 s.mu.Lock()
194 reg := s.registry
195 s.mu.Unlock()
196 s.write(frame(reg, 1, appendU32(nil, name)))
197}
198
199// describe sends mode, scale, name (version >= 4) and done for an output.
200func (s *wireServer) describe(id, version uint32, o outputSpec) {
201 mode := appendU32(nil, 3) // current|preferred
202 mode = appendU32(mode, uint32(o.width))
203 mode = appendU32(mode, uint32(o.height))
204 mode = appendU32(mode, 60000)
205 s.write(frame(id, 1, mode))
206 s.write(frame(id, 3, appendU32(nil, uint32(o.scale))))
207 if version >= 4 {
208 s.write(frame(id, 4, appendStr(nil, o.name)))
209 }
210 s.write(frame(id, 2, nil))
211}
212
213// waitBound waits until the client bound global and returns the object.
214func (s *wireServer) waitBound(global uint32) boundObject {
215 s.t.Helper()
216 deadline := time.AfterFunc(5*time.Second, func() {
217 s.mu.Lock()
218 s.cond.Broadcast()
219 s.mu.Unlock()
220 })
221 defer deadline.Stop()
222 start := time.Now()
223 s.mu.Lock()
224 defer s.mu.Unlock()
225 for {
226 if b, ok := s.bound[global]; ok {
227 return b
228 }
229 if time.Since(start) > 5*time.Second {
230 s.t.Fatalf("global %d never bound", global)
231 }
232 s.cond.Wait()
233 }
234}
235
236func (s *wireServer) closeConn() { _ = s.conn.Close() }
237
238// connectWire connects a Conn to the wire server.
239func connectWire(t *testing.T, globals []wireGlobal, outputs map[uint32]outputSpec) (*neferclient.Conn, *wireServer) {
240 t.Helper()
241 client, srv := startWireServer(t, globals, outputs)
242 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
243 t.Cleanup(cancel)
244 c, err := neferclient.ConnectConn(ctx, client)
245 if err != nil {
246 t.Fatalf("connect: %v", err)
247 }
248 t.Cleanup(func() { _ = c.Close() })
249 return c, srv
250}
251
252const outputIface = wayland.OutputInterface