v0.8.0 5f793eb

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. 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}

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