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