1package server
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "runtime"
8 "strings"
9 "sync/atomic"
10 "unsafe"
11
12 "github.com/bnema/purego"
13 "golang.org/x/sys/unix"
14)
15
16// Handler receives every request sent to a resource. args borrows libwayland's
17// argument array and must not be retained after the handler returns.
18type Handler func(r *Resource, opcode uint32, args []Arg)
19
20// BindFunc runs when a client binds a global.
21type BindFunc func(c Client, version, id uint32)
22
23// Client is a connected wl_client.
24type Client struct{ c uintptr }
25
26// Resource is a server-side wl_resource.
27type Resource struct {
28 c uintptr
29 id uint32
30 version int32
31 client uintptr
32 iface *Interface
33 handler Handler
34 OnDestroy func()
35 gone bool
36 bare bool // created by libwayland, no destroy callback
37
38 // Data holds the owner's state, typically the generated wrapper.
39 Data any
40}
41
42// live holds Go state reachable from C callbacks. It is only touched on the
43// display goroutine, so it needs no lock.
44var live = struct {
45 resources map[uintptr]*Resource
46 binds map[uintptr]BindFunc
47 nextBind uintptr
48}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}}
49
50// Display owns a wl_display. All methods except Do must run on the goroutine
51// that calls Run, or before Run starts.
52// Do wakes the loop through an eventfd so calls run at once instead of after
53// the dispatch timeout.
54type Display struct {
55 c uintptr
56 loop uintptr
57 calls chan func()
58 wake int
59 // wakeSrc owns libwayland's duplicate of wake; only removing the source
60 // closes it, destroying the display does not.
61 wakeSrc uintptr
62 stopped chan struct{}
63 state atomic.Uint32
64}
65
66func NewDisplay() (*Display, error) {
67 if err := load(); err != nil {
68 return nil, err
69 }
70 c := wlDisplayCreate()
71 if c == 0 {
72 return nil, errors.New("purego-libwayland: wl_display_create failed")
73 }
74 wake, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
75 if err != nil {
76 wlDisplayDestroy(c)
77 return nil, fmt.Errorf("purego-libwayland: eventfd: %w", err)
78 }
79 d := &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func(), 64), wake: wake, stopped: make(chan struct{})}
80 // The callback only drains the counter; Run then executes queued calls.
81 if d.wakeSrc = wlEventLoopAddFD(d.loop, int32(wake), 1 /* WL_EVENT_READABLE */, cbWake, 0); d.wakeSrc == 0 {
82 unix.Close(wake)
83 wlDisplayDestroy(c)
84 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
85 }
86 return d, nil
87}
88
89// AddSocketFD serves clients on an already bound, listening unix socket.
90func (d *Display) AddSocketFD(fd int) error {
91 if wlDisplayAddSocketFD(d.c, int32(fd)) != 0 {
92 return fmt.Errorf("purego-libwayland: wl_display_add_socket_fd(%d) failed", fd)
93 }
94 return nil
95}
96
97func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error {
98 _, err := d.AddGlobal(iface, version, bindFn)
99 return err
100}
101
102// Global is a global that can be withdrawn, such as a wl_output on hotplug.
103type Global struct{ c uintptr }
104
105// AddGlobal advertises a global and returns a handle to remove it later.
106func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
107 live.nextBind++
108 token := live.nextBind
109 live.binds[token] = bindFn
110 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
111 if c == 0 {
112 delete(live.binds, token)
113 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
114 }
115 return &Global{c: c}, nil
116}
117
118// Remove withdraws the global: clients get wl_registry.global_remove.
119// Existing resources stay valid. A client that binds before it sees the
120// removal still gets its bind function called, so it always receives a
121// resource (the protocol allows binding a just-removed global); the bind
122// function should then send inert state. The global is freed with the
123// display. Display goroutine only; idempotent.
124func (g *Global) Remove() {
125 if g == nil || g.c == 0 {
126 return
127 }
128 wlGlobalRemove(g.c)
129 g.c = 0
130}
131
132// AckGlobalRemove implements wl_fixes.ack_global_remove: libwayland checks
133// that registry announced the removed global name, posting
134// wl_fixes.invalid_ack_remove on fixes otherwise. It reports false when
135// libwayland-server is older than 1.26 and has no support.
136// Display goroutine only.
137func AckGlobalRemove(fixes, registry *Resource, name uint32) bool {
138 if symFixesAckGlobalRemove == 0 {
139 return false
140 }
141 if fixes == nil || fixes.gone || registry == nil || registry.gone {
142 return true
143 }
144 wlFixesHandleAckGlobalRemove(fixes.c, registry.c, name)
145 return true
146}
147
148// Close destroys a display that has not started Run. It is a no-op once Run
149// has started; Run owns destruction in that case. Call before Run starts.
150func (d *Display) Close() {
151 if !d.state.CompareAndSwap(0, 1) {
152 return
153 }
154 d.destroy()
155 close(d.stopped)
156}
157
158// destroy frees the display and the wake-up fds.
159func (d *Display) destroy() {
160 wlDisplayDestroyClients(d.c)
161 wlEventSourceRemove(d.wakeSrc)
162 wlDisplayDestroy(d.c)
163 unix.Close(d.wake)
164}
165
166// Run dispatches libwayland on one OS-locked goroutine until ctx is done.
167func (d *Display) Run(ctx context.Context) error {
168 if !d.state.CompareAndSwap(0, 2) {
169 return errors.New("purego-libwayland: display already started or closed")
170 }
171 runtime.LockOSThread()
172 defer runtime.UnlockOSThread()
173 defer close(d.stopped)
174 defer d.destroy()
175 consecutiveFailures := 0
176 for {
177 if ctx.Err() != nil {
178 return nil
179 }
180 // Run every queued call; each Do also wrote the eventfd.
181 drain:
182 for {
183 select {
184 case fn := <-d.calls:
185 fn()
186 default:
187 break drain
188 }
189 }
190 wlDisplayFlushClients(d.c)
191 // The timeout only bounds how late ctx cancellation is seen.
192 if wlEventLoopDispatch(d.loop, 100) < 0 {
193 errno := lastErrno()
194 consecutiveFailures++
195 // epoll_wait is interrupted by Go's preemption signals (EINTR); a persistent failure (EBADF, EINVAL) repeats.
196 if consecutiveFailures < 16 {
197 continue
198 }
199 return fmt.Errorf("purego-libwayland: wl_event_loop_dispatch failed %d times in a row: %w", consecutiveFailures, errno)
200 }
201 consecutiveFailures = 0
202 wlDisplayFlushClients(d.c)
203 }
204}
205
206// Do runs fn on the display goroutine and waits for it. It returns false if
207// the display loop has stopped (fn was not run, or its run was not confirmed).
208// Do must not be called from the display goroutine: it would deadlock.
209func (d *Display) Do(fn func()) bool {
210 done := make(chan struct{})
211 select {
212 case d.calls <- func() { fn(); close(done) }:
213 case <-d.stopped:
214 return false
215 }
216 var one = [8]byte{1}
217 _, _ = unix.Write(d.wake, one[:])
218 select {
219 case <-done:
220 return true
221 case <-d.stopped:
222 // If both are ready, prefer confirmation of a completed call.
223 select {
224 case <-done:
225 return true
226 default:
227 return false
228 }
229 }
230}
231
232// Stopped is closed after Run returns and the display is destroyed.
233func (d *Display) Stopped() <-chan struct{} { return d.stopped }
234
235// LiveResources reports resources not yet destroyed. Display goroutine only.
236func LiveResources() int { return len(live.resources) }
237
238// Credentials are the peer credentials of a client, read from the socket
239// (SO_PEERCRED) when it connected. UID/GID are effective IDs in the server's
240// namespaces; an unmappable ID is the namespace overflow ID. PID may be zero
241// even for a valid client when its PID namespace is not visible to the server.
242type Credentials struct {
243 PID int
244 UID uint32
245 GID uint32
246}
247
248var credScratch [3]int32
249
250var errInvalidClient = errors.New("purego-libwayland: invalid client")
251
252// Credentials returns the peer credentials of the client. It returns an error
253// for an invalid (zero) Client. It does not allocate. Display goroutine only.
254func (c Client) Credentials() (Credentials, error) {
255 if c.c == 0 {
256 return Credentials{}, errInvalidClient
257 }
258 // Package-level scratch (display goroutine only) avoids heap-escaping locals.
259 purego.Syscall6(symClientGetCredentials, c.c, uintptr(unsafe.Pointer(&credScratch[0])), uintptr(unsafe.Pointer(&credScratch[1])), uintptr(unsafe.Pointer(&credScratch[2])), 0, 0)
260 return Credentials{PID: int(credScratch[0]), UID: uint32(credScratch[1]), GID: uint32(credScratch[2])}, nil
261}
262
263// PID is the process ID of the client, from Credentials. It is zero for an
264// invalid client or a peer outside the server's visible PID namespaces.
265// Display goroutine only.
266func (c Client) PID() int {
267 cr, err := c.Credentials()
268 if err != nil {
269 return 0
270 }
271 return cr.PID
272}
273
274func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) {
275 rc := wlResourceCreate(c.c, iface.c, version, id)
276 if rc == 0 {
277 return nil, fmt.Errorf("purego-libwayland: wl_resource_create(%s) failed", iface.Name)
278 }
279 // id 0 asks libwayland to allocate a server-side ID.
280 if id == 0 {
281 id = wlResourceGetID(rc)
282 }
283 r := &Resource{c: rc, id: id, version: version, client: c.c, iface: iface, handler: h}
284 live.resources[rc] = r
285 wlResourceSetDispatcher(rc, cbDispatcher, implMarker(), 0, cbDestroy)
286 return r, nil
287}
288
289func (r *Resource) Alive() bool { return !r.gone }
290func (r *Resource) ID() uint32 {
291 if r.gone {
292 return 0
293 }
294 return r.id
295}
296func (r *Resource) Version() int32 {
297 if r.gone {
298 return 0
299 }
300 return r.version
301}
302func (r *Resource) Client() Client {
303 if r.gone {
304 return Client{}
305 }
306 return Client{r.client}
307}
308func (r *Resource) Iface() *Interface { return r.iface }
309
310// Destroy frees the resource. It is a no-op once the resource is gone, so
311// generated destructor requests and handlers may both call it.
312func (r *Resource) Destroy() {
313 if !r.gone {
314 wlResourceDestroy(r.c)
315 // A bare handle gets no destroy callback to mark it gone.
316 r.gone = r.gone || r.bare
317 }
318}
319
320// eventArgs holds the wl_argument array of the event being posted.
321// libwayland copies it before wl_resource_post_event_array returns.
322// Display goroutine only; post_event_array does not call back into Go, so
323// PostEvent is not re-entrant. No protocol event has more than 8 arguments.
324var eventArgs [16]Arg
325
326// PostEvent sends an event. Arguments must match the event signature; build
327// string and array arguments with a Pinner and unpin it after this returns.
328// Calling it after destruction is safe and does nothing.
329func (r *Resource) PostEvent(opcode uint32, args ...Arg) {
330 if r.gone {
331 return
332 }
333 if len(args) > len(eventArgs) {
334 panic("purego-libwayland: too many event arguments")
335 }
336 var p unsafe.Pointer
337 if len(args) > 0 {
338 copy(eventArgs[:], args)
339 p = unsafe.Pointer(&eventArgs[0])
340 }
341 wlResourcePostEventArr(r.c, opcode, p)
342}
343
344// PostError sends a protocol error and disconnects the client.
345// Calling it after destruction is safe and does nothing.
346func (r *Resource) PostError(code uint32, msg string) {
347 if r.gone {
348 return
349 }
350 // wl_resource_post_error takes a printf format; escape it and pass no
351 // variadic arguments (purego clears AL, as SysV varargs require).
352 b := append([]byte(strings.ReplaceAll(msg, "%", "%%")), 0)
353 var pin runtime.Pinner
354 pin.Pin(&b[0])
355 defer pin.Unpin()
356 wlResourcePostError(r.c, code, uintptr(unsafe.Pointer(&b[0])))
357 runtime.KeepAlive(b)
358}
359
360// implMarker is a non-nil implementation pointer. libwayland only passes it
361// back to our dispatcher.
362func implMarker() uintptr {
363 if marker == 0 {
364 tablesMu.Lock()
365 if err := tables.reserve(8); err != nil {
366 panic(err)
367 }
368 _, marker = tables.alloc(8)
369 tablesMu.Unlock()
370 }
371 return marker
372}
373
374var marker uintptr
375
376// --- C callbacks (display goroutine) ---
377
378func dispatch(_ uintptr, target uintptr, opcode uint32, _ unsafe.Pointer, args unsafe.Pointer) int32 {
379 r := live.resources[target]
380 if r == nil || opcode >= uint32(len(r.iface.Requests)) {
381 return 0
382 }
383 m := &r.iface.Requests[opcode]
384 if r.handler == nil {
385 closeRequestFDs(m.fdPositions, args)
386 return 0
387 }
388 var a []Arg
389 if m.argc > 0 {
390 a = unsafe.Slice((*Arg)(args), m.argc)
391 }
392 r.handler(r, opcode, a)
393 return 0
394}
395
396func destroyed(res uintptr) {
397 r := live.resources[res]
398 delete(live.resources, res)
399 if r == nil {
400 return
401 }
402 r.gone = true
403 if r.OnDestroy != nil {
404 r.OnDestroy()
405 }
406}
407
408// wake drains a Display's eventfd (wl_event_loop_fd_func_t).
409func wake(fd int32, mask uint32, data uintptr) int32 {
410 var buf [8]byte
411 _, _ = unix.Read(int(fd), buf[:])
412 return 0
413}
414
415func bind(client uintptr, data uintptr, version uint32, id uint32) {
416 if fn := live.binds[data]; fn != nil {
417 fn(Client{client}, version, id)
418 }
419}
420
421// closeRequestFDs takes ownership of fd arguments when there is no handler.
422func closeRequestFDs(positions []int, args unsafe.Pointer) {
423 for _, i := range positions {
424 _ = unix.Close((*Arg)(unsafe.Add(args, i*8)).Fd())
425 }
426}
427
428// argCount counts arguments in a libwayland signature ("?oii", "2n", ...).
429func argCount(sig string) int {
430 n := 0
431 for _, ch := range sig {
432 if ch != '?' && (ch < '0' || ch > '9') {
433 n++
434 }
435 }
436 return n
437}