v0.6.0 d4696de

display.go RawHistoryFolder

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

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