v0.7.0 f00b1b1

display.go RawHistoryFolder

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}

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