detached 48014c1

Merge 15e9011315e8e7cae218026acb51a820fc858efc into f5b8db49413730383c33654512df6fac6fba210e

commit
48014c1872a767f12eb45b9d1316f58770ce7aa0
author
ermite · · 2026-10-22 10:23 UTC
merge of
f5b8db4 + 15e9011 · diff against the first parent
signature
unsigned
6 files · +536 -24
M README.md +22 -0
@@ -20,6 +20,28 @@ CGO_ENABLED=0 go build ./...
2020
2121 Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https://github.com/bnema/go-wayland-bindings), one package per protocol under `server/<pkg>`, for example `server/xdgshell` and `server/wlrlayershell`. They build on this runtime's `server` package.
2222
23+## Display integration
24+
25+`Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see
26+and bind each global; passing `nil` removes the filter. Set it before clients
27+receive restricted globals. Global handles are stable, including during creation;
28+filters must not create globals or block. Filter by `g.Interface()` (for example,
29+`g.Interface() != "xwayland_shell_v1" || c == xwaylandClient`), not a captured
30+handle assigned by `AddGlobal`: creation can invoke the filter before that
31+assignment. `Interface()` and `Version()` expose stored metadata without
32+allocation, including during creation.
33+
34+`Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with
35+writable interest initially disabled. `Source.SetWritable(true)` arms writable
36+callbacks after `EAGAIN`; `SetWritable(false)` disarms them when output drains.
37+Both return errors on registration/update failures. Readable interest remains
38+active. Callbacks run on the display goroutine with no dispatch allocations.
39+`AddFD` remains the readable-only API. Hangup/error readiness calls onReadable;
40+remove the source on EOF/error. The caller owns the original fd.
41+
42+These APIs, like other libwayland operations, belong on the display's OS-locked
43+goroutine (use `Do` or `Post` from other goroutines).
44+
2345 ## License
2446
2547 MIT, see `LICENSE`.
@@ -20,6 +20,28 @@ CGO_ENABLED=0 go build ./...
20 20
21 Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https://github.com/bnema/go-wayland-bindings), one package per protocol under `server/<pkg>`, for example `server/xdgshell` and `server/wlrlayershell`. They build on this runtime's `server` package.21 Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https://github.com/bnema/go-wayland-bindings), one package per protocol under `server/<pkg>`, for example `server/xdgshell` and `server/wlrlayershell`. They build on this runtime's `server` package.
22 22
23+## Display integration
24+
25+`Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see
26+and bind each global; passing `nil` removes the filter. Set it before clients
27+receive restricted globals. Global handles are stable, including during creation;
28+filters must not create globals or block. Filter by `g.Interface()` (for example,
29+`g.Interface() != "xwayland_shell_v1" || c == xwaylandClient`), not a captured
30+handle assigned by `AddGlobal`: creation can invoke the filter before that
31+assignment. `Interface()` and `Version()` expose stored metadata without
32+allocation, including during creation.
33+
34+`Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with
35+writable interest initially disabled. `Source.SetWritable(true)` arms writable
36+callbacks after `EAGAIN`; `SetWritable(false)` disarms them when output drains.
37+Both return errors on registration/update failures. Readable interest remains
38+active. Callbacks run on the display goroutine with no dispatch allocations.
39+`AddFD` remains the readable-only API. Hangup/error readiness calls onReadable;
40+remove the source on EOF/error. The caller owns the original fd.
41+
42+These APIs, like other libwayland operations, belong on the display's OS-locked
43+goroutine (use `Do` or `Post` from other goroutines).
44+
23 ## License45 ## License
24 46
25 MIT, see `LICENSE`.47 MIT, see `LICENSE`.
M go.mod +1 -1
@@ -3,6 +3,6 @@ module github.com/bnema/purego-libwayland
33 go 1.27
44
55 require (
6- github.com/bnema/purego v0.13.0-bnema.1
6+ github.com/bnema/purego v0.13.0-bnema.2
77 golang.org/x/sys v0.48.0
88 )
@@ -3,6 +3,6 @@ module github.com/bnema/purego-libwayland
3 go 1.273 go 1.27
4 4
5 require (5 require (
6- github.com/bnema/purego v0.13.0-bnema.16+ github.com/bnema/purego v0.13.0-bnema.2
7 golang.org/x/sys v0.48.07 golang.org/x/sys v0.48.0
8 )8 )
M go.sum +2 -2 generated file: collapsed
M server/display.go +109 -15
@@ -50,10 +50,12 @@ var live = struct {
5050 nextBind uintptr
5151 // clientGone maps wl_listener addresses to OnDestroy functions.
5252 clientGone map[uintptr]func()
53- // fds maps AddFD tokens to their readable callbacks.
54- fds map[uintptr]func()
55- nextFD uintptr
56-}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}, fds: 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{}}
5759
5860 // Display owns a wl_display. All methods except Do and Post must run on the
5961 // goroutine that calls Run, or before Run starts.
@@ -88,7 +90,9 @@ type Display struct {
8890 wakeWrites atomic.Uint64
8991
9092 // sources are the AddFD sources not yet removed (display goroutine only).
91- sources map[*Source]struct{}
93+ sources map[*Source]struct{}
94+ filter func(Client, *Global) bool
95+ pendingGlobal *Global
9296 }
9397
9498 // maxDrainRounds bounds how many times one loop iteration re-drains calls and
@@ -116,6 +120,7 @@ func NewDisplay() (*Display, error) {
116120 wlDisplayDestroy(c)
117121 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
118122 }
123+ live.displays[c] = d
119124 return d, nil
120125 }
121126
@@ -133,19 +138,65 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc)
133138 }
134139
135140 // Global is a global that can be withdrawn, such as a wl_output on hotplug.
136-type Global struct{ c uintptr }
141+type 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.
150+func (g *Global) Interface() string { return g.interfaceName }
151+
152+// Version returns the advertised version, including during AddGlobal filtering.
153+func (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.
159+func (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+
168+func 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+}
137183
138184 // AddGlobal advertises a global and returns a handle to remove it later.
139185 func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
140186 live.nextBind++
141187 token := live.nextBind
142188 live.binds[token] = bindFn
189+ g := &Global{d: d, interfaceName: iface.Name, version: version}
190+ d.pendingGlobal = g
143191 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
192+ d.pendingGlobal = nil
144193 if c == 0 {
145194 delete(live.binds, token)
146195 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
147196 }
148- return &Global{c: c}, nil
197+ g.c = c
198+ live.globals[c] = g
199+ return g, nil
149200 }
150201
151202 // Remove withdraws the global: clients get wl_registry.global_remove.
@@ -204,6 +255,14 @@ func (d *Display) destroy() {
204255 wlDisplayDestroyClients(d.c)
205256 wlEventSourceRemove(d.wakeSrc)
206257 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
207266 unix.Close(d.wake)
208267 }
209268
@@ -390,9 +449,10 @@ func (d *Display) Post(fn func()) {
390449
391450 // Source is an fd registered with the display's event loop by AddFD.
392451 type Source struct {
393- d *Display
394- c uintptr
395- token uintptr
452+ d *Display
453+ c uintptr
454+ token uintptr
455+ onReadable, onWritable func()
396456 }
397457
398458 // AddFD calls onReadable on the display goroutine whenever fd is readable. The
@@ -410,6 +470,14 @@ type Source struct {
410470 // goroutine or before Run starts. Sources not removed are released when the
411471 // display is destroyed.
412472 func (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.
480+func (d *Display) AddFDRW(fd int, onReadable, onWritable func()) (*Source, error) {
413481 if fd < 0 {
414482 return nil, fmt.Errorf("purego-libwayland: AddFD: invalid fd %d", fd)
415483 }
@@ -421,17 +489,38 @@ func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {
421489 }
422490 live.nextFD++
423491 token := live.nextFD
424- live.fds[token] = onReadable
492+ s := &Source{d: d, token: token, onReadable: onReadable, onWritable: onWritable}
493+ live.fds[token] = s
425494 c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token)
426495 if c == 0 {
427496 delete(live.fds, token)
428497 return nil, fmt.Errorf("purego-libwayland: wl_event_loop_add_fd(%d) failed", fd)
429498 }
430- s := &Source{d: d, c: c, token: token}
499+ s.c = c
431500 d.sources[s] = struct{}{}
432501 return s, nil
433502 }
434503
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.
507+func (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+
435524 // Remove stops the callbacks and releases libwayland's duplicate of the fd.
436525 // It may be called from the source's own callback. Display goroutine only;
437526 // idempotent, and a no-op after the display was destroyed.
@@ -658,9 +747,14 @@ func wake(fd int32, mask uint32, data uintptr) int32 {
658747 }
659748
660749 // fdReadable dispatches an AddFD source (wl_event_loop_fd_func_t).
661-func fdReadable(_ int32, _ uint32, data uintptr) int32 {
662- if fn := live.fds[data]; fn != nil {
663- fn()
750+func 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+ }
664758 }
665759 return 0
666760 }
@@ -50,10 +50,12 @@ var live = struct {
50 nextBind uintptr50 nextBind uintptr
51 // clientGone maps wl_listener addresses to OnDestroy functions.51 // clientGone maps wl_listener addresses to OnDestroy functions.
52 clientGone map[uintptr]func()52 clientGone map[uintptr]func()
53- // fds maps AddFD tokens to their readable callbacks.53+ // fds maps AddFD/AddFDRW tokens to their sources.
54- fds map[uintptr]func()54+ fds map[uintptr]*Source
55- nextFD uintptr55+ nextFD uintptr
56-}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}, fds: map[uintptr]func(){}}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{}}
57 59
58 // Display owns a wl_display. All methods except Do and Post must run on the60 // Display owns a wl_display. All methods except Do and Post must run on the
59 // goroutine that calls Run, or before Run starts.61 // goroutine that calls Run, or before Run starts.
@@ -88,7 +90,9 @@ type Display struct {
88 wakeWrites atomic.Uint6490 wakeWrites atomic.Uint64
89 91
90 // sources are the AddFD sources not yet removed (display goroutine only).92 // sources are the AddFD sources not yet removed (display goroutine only).
91- sources map[*Source]struct{}93+ sources map[*Source]struct{}
94+ filter func(Client, *Global) bool
95+ pendingGlobal *Global
92 }96 }
93 97
94 // maxDrainRounds bounds how many times one loop iteration re-drains calls and98 // maxDrainRounds bounds how many times one loop iteration re-drains calls and
@@ -116,6 +120,7 @@ func NewDisplay() (*Display, error) {
116 wlDisplayDestroy(c)120 wlDisplayDestroy(c)
117 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")121 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
118 }122 }
123+ live.displays[c] = d
119 return d, nil124 return d, nil
120 }125 }
121 126
@@ -133,19 +138,65 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc)
133 }138 }
134 139
135 // Global is a global that can be withdrawn, such as a wl_output on hotplug.140 // Global is a global that can be withdrawn, such as a wl_output on hotplug.
136-type Global struct{ c uintptr }141+type 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.
150+func (g *Global) Interface() string { return g.interfaceName }
151+
152+// Version returns the advertised version, including during AddGlobal filtering.
153+func (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.
159+func (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+
168+func 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+}
137 183
138 // AddGlobal advertises a global and returns a handle to remove it later.184 // AddGlobal advertises a global and returns a handle to remove it later.
139 func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {185 func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
140 live.nextBind++186 live.nextBind++
141 token := live.nextBind187 token := live.nextBind
142 live.binds[token] = bindFn188 live.binds[token] = bindFn
189+ g := &Global{d: d, interfaceName: iface.Name, version: version}
190+ d.pendingGlobal = g
143 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)191 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
192+ d.pendingGlobal = nil
144 if c == 0 {193 if c == 0 {
145 delete(live.binds, token)194 delete(live.binds, token)
146 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)195 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
147 }196 }
148- return &Global{c: c}, nil197+ g.c = c
198+ live.globals[c] = g
199+ return g, nil
149 }200 }
150 201
151 // Remove withdraws the global: clients get wl_registry.global_remove.202 // Remove withdraws the global: clients get wl_registry.global_remove.
@@ -204,6 +255,14 @@ func (d *Display) destroy() {
204 wlDisplayDestroyClients(d.c)255 wlDisplayDestroyClients(d.c)
205 wlEventSourceRemove(d.wakeSrc)256 wlEventSourceRemove(d.wakeSrc)
206 wlDisplayDestroy(d.c)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
207 unix.Close(d.wake)266 unix.Close(d.wake)
208 }267 }
209 268
@@ -390,9 +449,10 @@ func (d *Display) Post(fn func()) {
390 449
391 // Source is an fd registered with the display's event loop by AddFD.450 // Source is an fd registered with the display's event loop by AddFD.
392 type Source struct {451 type Source struct {
393- d *Display452+ d *Display
394- c uintptr453+ c uintptr
395- token uintptr454+ token uintptr
455+ onReadable, onWritable func()
396 }456 }
397 457
398 // AddFD calls onReadable on the display goroutine whenever fd is readable. The458 // AddFD calls onReadable on the display goroutine whenever fd is readable. The
@@ -410,6 +470,14 @@ type Source struct {
410 // goroutine or before Run starts. Sources not removed are released when the470 // goroutine or before Run starts. Sources not removed are released when the
411 // display is destroyed.471 // display is destroyed.
412 func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {472 func (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.
480+func (d *Display) AddFDRW(fd int, onReadable, onWritable func()) (*Source, error) {
413 if fd < 0 {481 if fd < 0 {
414 return nil, fmt.Errorf("purego-libwayland: AddFD: invalid fd %d", fd)482 return nil, fmt.Errorf("purego-libwayland: AddFD: invalid fd %d", fd)
415 }483 }
@@ -421,17 +489,38 @@ func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {
421 }489 }
422 live.nextFD++490 live.nextFD++
423 token := live.nextFD491 token := live.nextFD
424- live.fds[token] = onReadable492+ s := &Source{d: d, token: token, onReadable: onReadable, onWritable: onWritable}
493+ live.fds[token] = s
425 c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token)494 c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token)
426 if c == 0 {495 if c == 0 {
427 delete(live.fds, token)496 delete(live.fds, token)
428 return nil, fmt.Errorf("purego-libwayland: wl_event_loop_add_fd(%d) failed", fd)497 return nil, fmt.Errorf("purego-libwayland: wl_event_loop_add_fd(%d) failed", fd)
429 }498 }
430- s := &Source{d: d, c: c, token: token}499+ s.c = c
431 d.sources[s] = struct{}{}500 d.sources[s] = struct{}{}
432 return s, nil501 return s, nil
433 }502 }
434 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.
507+func (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+
435 // Remove stops the callbacks and releases libwayland's duplicate of the fd.524 // Remove stops the callbacks and releases libwayland's duplicate of the fd.
436 // It may be called from the source's own callback. Display goroutine only;525 // It may be called from the source's own callback. Display goroutine only;
437 // idempotent, and a no-op after the display was destroyed.526 // idempotent, and a no-op after the display was destroyed.
@@ -658,9 +747,14 @@ func wake(fd int32, mask uint32, data uintptr) int32 {
658 }747 }
659 748
660 // fdReadable dispatches an AddFD source (wl_event_loop_fd_func_t).749 // fdReadable dispatches an AddFD source (wl_event_loop_fd_func_t).
661-func fdReadable(_ int32, _ uint32, data uintptr) int32 {750+func fdReadable(_ int32, mask uint32, data uintptr) int32 {
662- if fn := live.fds[data]; fn != nil {751+ if s := live.fds[data]; s != nil {
663- fn()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+ }
664 }758 }
665 return 0759 return 0
666 }760 }
M server/lib.go +19 -6
@@ -33,17 +33,19 @@ var (
3333 symClientGetCredentials, symClientGetFD, symClientCreate uintptr
3434 symClientAddDestroyListener, symListRemove uintptr
3535 symErrnoLocation uintptr
36+ symDisplaySetGlobalFilter, symEventSourceFDUpdate uintptr
3637 // wl_fixes support (libwayland 1.26+); zero on older libraries.
3738 symFixesAckGlobalRemove uintptr
3839
3940 // Shared C callbacks. purego callbacks are never freed, so there is one
4041 // of each for the whole process.
41- cbDispatcher uintptr
42- cbDestroy uintptr
43- cbClientGone uintptr
44- cbBind uintptr
45- cbWake uintptr
46- cbFD uintptr
42+ cbDispatcher uintptr
43+ cbDestroy uintptr
44+ cbClientGone uintptr
45+ cbBind uintptr
46+ cbWake uintptr
47+ cbFD uintptr
48+ cbGlobalFilter uintptr
4749 )
4850
4951 func load() error {
@@ -74,6 +76,8 @@ func load() error {
7476 name string
7577 sym *uintptr
7678 }{
79+ {"wl_display_set_global_filter", &symDisplaySetGlobalFilter},
80+ {"wl_event_source_fd_update", &symEventSourceFDUpdate},
7781 {"wl_display_flush_clients", &symDisplayFlushClients},
7882 {"wl_event_loop_dispatch", &symEventLoopDispatch},
7983 {"wl_resource_create", &symResourceCreate},
@@ -128,6 +132,7 @@ func load() error {
128132 cbWake = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
129133 return uintptr(uint32(wake(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))
130134 })
135+ cbGlobalFilter = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr { return uintptr(globalFilter(a.Int(0), a.Int(1), a.Int(2))) })
131136 cbFD = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
132137 return uintptr(uint32(fdReadable(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))
133138 })
@@ -168,3 +173,11 @@ func wlResourceDestroy(resource uintptr) {
168173 func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) {
169174 purego.Syscall6(symFixesAckGlobalRemove, fixes, registry, uintptr(name), 0, 0, 0)
170175 }
176+
177+func wlDisplaySetGlobalFilter(display, callback, data uintptr) {
178+ purego.Syscall6(symDisplaySetGlobalFilter, display, callback, data, 0, 0, 0)
179+}
180+func wlEventSourceFDUpdate(source uintptr, mask uint32) int32 {
181+ r, _, _ := purego.Syscall6(symEventSourceFDUpdate, source, uintptr(mask), 0, 0, 0, 0)
182+ return int32(r)
183+}
@@ -33,17 +33,19 @@ var (
33 symClientGetCredentials, symClientGetFD, symClientCreate uintptr33 symClientGetCredentials, symClientGetFD, symClientCreate uintptr
34 symClientAddDestroyListener, symListRemove uintptr34 symClientAddDestroyListener, symListRemove uintptr
35 symErrnoLocation uintptr35 symErrnoLocation uintptr
36+ symDisplaySetGlobalFilter, symEventSourceFDUpdate uintptr
36 // wl_fixes support (libwayland 1.26+); zero on older libraries.37 // wl_fixes support (libwayland 1.26+); zero on older libraries.
37 symFixesAckGlobalRemove uintptr38 symFixesAckGlobalRemove uintptr
38 39
39 // Shared C callbacks. purego callbacks are never freed, so there is one40 // Shared C callbacks. purego callbacks are never freed, so there is one
40 // of each for the whole process.41 // of each for the whole process.
41- cbDispatcher uintptr42+ cbDispatcher uintptr
42- cbDestroy uintptr43+ cbDestroy uintptr
43- cbClientGone uintptr44+ cbClientGone uintptr
44- cbBind uintptr45+ cbBind uintptr
45- cbWake uintptr46+ cbWake uintptr
46- cbFD uintptr47+ cbFD uintptr
48+ cbGlobalFilter uintptr
47 )49 )
48 50
49 func load() error {51 func load() error {
@@ -74,6 +76,8 @@ func load() error {
74 name string76 name string
75 sym *uintptr77 sym *uintptr
76 }{78 }{
79+ {"wl_display_set_global_filter", &symDisplaySetGlobalFilter},
80+ {"wl_event_source_fd_update", &symEventSourceFDUpdate},
77 {"wl_display_flush_clients", &symDisplayFlushClients},81 {"wl_display_flush_clients", &symDisplayFlushClients},
78 {"wl_event_loop_dispatch", &symEventLoopDispatch},82 {"wl_event_loop_dispatch", &symEventLoopDispatch},
79 {"wl_resource_create", &symResourceCreate},83 {"wl_resource_create", &symResourceCreate},
@@ -128,6 +132,7 @@ func load() error {
128 cbWake = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {132 cbWake = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
129 return uintptr(uint32(wake(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))133 return uintptr(uint32(wake(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))
130 })134 })
135+ cbGlobalFilter = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr { return uintptr(globalFilter(a.Int(0), a.Int(1), a.Int(2))) })
131 cbFD = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {136 cbFD = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
132 return uintptr(uint32(fdReadable(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))137 return uintptr(uint32(fdReadable(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))
133 })138 })
@@ -168,3 +173,11 @@ func wlResourceDestroy(resource uintptr) {
168 func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) {173 func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) {
169 purego.Syscall6(symFixesAckGlobalRemove, fixes, registry, uintptr(name), 0, 0, 0)174 purego.Syscall6(symFixesAckGlobalRemove, fixes, registry, uintptr(name), 0, 0, 0)
170 }175 }
176+
177+func wlDisplaySetGlobalFilter(display, callback, data uintptr) {
178+ purego.Syscall6(symDisplaySetGlobalFilter, display, callback, data, 0, 0, 0)
179+}
180+func wlEventSourceFDUpdate(source uintptr, mask uint32) int32 {
181+ r, _, _ := purego.Syscall6(symEventSourceFDUpdate, source, uintptr(mask), 0, 0, 0, 0)
182+ return int32(r)
183+}
A server/xwm_test.go +383 -0
@@ -0,0 +1,383 @@
1+package server
2+
3+import (
4+ "encoding/binary"
5+ "runtime"
6+ "testing"
7+
8+ "github.com/bnema/purego"
9+ "golang.org/x/sys/unix"
10+)
11+
12+func xwmDisplay(t testing.TB) *Display {
13+ t.Helper()
14+ runtime.LockOSThread()
15+ t.Cleanup(runtime.UnlockOSThread)
16+ d, err := NewDisplay()
17+ if err != nil {
18+ t.Fatal(err)
19+ }
20+ t.Cleanup(d.Close)
21+ return d
22+}
23+
24+func xwmClient(t testing.TB, d *Display) (Client, int) {
25+ t.Helper()
26+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ t.Cleanup(func() { unix.Close(fds[1]) })
31+ c, err := d.CreateClient(fds[0])
32+ if err != nil {
33+ unix.Close(fds[0])
34+ t.Fatal(err)
35+ }
36+ return c, fds[1]
37+}
38+
39+// registry requests exercise libwayland's real advertisement/filter path.
40+func registry(t testing.TB, d *Display, fd int) int {
41+ t.Helper()
42+ request := [12]byte{}
43+ binary.NativeEndian.PutUint32(request[:4], 1)
44+ binary.NativeEndian.PutUint32(request[4:8], 12<<16|1)
45+ binary.NativeEndian.PutUint32(request[8:], 2)
46+ if _, err := unix.Write(fd, request[:]); err != nil {
47+ t.Fatal(err)
48+ }
49+ if xwmDispatch(d) < 0 {
50+ t.Fatalf("dispatch failed: errno=%v", lastErrno())
51+ }
52+ wlDisplayFlushClients(d.c)
53+ var buf [4096]byte
54+ n, err := unix.Read(fd, buf[:])
55+ if err == unix.EAGAIN {
56+ return 0
57+ }
58+ if err != nil {
59+ t.Fatal(err)
60+ }
61+ return n
62+}
63+
64+func TestGlobalFilter(t *testing.T) {
65+ d := xwmDisplay(t)
66+ iface := &Interface{Name: "xwm_filter_test", Version: 1}
67+ if err := NewInterfaces(iface); err != nil {
68+ t.Fatal(err)
69+ }
70+ g, err := d.AddGlobal(iface, 1, nil)
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ allowed, a := xwmClient(t, d)
75+ _, b := xwmClient(t, d)
76+ calls := 0
77+ d.SetGlobalFilter(func(c Client, got *Global) bool {
78+ calls++
79+ if got != g {
80+ t.Error("wrong global handle")
81+ }
82+ return c == allowed
83+ })
84+ if registry(t, d, a) == 0 {
85+ t.Fatal("allowed client saw no global")
86+ }
87+ if registry(t, d, b) != 0 {
88+ t.Fatal("denied client saw global")
89+ }
90+ if calls != 2 {
91+ t.Fatalf("filter calls = %d", calls)
92+ }
93+ allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) })
94+ if allocs != 0 {
95+ t.Fatalf("filter allocs = %g", allocs)
96+ }
97+ // Creating a global with registries already present invokes the filter
98+ // before AddGlobal returns, but still passes the eventual handle.
99+ var during *Global
100+ d.SetGlobalFilter(func(_ Client, got *Global) bool { during = got; return false })
101+ second, err := d.AddGlobal(iface, 1, nil)
102+ if err != nil {
103+ t.Fatal(err)
104+ }
105+ if during != second {
106+ t.Fatal("creation-time filter handle differs")
107+ }
108+ d.SetGlobalFilter(nil)
109+ _, c := xwmClient(t, d)
110+ if registry(t, d, c) == 0 {
111+ t.Fatal("nil did not remove filter")
112+ }
113+ g.Remove()
114+ g.Remove()
115+}
116+
117+func TestWritableInterest(t *testing.T) {
118+ d := xwmDisplay(t)
119+ fds := xwmSocketpair(t)
120+ fd := fds[0]
121+ reads, writes := 0, 0
122+ s, err := d.AddFDRW(fd, func() { reads++ }, func() { writes++ })
123+ if err != nil {
124+ t.Fatal(err)
125+ }
126+ dispatch := func() {
127+ if xwmDispatch(d) < 0 {
128+ t.Fatalf("dispatch failed: errno=%v", lastErrno())
129+ }
130+ }
131+ dispatch()
132+ if writes != 0 {
133+ t.Fatal("writable starts enabled")
134+ }
135+ if err := s.SetWritable(true); err != nil {
136+ t.Fatal(err)
137+ }
138+ dispatch()
139+ if writes != 1 || reads != 0 {
140+ t.Fatalf("reads=%d writes=%d", reads, writes)
141+ }
142+ allocs := testing.AllocsPerRun(1000, dispatch)
143+ if allocs != 0 {
144+ t.Fatalf("fd dispatch allocs = %g", allocs)
145+ }
146+ before := writes
147+ if err := s.SetWritable(false); err != nil {
148+ t.Fatal(err)
149+ }
150+ dispatch()
151+ if writes != before {
152+ t.Fatal("writable not disabled")
153+ }
154+ // Readable callbacks also stay allocation-free through the real C loop.
155+ serverFD := fds[1]
156+ var payload = [1]byte{1}
157+ if _, err := unix.Write(serverFD, payload[:]); err != nil {
158+ t.Fatal(err)
159+ }
160+ allocs = testing.AllocsPerRun(1000, dispatch)
161+ if allocs != 0 {
162+ t.Fatalf("readable dispatch allocs = %g", allocs)
163+ }
164+ if reads == 0 {
165+ t.Fatal("readable interest lost")
166+ }
167+ if _, err := unix.Read(fd, payload[:]); err != nil {
168+ t.Fatal(err)
169+ }
170+ reads = 0
171+ // Combined masks, hangup routing and removal during readable delivery.
172+ fdReadable(0, 1|2, s.token)
173+ if reads != 1 || writes != before+1 {
174+ t.Fatal("combined mask not delivered")
175+ }
176+ s.onReadable = func() { reads++; s.Remove() }
177+ fdReadable(0, 4|8|2, s.token)
178+ if reads != 2 || writes != before+1 {
179+ t.Fatal("removal did not suppress writable")
180+ }
181+ if err := s.SetWritable(true); err != nil {
182+ t.Fatal(err)
183+ }
184+ plain, err := d.AddFD(fd, func() {})
185+ if err != nil {
186+ t.Fatal(err)
187+ }
188+ if plain.SetWritable(true) == nil {
189+ t.Fatal("enabled without writable callback")
190+ }
191+}
192+
193+func BenchmarkGlobalFilter(b *testing.B) {
194+ d := xwmDisplay(b)
195+ iface := &Interface{Name: "xwm_filter_bench", Version: 1}
196+ if err := NewInterfaces(iface); err != nil {
197+ b.Fatal(err)
198+ }
199+ g, err := d.AddGlobal(iface, 1, nil)
200+ if err != nil {
201+ b.Fatal(err)
202+ }
203+ c, _ := xwmClient(b, d)
204+ d.SetGlobalFilter(func(Client, *Global) bool { return true })
205+ b.ReportAllocs()
206+ b.ResetTimer()
207+ for b.Loop() {
208+ purego.Syscall6(cbGlobalFilter, c.c, g.c, d.c, 0, 0, 0)
209+ }
210+}
211+
212+func BenchmarkFDCallback(b *testing.B) {
213+ d := xwmDisplay(b)
214+ fds := xwmSocketpair(b)
215+ fd := fds[0]
216+ s, err := d.AddFDRW(fd, func() {}, func() {})
217+ if err != nil {
218+ b.Fatal(err)
219+ }
220+ if err := s.SetWritable(true); err != nil {
221+ b.Fatal(err)
222+ }
223+ b.ReportAllocs()
224+ b.ResetTimer()
225+ for b.Loop() {
226+ xwmDispatch(d)
227+ }
228+}
229+
230+func TestLateRestrictedGlobal(t *testing.T) {
231+ d := xwmDisplay(t)
232+ allowed, a := xwmClient(t, d)
233+ _, denied := xwmClient(t, d)
234+ if registry(t, d, a) != 0 || registry(t, d, denied) != 0 {
235+ t.Fatal("unexpected globals")
236+ }
237+ iface := &Interface{Name: "xwm_late_restricted", Version: 2}
238+ if err := NewInterfaces(iface); err != nil {
239+ t.Fatal(err)
240+ }
241+ calls, binds := 0, 0
242+ policy := func(c Client, g *Global) bool {
243+ calls++
244+ if g.Version() != 2 {
245+ t.Error("creation-time version unavailable")
246+ }
247+ return g.Interface() != iface.Name || c == allowed
248+ }
249+ d.SetGlobalFilter(policy)
250+ g, err := d.AddGlobal(iface, 2, func(Client, uint32, uint32) { binds++ })
251+ if err != nil {
252+ t.Fatal(err)
253+ }
254+ if calls != 2 {
255+ t.Fatalf("creation-time calls = %d", calls)
256+ }
257+ wlDisplayFlushClients(d.c)
258+ var buf [4096]byte
259+ n, err := unix.Read(a, buf[:])
260+ if err != nil || n < 16 {
261+ t.Fatalf("allowed announcement: n=%d err=%v", n, err)
262+ }
263+ name := binary.NativeEndian.Uint32(buf[8:12])
264+ if n, err := unix.Read(denied, buf[:]); err != unix.EAGAIN || n > 0 {
265+ t.Fatalf("denied announcement: n=%d err=%v", n, err)
266+ }
267+ if allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) }); allocs != 0 {
268+ t.Fatalf("metadata policy allocs = %g", allocs)
269+ }
270+ // Guessing a hidden global's numeric name must not bypass the filter.
271+ strLen := len(iface.Name) + 1
272+ padded := (strLen + 3) &^ 3
273+ req := make([]byte, 24+padded)
274+ binary.NativeEndian.PutUint32(req[0:4], 2) // wl_registry
275+ binary.NativeEndian.PutUint32(req[4:8], uint32(len(req))<<16) // bind
276+ binary.NativeEndian.PutUint32(req[8:12], name)
277+ binary.NativeEndian.PutUint32(req[12:16], uint32(strLen))
278+ copy(req[16:], iface.Name)
279+ binary.NativeEndian.PutUint32(req[16+padded:], 2)
280+ binary.NativeEndian.PutUint32(req[20+padded:], 3)
281+ if _, err := unix.Write(denied, req); err != nil {
282+ t.Fatal(err)
283+ }
284+ if xwmDispatch(d) < 0 {
285+ t.Fatal("dispatch")
286+ }
287+ wlDisplayFlushClients(d.c)
288+ if binds != 0 {
289+ t.Fatal("denied bind reached handler")
290+ }
291+ n, err = unix.Read(denied, buf[:])
292+ if err != nil || n < 12 || binary.NativeEndian.Uint32(buf[:4]) != 1 || binary.NativeEndian.Uint32(buf[4:8])&0xffff != 0 {
293+ t.Fatalf("expected wl_display.error: n=%d err=%v", n, err)
294+ }
295+}
296+
297+func TestWritableBackpressureAndRemoval(t *testing.T) {
298+ d := xwmDisplay(t)
299+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
300+ if err != nil {
301+ t.Fatal(err)
302+ }
303+ defer unix.Close(fds[0])
304+ defer unix.Close(fds[1])
305+ if err := unix.SetsockoptInt(fds[0], unix.SOL_SOCKET, unix.SO_SNDBUF, 4096); err != nil {
306+ t.Fatal(err)
307+ }
308+ var payload [4096]byte
309+ total := 0
310+ for attempts := 0; attempts < 1024; attempts++ {
311+ n, err := unix.Write(fds[0], payload[:])
312+ if err == unix.EAGAIN {
313+ break
314+ }
315+ if err != nil {
316+ t.Fatal(err)
317+ }
318+ total += n
319+ }
320+ if _, err := unix.Write(fds[0], payload[:]); err != unix.EAGAIN {
321+ t.Fatalf("not backpressured: %v", err)
322+ }
323+ writes := 0
324+ var s *Source
325+ s, err = d.AddFDRW(fds[0], func() { t.Error("unexpected readable") }, func() {
326+ writes++
327+ if n, err := unix.Write(fds[0], payload[:1]); err != nil || n != 1 {
328+ t.Errorf("resume: n=%d err=%v", n, err)
329+ }
330+ if err := s.SetWritable(false); err != nil {
331+ t.Error(err)
332+ }
333+ s.Remove() // Removal from a real C-dispatched callback.
334+ })
335+ if err != nil {
336+ t.Fatal(err)
337+ }
338+ if err := s.SetWritable(true); err != nil {
339+ t.Fatal(err)
340+ }
341+ xwmDispatch(d)
342+ if writes != 0 {
343+ t.Fatal("writable fired before peer drained")
344+ }
345+ for remaining := total; remaining > 0; {
346+ n, err := unix.Read(fds[1], payload[:])
347+ if err != nil || n == 0 {
348+ t.Fatalf("drain: n=%d err=%v", n, err)
349+ }
350+ remaining -= n
351+ }
352+ xwmDispatch(d)
353+ if writes != 1 || s.c != 0 {
354+ t.Fatalf("writes=%d source=%x", writes, s.c)
355+ }
356+ xwmDispatch(d)
357+ if writes != 1 {
358+ t.Fatal("removed source dispatched again")
359+ }
360+}
361+
362+// The Go runtime can interrupt epoll_wait with its preemption signal.
363+func xwmDispatch(d *Display) int32 {
364+ for attempt := 0; attempt < 16; attempt++ {
365+ result := wlEventLoopDispatch(d.loop, 0)
366+ if result >= 0 || lastErrno() != unix.EINTR {
367+ return result
368+ }
369+ }
370+ return -1
371+}
372+
373+// FD sources use raw sockets, not Wayland clients: arbitrary payloads must
374+// never enter libwayland's protocol transport or its flush/error handling.
375+func xwmSocketpair(t testing.TB) [2]int {
376+ t.Helper()
377+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
378+ if err != nil {
379+ t.Fatal(err)
380+ }
381+ t.Cleanup(func() { unix.Close(fds[0]); unix.Close(fds[1]) })
382+ return fds
383+}
@@ -0,0 +1,383 @@
1+package server
2+
3+import (
4+ "encoding/binary"
5+ "runtime"
6+ "testing"
7+
8+ "github.com/bnema/purego"
9+ "golang.org/x/sys/unix"
10+)
11+
12+func xwmDisplay(t testing.TB) *Display {
13+ t.Helper()
14+ runtime.LockOSThread()
15+ t.Cleanup(runtime.UnlockOSThread)
16+ d, err := NewDisplay()
17+ if err != nil {
18+ t.Fatal(err)
19+ }
20+ t.Cleanup(d.Close)
21+ return d
22+}
23+
24+func xwmClient(t testing.TB, d *Display) (Client, int) {
25+ t.Helper()
26+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ t.Cleanup(func() { unix.Close(fds[1]) })
31+ c, err := d.CreateClient(fds[0])
32+ if err != nil {
33+ unix.Close(fds[0])
34+ t.Fatal(err)
35+ }
36+ return c, fds[1]
37+}
38+
39+// registry requests exercise libwayland's real advertisement/filter path.
40+func registry(t testing.TB, d *Display, fd int) int {
41+ t.Helper()
42+ request := [12]byte{}
43+ binary.NativeEndian.PutUint32(request[:4], 1)
44+ binary.NativeEndian.PutUint32(request[4:8], 12<<16|1)
45+ binary.NativeEndian.PutUint32(request[8:], 2)
46+ if _, err := unix.Write(fd, request[:]); err != nil {
47+ t.Fatal(err)
48+ }
49+ if xwmDispatch(d) < 0 {
50+ t.Fatalf("dispatch failed: errno=%v", lastErrno())
51+ }
52+ wlDisplayFlushClients(d.c)
53+ var buf [4096]byte
54+ n, err := unix.Read(fd, buf[:])
55+ if err == unix.EAGAIN {
56+ return 0
57+ }
58+ if err != nil {
59+ t.Fatal(err)
60+ }
61+ return n
62+}
63+
64+func TestGlobalFilter(t *testing.T) {
65+ d := xwmDisplay(t)
66+ iface := &Interface{Name: "xwm_filter_test", Version: 1}
67+ if err := NewInterfaces(iface); err != nil {
68+ t.Fatal(err)
69+ }
70+ g, err := d.AddGlobal(iface, 1, nil)
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ allowed, a := xwmClient(t, d)
75+ _, b := xwmClient(t, d)
76+ calls := 0
77+ d.SetGlobalFilter(func(c Client, got *Global) bool {
78+ calls++
79+ if got != g {
80+ t.Error("wrong global handle")
81+ }
82+ return c == allowed
83+ })
84+ if registry(t, d, a) == 0 {
85+ t.Fatal("allowed client saw no global")
86+ }
87+ if registry(t, d, b) != 0 {
88+ t.Fatal("denied client saw global")
89+ }
90+ if calls != 2 {
91+ t.Fatalf("filter calls = %d", calls)
92+ }
93+ allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) })
94+ if allocs != 0 {
95+ t.Fatalf("filter allocs = %g", allocs)
96+ }
97+ // Creating a global with registries already present invokes the filter
98+ // before AddGlobal returns, but still passes the eventual handle.
99+ var during *Global
100+ d.SetGlobalFilter(func(_ Client, got *Global) bool { during = got; return false })
101+ second, err := d.AddGlobal(iface, 1, nil)
102+ if err != nil {
103+ t.Fatal(err)
104+ }
105+ if during != second {
106+ t.Fatal("creation-time filter handle differs")
107+ }
108+ d.SetGlobalFilter(nil)
109+ _, c := xwmClient(t, d)
110+ if registry(t, d, c) == 0 {
111+ t.Fatal("nil did not remove filter")
112+ }
113+ g.Remove()
114+ g.Remove()
115+}
116+
117+func TestWritableInterest(t *testing.T) {
118+ d := xwmDisplay(t)
119+ fds := xwmSocketpair(t)
120+ fd := fds[0]
121+ reads, writes := 0, 0
122+ s, err := d.AddFDRW(fd, func() { reads++ }, func() { writes++ })
123+ if err != nil {
124+ t.Fatal(err)
125+ }
126+ dispatch := func() {
127+ if xwmDispatch(d) < 0 {
128+ t.Fatalf("dispatch failed: errno=%v", lastErrno())
129+ }
130+ }
131+ dispatch()
132+ if writes != 0 {
133+ t.Fatal("writable starts enabled")
134+ }
135+ if err := s.SetWritable(true); err != nil {
136+ t.Fatal(err)
137+ }
138+ dispatch()
139+ if writes != 1 || reads != 0 {
140+ t.Fatalf("reads=%d writes=%d", reads, writes)
141+ }
142+ allocs := testing.AllocsPerRun(1000, dispatch)
143+ if allocs != 0 {
144+ t.Fatalf("fd dispatch allocs = %g", allocs)
145+ }
146+ before := writes
147+ if err := s.SetWritable(false); err != nil {
148+ t.Fatal(err)
149+ }
150+ dispatch()
151+ if writes != before {
152+ t.Fatal("writable not disabled")
153+ }
154+ // Readable callbacks also stay allocation-free through the real C loop.
155+ serverFD := fds[1]
156+ var payload = [1]byte{1}
157+ if _, err := unix.Write(serverFD, payload[:]); err != nil {
158+ t.Fatal(err)
159+ }
160+ allocs = testing.AllocsPerRun(1000, dispatch)
161+ if allocs != 0 {
162+ t.Fatalf("readable dispatch allocs = %g", allocs)
163+ }
164+ if reads == 0 {
165+ t.Fatal("readable interest lost")
166+ }
167+ if _, err := unix.Read(fd, payload[:]); err != nil {
168+ t.Fatal(err)
169+ }
170+ reads = 0
171+ // Combined masks, hangup routing and removal during readable delivery.
172+ fdReadable(0, 1|2, s.token)
173+ if reads != 1 || writes != before+1 {
174+ t.Fatal("combined mask not delivered")
175+ }
176+ s.onReadable = func() { reads++; s.Remove() }
177+ fdReadable(0, 4|8|2, s.token)
178+ if reads != 2 || writes != before+1 {
179+ t.Fatal("removal did not suppress writable")
180+ }
181+ if err := s.SetWritable(true); err != nil {
182+ t.Fatal(err)
183+ }
184+ plain, err := d.AddFD(fd, func() {})
185+ if err != nil {
186+ t.Fatal(err)
187+ }
188+ if plain.SetWritable(true) == nil {
189+ t.Fatal("enabled without writable callback")
190+ }
191+}
192+
193+func BenchmarkGlobalFilter(b *testing.B) {
194+ d := xwmDisplay(b)
195+ iface := &Interface{Name: "xwm_filter_bench", Version: 1}
196+ if err := NewInterfaces(iface); err != nil {
197+ b.Fatal(err)
198+ }
199+ g, err := d.AddGlobal(iface, 1, nil)
200+ if err != nil {
201+ b.Fatal(err)
202+ }
203+ c, _ := xwmClient(b, d)
204+ d.SetGlobalFilter(func(Client, *Global) bool { return true })
205+ b.ReportAllocs()
206+ b.ResetTimer()
207+ for b.Loop() {
208+ purego.Syscall6(cbGlobalFilter, c.c, g.c, d.c, 0, 0, 0)
209+ }
210+}
211+
212+func BenchmarkFDCallback(b *testing.B) {
213+ d := xwmDisplay(b)
214+ fds := xwmSocketpair(b)
215+ fd := fds[0]
216+ s, err := d.AddFDRW(fd, func() {}, func() {})
217+ if err != nil {
218+ b.Fatal(err)
219+ }
220+ if err := s.SetWritable(true); err != nil {
221+ b.Fatal(err)
222+ }
223+ b.ReportAllocs()
224+ b.ResetTimer()
225+ for b.Loop() {
226+ xwmDispatch(d)
227+ }
228+}
229+
230+func TestLateRestrictedGlobal(t *testing.T) {
231+ d := xwmDisplay(t)
232+ allowed, a := xwmClient(t, d)
233+ _, denied := xwmClient(t, d)
234+ if registry(t, d, a) != 0 || registry(t, d, denied) != 0 {
235+ t.Fatal("unexpected globals")
236+ }
237+ iface := &Interface{Name: "xwm_late_restricted", Version: 2}
238+ if err := NewInterfaces(iface); err != nil {
239+ t.Fatal(err)
240+ }
241+ calls, binds := 0, 0
242+ policy := func(c Client, g *Global) bool {
243+ calls++
244+ if g.Version() != 2 {
245+ t.Error("creation-time version unavailable")
246+ }
247+ return g.Interface() != iface.Name || c == allowed
248+ }
249+ d.SetGlobalFilter(policy)
250+ g, err := d.AddGlobal(iface, 2, func(Client, uint32, uint32) { binds++ })
251+ if err != nil {
252+ t.Fatal(err)
253+ }
254+ if calls != 2 {
255+ t.Fatalf("creation-time calls = %d", calls)
256+ }
257+ wlDisplayFlushClients(d.c)
258+ var buf [4096]byte
259+ n, err := unix.Read(a, buf[:])
260+ if err != nil || n < 16 {
261+ t.Fatalf("allowed announcement: n=%d err=%v", n, err)
262+ }
263+ name := binary.NativeEndian.Uint32(buf[8:12])
264+ if n, err := unix.Read(denied, buf[:]); err != unix.EAGAIN || n > 0 {
265+ t.Fatalf("denied announcement: n=%d err=%v", n, err)
266+ }
267+ if allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) }); allocs != 0 {
268+ t.Fatalf("metadata policy allocs = %g", allocs)
269+ }
270+ // Guessing a hidden global's numeric name must not bypass the filter.
271+ strLen := len(iface.Name) + 1
272+ padded := (strLen + 3) &^ 3
273+ req := make([]byte, 24+padded)
274+ binary.NativeEndian.PutUint32(req[0:4], 2) // wl_registry
275+ binary.NativeEndian.PutUint32(req[4:8], uint32(len(req))<<16) // bind
276+ binary.NativeEndian.PutUint32(req[8:12], name)
277+ binary.NativeEndian.PutUint32(req[12:16], uint32(strLen))
278+ copy(req[16:], iface.Name)
279+ binary.NativeEndian.PutUint32(req[16+padded:], 2)
280+ binary.NativeEndian.PutUint32(req[20+padded:], 3)
281+ if _, err := unix.Write(denied, req); err != nil {
282+ t.Fatal(err)
283+ }
284+ if xwmDispatch(d) < 0 {
285+ t.Fatal("dispatch")
286+ }
287+ wlDisplayFlushClients(d.c)
288+ if binds != 0 {
289+ t.Fatal("denied bind reached handler")
290+ }
291+ n, err = unix.Read(denied, buf[:])
292+ if err != nil || n < 12 || binary.NativeEndian.Uint32(buf[:4]) != 1 || binary.NativeEndian.Uint32(buf[4:8])&0xffff != 0 {
293+ t.Fatalf("expected wl_display.error: n=%d err=%v", n, err)
294+ }
295+}
296+
297+func TestWritableBackpressureAndRemoval(t *testing.T) {
298+ d := xwmDisplay(t)
299+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
300+ if err != nil {
301+ t.Fatal(err)
302+ }
303+ defer unix.Close(fds[0])
304+ defer unix.Close(fds[1])
305+ if err := unix.SetsockoptInt(fds[0], unix.SOL_SOCKET, unix.SO_SNDBUF, 4096); err != nil {
306+ t.Fatal(err)
307+ }
308+ var payload [4096]byte
309+ total := 0
310+ for attempts := 0; attempts < 1024; attempts++ {
311+ n, err := unix.Write(fds[0], payload[:])
312+ if err == unix.EAGAIN {
313+ break
314+ }
315+ if err != nil {
316+ t.Fatal(err)
317+ }
318+ total += n
319+ }
320+ if _, err := unix.Write(fds[0], payload[:]); err != unix.EAGAIN {
321+ t.Fatalf("not backpressured: %v", err)
322+ }
323+ writes := 0
324+ var s *Source
325+ s, err = d.AddFDRW(fds[0], func() { t.Error("unexpected readable") }, func() {
326+ writes++
327+ if n, err := unix.Write(fds[0], payload[:1]); err != nil || n != 1 {
328+ t.Errorf("resume: n=%d err=%v", n, err)
329+ }
330+ if err := s.SetWritable(false); err != nil {
331+ t.Error(err)
332+ }
333+ s.Remove() // Removal from a real C-dispatched callback.
334+ })
335+ if err != nil {
336+ t.Fatal(err)
337+ }
338+ if err := s.SetWritable(true); err != nil {
339+ t.Fatal(err)
340+ }
341+ xwmDispatch(d)
342+ if writes != 0 {
343+ t.Fatal("writable fired before peer drained")
344+ }
345+ for remaining := total; remaining > 0; {
346+ n, err := unix.Read(fds[1], payload[:])
347+ if err != nil || n == 0 {
348+ t.Fatalf("drain: n=%d err=%v", n, err)
349+ }
350+ remaining -= n
351+ }
352+ xwmDispatch(d)
353+ if writes != 1 || s.c != 0 {
354+ t.Fatalf("writes=%d source=%x", writes, s.c)
355+ }
356+ xwmDispatch(d)
357+ if writes != 1 {
358+ t.Fatal("removed source dispatched again")
359+ }
360+}
361+
362+// The Go runtime can interrupt epoll_wait with its preemption signal.
363+func xwmDispatch(d *Display) int32 {
364+ for attempt := 0; attempt < 16; attempt++ {
365+ result := wlEventLoopDispatch(d.loop, 0)
366+ if result >= 0 || lastErrno() != unix.EINTR {
367+ return result
368+ }
369+ }
370+ return -1
371+}
372+
373+// FD sources use raw sockets, not Wayland clients: arbitrary payloads must
374+// never enter libwayland's protocol transport or its flush/error handling.
375+func xwmSocketpair(t testing.TB) [2]int {
376+ t.Helper()
377+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
378+ if err != nil {
379+ t.Fatal(err)
380+ }
381+ t.Cleanup(func() { unix.Close(fds[0]); unix.Close(fds[1]) })
382+ return fds
383+}

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