v0.5.0 ad674f5

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

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