Merge 15e9011315e8e7cae218026acb51a820fc858efc into f5b8db49413730383c33654512df6fac6fba210e
6 files · +536 -24
M README.md +22 -0
| @@ -20,6 +20,28 @@ CGO_ENABLED=0 go build ./... | |||
| 20 | 20 | ||
| 21 | 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 | 45 | ## License | |
| 24 | 46 | ||
| 25 | 47 | 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 | ## License | 45 | ## 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 | |||
| 3 | 3 | go 1.27 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | - | github.com/bnema/purego v0.13.0-bnema.1 | |
| 6 | + | github.com/bnema/purego v0.13.0-bnema.2 | |
| 7 | 7 | golang.org/x/sys v0.48.0 | |
| 8 | 8 | ) | |
| @@ -3,6 +3,6 @@ module github.com/bnema/purego-libwayland | |||||
| 3 | go 1.27 | 3 | go 1.27 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | - | github.com/bnema/purego v0.13.0-bnema.1 | 6 | + | github.com/bnema/purego v0.13.0-bnema.2 |
| 7 | golang.org/x/sys v0.48.0 | 7 | golang.org/x/sys v0.48.0 | ||
| 8 | ) | 8 | ) | ||
M go.sum +2 -2
| @@ -1,4 +1,4 @@ | |||
| 1 | - | github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8= | |
| 2 | - | github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM= | |
| 1 | + | github.com/bnema/purego v0.13.0-bnema.2 h1:FgGAFNswiShd+A4lFuP5broqjOzW4Lf8f6OZKjgXzAM= | |
| 2 | + | github.com/bnema/purego v0.13.0-bnema.2/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM= | |
| 3 | 3 | golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= | |
| 4 | 4 | golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= | |
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8= | 1 | + | github.com/bnema/purego v0.13.0-bnema.2 h1:FgGAFNswiShd+A4lFuP5broqjOzW4Lf8f6OZKjgXzAM= |
| 2 | - | github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM= | 2 | + | github.com/bnema/purego v0.13.0-bnema.2/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM= |
| 3 | golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= | 3 | golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= | ||
| 4 | golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= | 4 | golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= | ||
M server/display.go +109 -15
| @@ -50,10 +50,12 @@ var live = struct { | |||
| 50 | 50 | nextBind uintptr | |
| 51 | 51 | // clientGone maps wl_listener addresses to OnDestroy functions. | |
| 52 | 52 | 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{}} | |
| 57 | 59 | ||
| 58 | 60 | // Display owns a wl_display. All methods except Do and Post must run on the | |
| 59 | 61 | // goroutine that calls Run, or before Run starts. | |
| @@ -88,7 +90,9 @@ type Display struct { | |||
| 88 | 90 | wakeWrites atomic.Uint64 | |
| 89 | 91 | ||
| 90 | 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 | 98 | // maxDrainRounds bounds how many times one loop iteration re-drains calls and | |
| @@ -116,6 +120,7 @@ func NewDisplay() (*Display, error) { | |||
| 116 | 120 | wlDisplayDestroy(c) | |
| 117 | 121 | return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed") | |
| 118 | 122 | } | |
| 123 | + | live.displays[c] = d | |
| 119 | 124 | 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 | 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 | 184 | // AddGlobal advertises a global and returns a handle to remove it later. | |
| 139 | 185 | func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) { | |
| 140 | 186 | live.nextBind++ | |
| 141 | 187 | token := live.nextBind | |
| 142 | 188 | live.binds[token] = bindFn | |
| 189 | + | g := &Global{d: d, interfaceName: iface.Name, version: version} | |
| 190 | + | d.pendingGlobal = g | |
| 143 | 191 | c := wlGlobalCreate(d.c, iface.c, version, token, cbBind) | |
| 192 | + | d.pendingGlobal = nil | |
| 144 | 193 | if c == 0 { | |
| 145 | 194 | delete(live.binds, token) | |
| 146 | 195 | return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name) | |
| 147 | 196 | } | |
| 148 | - | return &Global{c: c}, nil | |
| 197 | + | g.c = c | |
| 198 | + | live.globals[c] = g | |
| 199 | + | return g, nil | |
| 149 | 200 | } | |
| 150 | 201 | ||
| 151 | 202 | // Remove withdraws the global: clients get wl_registry.global_remove. | |
| @@ -204,6 +255,14 @@ func (d *Display) destroy() { | |||
| 204 | 255 | wlDisplayDestroyClients(d.c) | |
| 205 | 256 | wlEventSourceRemove(d.wakeSrc) | |
| 206 | 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 | 266 | unix.Close(d.wake) | |
| 208 | 267 | } | |
| 209 | 268 | ||
| @@ -390,9 +449,10 @@ func (d *Display) Post(fn func()) { | |||
| 390 | 449 | ||
| 391 | 450 | // Source is an fd registered with the display's event loop by AddFD. | |
| 392 | 451 | 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() | |
| 396 | 456 | } | |
| 397 | 457 | ||
| 398 | 458 | // AddFD calls onReadable on the display goroutine whenever fd is readable. The | |
| @@ -410,6 +470,14 @@ type Source struct { | |||
| 410 | 470 | // goroutine or before Run starts. Sources not removed are released when the | |
| 411 | 471 | // display is destroyed. | |
| 412 | 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 | 481 | if fd < 0 { | |
| 414 | 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 | 490 | live.nextFD++ | |
| 423 | 491 | token := live.nextFD | |
| 424 | - | live.fds[token] = onReadable | |
| 492 | + | s := &Source{d: d, token: token, onReadable: onReadable, onWritable: onWritable} | |
| 493 | + | live.fds[token] = s | |
| 425 | 494 | c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token) | |
| 426 | 495 | if c == 0 { | |
| 427 | 496 | delete(live.fds, token) | |
| 428 | 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 | 500 | d.sources[s] = struct{}{} | |
| 432 | 501 | 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 | 524 | // Remove stops the callbacks and releases libwayland's duplicate of the fd. | |
| 436 | 525 | // It may be called from the source's own callback. Display goroutine only; | |
| 437 | 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 | 749 | // 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 | + | } | |
| 664 | 758 | } | |
| 665 | 759 | return 0 | |
| 666 | 760 | } | |
| @@ -50,10 +50,12 @@ var live = struct { | |||||
| 50 | nextBind uintptr | 50 | 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 uintptr | 55 | + | 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 the | 60 | // 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.Uint64 | 90 | 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 and | 98 | // 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, nil | 124 | 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.nextBind | 187 | token := live.nextBind | ||
| 142 | live.binds[token] = bindFn | 188 | 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}, nil | 197 | + | 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 *Display | 452 | + | d *Display |
| 394 | - | c uintptr | 453 | + | c uintptr |
| 395 | - | token uintptr | 454 | + | token uintptr |
| 455 | + | onReadable, onWritable func() | |||
| 396 | } | 456 | } | ||
| 397 | 457 | ||||
| 398 | // AddFD calls onReadable on the display goroutine whenever fd is readable. The | 458 | // 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 the | 470 | // 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.nextFD | 491 | token := live.nextFD | ||
| 424 | - | live.fds[token] = onReadable | 492 | + | 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, nil | 501 | 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 0 | 759 | return 0 | ||
| 666 | } | 760 | } | ||
M server/lib.go +19 -6
| @@ -33,17 +33,19 @@ var ( | |||
| 33 | 33 | symClientGetCredentials, symClientGetFD, symClientCreate uintptr | |
| 34 | 34 | symClientAddDestroyListener, symListRemove uintptr | |
| 35 | 35 | symErrnoLocation uintptr | |
| 36 | + | symDisplaySetGlobalFilter, symEventSourceFDUpdate uintptr | |
| 36 | 37 | // wl_fixes support (libwayland 1.26+); zero on older libraries. | |
| 37 | 38 | symFixesAckGlobalRemove uintptr | |
| 38 | 39 | ||
| 39 | 40 | // Shared C callbacks. purego callbacks are never freed, so there is one | |
| 40 | 41 | // 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 | |
| 47 | 49 | ) | |
| 48 | 50 | ||
| 49 | 51 | func load() error { | |
| @@ -74,6 +76,8 @@ func load() error { | |||
| 74 | 76 | name string | |
| 75 | 77 | sym *uintptr | |
| 76 | 78 | }{ | |
| 79 | + | {"wl_display_set_global_filter", &symDisplaySetGlobalFilter}, | |
| 80 | + | {"wl_event_source_fd_update", &symEventSourceFDUpdate}, | |
| 77 | 81 | {"wl_display_flush_clients", &symDisplayFlushClients}, | |
| 78 | 82 | {"wl_event_loop_dispatch", &symEventLoopDispatch}, | |
| 79 | 83 | {"wl_resource_create", &symResourceCreate}, | |
| @@ -128,6 +132,7 @@ func load() error { | |||
| 128 | 132 | cbWake = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr { | |
| 129 | 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 | 136 | cbFD = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr { | |
| 132 | 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 | 173 | func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) { | |
| 169 | 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 | + | } | |
| @@ -33,17 +33,19 @@ var ( | |||||
| 33 | symClientGetCredentials, symClientGetFD, symClientCreate uintptr | 33 | symClientGetCredentials, symClientGetFD, symClientCreate uintptr | ||
| 34 | symClientAddDestroyListener, symListRemove uintptr | 34 | symClientAddDestroyListener, symListRemove uintptr | ||
| 35 | symErrnoLocation uintptr | 35 | 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 uintptr | 38 | symFixesAckGlobalRemove uintptr | ||
| 38 | 39 | ||||
| 39 | // Shared C callbacks. purego callbacks are never freed, so there is one | 40 | // 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 uintptr | 42 | + | cbDispatcher uintptr |
| 42 | - | cbDestroy uintptr | 43 | + | cbDestroy uintptr |
| 43 | - | cbClientGone uintptr | 44 | + | cbClientGone uintptr |
| 44 | - | cbBind uintptr | 45 | + | cbBind uintptr |
| 45 | - | cbWake uintptr | 46 | + | cbWake uintptr |
| 46 | - | cbFD uintptr | 47 | + | 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 string | 76 | name string | ||
| 75 | sym *uintptr | 77 | 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 | + | } | |||