v0.12.0 15e9011

display.go RawHistoryFolder

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

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