1package server
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "runtime"
8 "strings"
9 "sync"
10 "sync/atomic"
11 "unsafe"
12
13 "github.com/bnema/purego"
14 "golang.org/x/sys/unix"
15)
16
17// Handler receives every request sent to a resource. args borrows libwayland's
18// argument array and must not be retained after the handler returns.
19type Handler func(r *Resource, opcode uint32, args []Arg)
20
21// BindFunc runs when a client binds a global.
22type BindFunc func(c Client, version, id uint32)
23
24// Client is a connected wl_client. It is a comparable handle to the C object,
25// usable as a map key until the client is destroyed (see Client.OnDestroy).
26type Client struct{ c uintptr }
27
28// Resource is a server-side wl_resource.
29type Resource struct {
30 c uintptr
31 id uint32
32 version int32
33 client uintptr
34 iface *Interface
35 handler Handler
36 OnDestroy func()
37 gone bool
38 bare bool // created by libwayland, no destroy callback
39
40 // Data holds the owner's state, typically the generated wrapper.
41 Data any
42}
43
44// live holds Go state reachable from C callbacks. It is only touched on the
45// display goroutine, so it needs no lock.
46var live = struct {
47 resources map[uintptr]*Resource
48 binds map[uintptr]BindFunc
49 nextBind uintptr
50 // clientGone maps wl_listener addresses to OnDestroy functions.
51 clientGone map[uintptr]func()
52 // fds maps AddFD tokens to their readable callbacks.
53 fds map[uintptr]func()
54 nextFD uintptr
55}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}, fds: map[uintptr]func(){}}
56
57// Display owns a wl_display. All methods except Do and Post must run on the
58// goroutine that calls Run, or before Run starts.
59//
60// Resource, bind and client-listener state is shared by the whole process, so
61// only one Display may be running at a time per process.
62//
63// Do and Post wake the loop through an eventfd so calls run at once instead of
64// after the dispatch timeout.
65type Display struct {
66 c uintptr
67 loop uintptr
68 calls chan func()
69 wake int
70 // wakeSrc owns libwayland's duplicate of wake; only removing the source
71 // closes it, destroying the display does not.
72 wakeSrc uintptr
73 stopped chan struct{}
74 state atomic.Uint32
75
76 // postMu guards posts and closed. spare is only used by the display
77 // goroutine: it is the emptied batch swapped back in on the next drain, so
78 // steady-state Post does not allocate.
79 postMu sync.Mutex
80 posts []func()
81 spare []func()
82 // armed is true while an eventfd wake for posts is pending, so a burst of
83 // Posts writes the eventfd once.
84 armed atomic.Bool
85 closed atomic.Bool // set under postMu before the wake fd is closed
86 // wakeWrites counts eventfd writes made for posts (diagnostics and tests).
87 wakeWrites atomic.Uint64
88
89 // sources are the AddFD sources not yet removed (display goroutine only).
90 sources map[*Source]struct{}
91
92 // dispatchMS bounds one event loop wait; it only bounds how late ctx
93 // cancellation is seen. Tests raise it so a lost wake hangs.
94 dispatchMS int32
95}
96
97// maxDrainRounds bounds how many times one loop iteration re-drains calls and
98// posts that were queued by callbacks, so a self-posting callback cannot starve
99// libwayland's dispatch and client flushing.
100const maxDrainRounds = 8
101
102func NewDisplay() (*Display, error) {
103 if err := load(); err != nil {
104 return nil, err
105 }
106 c := wlDisplayCreate()
107 if c == 0 {
108 return nil, errors.New("purego-libwayland: wl_display_create failed")
109 }
110 wake, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
111 if err != nil {
112 wlDisplayDestroy(c)
113 return nil, fmt.Errorf("purego-libwayland: eventfd: %w", err)
114 }
115 d := &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func(), 64), wake: wake, stopped: make(chan struct{}), sources: map[*Source]struct{}{}, dispatchMS: 100}
116 // The callback only drains the counter; Run then executes queued calls.
117 if d.wakeSrc = wlEventLoopAddFD(d.loop, int32(wake), 1 /* WL_EVENT_READABLE */, cbWake, 0); d.wakeSrc == 0 {
118 unix.Close(wake)
119 wlDisplayDestroy(c)
120 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
121 }
122 return d, nil
123}
124
125// AddSocketFD serves clients on an already bound, listening unix socket.
126func (d *Display) AddSocketFD(fd int) error {
127 if wlDisplayAddSocketFD(d.c, int32(fd)) != 0 {
128 return fmt.Errorf("purego-libwayland: wl_display_add_socket_fd(%d) failed", fd)
129 }
130 return nil
131}
132
133func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error {
134 _, err := d.AddGlobal(iface, version, bindFn)
135 return err
136}
137
138// Global is a global that can be withdrawn, such as a wl_output on hotplug.
139type Global struct{ c uintptr }
140
141// AddGlobal advertises a global and returns a handle to remove it later.
142func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
143 live.nextBind++
144 token := live.nextBind
145 live.binds[token] = bindFn
146 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
147 if c == 0 {
148 delete(live.binds, token)
149 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
150 }
151 return &Global{c: c}, nil
152}
153
154// Remove withdraws the global: clients get wl_registry.global_remove.
155// Existing resources stay valid. A client that binds before it sees the
156// removal still gets its bind function called, so it always receives a
157// resource (the protocol allows binding a just-removed global); the bind
158// function should then send inert state. The global is freed with the
159// display. Display goroutine only; idempotent.
160func (g *Global) Remove() {
161 if g == nil || g.c == 0 {
162 return
163 }
164 wlGlobalRemove(g.c)
165 g.c = 0
166}
167
168// AckGlobalRemove implements wl_fixes.ack_global_remove: libwayland checks
169// that registry announced the removed global name, posting
170// wl_fixes.invalid_ack_remove on fixes otherwise. It reports false when
171// libwayland-server is older than 1.26 and has no support.
172// Display goroutine only.
173func AckGlobalRemove(fixes, registry *Resource, name uint32) bool {
174 if symFixesAckGlobalRemove == 0 {
175 return false
176 }
177 if fixes == nil || fixes.gone || registry == nil || registry.gone {
178 return true
179 }
180 wlFixesHandleAckGlobalRemove(fixes.c, registry.c, name)
181 return true
182}
183
184// Close destroys a display that has not started Run. It is a no-op once Run
185// has started; Run owns destruction in that case. Call before Run starts.
186func (d *Display) Close() {
187 if !d.state.CompareAndSwap(0, 1) {
188 return
189 }
190 d.destroy()
191 close(d.stopped)
192}
193
194// destroy frees the display and the wake-up fds.
195func (d *Display) destroy() {
196 // Stop accepting posts before the wake fd is closed (its number could be
197 // reused), and drop what never ran.
198 d.postMu.Lock()
199 d.closed.Store(true)
200 clear(d.posts)
201 d.posts, d.spare = nil, nil
202 d.postMu.Unlock()
203 for s := range d.sources {
204 s.remove()
205 }
206 clear(d.sources)
207 wlDisplayDestroyClients(d.c)
208 wlEventSourceRemove(d.wakeSrc)
209 wlDisplayDestroy(d.c)
210 unix.Close(d.wake)
211}
212
213// Run dispatches libwayland on one OS-locked goroutine until ctx is done.
214func (d *Display) Run(ctx context.Context) error {
215 if !d.state.CompareAndSwap(0, 2) {
216 return errors.New("purego-libwayland: display already started or closed")
217 }
218 runtime.LockOSThread()
219 defer runtime.UnlockOSThread()
220 defer close(d.stopped)
221 defer d.destroy()
222 consecutiveFailures := 0
223 for {
224 if ctx.Err() != nil {
225 return nil
226 }
227 // Run every queued post and call. Wakes are coalesced, so the eventfd
228 // is not written once per item; it only makes the dispatch below
229 // return at once when work arrives while the loop is asleep.
230 d.drain()
231 wlDisplayFlushClients(d.c)
232 // The timeout only bounds how late ctx cancellation is seen.
233 if wlEventLoopDispatch(d.loop, d.dispatchMS) < 0 {
234 errno := lastErrno()
235 consecutiveFailures++
236 // epoll_wait is interrupted by Go's preemption signals (EINTR); a persistent failure (EBADF, EINVAL) repeats.
237 if consecutiveFailures < 16 {
238 continue
239 }
240 return fmt.Errorf("purego-libwayland: wl_event_loop_dispatch failed %d times in a row: %w", consecutiveFailures, errno)
241 }
242 consecutiveFailures = 0
243 wlDisplayFlushClients(d.c)
244 }
245}
246
247// drain runs queued posts and Do calls until none are left, at most
248// maxDrainRounds rounds. Posts are taken again after each call is received and
249// before it runs, so a Post that happened before a Do always runs before that
250// Do's function. When the cap is hit the loop is woken again so the next
251// dispatch returns at once.
252func (d *Display) drain() {
253 for range maxDrainRounds {
254 ran := d.runPosts()
255 calls:
256 for {
257 select {
258 case fn := <-d.calls:
259 d.runPosts()
260 fn()
261 ran = true
262 default:
263 break calls
264 }
265 }
266 if !ran {
267 return
268 }
269 }
270 d.armed.Store(true)
271 d.wakeLoop()
272}
273
274// runPosts runs the posts queued so far and reports whether there were any.
275func (d *Display) runPosts() bool {
276 // Clear armed before taking the batch: a Post that lands after the swap
277 // sees armed false and writes the eventfd again.
278 d.armed.Store(false)
279 d.postMu.Lock()
280 batch := d.posts
281 d.posts, d.spare = d.spare, nil
282 d.postMu.Unlock()
283 for _, fn := range batch {
284 fn()
285 }
286 clear(batch)
287 d.spare = batch[:0]
288 return len(batch) > 0
289}
290
291// wakeOne is the eventfd increment. It is read-only, so sharing it between
292// goroutines is safe and avoids a per-call buffer (which escapes under -race).
293var wakeOne = [8]byte{1}
294
295// wakeLoop writes the eventfd on behalf of posts. The caller must hold postMu
296// (or be the display goroutine, which destroy cannot overlap) so the fd cannot
297// be closed, and its number reused, underneath the write.
298func (d *Display) wakeLoop() {
299 d.wakeWrites.Add(1)
300 _, _ = unix.Write(d.wake, wakeOne[:])
301}
302
303// wakeGuarded wakes the loop from any goroutine: it takes postMu and skips the
304// write once the display is closed, so a late caller never writes to a reused
305// fd number.
306func (d *Display) wakeGuarded() {
307 d.postMu.Lock()
308 defer d.postMu.Unlock()
309 if d.closed.Load() {
310 return
311 }
312 _, _ = unix.Write(d.wake, wakeOne[:])
313}
314
315// Post queues fn to run on the display goroutine and returns at once. Unlike
316// Do it never waits, and unlike Do it may be called from any goroutine,
317// including the display goroutine itself.
318//
319// Posts run in the order they were made, and a Post made before a Do (by the
320// same goroutine, or otherwise known to happen before it) runs before that Do's
321// function. A Post made by a posted function or a Do function runs in a later
322// drain round of the same loop iteration (at most 8 rounds; past that the loop
323// is woken again and continues after one libwayland dispatch). Wakes are coalesced:
324// any number of Posts while one wake is pending write the eventfd once.
325//
326// fn must not block: it runs on the goroutine that serves every client. After
327// the display has stopped or been closed, Post silently drops fn, and posts
328// that had not run when the display stopped are dropped too. In steady state
329// Post does not allocate.
330//
331// The queue is unbounded: Post applies no backpressure, so a producer that
332// outpaces the display goroutine grows memory without limit. Callers that can
333// produce faster than the loop runs must bound or coalesce their own posts.
334func (d *Display) Post(fn func()) {
335 if fn == nil {
336 return
337 }
338 d.postMu.Lock()
339 defer d.postMu.Unlock()
340 if d.closed.Load() {
341 return
342 }
343 d.posts = append(d.posts, fn)
344 // Written under postMu so destroy cannot close the fd underneath.
345 if d.armed.CompareAndSwap(false, true) {
346 d.wakeLoop()
347 }
348}
349
350// Source is an fd registered with the display's event loop by AddFD.
351type Source struct {
352 d *Display
353 c uintptr
354 token uintptr
355}
356
357// AddFD calls onReadable on the display goroutine whenever fd is readable. The
358// registration is level-triggered: onReadable must consume the readiness (read
359// the eventfd or timerfd, accept, ...) or it is called again at once. It
360// should not block.
361//
362// onReadable is also called when the fd hangs up or errors (EPOLLHUP,
363// EPOLLERR), which level-triggered polling keeps reporting. The callback should
364// read the fd, and on EOF or error call Remove (it may do so from within the
365// callback), or it is called again at once.
366//
367// libwayland duplicates fd; the caller still owns and must close its own
368// descriptor, and should do so after Remove. Call AddFD only on the display
369// goroutine or before Run starts. Sources not removed are released when the
370// display is destroyed.
371func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {
372 if fd < 0 {
373 return nil, fmt.Errorf("purego-libwayland: AddFD: invalid fd %d", fd)
374 }
375 if onReadable == nil {
376 return nil, errors.New("purego-libwayland: AddFD: nil callback")
377 }
378 if d.closed.Load() {
379 return nil, errors.New("purego-libwayland: AddFD: display closed")
380 }
381 live.nextFD++
382 token := live.nextFD
383 live.fds[token] = onReadable
384 c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token)
385 if c == 0 {
386 delete(live.fds, token)
387 return nil, fmt.Errorf("purego-libwayland: wl_event_loop_add_fd(%d) failed", fd)
388 }
389 s := &Source{d: d, c: c, token: token}
390 d.sources[s] = struct{}{}
391 return s, nil
392}
393
394// Remove stops the callbacks and releases libwayland's duplicate of the fd.
395// It may be called from the source's own callback. Display goroutine only;
396// idempotent, and a no-op after the display was destroyed.
397func (s *Source) Remove() {
398 if s == nil || s.c == 0 {
399 return
400 }
401 delete(s.d.sources, s)
402 s.remove()
403}
404
405func (s *Source) remove() {
406 delete(live.fds, s.token)
407 wlEventSourceRemove(s.c)
408 s.c = 0
409}
410
411// Do runs fn on the display goroutine and waits for it. It returns false if
412// the display loop has stopped (fn was not run, or its run was not confirmed).
413// Do must not be called from the display goroutine: it would deadlock.
414func (d *Display) Do(fn func()) bool {
415 done := make(chan struct{})
416 select {
417 case d.calls <- func() { fn(); close(done) }:
418 case <-d.stopped:
419 return false
420 }
421 d.wakeGuarded()
422 select {
423 case <-done:
424 return true
425 case <-d.stopped:
426 // If both are ready, prefer confirmation of a completed call.
427 select {
428 case <-done:
429 return true
430 default:
431 return false
432 }
433 }
434}
435
436// Stopped is closed after Run returns and the display is destroyed.
437func (d *Display) Stopped() <-chan struct{} { return d.stopped }
438
439// LiveResources reports resources not yet destroyed. Display goroutine only.
440func LiveResources() int { return len(live.resources) }
441
442// Credentials are the peer credentials of a client, read from the socket
443// (SO_PEERCRED) when it connected. UID/GID are effective IDs in the server's
444// namespaces; an unmappable ID is the namespace overflow ID. PID may be zero
445// even for a valid client when its PID namespace is not visible to the server.
446type Credentials struct {
447 PID int
448 UID uint32
449 GID uint32
450}
451
452var credScratch [3]int32
453
454var errInvalidClient = errors.New("purego-libwayland: invalid client")
455
456// Credentials returns the peer credentials of the client. It returns an error
457// for an invalid (zero) Client. It does not allocate. Display goroutine only.
458func (c Client) Credentials() (Credentials, error) {
459 if c.c == 0 {
460 return Credentials{}, errInvalidClient
461 }
462 // Package-level scratch (display goroutine only) avoids heap-escaping locals.
463 purego.Syscall6(symClientGetCredentials, c.c, uintptr(unsafe.Pointer(&credScratch[0])), uintptr(unsafe.Pointer(&credScratch[1])), uintptr(unsafe.Pointer(&credScratch[2])), 0, 0)
464 return Credentials{PID: int(credScratch[0]), UID: uint32(credScratch[1]), GID: uint32(credScratch[2])}, nil
465}
466
467// PID is the process ID of the client, from Credentials. It is zero for an
468// invalid client or a peer outside the server's visible PID namespaces.
469// Display goroutine only.
470func (c Client) PID() int {
471 cr, err := c.Credentials()
472 if err != nil {
473 return 0
474 }
475 return cr.PID
476}
477
478func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) {
479 rc := wlResourceCreate(c.c, iface.c, version, id)
480 if rc == 0 {
481 return nil, fmt.Errorf("purego-libwayland: wl_resource_create(%s) failed", iface.Name)
482 }
483 // id 0 asks libwayland to allocate a server-side ID.
484 if id == 0 {
485 id = wlResourceGetID(rc)
486 }
487 r := &Resource{c: rc, id: id, version: version, client: c.c, iface: iface, handler: h}
488 live.resources[rc] = r
489 wlResourceSetDispatcher(rc, cbDispatcher, implMarker(), 0, cbDestroy)
490 return r, nil
491}
492
493func (r *Resource) Alive() bool { return !r.gone }
494func (r *Resource) ID() uint32 {
495 if r.gone {
496 return 0
497 }
498 return r.id
499}
500func (r *Resource) Version() int32 {
501 if r.gone {
502 return 0
503 }
504 return r.version
505}
506func (r *Resource) Client() Client {
507 if r.gone {
508 return Client{}
509 }
510 return Client{r.client}
511}
512func (r *Resource) Iface() *Interface { return r.iface }
513
514// Destroy frees the resource. It is a no-op once the resource is gone, so
515// generated destructor requests and handlers may both call it.
516func (r *Resource) Destroy() {
517 if !r.gone {
518 wlResourceDestroy(r.c)
519 // A bare handle gets no destroy callback to mark it gone.
520 r.gone = r.gone || r.bare
521 }
522}
523
524// eventArgs holds the wl_argument array of the event being posted.
525// libwayland copies it before wl_resource_post_event_array returns.
526// Display goroutine only; post_event_array does not call back into Go, so
527// PostEvent is not re-entrant. No protocol event has more than 8 arguments.
528var eventArgs [16]Arg
529
530// PostEvent sends an event. Arguments must match the event signature; build
531// string and array arguments with a Pinner and unpin it after this returns.
532// Calling it after destruction is safe and does nothing.
533func (r *Resource) PostEvent(opcode uint32, args ...Arg) {
534 if r.gone {
535 return
536 }
537 if len(args) > len(eventArgs) {
538 panic("purego-libwayland: too many event arguments")
539 }
540 var p unsafe.Pointer
541 if len(args) > 0 {
542 copy(eventArgs[:], args)
543 p = unsafe.Pointer(&eventArgs[0])
544 }
545 wlResourcePostEventArr(r.c, opcode, p)
546}
547
548// PostError sends a protocol error and disconnects the client.
549// Calling it after destruction is safe and does nothing.
550func (r *Resource) PostError(code uint32, msg string) {
551 if r.gone {
552 return
553 }
554 // wl_resource_post_error takes a printf format; escape it and pass no
555 // variadic arguments (purego clears AL, as SysV varargs require).
556 b := append([]byte(strings.ReplaceAll(msg, "%", "%%")), 0)
557 var pin runtime.Pinner
558 pin.Pin(&b[0])
559 defer pin.Unpin()
560 wlResourcePostError(r.c, code, uintptr(unsafe.Pointer(&b[0])))
561 runtime.KeepAlive(b)
562}
563
564// implMarker is a non-nil implementation pointer. libwayland only passes it
565// back to our dispatcher.
566func implMarker() uintptr {
567 if marker == 0 {
568 tablesMu.Lock()
569 if err := tables.reserve(8); err != nil {
570 panic(err)
571 }
572 _, marker = tables.alloc(8)
573 tablesMu.Unlock()
574 }
575 return marker
576}
577
578var marker uintptr
579
580// --- C callbacks (display goroutine) ---
581
582func dispatch(_ uintptr, target uintptr, opcode uint32, _ unsafe.Pointer, args unsafe.Pointer) int32 {
583 r := live.resources[target]
584 if r == nil || opcode >= uint32(len(r.iface.Requests)) {
585 return 0
586 }
587 m := &r.iface.Requests[opcode]
588 if r.handler == nil {
589 closeRequestFDs(m.fdPositions, args)
590 return 0
591 }
592 var a []Arg
593 if m.argc > 0 {
594 a = unsafe.Slice((*Arg)(args), m.argc)
595 }
596 r.handler(r, opcode, a)
597 return 0
598}
599
600func destroyed(res uintptr) {
601 r := live.resources[res]
602 delete(live.resources, res)
603 if r == nil {
604 return
605 }
606 r.gone = true
607 if r.OnDestroy != nil {
608 r.OnDestroy()
609 }
610}
611
612// wake drains a Display's eventfd (wl_event_loop_fd_func_t).
613func wake(fd int32, mask uint32, data uintptr) int32 {
614 var buf [8]byte
615 _, _ = unix.Read(int(fd), buf[:])
616 return 0
617}
618
619// fdReadable dispatches an AddFD source (wl_event_loop_fd_func_t).
620func fdReadable(_ int32, _ uint32, data uintptr) int32 {
621 if fn := live.fds[data]; fn != nil {
622 fn()
623 }
624 return 0
625}
626
627func bind(client uintptr, data uintptr, version uint32, id uint32) {
628 if fn := live.binds[data]; fn != nil {
629 fn(Client{client}, version, id)
630 }
631}
632
633// closeRequestFDs takes ownership of fd arguments when there is no handler.
634func closeRequestFDs(positions []int, args unsafe.Pointer) {
635 for _, i := range positions {
636 _ = unix.Close((*Arg)(unsafe.Add(args, i*8)).Fd())
637 }
638}
639
640// argCount counts arguments in a libwayland signature ("?oii", "2n", ...).
641func argCount(sig string) int {
642 n := 0
643 for _, ch := range sig {
644 if ch != '?' && (ch < '0' || ch > '9') {
645 n++
646 }
647 }
648 return n
649}