fix(server): expose global metadata before advertisement filtering
3 files · +167 -7
M README.md +5 -1
| @@ -25,7 +25,11 @@ Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https | |||
| 25 | 25 | `Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see | |
| 26 | 26 | and bind each global; passing `nil` removes the filter. Set it before clients | |
| 27 | 27 | receive restricted globals. Global handles are stable, including during creation; | |
| 28 | - | filters must not create globals or block. | |
| 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. | |
| 29 | 33 | ||
| 30 | 34 | `Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with | |
| 31 | 35 | writable interest initially disabled. `Source.SetWritable(true)` arms writable | |
| @@ -25,7 +25,11 @@ Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https | |||||
| 25 | `Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see | 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 | 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; | 27 | receive restricted globals. Global handles are stable, including during creation; | ||
| 28 | - | filters must not create globals or block. | 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. | |||
| 29 | 33 | ||||
| 30 | `Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with | 34 | `Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with | ||
| 31 | writable interest initially disabled. `Source.SetWritable(true)` arms writable | 35 | writable interest initially disabled. `Source.SetWritable(true)` arms writable | ||
M server/display.go +12 -3
| @@ -139,10 +139,19 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) | |||
| 139 | 139 | ||
| 140 | 140 | // Global is a global that can be withdrawn, such as a wl_output on hotplug. | |
| 141 | 141 | type Global struct { | |
| 142 | - | c uintptr | |
| 143 | - | d *Display | |
| 142 | + | c uintptr | |
| 143 | + | d *Display | |
| 144 | + | interfaceName string | |
| 145 | + | version int32 | |
| 144 | 146 | } | |
| 145 | 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 | + | ||
| 146 | 155 | // SetGlobalFilter controls global visibility and binding for each client. nil | |
| 147 | 156 | // removes the filter. Install it before advertising restricted globals; it does | |
| 148 | 157 | // not retract announcements already sent. The callback must not block or create | |
| @@ -177,7 +186,7 @@ func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (* | |||
| 177 | 186 | live.nextBind++ | |
| 178 | 187 | token := live.nextBind | |
| 179 | 188 | live.binds[token] = bindFn | |
| 180 | - | g := &Global{d: d} | |
| 189 | + | g := &Global{d: d, interfaceName: iface.Name, version: version} | |
| 181 | 190 | d.pendingGlobal = g | |
| 182 | 191 | c := wlGlobalCreate(d.c, iface.c, version, token, cbBind) | |
| 183 | 192 | d.pendingGlobal = nil | |
| @@ -139,10 +139,19 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) | |||||
| 139 | 139 | ||||
| 140 | // 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. | ||
| 141 | type Global struct { | 141 | type Global struct { | ||
| 142 | - | c uintptr | 142 | + | c uintptr |
| 143 | - | d *Display | 143 | + | d *Display |
| 144 | + | interfaceName string | |||
| 145 | + | version int32 | |||
| 144 | } | 146 | } | ||
| 145 | 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 | + | ||||
| 146 | // SetGlobalFilter controls global visibility and binding for each client. nil | 155 | // SetGlobalFilter controls global visibility and binding for each client. nil | ||
| 147 | // removes the filter. Install it before advertising restricted globals; it does | 156 | // removes the filter. Install it before advertising restricted globals; it does | ||
| 148 | // not retract announcements already sent. The callback must not block or create | 157 | // not retract announcements already sent. The callback must not block or create | ||
| @@ -177,7 +186,7 @@ func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (* | |||||
| 177 | live.nextBind++ | 186 | live.nextBind++ | ||
| 178 | token := live.nextBind | 187 | token := live.nextBind | ||
| 179 | live.binds[token] = bindFn | 188 | live.binds[token] = bindFn | ||
| 180 | - | g := &Global{d: d} | 189 | + | g := &Global{d: d, interfaceName: iface.Name, version: version} |
| 181 | d.pendingGlobal = g | 190 | d.pendingGlobal = g | ||
| 182 | c := wlGlobalCreate(d.c, iface.c, version, token, cbBind) | 191 | c := wlGlobalCreate(d.c, iface.c, version, token, cbBind) | ||
| 183 | d.pendingGlobal = nil | 192 | d.pendingGlobal = nil | ||
M server/xwm_test.go +150 -3
| @@ -46,7 +46,7 @@ func registry(t testing.TB, d *Display, fd int) int { | |||
| 46 | 46 | if _, err := unix.Write(fd, request[:]); err != nil { | |
| 47 | 47 | t.Fatal(err) | |
| 48 | 48 | } | |
| 49 | - | if wlEventLoopDispatch(d.loop, 0) < 0 { | |
| 49 | + | if xwmDispatch(d) < 0 { | |
| 50 | 50 | t.Fatal("dispatch failed") | |
| 51 | 51 | } | |
| 52 | 52 | wlDisplayFlushClients(d.c) | |
| @@ -123,7 +123,7 @@ func TestWritableInterest(t *testing.T) { | |||
| 123 | 123 | t.Fatal(err) | |
| 124 | 124 | } | |
| 125 | 125 | dispatch := func() { | |
| 126 | - | if wlEventLoopDispatch(d.loop, 0) < 0 { | |
| 126 | + | if xwmDispatch(d) < 0 { | |
| 127 | 127 | t.Fatal("dispatch failed") | |
| 128 | 128 | } | |
| 129 | 129 | } | |
| @@ -224,6 +224,153 @@ func BenchmarkFDCallback(b *testing.B) { | |||
| 224 | 224 | b.ReportAllocs() | |
| 225 | 225 | b.ResetTimer() | |
| 226 | 226 | for b.Loop() { | |
| 227 | - | wlEventLoopDispatch(d.loop, 0) | |
| 227 | + | xwmDispatch(d) | |
| 228 | 228 | } | |
| 229 | 229 | } | |
| 230 | + | ||
| 231 | + | func TestLateRestrictedGlobal(t *testing.T) { | |
| 232 | + | d := xwmDisplay(t) | |
| 233 | + | allowed, a := xwmClient(t, d) | |
| 234 | + | _, denied := xwmClient(t, d) | |
| 235 | + | if registry(t, d, a) != 0 || registry(t, d, denied) != 0 { | |
| 236 | + | t.Fatal("unexpected globals") | |
| 237 | + | } | |
| 238 | + | iface := &Interface{Name: "xwm_late_restricted", Version: 2} | |
| 239 | + | if err := NewInterfaces(iface); err != nil { | |
| 240 | + | t.Fatal(err) | |
| 241 | + | } | |
| 242 | + | calls, binds := 0, 0 | |
| 243 | + | policy := func(c Client, g *Global) bool { | |
| 244 | + | calls++ | |
| 245 | + | if g.Version() != 2 { | |
| 246 | + | t.Error("creation-time version unavailable") | |
| 247 | + | } | |
| 248 | + | return g.Interface() != iface.Name || c == allowed | |
| 249 | + | } | |
| 250 | + | d.SetGlobalFilter(policy) | |
| 251 | + | g, err := d.AddGlobal(iface, 2, func(Client, uint32, uint32) { binds++ }) | |
| 252 | + | if err != nil { | |
| 253 | + | t.Fatal(err) | |
| 254 | + | } | |
| 255 | + | if calls != 2 { | |
| 256 | + | t.Fatalf("creation-time calls = %d", calls) | |
| 257 | + | } | |
| 258 | + | wlDisplayFlushClients(d.c) | |
| 259 | + | var buf [4096]byte | |
| 260 | + | n, err := unix.Read(a, buf[:]) | |
| 261 | + | if err != nil || n < 16 { | |
| 262 | + | t.Fatalf("allowed announcement: n=%d err=%v", n, err) | |
| 263 | + | } | |
| 264 | + | name := binary.NativeEndian.Uint32(buf[8:12]) | |
| 265 | + | if n, err := unix.Read(denied, buf[:]); err != unix.EAGAIN || n > 0 { | |
| 266 | + | t.Fatalf("denied announcement: n=%d err=%v", n, err) | |
| 267 | + | } | |
| 268 | + | if allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) }); allocs != 0 { | |
| 269 | + | t.Fatalf("metadata policy allocs = %g", allocs) | |
| 270 | + | } | |
| 271 | + | // Guessing a hidden global's numeric name must not bypass the filter. | |
| 272 | + | strLen := len(iface.Name) + 1 | |
| 273 | + | padded := (strLen + 3) &^ 3 | |
| 274 | + | req := make([]byte, 24+padded) | |
| 275 | + | binary.NativeEndian.PutUint32(req[0:4], 2) // wl_registry | |
| 276 | + | binary.NativeEndian.PutUint32(req[4:8], uint32(len(req))<<16) // bind | |
| 277 | + | binary.NativeEndian.PutUint32(req[8:12], name) | |
| 278 | + | binary.NativeEndian.PutUint32(req[12:16], uint32(strLen)) | |
| 279 | + | copy(req[16:], iface.Name) | |
| 280 | + | binary.NativeEndian.PutUint32(req[16+padded:], 2) | |
| 281 | + | binary.NativeEndian.PutUint32(req[20+padded:], 3) | |
| 282 | + | if _, err := unix.Write(denied, req); err != nil { | |
| 283 | + | t.Fatal(err) | |
| 284 | + | } | |
| 285 | + | if xwmDispatch(d) < 0 { | |
| 286 | + | t.Fatal("dispatch") | |
| 287 | + | } | |
| 288 | + | wlDisplayFlushClients(d.c) | |
| 289 | + | if binds != 0 { | |
| 290 | + | t.Fatal("denied bind reached handler") | |
| 291 | + | } | |
| 292 | + | n, err = unix.Read(denied, buf[:]) | |
| 293 | + | if err != nil || n < 12 || binary.NativeEndian.Uint32(buf[:4]) != 1 || binary.NativeEndian.Uint32(buf[4:8])&0xffff != 0 { | |
| 294 | + | t.Fatalf("expected wl_display.error: n=%d err=%v", n, err) | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | func TestWritableBackpressureAndRemoval(t *testing.T) { | |
| 299 | + | d := xwmDisplay(t) | |
| 300 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0) | |
| 301 | + | if err != nil { | |
| 302 | + | t.Fatal(err) | |
| 303 | + | } | |
| 304 | + | defer unix.Close(fds[0]) | |
| 305 | + | defer unix.Close(fds[1]) | |
| 306 | + | if err := unix.SetsockoptInt(fds[0], unix.SOL_SOCKET, unix.SO_SNDBUF, 4096); err != nil { | |
| 307 | + | t.Fatal(err) | |
| 308 | + | } | |
| 309 | + | var payload [4096]byte | |
| 310 | + | total := 0 | |
| 311 | + | for attempts := 0; attempts < 1024; attempts++ { | |
| 312 | + | n, err := unix.Write(fds[0], payload[:]) | |
| 313 | + | if err == unix.EAGAIN { | |
| 314 | + | break | |
| 315 | + | } | |
| 316 | + | if err != nil { | |
| 317 | + | t.Fatal(err) | |
| 318 | + | } | |
| 319 | + | total += n | |
| 320 | + | } | |
| 321 | + | if _, err := unix.Write(fds[0], payload[:]); err != unix.EAGAIN { | |
| 322 | + | t.Fatalf("not backpressured: %v", err) | |
| 323 | + | } | |
| 324 | + | writes := 0 | |
| 325 | + | var s *Source | |
| 326 | + | s, err = d.AddFDRW(fds[0], func() { t.Error("unexpected readable") }, func() { | |
| 327 | + | writes++ | |
| 328 | + | if n, err := unix.Write(fds[0], payload[:1]); err != nil || n != 1 { | |
| 329 | + | t.Errorf("resume: n=%d err=%v", n, err) | |
| 330 | + | } | |
| 331 | + | if err := s.SetWritable(false); err != nil { | |
| 332 | + | t.Error(err) | |
| 333 | + | } | |
| 334 | + | s.Remove() // Removal from a real C-dispatched callback. | |
| 335 | + | }) | |
| 336 | + | if err != nil { | |
| 337 | + | t.Fatal(err) | |
| 338 | + | } | |
| 339 | + | if err := s.SetWritable(true); err != nil { | |
| 340 | + | t.Fatal(err) | |
| 341 | + | } | |
| 342 | + | xwmDispatch(d) | |
| 343 | + | if writes != 0 { | |
| 344 | + | t.Fatal("writable fired before peer drained") | |
| 345 | + | } | |
| 346 | + | for remaining := total; remaining > 0; { | |
| 347 | + | n, err := unix.Read(fds[1], payload[:]) | |
| 348 | + | if err != nil || n == 0 { | |
| 349 | + | t.Fatalf("drain: n=%d err=%v", n, err) | |
| 350 | + | } | |
| 351 | + | remaining -= n | |
| 352 | + | } | |
| 353 | + | xwmDispatch(d) | |
| 354 | + | if writes != 1 || s.c != 0 { | |
| 355 | + | t.Fatalf("writes=%d source=%x", writes, s.c) | |
| 356 | + | } | |
| 357 | + | xwmDispatch(d) | |
| 358 | + | if writes != 1 { | |
| 359 | + | t.Fatal("removed source dispatched again") | |
| 360 | + | } | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | func TestSetWritableKernelError(t *testing.T) { | |
| 364 | + | t.Skip("A live source owns a private duplicated fd; the public API cannot invalidate it. Removed sources are no-ops. Forcing epoll_ctl failure requires a fake or corrupting libwayland-owned state.") | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | // The Go runtime can interrupt epoll_wait with its preemption signal. | |
| 368 | + | func xwmDispatch(d *Display) int32 { | |
| 369 | + | for attempt := 0; attempt < 16; attempt++ { | |
| 370 | + | result := wlEventLoopDispatch(d.loop, 0) | |
| 371 | + | if result >= 0 || lastErrno() != unix.EINTR { | |
| 372 | + | return result | |
| 373 | + | } | |
| 374 | + | } | |
| 375 | + | return -1 | |
| 376 | + | } | |
| @@ -46,7 +46,7 @@ func registry(t testing.TB, d *Display, fd int) int { | |||||
| 46 | if _, err := unix.Write(fd, request[:]); err != nil { | 46 | if _, err := unix.Write(fd, request[:]); err != nil { | ||
| 47 | t.Fatal(err) | 47 | t.Fatal(err) | ||
| 48 | } | 48 | } | ||
| 49 | - | if wlEventLoopDispatch(d.loop, 0) < 0 { | 49 | + | if xwmDispatch(d) < 0 { |
| 50 | t.Fatal("dispatch failed") | 50 | t.Fatal("dispatch failed") | ||
| 51 | } | 51 | } | ||
| 52 | wlDisplayFlushClients(d.c) | 52 | wlDisplayFlushClients(d.c) | ||
| @@ -123,7 +123,7 @@ func TestWritableInterest(t *testing.T) { | |||||
| 123 | t.Fatal(err) | 123 | t.Fatal(err) | ||
| 124 | } | 124 | } | ||
| 125 | dispatch := func() { | 125 | dispatch := func() { | ||
| 126 | - | if wlEventLoopDispatch(d.loop, 0) < 0 { | 126 | + | if xwmDispatch(d) < 0 { |
| 127 | t.Fatal("dispatch failed") | 127 | t.Fatal("dispatch failed") | ||
| 128 | } | 128 | } | ||
| 129 | } | 129 | } | ||
| @@ -224,6 +224,153 @@ func BenchmarkFDCallback(b *testing.B) { | |||||
| 224 | b.ReportAllocs() | 224 | b.ReportAllocs() | ||
| 225 | b.ResetTimer() | 225 | b.ResetTimer() | ||
| 226 | for b.Loop() { | 226 | for b.Loop() { | ||
| 227 | - | wlEventLoopDispatch(d.loop, 0) | 227 | + | xwmDispatch(d) |
| 228 | } | 228 | } | ||
| 229 | } | 229 | } | ||
| 230 | + | ||||
| 231 | + | func TestLateRestrictedGlobal(t *testing.T) { | |||
| 232 | + | d := xwmDisplay(t) | |||
| 233 | + | allowed, a := xwmClient(t, d) | |||
| 234 | + | _, denied := xwmClient(t, d) | |||
| 235 | + | if registry(t, d, a) != 0 || registry(t, d, denied) != 0 { | |||
| 236 | + | t.Fatal("unexpected globals") | |||
| 237 | + | } | |||
| 238 | + | iface := &Interface{Name: "xwm_late_restricted", Version: 2} | |||
| 239 | + | if err := NewInterfaces(iface); err != nil { | |||
| 240 | + | t.Fatal(err) | |||
| 241 | + | } | |||
| 242 | + | calls, binds := 0, 0 | |||
| 243 | + | policy := func(c Client, g *Global) bool { | |||
| 244 | + | calls++ | |||
| 245 | + | if g.Version() != 2 { | |||
| 246 | + | t.Error("creation-time version unavailable") | |||
| 247 | + | } | |||
| 248 | + | return g.Interface() != iface.Name || c == allowed | |||
| 249 | + | } | |||
| 250 | + | d.SetGlobalFilter(policy) | |||
| 251 | + | g, err := d.AddGlobal(iface, 2, func(Client, uint32, uint32) { binds++ }) | |||
| 252 | + | if err != nil { | |||
| 253 | + | t.Fatal(err) | |||
| 254 | + | } | |||
| 255 | + | if calls != 2 { | |||
| 256 | + | t.Fatalf("creation-time calls = %d", calls) | |||
| 257 | + | } | |||
| 258 | + | wlDisplayFlushClients(d.c) | |||
| 259 | + | var buf [4096]byte | |||
| 260 | + | n, err := unix.Read(a, buf[:]) | |||
| 261 | + | if err != nil || n < 16 { | |||
| 262 | + | t.Fatalf("allowed announcement: n=%d err=%v", n, err) | |||
| 263 | + | } | |||
| 264 | + | name := binary.NativeEndian.Uint32(buf[8:12]) | |||
| 265 | + | if n, err := unix.Read(denied, buf[:]); err != unix.EAGAIN || n > 0 { | |||
| 266 | + | t.Fatalf("denied announcement: n=%d err=%v", n, err) | |||
| 267 | + | } | |||
| 268 | + | if allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) }); allocs != 0 { | |||
| 269 | + | t.Fatalf("metadata policy allocs = %g", allocs) | |||
| 270 | + | } | |||
| 271 | + | // Guessing a hidden global's numeric name must not bypass the filter. | |||
| 272 | + | strLen := len(iface.Name) + 1 | |||
| 273 | + | padded := (strLen + 3) &^ 3 | |||
| 274 | + | req := make([]byte, 24+padded) | |||
| 275 | + | binary.NativeEndian.PutUint32(req[0:4], 2) // wl_registry | |||
| 276 | + | binary.NativeEndian.PutUint32(req[4:8], uint32(len(req))<<16) // bind | |||
| 277 | + | binary.NativeEndian.PutUint32(req[8:12], name) | |||
| 278 | + | binary.NativeEndian.PutUint32(req[12:16], uint32(strLen)) | |||
| 279 | + | copy(req[16:], iface.Name) | |||
| 280 | + | binary.NativeEndian.PutUint32(req[16+padded:], 2) | |||
| 281 | + | binary.NativeEndian.PutUint32(req[20+padded:], 3) | |||
| 282 | + | if _, err := unix.Write(denied, req); err != nil { | |||
| 283 | + | t.Fatal(err) | |||
| 284 | + | } | |||
| 285 | + | if xwmDispatch(d) < 0 { | |||
| 286 | + | t.Fatal("dispatch") | |||
| 287 | + | } | |||
| 288 | + | wlDisplayFlushClients(d.c) | |||
| 289 | + | if binds != 0 { | |||
| 290 | + | t.Fatal("denied bind reached handler") | |||
| 291 | + | } | |||
| 292 | + | n, err = unix.Read(denied, buf[:]) | |||
| 293 | + | if err != nil || n < 12 || binary.NativeEndian.Uint32(buf[:4]) != 1 || binary.NativeEndian.Uint32(buf[4:8])&0xffff != 0 { | |||
| 294 | + | t.Fatalf("expected wl_display.error: n=%d err=%v", n, err) | |||
| 295 | + | } | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | func TestWritableBackpressureAndRemoval(t *testing.T) { | |||
| 299 | + | d := xwmDisplay(t) | |||
| 300 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0) | |||
| 301 | + | if err != nil { | |||
| 302 | + | t.Fatal(err) | |||
| 303 | + | } | |||
| 304 | + | defer unix.Close(fds[0]) | |||
| 305 | + | defer unix.Close(fds[1]) | |||
| 306 | + | if err := unix.SetsockoptInt(fds[0], unix.SOL_SOCKET, unix.SO_SNDBUF, 4096); err != nil { | |||
| 307 | + | t.Fatal(err) | |||
| 308 | + | } | |||
| 309 | + | var payload [4096]byte | |||
| 310 | + | total := 0 | |||
| 311 | + | for attempts := 0; attempts < 1024; attempts++ { | |||
| 312 | + | n, err := unix.Write(fds[0], payload[:]) | |||
| 313 | + | if err == unix.EAGAIN { | |||
| 314 | + | break | |||
| 315 | + | } | |||
| 316 | + | if err != nil { | |||
| 317 | + | t.Fatal(err) | |||
| 318 | + | } | |||
| 319 | + | total += n | |||
| 320 | + | } | |||
| 321 | + | if _, err := unix.Write(fds[0], payload[:]); err != unix.EAGAIN { | |||
| 322 | + | t.Fatalf("not backpressured: %v", err) | |||
| 323 | + | } | |||
| 324 | + | writes := 0 | |||
| 325 | + | var s *Source | |||
| 326 | + | s, err = d.AddFDRW(fds[0], func() { t.Error("unexpected readable") }, func() { | |||
| 327 | + | writes++ | |||
| 328 | + | if n, err := unix.Write(fds[0], payload[:1]); err != nil || n != 1 { | |||
| 329 | + | t.Errorf("resume: n=%d err=%v", n, err) | |||
| 330 | + | } | |||
| 331 | + | if err := s.SetWritable(false); err != nil { | |||
| 332 | + | t.Error(err) | |||
| 333 | + | } | |||
| 334 | + | s.Remove() // Removal from a real C-dispatched callback. | |||
| 335 | + | }) | |||
| 336 | + | if err != nil { | |||
| 337 | + | t.Fatal(err) | |||
| 338 | + | } | |||
| 339 | + | if err := s.SetWritable(true); err != nil { | |||
| 340 | + | t.Fatal(err) | |||
| 341 | + | } | |||
| 342 | + | xwmDispatch(d) | |||
| 343 | + | if writes != 0 { | |||
| 344 | + | t.Fatal("writable fired before peer drained") | |||
| 345 | + | } | |||
| 346 | + | for remaining := total; remaining > 0; { | |||
| 347 | + | n, err := unix.Read(fds[1], payload[:]) | |||
| 348 | + | if err != nil || n == 0 { | |||
| 349 | + | t.Fatalf("drain: n=%d err=%v", n, err) | |||
| 350 | + | } | |||
| 351 | + | remaining -= n | |||
| 352 | + | } | |||
| 353 | + | xwmDispatch(d) | |||
| 354 | + | if writes != 1 || s.c != 0 { | |||
| 355 | + | t.Fatalf("writes=%d source=%x", writes, s.c) | |||
| 356 | + | } | |||
| 357 | + | xwmDispatch(d) | |||
| 358 | + | if writes != 1 { | |||
| 359 | + | t.Fatal("removed source dispatched again") | |||
| 360 | + | } | |||
| 361 | + | } | |||
| 362 | + | ||||
| 363 | + | func TestSetWritableKernelError(t *testing.T) { | |||
| 364 | + | t.Skip("A live source owns a private duplicated fd; the public API cannot invalidate it. Removed sources are no-ops. Forcing epoll_ctl failure requires a fake or corrupting libwayland-owned state.") | |||
| 365 | + | } | |||
| 366 | + | ||||
| 367 | + | // The Go runtime can interrupt epoll_wait with its preemption signal. | |||
| 368 | + | func xwmDispatch(d *Display) int32 { | |||
| 369 | + | for attempt := 0; attempt < 16; attempt++ { | |||
| 370 | + | result := wlEventLoopDispatch(d.loop, 0) | |||
| 371 | + | if result >= 0 || lastErrno() != unix.EINTR { | |||
| 372 | + | return result | |||
| 373 | + | } | |||
| 374 | + | } | |||
| 375 | + | return -1 | |||
| 376 | + | } | |||