v0.10.0 64cf1d3

display.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 14

↑↓ moveenter openesc close