feat: add connection, event queue, outputs and fd watching
Port the reader/pause-gate design of nefergui's platform connection onto go-wayland-bindings. The reader goroutine only decodes events into a fixed 256-slot ring; Dispatch applies them and calls the Handler on the owner goroutine. wl_output state is tracked per global, WatchFD/UnwatchFD wrap a one-shot epoll served by a second goroutine, and Bind is a generic method that pauses the reader around the bind.
- commit
- 8f00b92c2bbe1431cbefff32480154b0201de25e
- author
- brice · · 2026-10-01 15:30 UTC
- parent
- ccab19b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +1083 -1
M .mockery.yml +8 -1
| @@ -5,4 +5,11 @@ structname: '{{.Mock}}{{.InterfaceName}}' | |||
| 5 | 5 | template: testify | |
| 6 | 6 | template-schema: '{{.Template}}.schema.json' | |
| 7 | 7 | require-template-schema-exists: true | |
| 8 | - | packages: {} | |
| 8 | + | packages: | |
| 9 | + | github.com/bnema/neferclient: | |
| 10 | + | config: | |
| 11 | + | dir: mocks | |
| 12 | + | pkgname: neferclientmocks | |
| 13 | + | filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go' | |
| 14 | + | interfaces: | |
| 15 | + | Handler: | |
| @@ -5,4 +5,11 @@ structname: '{{.Mock}}{{.InterfaceName}}' | |||||
| 5 | template: testify | 5 | template: testify | ||
| 6 | template-schema: '{{.Template}}.schema.json' | 6 | template-schema: '{{.Template}}.schema.json' | ||
| 7 | require-template-schema-exists: true | 7 | require-template-schema-exists: true | ||
| 8 | - | packages: {} | 8 | + | packages: |
| 9 | + | github.com/bnema/neferclient: | |||
| 10 | + | config: | |||
| 11 | + | dir: mocks | |||
| 12 | + | pkgname: neferclientmocks | |||
| 13 | + | filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go' | |||
| 14 | + | interfaces: | |||
| 15 | + | Handler: | |||
A conn.go +572 -0
| @@ -0,0 +1,572 @@ | |||
| 1 | + | package neferclient | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "fmt" | |
| 7 | + | "net" | |
| 8 | + | "os" | |
| 9 | + | "path/filepath" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/go-wayland-bindings/client/wayland" | |
| 13 | + | "github.com/bnema/wlturbo" | |
| 14 | + | "github.com/bnema/wlturbo/wl" | |
| 15 | + | "golang.org/x/sys/unix" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // dialTimeout bounds the unix-socket dial; a local dial is normally instant. | |
| 19 | + | const dialTimeout = 5 * time.Second | |
| 20 | + | ||
| 21 | + | // maxOutputVersion is the wl_output version bound: 4 adds names. | |
| 22 | + | const maxOutputVersion = 4 | |
| 23 | + | ||
| 24 | + | var ( | |
| 25 | + | // ErrClosed is returned by calls on a closed Conn. | |
| 26 | + | ErrClosed = errors.New("neferclient: connection closed") | |
| 27 | + | // ErrGlobalNotFound is returned by [Conn.Bind] when the compositor does | |
| 28 | + | // not announce the interface. Test it with errors.Is. | |
| 29 | + | ErrGlobalNotFound = wlturbo.ErrGlobalNotFound | |
| 30 | + | ) | |
| 31 | + | ||
| 32 | + | // Output describes one wl_output after its latest wl_output.done. | |
| 33 | + | type Output struct { | |
| 34 | + | Global uint32 // registry name; identifies the output for its lifetime | |
| 35 | + | Name string // wl_output.name, empty before version 4 | |
| 36 | + | Scale int32 // integer scale, 1 until announced | |
| 37 | + | Width, Height int32 // current mode in physical pixels | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // outputEntry holds the owner-side state of one bound wl_output. Properties | |
| 41 | + | // accumulate in the pending fields and become visible at wl_output.done. | |
| 42 | + | type outputEntry struct { | |
| 43 | + | proxy *wayland.Output | |
| 44 | + | done bool // first wl_output.done applied; the output is public | |
| 45 | + | pub Output | |
| 46 | + | ||
| 47 | + | nameBuf [maxNameLen]byte | |
| 48 | + | nameLen int | |
| 49 | + | scale int32 | |
| 50 | + | width, height int32 | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // Conn is one Wayland connection. Two goroutines run beside the owner: the | |
| 54 | + | // reader, which only decodes wire events into a fixed queue, and an epoll | |
| 55 | + | // waiter for [Conn.WatchFD]. Everything else — event handling, requests, | |
| 56 | + | // Bind, Roundtrip, Outputs, Close — belongs to the owner goroutine, the one | |
| 57 | + | // that calls [Conn.Dispatch]. | |
| 58 | + | type Conn struct { | |
| 59 | + | sock net.Conn | |
| 60 | + | display *wl.Display | |
| 61 | + | wlctx *wl.Context | |
| 62 | + | q *queue | |
| 63 | + | ||
| 64 | + | readerStarted bool | |
| 65 | + | readerDone chan struct{} | |
| 66 | + | pauseDepth int | |
| 67 | + | ||
| 68 | + | epfd, efd int | |
| 69 | + | epollStart bool | |
| 70 | + | epollDone chan struct{} | |
| 71 | + | watched map[int32]uint64 // fd -> caller id | |
| 72 | + | ||
| 73 | + | outputs []*outputEntry // sorted by Global | |
| 74 | + | view []Output // reused by Outputs | |
| 75 | + | ||
| 76 | + | ready bool // Connect finished; announcements go to the Handler | |
| 77 | + | dispatching bool | |
| 78 | + | closed bool | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // Connect dials the compositor and completes the registry setup: every | |
| 82 | + | // wl_output announced at that point is bound and its first description is | |
| 83 | + | // available from [Conn.Outputs]. display names the socket relative to | |
| 84 | + | // $XDG_RUNTIME_DIR, or is an absolute path; "" uses $WAYLAND_DISPLAY, then | |
| 85 | + | // "wayland-0". Cancelling ctx abandons setup; afterwards ctx is not used. | |
| 86 | + | func Connect(ctx context.Context, display string) (*Conn, error) { | |
| 87 | + | if ctx == nil { | |
| 88 | + | return nil, errors.New("neferclient: nil context") | |
| 89 | + | } | |
| 90 | + | if err := ctx.Err(); err != nil { | |
| 91 | + | return nil, err | |
| 92 | + | } | |
| 93 | + | path, err := socketPath(display) | |
| 94 | + | if err != nil { | |
| 95 | + | return nil, err | |
| 96 | + | } | |
| 97 | + | sock, err := (&net.Dialer{Timeout: dialTimeout}).DialContext(ctx, "unix", path) | |
| 98 | + | if err != nil { | |
| 99 | + | return nil, fmt.Errorf("failed to connect to Wayland: %w", err) | |
| 100 | + | } | |
| 101 | + | return connectConn(ctx, sock) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func socketPath(name string) (string, error) { | |
| 105 | + | if name == "" { | |
| 106 | + | name = os.Getenv("WAYLAND_DISPLAY") | |
| 107 | + | if name == "" { | |
| 108 | + | name = "wayland-0" | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | if filepath.IsAbs(name) { | |
| 112 | + | return name, nil | |
| 113 | + | } | |
| 114 | + | dir := os.Getenv("XDG_RUNTIME_DIR") | |
| 115 | + | if dir == "" { | |
| 116 | + | return "", errors.New("neferclient: XDG_RUNTIME_DIR not set") | |
| 117 | + | } | |
| 118 | + | return filepath.Join(dir, name), nil | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // connectConn runs the setup over an established connection and takes | |
| 122 | + | // ownership of it. | |
| 123 | + | func connectConn(ctx context.Context, sock net.Conn) (res *Conn, err error) { | |
| 124 | + | display, err := wlturbo.ConnectFromConn(sock) | |
| 125 | + | if err != nil { | |
| 126 | + | _ = sock.Close() | |
| 127 | + | return nil, err | |
| 128 | + | } | |
| 129 | + | c := &Conn{ | |
| 130 | + | sock: sock, | |
| 131 | + | display: display, | |
| 132 | + | wlctx: display.Context(), | |
| 133 | + | q: newQueue(true), // no reader yet: owner-side events are parked | |
| 134 | + | efd: -1, | |
| 135 | + | epfd: -1, | |
| 136 | + | watched: make(map[int32]uint64), | |
| 137 | + | } | |
| 138 | + | watch := context.AfterFunc(ctx, func() { _ = display.Close() }) | |
| 139 | + | defer func() { | |
| 140 | + | if !watch() && err == nil { | |
| 141 | + | err = ctx.Err() | |
| 142 | + | } | |
| 143 | + | if err != nil { | |
| 144 | + | _ = c.Close() | |
| 145 | + | res = nil | |
| 146 | + | if ctx.Err() != nil { | |
| 147 | + | err = errors.Join(ctx.Err(), err) | |
| 148 | + | } | |
| 149 | + | } | |
| 150 | + | }() | |
| 151 | + | if c.epfd, err = unix.EpollCreate1(unix.EPOLL_CLOEXEC); err != nil { | |
| 152 | + | return nil, fmt.Errorf("neferclient: epoll: %w", err) | |
| 153 | + | } | |
| 154 | + | if c.efd, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil { | |
| 155 | + | return nil, fmt.Errorf("neferclient: eventfd: %w", err) | |
| 156 | + | } | |
| 157 | + | ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: int32(c.efd)} | |
| 158 | + | if err = unix.EpollCtl(c.epfd, unix.EPOLL_CTL_ADD, c.efd, &ev); err != nil { | |
| 159 | + | return nil, fmt.Errorf("neferclient: epoll: %w", err) | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | reg := display.Registry() | |
| 163 | + | reg.AddHandler(wayland.OutputInterface, func(_ *wl.Registry, name, version uint32) { | |
| 164 | + | e := event{kind: evGlobal, global: name, version: version} | |
| 165 | + | c.q.post(&e) | |
| 166 | + | }) | |
| 167 | + | reg.AddGlobalRemoveHandler(removeHook{c.q}) | |
| 168 | + | ||
| 169 | + | // With no reader running, the owner's own roundtrips park every event in | |
| 170 | + | // the queue's held list, in order; applying it here is the startup drain. | |
| 171 | + | if err = display.Roundtrip(); err != nil { | |
| 172 | + | return nil, err | |
| 173 | + | } | |
| 174 | + | if err = c.applyHeld(); err != nil { | |
| 175 | + | return nil, err | |
| 176 | + | } | |
| 177 | + | if err = display.Roundtrip(); err != nil { // wl_output properties follow the binds | |
| 178 | + | return nil, err | |
| 179 | + | } | |
| 180 | + | if err = c.applyHeld(); err != nil { | |
| 181 | + | return nil, err | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | c.ready = true | |
| 185 | + | c.readerDone = make(chan struct{}) | |
| 186 | + | c.readerStarted = true | |
| 187 | + | c.epollDone = make(chan struct{}) | |
| 188 | + | c.epollStart = true | |
| 189 | + | c.q.setPausing(false) | |
| 190 | + | go c.readLoop() | |
| 191 | + | go c.epollLoop() | |
| 192 | + | return c, nil | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | // removeHook forwards registry global_remove to the queue. | |
| 196 | + | type removeHook struct{ q *queue } | |
| 197 | + | ||
| 198 | + | func (h removeHook) HandleRegistryGlobalRemove(e wl.RegistryGlobalRemoveEvent) { | |
| 199 | + | ev := event{kind: evGlobalRemove, global: e.Name} | |
| 200 | + | h.q.post(&ev) | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // setupHandler applies setup events without notifying anyone and keeps the | |
| 204 | + | // first failure, which makes Connect fail. | |
| 205 | + | type setupHandler struct { | |
| 206 | + | nopHandler | |
| 207 | + | err *error | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | func (h setupHandler) Error(err error) { | |
| 211 | + | if *h.err == nil { | |
| 212 | + | *h.err = err | |
| 213 | + | } | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | // applyHeld applies the events parked during setup. | |
| 217 | + | func (c *Conn) applyHeld() error { | |
| 218 | + | var err error | |
| 219 | + | h := setupHandler{err: &err} | |
| 220 | + | held := c.q.takeHeld() | |
| 221 | + | for i := range held { | |
| 222 | + | c.apply(&held[i], h) | |
| 223 | + | } | |
| 224 | + | return err | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | // readLoop is the reader goroutine. It only calls Display.Dispatch; the | |
| 228 | + | // generated handlers it triggers only push events into the queue. | |
| 229 | + | func (c *Conn) readLoop() { | |
| 230 | + | defer close(c.readerDone) | |
| 231 | + | defer c.q.readerDone() | |
| 232 | + | for { | |
| 233 | + | if !c.q.gate() { | |
| 234 | + | return | |
| 235 | + | } | |
| 236 | + | err := c.display.Dispatch() | |
| 237 | + | if err == nil || errors.Is(err, os.ErrDeadlineExceeded) { | |
| 238 | + | continue // a pause request interrupted the read | |
| 239 | + | } | |
| 240 | + | if !c.q.isStopped() { | |
| 241 | + | c.q.setFatal(err) | |
| 242 | + | } | |
| 243 | + | return | |
| 244 | + | } | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // epollLoop is the second goroutine: it waits for watched descriptors and the | |
| 248 | + | // shutdown eventfd. | |
| 249 | + | func (c *Conn) epollLoop() { | |
| 250 | + | defer close(c.epollDone) | |
| 251 | + | var evs [8]unix.EpollEvent | |
| 252 | + | for { | |
| 253 | + | n, err := unix.EpollWait(c.epfd, evs[:], -1) | |
| 254 | + | if err != nil { | |
| 255 | + | if errors.Is(err, unix.EINTR) { | |
| 256 | + | continue | |
| 257 | + | } | |
| 258 | + | c.q.setFatal(fmt.Errorf("neferclient: epoll wait: %w", err)) | |
| 259 | + | return | |
| 260 | + | } | |
| 261 | + | for i := range n { | |
| 262 | + | if evs[i].Fd == int32(c.efd) { | |
| 263 | + | return | |
| 264 | + | } | |
| 265 | + | e := event{kind: evFDReady, fd: evs[i].Fd} | |
| 266 | + | c.q.post(&e) | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | // pause parks the reader between two reads and returns once it is parked. | |
| 272 | + | // Nested calls on the owner goroutine share the first pause. Before the reader | |
| 273 | + | // starts, the queue already parks everything and pause does nothing. | |
| 274 | + | func (c *Conn) pause() { | |
| 275 | + | if !c.readerStarted { | |
| 276 | + | return | |
| 277 | + | } | |
| 278 | + | c.pauseDepth++ | |
| 279 | + | if c.pauseDepth > 1 { | |
| 280 | + | return | |
| 281 | + | } | |
| 282 | + | c.q.setPausing(true) | |
| 283 | + | _ = c.sock.SetReadDeadline(time.Unix(1, 0)) // wakes a blocked read | |
| 284 | + | c.q.awaitParked() | |
| 285 | + | _ = c.sock.SetReadDeadline(time.Time{}) | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | func (c *Conn) resume() { | |
| 289 | + | if !c.readerStarted { | |
| 290 | + | return | |
| 291 | + | } | |
| 292 | + | c.pauseDepth-- | |
| 293 | + | if c.pauseDepth == 0 { | |
| 294 | + | c.q.setPausing(false) | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | // Wake returns a channel with capacity 1 that is signaled whenever events are | |
| 299 | + | // queued: call [Conn.Dispatch] when it fires. A signal can be stale (the | |
| 300 | + | // events were already drained), so Dispatch may find nothing to do. | |
| 301 | + | func (c *Conn) Wake() <-chan struct{} { return c.q.wake } | |
| 302 | + | ||
| 303 | + | // Context returns the wlturbo context that generated proxies are created on. | |
| 304 | + | func (c *Conn) Context() *wl.Context { return c.wlctx } | |
| 305 | + | ||
| 306 | + | // Display returns the underlying wlturbo display. Only the owner goroutine | |
| 307 | + | // may call Dispatch or Roundtrip on it; use [Conn.Dispatch] and | |
| 308 | + | // [Conn.Roundtrip] instead. | |
| 309 | + | func (c *Conn) Display() *wl.Display { return c.display } | |
| 310 | + | ||
| 311 | + | // Dispatch applies the events the reader queued and calls h for each one that | |
| 312 | + | // matters to the application, on the calling goroutine. It handles at most one | |
| 313 | + | // ring of events per call and signals [Conn.Wake] again if more are pending. | |
| 314 | + | // A nil h discards the notifications but still applies the state changes. It | |
| 315 | + | // returns the transport error once the queue is drained after the connection | |
| 316 | + | // failed, and [ErrClosed] after Close. | |
| 317 | + | func (c *Conn) Dispatch(h Handler) error { | |
| 318 | + | if c.closed { | |
| 319 | + | return ErrClosed | |
| 320 | + | } | |
| 321 | + | if c.dispatching { | |
| 322 | + | return errors.New("neferclient: Dispatch called from a Handler") | |
| 323 | + | } | |
| 324 | + | if h == nil { | |
| 325 | + | h = nopHandler{} | |
| 326 | + | } | |
| 327 | + | c.dispatching = true | |
| 328 | + | defer func() { c.dispatching = false }() | |
| 329 | + | var ev event | |
| 330 | + | for range ringSize { | |
| 331 | + | if !c.q.pop(&ev) { | |
| 332 | + | return c.q.fatalErr() | |
| 333 | + | } | |
| 334 | + | c.apply(&ev, h) | |
| 335 | + | if c.closed { | |
| 336 | + | return ErrClosed | |
| 337 | + | } | |
| 338 | + | } | |
| 339 | + | if c.q.pending() { | |
| 340 | + | c.q.signal() | |
| 341 | + | return nil | |
| 342 | + | } | |
| 343 | + | return c.q.fatalErr() | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | // apply runs on the owner goroutine. | |
| 347 | + | func (c *Conn) apply(ev *event, h Handler) { | |
| 348 | + | switch ev.kind { | |
| 349 | + | case evGlobal: | |
| 350 | + | if err := c.bindOutput(ev.global, ev.version); err != nil { | |
| 351 | + | h.Error(err) | |
| 352 | + | } | |
| 353 | + | case evGlobalRemove: | |
| 354 | + | c.removeOutput(ev.global, h) | |
| 355 | + | case evOutputName: | |
| 356 | + | if o := c.entry(ev.global); o != nil { | |
| 357 | + | o.nameLen = copy(o.nameBuf[:], ev.nameBytes()) | |
| 358 | + | } | |
| 359 | + | case evOutputScale: | |
| 360 | + | if o := c.entry(ev.global); o != nil && ev.a > 0 { | |
| 361 | + | o.scale = ev.a | |
| 362 | + | } | |
| 363 | + | case evOutputMode: | |
| 364 | + | if o := c.entry(ev.global); o != nil && ev.flags&1 != 0 { // current mode | |
| 365 | + | o.width, o.height = ev.a, ev.b | |
| 366 | + | } | |
| 367 | + | case evOutputDone: | |
| 368 | + | if o := c.entry(ev.global); o != nil { | |
| 369 | + | c.commitOutput(o, h) | |
| 370 | + | } | |
| 371 | + | case evFDReady: | |
| 372 | + | id, ok := c.watched[ev.fd] | |
| 373 | + | if !ok { | |
| 374 | + | return // unwatched while the event was queued | |
| 375 | + | } | |
| 376 | + | h.FDReady(id) | |
| 377 | + | if _, ok := c.watched[ev.fd]; ok { // the handler may have unwatched it | |
| 378 | + | if err := c.armFD(int(ev.fd), unix.EPOLL_CTL_MOD); err != nil { | |
| 379 | + | h.Error(err) | |
| 380 | + | } | |
| 381 | + | } | |
| 382 | + | } | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | func (c *Conn) entry(global uint32) *outputEntry { | |
| 386 | + | for _, o := range c.outputs { | |
| 387 | + | if o.pub.Global == global { | |
| 388 | + | return o | |
| 389 | + | } | |
| 390 | + | } | |
| 391 | + | return nil | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | func (c *Conn) bindOutput(global, version uint32) error { | |
| 395 | + | if version == 0 || c.entry(global) != nil { | |
| 396 | + | return nil | |
| 397 | + | } | |
| 398 | + | o := &outputEntry{pub: Output{Global: global, Scale: 1}, scale: 1} | |
| 399 | + | out := wayland.NewOutput(c.wlctx) | |
| 400 | + | q := c.q | |
| 401 | + | // Handlers go on before the bind request is sent, so no early event of the | |
| 402 | + | // new object is lost. They run on the reader goroutine and only queue. | |
| 403 | + | out.OnName(func(name string) { | |
| 404 | + | e := event{kind: evOutputName, global: global} | |
| 405 | + | e.setName(name) | |
| 406 | + | q.post(&e) | |
| 407 | + | }) | |
| 408 | + | out.OnScale(func(factor int32) { | |
| 409 | + | e := event{kind: evOutputScale, global: global, a: factor} | |
| 410 | + | q.post(&e) | |
| 411 | + | }) | |
| 412 | + | out.OnMode(func(flags uint32, width, height, _ int32) { | |
| 413 | + | e := event{kind: evOutputMode, global: global, flags: flags, a: width, b: height} | |
| 414 | + | q.post(&e) | |
| 415 | + | }) | |
| 416 | + | out.OnDone(func() { | |
| 417 | + | e := event{kind: evOutputDone, global: global} | |
| 418 | + | q.post(&e) | |
| 419 | + | }) | |
| 420 | + | if err := c.display.Registry().Bind(global, wayland.OutputInterface, min(version, maxOutputVersion), out); err != nil { | |
| 421 | + | c.wlctx.Unregister(out) | |
| 422 | + | return fmt.Errorf("neferclient: bind wl_output %d: %w", global, err) | |
| 423 | + | } | |
| 424 | + | o.proxy = out | |
| 425 | + | i := len(c.outputs) | |
| 426 | + | c.outputs = append(c.outputs, o) | |
| 427 | + | for ; i > 0 && c.outputs[i-1].pub.Global > global; i-- { | |
| 428 | + | c.outputs[i], c.outputs[i-1] = c.outputs[i-1], c.outputs[i] | |
| 429 | + | } | |
| 430 | + | return nil | |
| 431 | + | } | |
| 432 | + | ||
| 433 | + | func (c *Conn) commitOutput(o *outputEntry, h Handler) { | |
| 434 | + | if string(o.nameBuf[:o.nameLen]) != o.pub.Name { | |
| 435 | + | o.pub.Name = string(o.nameBuf[:o.nameLen]) | |
| 436 | + | } | |
| 437 | + | o.pub.Scale, o.pub.Width, o.pub.Height = o.scale, o.width, o.height | |
| 438 | + | if !o.done { | |
| 439 | + | o.done = true | |
| 440 | + | if c.ready { | |
| 441 | + | h.OutputAdded(&o.pub) | |
| 442 | + | } | |
| 443 | + | } | |
| 444 | + | } | |
| 445 | + | ||
| 446 | + | func (c *Conn) removeOutput(global uint32, h Handler) { | |
| 447 | + | for i, o := range c.outputs { | |
| 448 | + | if o.pub.Global != global { | |
| 449 | + | continue | |
| 450 | + | } | |
| 451 | + | if err := o.proxy.Release(); err != nil { | |
| 452 | + | c.wlctx.Unregister(o.proxy) // version 1 and 2 have no release | |
| 453 | + | } | |
| 454 | + | c.outputs = append(c.outputs[:i], c.outputs[i+1:]...) | |
| 455 | + | if o.done { | |
| 456 | + | h.OutputRemoved(global) | |
| 457 | + | } | |
| 458 | + | return | |
| 459 | + | } | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | // Outputs returns the outputs that finished their first description, ordered | |
| 463 | + | // by Global. The slice is reused by the next call; owner goroutine only. | |
| 464 | + | func (c *Conn) Outputs() []Output { | |
| 465 | + | c.view = c.view[:0] | |
| 466 | + | for _, o := range c.outputs { | |
| 467 | + | if o.done { | |
| 468 | + | c.view = append(c.view, o.pub) | |
| 469 | + | } | |
| 470 | + | } | |
| 471 | + | return c.view | |
| 472 | + | } | |
| 473 | + | ||
| 474 | + | // Roundtrip sends wl_display.sync and dispatches until it returns. The reader | |
| 475 | + | // is paused meanwhile; the events it handled are queued for [Conn.Dispatch]. | |
| 476 | + | func (c *Conn) Roundtrip() error { | |
| 477 | + | if c.closed { | |
| 478 | + | return ErrClosed | |
| 479 | + | } | |
| 480 | + | c.pause() | |
| 481 | + | defer c.resume() | |
| 482 | + | return c.display.Roundtrip() | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | // Bind binds the global named iface at min(announced, version) and returns the | |
| 486 | + | // proxy and the negotiated version. newProxy creates the proxy on the given | |
| 487 | + | // context; register its event handlers inside newProxy: the reader is paused | |
| 488 | + | // until Bind returns, but the bound object's first events can arrive right | |
| 489 | + | // after, so a handler added later may miss them. A missing global yields an | |
| 490 | + | // error matching [ErrGlobalNotFound]. | |
| 491 | + | func (c *Conn) Bind[P wl.Proxy](iface string, version uint32, newProxy func(*wl.Context) P) (P, uint32, error) { | |
| 492 | + | var zero P | |
| 493 | + | if c.closed { | |
| 494 | + | return zero, 0, ErrClosed | |
| 495 | + | } | |
| 496 | + | c.pause() | |
| 497 | + | defer c.resume() | |
| 498 | + | p := newProxy(c.wlctx) | |
| 499 | + | v, err := c.display.Registry().BindNegotiated(iface, version, p) | |
| 500 | + | if err != nil { | |
| 501 | + | c.wlctx.Unregister(p) | |
| 502 | + | return zero, 0, err | |
| 503 | + | } | |
| 504 | + | return p, v, nil | |
| 505 | + | } | |
| 506 | + | ||
| 507 | + | // WatchFD reports readability of fd through Handler.FDReady(id). The watch is | |
| 508 | + | // one-shot: it is re-armed after each FDReady call returns. fd stays owned by | |
| 509 | + | // the caller and must be unwatched before it is closed. | |
| 510 | + | func (c *Conn) WatchFD(fd int, id uint64) error { | |
| 511 | + | if c.closed { | |
| 512 | + | return ErrClosed | |
| 513 | + | } | |
| 514 | + | if _, ok := c.watched[int32(fd)]; ok { | |
| 515 | + | return fmt.Errorf("neferclient: fd %d already watched", fd) | |
| 516 | + | } | |
| 517 | + | if err := c.armFD(fd, unix.EPOLL_CTL_ADD); err != nil { | |
| 518 | + | return err | |
| 519 | + | } | |
| 520 | + | c.watched[int32(fd)] = id | |
| 521 | + | return nil | |
| 522 | + | } | |
| 523 | + | ||
| 524 | + | // UnwatchFD stops watching fd. A notification already queued is dropped. | |
| 525 | + | func (c *Conn) UnwatchFD(fd int) error { | |
| 526 | + | if c.closed { | |
| 527 | + | return ErrClosed | |
| 528 | + | } | |
| 529 | + | if _, ok := c.watched[int32(fd)]; !ok { | |
| 530 | + | return fmt.Errorf("neferclient: fd %d not watched", fd) | |
| 531 | + | } | |
| 532 | + | delete(c.watched, int32(fd)) | |
| 533 | + | if err := unix.EpollCtl(c.epfd, unix.EPOLL_CTL_DEL, fd, nil); err != nil { | |
| 534 | + | return fmt.Errorf("neferclient: unwatch fd %d: %w", fd, err) | |
| 535 | + | } | |
| 536 | + | return nil | |
| 537 | + | } | |
| 538 | + | ||
| 539 | + | func (c *Conn) armFD(fd, op int) error { | |
| 540 | + | ev := unix.EpollEvent{Events: unix.EPOLLIN | unix.EPOLLONESHOT, Fd: int32(fd)} | |
| 541 | + | if err := unix.EpollCtl(c.epfd, op, fd, &ev); err != nil { | |
| 542 | + | return fmt.Errorf("neferclient: watch fd %d: %w", fd, err) | |
| 543 | + | } | |
| 544 | + | return nil | |
| 545 | + | } | |
| 546 | + | ||
| 547 | + | // Close closes the connection and stops both goroutines. It never requests an | |
| 548 | + | // unlock or any other protocol shutdown; those are explicit calls. It is safe | |
| 549 | + | // to call more than once. | |
| 550 | + | func (c *Conn) Close() error { | |
| 551 | + | if c == nil || c.closed { | |
| 552 | + | return nil | |
| 553 | + | } | |
| 554 | + | c.closed = true | |
| 555 | + | c.q.stop() | |
| 556 | + | err := c.display.Close() | |
| 557 | + | if c.readerStarted { | |
| 558 | + | <-c.readerDone | |
| 559 | + | } | |
| 560 | + | if c.epollStart { | |
| 561 | + | var one = [8]byte{1} | |
| 562 | + | _, _ = unix.Write(c.efd, one[:]) | |
| 563 | + | <-c.epollDone | |
| 564 | + | } | |
| 565 | + | if c.epfd >= 0 { | |
| 566 | + | _ = unix.Close(c.epfd) | |
| 567 | + | } | |
| 568 | + | if c.efd >= 0 { | |
| 569 | + | _ = unix.Close(c.efd) | |
| 570 | + | } | |
| 571 | + | return err | |
| 572 | + | } | |
| @@ -0,0 +1,572 @@ | |||||
| 1 | + | package neferclient | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "fmt" | |||
| 7 | + | "net" | |||
| 8 | + | "os" | |||
| 9 | + | "path/filepath" | |||
| 10 | + | "time" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/go-wayland-bindings/client/wayland" | |||
| 13 | + | "github.com/bnema/wlturbo" | |||
| 14 | + | "github.com/bnema/wlturbo/wl" | |||
| 15 | + | "golang.org/x/sys/unix" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // dialTimeout bounds the unix-socket dial; a local dial is normally instant. | |||
| 19 | + | const dialTimeout = 5 * time.Second | |||
| 20 | + | ||||
| 21 | + | // maxOutputVersion is the wl_output version bound: 4 adds names. | |||
| 22 | + | const maxOutputVersion = 4 | |||
| 23 | + | ||||
| 24 | + | var ( | |||
| 25 | + | // ErrClosed is returned by calls on a closed Conn. | |||
| 26 | + | ErrClosed = errors.New("neferclient: connection closed") | |||
| 27 | + | // ErrGlobalNotFound is returned by [Conn.Bind] when the compositor does | |||
| 28 | + | // not announce the interface. Test it with errors.Is. | |||
| 29 | + | ErrGlobalNotFound = wlturbo.ErrGlobalNotFound | |||
| 30 | + | ) | |||
| 31 | + | ||||
| 32 | + | // Output describes one wl_output after its latest wl_output.done. | |||
| 33 | + | type Output struct { | |||
| 34 | + | Global uint32 // registry name; identifies the output for its lifetime | |||
| 35 | + | Name string // wl_output.name, empty before version 4 | |||
| 36 | + | Scale int32 // integer scale, 1 until announced | |||
| 37 | + | Width, Height int32 // current mode in physical pixels | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // outputEntry holds the owner-side state of one bound wl_output. Properties | |||
| 41 | + | // accumulate in the pending fields and become visible at wl_output.done. | |||
| 42 | + | type outputEntry struct { | |||
| 43 | + | proxy *wayland.Output | |||
| 44 | + | done bool // first wl_output.done applied; the output is public | |||
| 45 | + | pub Output | |||
| 46 | + | ||||
| 47 | + | nameBuf [maxNameLen]byte | |||
| 48 | + | nameLen int | |||
| 49 | + | scale int32 | |||
| 50 | + | width, height int32 | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // Conn is one Wayland connection. Two goroutines run beside the owner: the | |||
| 54 | + | // reader, which only decodes wire events into a fixed queue, and an epoll | |||
| 55 | + | // waiter for [Conn.WatchFD]. Everything else — event handling, requests, | |||
| 56 | + | // Bind, Roundtrip, Outputs, Close — belongs to the owner goroutine, the one | |||
| 57 | + | // that calls [Conn.Dispatch]. | |||
| 58 | + | type Conn struct { | |||
| 59 | + | sock net.Conn | |||
| 60 | + | display *wl.Display | |||
| 61 | + | wlctx *wl.Context | |||
| 62 | + | q *queue | |||
| 63 | + | ||||
| 64 | + | readerStarted bool | |||
| 65 | + | readerDone chan struct{} | |||
| 66 | + | pauseDepth int | |||
| 67 | + | ||||
| 68 | + | epfd, efd int | |||
| 69 | + | epollStart bool | |||
| 70 | + | epollDone chan struct{} | |||
| 71 | + | watched map[int32]uint64 // fd -> caller id | |||
| 72 | + | ||||
| 73 | + | outputs []*outputEntry // sorted by Global | |||
| 74 | + | view []Output // reused by Outputs | |||
| 75 | + | ||||
| 76 | + | ready bool // Connect finished; announcements go to the Handler | |||
| 77 | + | dispatching bool | |||
| 78 | + | closed bool | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // Connect dials the compositor and completes the registry setup: every | |||
| 82 | + | // wl_output announced at that point is bound and its first description is | |||
| 83 | + | // available from [Conn.Outputs]. display names the socket relative to | |||
| 84 | + | // $XDG_RUNTIME_DIR, or is an absolute path; "" uses $WAYLAND_DISPLAY, then | |||
| 85 | + | // "wayland-0". Cancelling ctx abandons setup; afterwards ctx is not used. | |||
| 86 | + | func Connect(ctx context.Context, display string) (*Conn, error) { | |||
| 87 | + | if ctx == nil { | |||
| 88 | + | return nil, errors.New("neferclient: nil context") | |||
| 89 | + | } | |||
| 90 | + | if err := ctx.Err(); err != nil { | |||
| 91 | + | return nil, err | |||
| 92 | + | } | |||
| 93 | + | path, err := socketPath(display) | |||
| 94 | + | if err != nil { | |||
| 95 | + | return nil, err | |||
| 96 | + | } | |||
| 97 | + | sock, err := (&net.Dialer{Timeout: dialTimeout}).DialContext(ctx, "unix", path) | |||
| 98 | + | if err != nil { | |||
| 99 | + | return nil, fmt.Errorf("failed to connect to Wayland: %w", err) | |||
| 100 | + | } | |||
| 101 | + | return connectConn(ctx, sock) | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | func socketPath(name string) (string, error) { | |||
| 105 | + | if name == "" { | |||
| 106 | + | name = os.Getenv("WAYLAND_DISPLAY") | |||
| 107 | + | if name == "" { | |||
| 108 | + | name = "wayland-0" | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | if filepath.IsAbs(name) { | |||
| 112 | + | return name, nil | |||
| 113 | + | } | |||
| 114 | + | dir := os.Getenv("XDG_RUNTIME_DIR") | |||
| 115 | + | if dir == "" { | |||
| 116 | + | return "", errors.New("neferclient: XDG_RUNTIME_DIR not set") | |||
| 117 | + | } | |||
| 118 | + | return filepath.Join(dir, name), nil | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // connectConn runs the setup over an established connection and takes | |||
| 122 | + | // ownership of it. | |||
| 123 | + | func connectConn(ctx context.Context, sock net.Conn) (res *Conn, err error) { | |||
| 124 | + | display, err := wlturbo.ConnectFromConn(sock) | |||
| 125 | + | if err != nil { | |||
| 126 | + | _ = sock.Close() | |||
| 127 | + | return nil, err | |||
| 128 | + | } | |||
| 129 | + | c := &Conn{ | |||
| 130 | + | sock: sock, | |||
| 131 | + | display: display, | |||
| 132 | + | wlctx: display.Context(), | |||
| 133 | + | q: newQueue(true), // no reader yet: owner-side events are parked | |||
| 134 | + | efd: -1, | |||
| 135 | + | epfd: -1, | |||
| 136 | + | watched: make(map[int32]uint64), | |||
| 137 | + | } | |||
| 138 | + | watch := context.AfterFunc(ctx, func() { _ = display.Close() }) | |||
| 139 | + | defer func() { | |||
| 140 | + | if !watch() && err == nil { | |||
| 141 | + | err = ctx.Err() | |||
| 142 | + | } | |||
| 143 | + | if err != nil { | |||
| 144 | + | _ = c.Close() | |||
| 145 | + | res = nil | |||
| 146 | + | if ctx.Err() != nil { | |||
| 147 | + | err = errors.Join(ctx.Err(), err) | |||
| 148 | + | } | |||
| 149 | + | } | |||
| 150 | + | }() | |||
| 151 | + | if c.epfd, err = unix.EpollCreate1(unix.EPOLL_CLOEXEC); err != nil { | |||
| 152 | + | return nil, fmt.Errorf("neferclient: epoll: %w", err) | |||
| 153 | + | } | |||
| 154 | + | if c.efd, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil { | |||
| 155 | + | return nil, fmt.Errorf("neferclient: eventfd: %w", err) | |||
| 156 | + | } | |||
| 157 | + | ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: int32(c.efd)} | |||
| 158 | + | if err = unix.EpollCtl(c.epfd, unix.EPOLL_CTL_ADD, c.efd, &ev); err != nil { | |||
| 159 | + | return nil, fmt.Errorf("neferclient: epoll: %w", err) | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | reg := display.Registry() | |||
| 163 | + | reg.AddHandler(wayland.OutputInterface, func(_ *wl.Registry, name, version uint32) { | |||
| 164 | + | e := event{kind: evGlobal, global: name, version: version} | |||
| 165 | + | c.q.post(&e) | |||
| 166 | + | }) | |||
| 167 | + | reg.AddGlobalRemoveHandler(removeHook{c.q}) | |||
| 168 | + | ||||
| 169 | + | // With no reader running, the owner's own roundtrips park every event in | |||
| 170 | + | // the queue's held list, in order; applying it here is the startup drain. | |||
| 171 | + | if err = display.Roundtrip(); err != nil { | |||
| 172 | + | return nil, err | |||
| 173 | + | } | |||
| 174 | + | if err = c.applyHeld(); err != nil { | |||
| 175 | + | return nil, err | |||
| 176 | + | } | |||
| 177 | + | if err = display.Roundtrip(); err != nil { // wl_output properties follow the binds | |||
| 178 | + | return nil, err | |||
| 179 | + | } | |||
| 180 | + | if err = c.applyHeld(); err != nil { | |||
| 181 | + | return nil, err | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | c.ready = true | |||
| 185 | + | c.readerDone = make(chan struct{}) | |||
| 186 | + | c.readerStarted = true | |||
| 187 | + | c.epollDone = make(chan struct{}) | |||
| 188 | + | c.epollStart = true | |||
| 189 | + | c.q.setPausing(false) | |||
| 190 | + | go c.readLoop() | |||
| 191 | + | go c.epollLoop() | |||
| 192 | + | return c, nil | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | // removeHook forwards registry global_remove to the queue. | |||
| 196 | + | type removeHook struct{ q *queue } | |||
| 197 | + | ||||
| 198 | + | func (h removeHook) HandleRegistryGlobalRemove(e wl.RegistryGlobalRemoveEvent) { | |||
| 199 | + | ev := event{kind: evGlobalRemove, global: e.Name} | |||
| 200 | + | h.q.post(&ev) | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // setupHandler applies setup events without notifying anyone and keeps the | |||
| 204 | + | // first failure, which makes Connect fail. | |||
| 205 | + | type setupHandler struct { | |||
| 206 | + | nopHandler | |||
| 207 | + | err *error | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | func (h setupHandler) Error(err error) { | |||
| 211 | + | if *h.err == nil { | |||
| 212 | + | *h.err = err | |||
| 213 | + | } | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | // applyHeld applies the events parked during setup. | |||
| 217 | + | func (c *Conn) applyHeld() error { | |||
| 218 | + | var err error | |||
| 219 | + | h := setupHandler{err: &err} | |||
| 220 | + | held := c.q.takeHeld() | |||
| 221 | + | for i := range held { | |||
| 222 | + | c.apply(&held[i], h) | |||
| 223 | + | } | |||
| 224 | + | return err | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | // readLoop is the reader goroutine. It only calls Display.Dispatch; the | |||
| 228 | + | // generated handlers it triggers only push events into the queue. | |||
| 229 | + | func (c *Conn) readLoop() { | |||
| 230 | + | defer close(c.readerDone) | |||
| 231 | + | defer c.q.readerDone() | |||
| 232 | + | for { | |||
| 233 | + | if !c.q.gate() { | |||
| 234 | + | return | |||
| 235 | + | } | |||
| 236 | + | err := c.display.Dispatch() | |||
| 237 | + | if err == nil || errors.Is(err, os.ErrDeadlineExceeded) { | |||
| 238 | + | continue // a pause request interrupted the read | |||
| 239 | + | } | |||
| 240 | + | if !c.q.isStopped() { | |||
| 241 | + | c.q.setFatal(err) | |||
| 242 | + | } | |||
| 243 | + | return | |||
| 244 | + | } | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | // epollLoop is the second goroutine: it waits for watched descriptors and the | |||
| 248 | + | // shutdown eventfd. | |||
| 249 | + | func (c *Conn) epollLoop() { | |||
| 250 | + | defer close(c.epollDone) | |||
| 251 | + | var evs [8]unix.EpollEvent | |||
| 252 | + | for { | |||
| 253 | + | n, err := unix.EpollWait(c.epfd, evs[:], -1) | |||
| 254 | + | if err != nil { | |||
| 255 | + | if errors.Is(err, unix.EINTR) { | |||
| 256 | + | continue | |||
| 257 | + | } | |||
| 258 | + | c.q.setFatal(fmt.Errorf("neferclient: epoll wait: %w", err)) | |||
| 259 | + | return | |||
| 260 | + | } | |||
| 261 | + | for i := range n { | |||
| 262 | + | if evs[i].Fd == int32(c.efd) { | |||
| 263 | + | return | |||
| 264 | + | } | |||
| 265 | + | e := event{kind: evFDReady, fd: evs[i].Fd} | |||
| 266 | + | c.q.post(&e) | |||
| 267 | + | } | |||
| 268 | + | } | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | // pause parks the reader between two reads and returns once it is parked. | |||
| 272 | + | // Nested calls on the owner goroutine share the first pause. Before the reader | |||
| 273 | + | // starts, the queue already parks everything and pause does nothing. | |||
| 274 | + | func (c *Conn) pause() { | |||
| 275 | + | if !c.readerStarted { | |||
| 276 | + | return | |||
| 277 | + | } | |||
| 278 | + | c.pauseDepth++ | |||
| 279 | + | if c.pauseDepth > 1 { | |||
| 280 | + | return | |||
| 281 | + | } | |||
| 282 | + | c.q.setPausing(true) | |||
| 283 | + | _ = c.sock.SetReadDeadline(time.Unix(1, 0)) // wakes a blocked read | |||
| 284 | + | c.q.awaitParked() | |||
| 285 | + | _ = c.sock.SetReadDeadline(time.Time{}) | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | func (c *Conn) resume() { | |||
| 289 | + | if !c.readerStarted { | |||
| 290 | + | return | |||
| 291 | + | } | |||
| 292 | + | c.pauseDepth-- | |||
| 293 | + | if c.pauseDepth == 0 { | |||
| 294 | + | c.q.setPausing(false) | |||
| 295 | + | } | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | // Wake returns a channel with capacity 1 that is signaled whenever events are | |||
| 299 | + | // queued: call [Conn.Dispatch] when it fires. A signal can be stale (the | |||
| 300 | + | // events were already drained), so Dispatch may find nothing to do. | |||
| 301 | + | func (c *Conn) Wake() <-chan struct{} { return c.q.wake } | |||
| 302 | + | ||||
| 303 | + | // Context returns the wlturbo context that generated proxies are created on. | |||
| 304 | + | func (c *Conn) Context() *wl.Context { return c.wlctx } | |||
| 305 | + | ||||
| 306 | + | // Display returns the underlying wlturbo display. Only the owner goroutine | |||
| 307 | + | // may call Dispatch or Roundtrip on it; use [Conn.Dispatch] and | |||
| 308 | + | // [Conn.Roundtrip] instead. | |||
| 309 | + | func (c *Conn) Display() *wl.Display { return c.display } | |||
| 310 | + | ||||
| 311 | + | // Dispatch applies the events the reader queued and calls h for each one that | |||
| 312 | + | // matters to the application, on the calling goroutine. It handles at most one | |||
| 313 | + | // ring of events per call and signals [Conn.Wake] again if more are pending. | |||
| 314 | + | // A nil h discards the notifications but still applies the state changes. It | |||
| 315 | + | // returns the transport error once the queue is drained after the connection | |||
| 316 | + | // failed, and [ErrClosed] after Close. | |||
| 317 | + | func (c *Conn) Dispatch(h Handler) error { | |||
| 318 | + | if c.closed { | |||
| 319 | + | return ErrClosed | |||
| 320 | + | } | |||
| 321 | + | if c.dispatching { | |||
| 322 | + | return errors.New("neferclient: Dispatch called from a Handler") | |||
| 323 | + | } | |||
| 324 | + | if h == nil { | |||
| 325 | + | h = nopHandler{} | |||
| 326 | + | } | |||
| 327 | + | c.dispatching = true | |||
| 328 | + | defer func() { c.dispatching = false }() | |||
| 329 | + | var ev event | |||
| 330 | + | for range ringSize { | |||
| 331 | + | if !c.q.pop(&ev) { | |||
| 332 | + | return c.q.fatalErr() | |||
| 333 | + | } | |||
| 334 | + | c.apply(&ev, h) | |||
| 335 | + | if c.closed { | |||
| 336 | + | return ErrClosed | |||
| 337 | + | } | |||
| 338 | + | } | |||
| 339 | + | if c.q.pending() { | |||
| 340 | + | c.q.signal() | |||
| 341 | + | return nil | |||
| 342 | + | } | |||
| 343 | + | return c.q.fatalErr() | |||
| 344 | + | } | |||
| 345 | + | ||||
| 346 | + | // apply runs on the owner goroutine. | |||
| 347 | + | func (c *Conn) apply(ev *event, h Handler) { | |||
| 348 | + | switch ev.kind { | |||
| 349 | + | case evGlobal: | |||
| 350 | + | if err := c.bindOutput(ev.global, ev.version); err != nil { | |||
| 351 | + | h.Error(err) | |||
| 352 | + | } | |||
| 353 | + | case evGlobalRemove: | |||
| 354 | + | c.removeOutput(ev.global, h) | |||
| 355 | + | case evOutputName: | |||
| 356 | + | if o := c.entry(ev.global); o != nil { | |||
| 357 | + | o.nameLen = copy(o.nameBuf[:], ev.nameBytes()) | |||
| 358 | + | } | |||
| 359 | + | case evOutputScale: | |||
| 360 | + | if o := c.entry(ev.global); o != nil && ev.a > 0 { | |||
| 361 | + | o.scale = ev.a | |||
| 362 | + | } | |||
| 363 | + | case evOutputMode: | |||
| 364 | + | if o := c.entry(ev.global); o != nil && ev.flags&1 != 0 { // current mode | |||
| 365 | + | o.width, o.height = ev.a, ev.b | |||
| 366 | + | } | |||
| 367 | + | case evOutputDone: | |||
| 368 | + | if o := c.entry(ev.global); o != nil { | |||
| 369 | + | c.commitOutput(o, h) | |||
| 370 | + | } | |||
| 371 | + | case evFDReady: | |||
| 372 | + | id, ok := c.watched[ev.fd] | |||
| 373 | + | if !ok { | |||
| 374 | + | return // unwatched while the event was queued | |||
| 375 | + | } | |||
| 376 | + | h.FDReady(id) | |||
| 377 | + | if _, ok := c.watched[ev.fd]; ok { // the handler may have unwatched it | |||
| 378 | + | if err := c.armFD(int(ev.fd), unix.EPOLL_CTL_MOD); err != nil { | |||
| 379 | + | h.Error(err) | |||
| 380 | + | } | |||
| 381 | + | } | |||
| 382 | + | } | |||
| 383 | + | } | |||
| 384 | + | ||||
| 385 | + | func (c *Conn) entry(global uint32) *outputEntry { | |||
| 386 | + | for _, o := range c.outputs { | |||
| 387 | + | if o.pub.Global == global { | |||
| 388 | + | return o | |||
| 389 | + | } | |||
| 390 | + | } | |||
| 391 | + | return nil | |||
| 392 | + | } | |||
| 393 | + | ||||
| 394 | + | func (c *Conn) bindOutput(global, version uint32) error { | |||
| 395 | + | if version == 0 || c.entry(global) != nil { | |||
| 396 | + | return nil | |||
| 397 | + | } | |||
| 398 | + | o := &outputEntry{pub: Output{Global: global, Scale: 1}, scale: 1} | |||
| 399 | + | out := wayland.NewOutput(c.wlctx) | |||
| 400 | + | q := c.q | |||
| 401 | + | // Handlers go on before the bind request is sent, so no early event of the | |||
| 402 | + | // new object is lost. They run on the reader goroutine and only queue. | |||
| 403 | + | out.OnName(func(name string) { | |||
| 404 | + | e := event{kind: evOutputName, global: global} | |||
| 405 | + | e.setName(name) | |||
| 406 | + | q.post(&e) | |||
| 407 | + | }) | |||
| 408 | + | out.OnScale(func(factor int32) { | |||
| 409 | + | e := event{kind: evOutputScale, global: global, a: factor} | |||
| 410 | + | q.post(&e) | |||
| 411 | + | }) | |||
| 412 | + | out.OnMode(func(flags uint32, width, height, _ int32) { | |||
| 413 | + | e := event{kind: evOutputMode, global: global, flags: flags, a: width, b: height} | |||
| 414 | + | q.post(&e) | |||
| 415 | + | }) | |||
| 416 | + | out.OnDone(func() { | |||
| 417 | + | e := event{kind: evOutputDone, global: global} | |||
| 418 | + | q.post(&e) | |||
| 419 | + | }) | |||
| 420 | + | if err := c.display.Registry().Bind(global, wayland.OutputInterface, min(version, maxOutputVersion), out); err != nil { | |||
| 421 | + | c.wlctx.Unregister(out) | |||
| 422 | + | return fmt.Errorf("neferclient: bind wl_output %d: %w", global, err) | |||
| 423 | + | } | |||
| 424 | + | o.proxy = out | |||
| 425 | + | i := len(c.outputs) | |||
| 426 | + | c.outputs = append(c.outputs, o) | |||
| 427 | + | for ; i > 0 && c.outputs[i-1].pub.Global > global; i-- { | |||
| 428 | + | c.outputs[i], c.outputs[i-1] = c.outputs[i-1], c.outputs[i] | |||
| 429 | + | } | |||
| 430 | + | return nil | |||
| 431 | + | } | |||
| 432 | + | ||||
| 433 | + | func (c *Conn) commitOutput(o *outputEntry, h Handler) { | |||
| 434 | + | if string(o.nameBuf[:o.nameLen]) != o.pub.Name { | |||
| 435 | + | o.pub.Name = string(o.nameBuf[:o.nameLen]) | |||
| 436 | + | } | |||
| 437 | + | o.pub.Scale, o.pub.Width, o.pub.Height = o.scale, o.width, o.height | |||
| 438 | + | if !o.done { | |||
| 439 | + | o.done = true | |||
| 440 | + | if c.ready { | |||
| 441 | + | h.OutputAdded(&o.pub) | |||
| 442 | + | } | |||
| 443 | + | } | |||
| 444 | + | } | |||
| 445 | + | ||||
| 446 | + | func (c *Conn) removeOutput(global uint32, h Handler) { | |||
| 447 | + | for i, o := range c.outputs { | |||
| 448 | + | if o.pub.Global != global { | |||
| 449 | + | continue | |||
| 450 | + | } | |||
| 451 | + | if err := o.proxy.Release(); err != nil { | |||
| 452 | + | c.wlctx.Unregister(o.proxy) // version 1 and 2 have no release | |||
| 453 | + | } | |||
| 454 | + | c.outputs = append(c.outputs[:i], c.outputs[i+1:]...) | |||
| 455 | + | if o.done { | |||
| 456 | + | h.OutputRemoved(global) | |||
| 457 | + | } | |||
| 458 | + | return | |||
| 459 | + | } | |||
| 460 | + | } | |||
| 461 | + | ||||
| 462 | + | // Outputs returns the outputs that finished their first description, ordered | |||
| 463 | + | // by Global. The slice is reused by the next call; owner goroutine only. | |||
| 464 | + | func (c *Conn) Outputs() []Output { | |||
| 465 | + | c.view = c.view[:0] | |||
| 466 | + | for _, o := range c.outputs { | |||
| 467 | + | if o.done { | |||
| 468 | + | c.view = append(c.view, o.pub) | |||
| 469 | + | } | |||
| 470 | + | } | |||
| 471 | + | return c.view | |||
| 472 | + | } | |||
| 473 | + | ||||
| 474 | + | // Roundtrip sends wl_display.sync and dispatches until it returns. The reader | |||
| 475 | + | // is paused meanwhile; the events it handled are queued for [Conn.Dispatch]. | |||
| 476 | + | func (c *Conn) Roundtrip() error { | |||
| 477 | + | if c.closed { | |||
| 478 | + | return ErrClosed | |||
| 479 | + | } | |||
| 480 | + | c.pause() | |||
| 481 | + | defer c.resume() | |||
| 482 | + | return c.display.Roundtrip() | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | // Bind binds the global named iface at min(announced, version) and returns the | |||
| 486 | + | // proxy and the negotiated version. newProxy creates the proxy on the given | |||
| 487 | + | // context; register its event handlers inside newProxy: the reader is paused | |||
| 488 | + | // until Bind returns, but the bound object's first events can arrive right | |||
| 489 | + | // after, so a handler added later may miss them. A missing global yields an | |||
| 490 | + | // error matching [ErrGlobalNotFound]. | |||
| 491 | + | func (c *Conn) Bind[P wl.Proxy](iface string, version uint32, newProxy func(*wl.Context) P) (P, uint32, error) { | |||
| 492 | + | var zero P | |||
| 493 | + | if c.closed { | |||
| 494 | + | return zero, 0, ErrClosed | |||
| 495 | + | } | |||
| 496 | + | c.pause() | |||
| 497 | + | defer c.resume() | |||
| 498 | + | p := newProxy(c.wlctx) | |||
| 499 | + | v, err := c.display.Registry().BindNegotiated(iface, version, p) | |||
| 500 | + | if err != nil { | |||
| 501 | + | c.wlctx.Unregister(p) | |||
| 502 | + | return zero, 0, err | |||
| 503 | + | } | |||
| 504 | + | return p, v, nil | |||
| 505 | + | } | |||
| 506 | + | ||||
| 507 | + | // WatchFD reports readability of fd through Handler.FDReady(id). The watch is | |||
| 508 | + | // one-shot: it is re-armed after each FDReady call returns. fd stays owned by | |||
| 509 | + | // the caller and must be unwatched before it is closed. | |||
| 510 | + | func (c *Conn) WatchFD(fd int, id uint64) error { | |||
| 511 | + | if c.closed { | |||
| 512 | + | return ErrClosed | |||
| 513 | + | } | |||
| 514 | + | if _, ok := c.watched[int32(fd)]; ok { | |||
| 515 | + | return fmt.Errorf("neferclient: fd %d already watched", fd) | |||
| 516 | + | } | |||
| 517 | + | if err := c.armFD(fd, unix.EPOLL_CTL_ADD); err != nil { | |||
| 518 | + | return err | |||
| 519 | + | } | |||
| 520 | + | c.watched[int32(fd)] = id | |||
| 521 | + | return nil | |||
| 522 | + | } | |||
| 523 | + | ||||
| 524 | + | // UnwatchFD stops watching fd. A notification already queued is dropped. | |||
| 525 | + | func (c *Conn) UnwatchFD(fd int) error { | |||
| 526 | + | if c.closed { | |||
| 527 | + | return ErrClosed | |||
| 528 | + | } | |||
| 529 | + | if _, ok := c.watched[int32(fd)]; !ok { | |||
| 530 | + | return fmt.Errorf("neferclient: fd %d not watched", fd) | |||
| 531 | + | } | |||
| 532 | + | delete(c.watched, int32(fd)) | |||
| 533 | + | if err := unix.EpollCtl(c.epfd, unix.EPOLL_CTL_DEL, fd, nil); err != nil { | |||
| 534 | + | return fmt.Errorf("neferclient: unwatch fd %d: %w", fd, err) | |||
| 535 | + | } | |||
| 536 | + | return nil | |||
| 537 | + | } | |||
| 538 | + | ||||
| 539 | + | func (c *Conn) armFD(fd, op int) error { | |||
| 540 | + | ev := unix.EpollEvent{Events: unix.EPOLLIN | unix.EPOLLONESHOT, Fd: int32(fd)} | |||
| 541 | + | if err := unix.EpollCtl(c.epfd, op, fd, &ev); err != nil { | |||
| 542 | + | return fmt.Errorf("neferclient: watch fd %d: %w", fd, err) | |||
| 543 | + | } | |||
| 544 | + | return nil | |||
| 545 | + | } | |||
| 546 | + | ||||
| 547 | + | // Close closes the connection and stops both goroutines. It never requests an | |||
| 548 | + | // unlock or any other protocol shutdown; those are explicit calls. It is safe | |||
| 549 | + | // to call more than once. | |||
| 550 | + | func (c *Conn) Close() error { | |||
| 551 | + | if c == nil || c.closed { | |||
| 552 | + | return nil | |||
| 553 | + | } | |||
| 554 | + | c.closed = true | |||
| 555 | + | c.q.stop() | |||
| 556 | + | err := c.display.Close() | |||
| 557 | + | if c.readerStarted { | |||
| 558 | + | <-c.readerDone | |||
| 559 | + | } | |||
| 560 | + | if c.epollStart { | |||
| 561 | + | var one = [8]byte{1} | |||
| 562 | + | _, _ = unix.Write(c.efd, one[:]) | |||
| 563 | + | <-c.epollDone | |||
| 564 | + | } | |||
| 565 | + | if c.epfd >= 0 { | |||
| 566 | + | _ = unix.Close(c.epfd) | |||
| 567 | + | } | |||
| 568 | + | if c.efd >= 0 { | |||
| 569 | + | _ = unix.Close(c.efd) | |||
| 570 | + | } | |||
| 571 | + | return err | |||
| 572 | + | } | |||
M go.mod +12 -0
| @@ -1,3 +1,15 @@ | |||
| 1 | 1 | module github.com/bnema/neferclient | |
| 2 | 2 | ||
| 3 | 3 | go 1.27 | |
| 4 | + | ||
| 5 | + | require ( | |
| 6 | + | github.com/bnema/go-wayland-bindings v0.1.0 | |
| 7 | + | github.com/bnema/wlturbo v0.6.0 | |
| 8 | + | github.com/stretchr/testify v1.12.1 | |
| 9 | + | golang.org/x/sys v0.48.0 | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | require ( | |
| 13 | + | github.com/stretchr/objx v0.5.3 // indirect | |
| 14 | + | go.yaml.in/yaml/v3 v3.0.5 // indirect | |
| 15 | + | ) | |
| @@ -1,3 +1,15 @@ | |||||
| 1 | module github.com/bnema/neferclient | 1 | module github.com/bnema/neferclient | ||
| 2 | 2 | ||||
| 3 | go 1.27 | 3 | go 1.27 | ||
| 4 | + | ||||
| 5 | + | require ( | |||
| 6 | + | github.com/bnema/go-wayland-bindings v0.1.0 | |||
| 7 | + | github.com/bnema/wlturbo v0.6.0 | |||
| 8 | + | github.com/stretchr/testify v1.12.1 | |||
| 9 | + | golang.org/x/sys v0.48.0 | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | require ( | |||
| 13 | + | github.com/stretchr/objx v0.5.3 // indirect | |||
| 14 | + | go.yaml.in/yaml/v3 v3.0.5 // indirect | |||
| 15 | + | ) | |||
A go.sum +8 -0 generated file: collapsed
A handler.go +35 -0
| @@ -0,0 +1,35 @@ | |||
| 1 | + | package neferclient | |
| 2 | + | ||
| 3 | + | // Handler receives the events [Conn.Dispatch] drains. Every method is called | |
| 4 | + | // on the goroutine that called Dispatch, never on the reader goroutine, so an | |
| 5 | + | // implementation may send requests and call [Conn.Bind], [Conn.Roundtrip], | |
| 6 | + | // [Conn.WatchFD] and [Conn.UnwatchFD] from inside any method. It must not call | |
| 7 | + | // Dispatch. | |
| 8 | + | type Handler interface { | |
| 9 | + | // OutputAdded reports an output that became complete after [Connect] | |
| 10 | + | // returned: the compositor sent its first wl_output.done. Outputs that | |
| 11 | + | // existed during Connect are not reported; read them with [Conn.Outputs]. | |
| 12 | + | // Later property changes are applied to [Conn.Outputs] silently. out is | |
| 13 | + | // valid only during the call. | |
| 14 | + | OutputAdded(out *Output) | |
| 15 | + | // OutputRemoved reports that the wl_output global went away. The output is | |
| 16 | + | // already gone from [Conn.Outputs]. | |
| 17 | + | OutputRemoved(global uint32) | |
| 18 | + | // FDReady reports that a descriptor registered with [Conn.WatchFD] is | |
| 19 | + | // readable. The watch is re-armed when the call returns. It can be | |
| 20 | + | // spurious: drain the descriptor without blocking. | |
| 21 | + | FDReady(id uint64) | |
| 22 | + | // Error reports a non-fatal failure while handling an event, for example | |
| 23 | + | // a wl_output that could not be bound. Fatal transport errors are returned | |
| 24 | + | // by Dispatch instead. | |
| 25 | + | Error(err error) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // nopHandler discards every event. Connect uses it to apply the events queued | |
| 29 | + | // during setup, and Dispatch uses it for a nil Handler. | |
| 30 | + | type nopHandler struct{} | |
| 31 | + | ||
| 32 | + | func (nopHandler) OutputAdded(*Output) {} | |
| 33 | + | func (nopHandler) OutputRemoved(uint32) {} | |
| 34 | + | func (nopHandler) FDReady(uint64) {} | |
| 35 | + | func (nopHandler) Error(error) {} | |
| @@ -0,0 +1,35 @@ | |||||
| 1 | + | package neferclient | |||
| 2 | + | ||||
| 3 | + | // Handler receives the events [Conn.Dispatch] drains. Every method is called | |||
| 4 | + | // on the goroutine that called Dispatch, never on the reader goroutine, so an | |||
| 5 | + | // implementation may send requests and call [Conn.Bind], [Conn.Roundtrip], | |||
| 6 | + | // [Conn.WatchFD] and [Conn.UnwatchFD] from inside any method. It must not call | |||
| 7 | + | // Dispatch. | |||
| 8 | + | type Handler interface { | |||
| 9 | + | // OutputAdded reports an output that became complete after [Connect] | |||
| 10 | + | // returned: the compositor sent its first wl_output.done. Outputs that | |||
| 11 | + | // existed during Connect are not reported; read them with [Conn.Outputs]. | |||
| 12 | + | // Later property changes are applied to [Conn.Outputs] silently. out is | |||
| 13 | + | // valid only during the call. | |||
| 14 | + | OutputAdded(out *Output) | |||
| 15 | + | // OutputRemoved reports that the wl_output global went away. The output is | |||
| 16 | + | // already gone from [Conn.Outputs]. | |||
| 17 | + | OutputRemoved(global uint32) | |||
| 18 | + | // FDReady reports that a descriptor registered with [Conn.WatchFD] is | |||
| 19 | + | // readable. The watch is re-armed when the call returns. It can be | |||
| 20 | + | // spurious: drain the descriptor without blocking. | |||
| 21 | + | FDReady(id uint64) | |||
| 22 | + | // Error reports a non-fatal failure while handling an event, for example | |||
| 23 | + | // a wl_output that could not be bound. Fatal transport errors are returned | |||
| 24 | + | // by Dispatch instead. | |||
| 25 | + | Error(err error) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // nopHandler discards every event. Connect uses it to apply the events queued | |||
| 29 | + | // during setup, and Dispatch uses it for a nil Handler. | |||
| 30 | + | type nopHandler struct{} | |||
| 31 | + | ||||
| 32 | + | func (nopHandler) OutputAdded(*Output) {} | |||
| 33 | + | func (nopHandler) OutputRemoved(uint32) {} | |||
| 34 | + | func (nopHandler) FDReady(uint64) {} | |||
| 35 | + | func (nopHandler) Error(error) {} | |||
A mocks/handler_mock.go +206 -0
| @@ -0,0 +1,206 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package neferclientmocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "github.com/bnema/neferclient" | |
| 9 | + | mock "github.com/stretchr/testify/mock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // NewMockHandler creates a new instance of MockHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 13 | + | // The first argument is typically a *testing.T value. | |
| 14 | + | func NewMockHandler(t interface { | |
| 15 | + | mock.TestingT | |
| 16 | + | Cleanup(func()) | |
| 17 | + | }) *MockHandler { | |
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 19 | + | helper.Helper() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | mock := &MockHandler{} | |
| 23 | + | mock.Mock.Test(t) | |
| 24 | + | ||
| 25 | + | t.Cleanup(func() { | |
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 27 | + | helper.Helper() | |
| 28 | + | } | |
| 29 | + | mock.AssertExpectations(t) | |
| 30 | + | }) | |
| 31 | + | ||
| 32 | + | return mock | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // MockHandler is an autogenerated mock type for the Handler type | |
| 36 | + | type MockHandler struct { | |
| 37 | + | mock.Mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | type MockHandler_Expecter struct { | |
| 41 | + | mock *mock.Mock | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func (_m *MockHandler) EXPECT() *MockHandler_Expecter { | |
| 45 | + | return &MockHandler_Expecter{mock: &_m.Mock} | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Error provides a mock function for the type MockHandler | |
| 49 | + | func (_mock *MockHandler) Error(err error) { | |
| 50 | + | _mock.Called(err) | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // MockHandler_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' | |
| 55 | + | type MockHandler_Error_Call struct { | |
| 56 | + | *mock.Call | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // Error is a helper method to define mock.On call | |
| 60 | + | // - err error | |
| 61 | + | func (_e *MockHandler_Expecter) Error(err any) *MockHandler_Error_Call { | |
| 62 | + | return &MockHandler_Error_Call{Call: _e.mock.On("Error", err)} | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func (_c *MockHandler_Error_Call) Run(run func(err error)) *MockHandler_Error_Call { | |
| 66 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 67 | + | var arg0 error | |
| 68 | + | if args[0] != nil { | |
| 69 | + | arg0 = args[0].(error) | |
| 70 | + | } | |
| 71 | + | run( | |
| 72 | + | arg0, | |
| 73 | + | ) | |
| 74 | + | }) | |
| 75 | + | return _c | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (_c *MockHandler_Error_Call) Return() *MockHandler_Error_Call { | |
| 79 | + | _c.Call.Return() | |
| 80 | + | return _c | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func (_c *MockHandler_Error_Call) RunAndReturn(run func(err error)) *MockHandler_Error_Call { | |
| 84 | + | _c.Run(run) | |
| 85 | + | return _c | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // FDReady provides a mock function for the type MockHandler | |
| 89 | + | func (_mock *MockHandler) FDReady(id uint64) { | |
| 90 | + | _mock.Called(id) | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // MockHandler_FDReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FDReady' | |
| 95 | + | type MockHandler_FDReady_Call struct { | |
| 96 | + | *mock.Call | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // FDReady is a helper method to define mock.On call | |
| 100 | + | // - id uint64 | |
| 101 | + | func (_e *MockHandler_Expecter) FDReady(id any) *MockHandler_FDReady_Call { | |
| 102 | + | return &MockHandler_FDReady_Call{Call: _e.mock.On("FDReady", id)} | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func (_c *MockHandler_FDReady_Call) Run(run func(id uint64)) *MockHandler_FDReady_Call { | |
| 106 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 107 | + | var arg0 uint64 | |
| 108 | + | if args[0] != nil { | |
| 109 | + | arg0 = args[0].(uint64) | |
| 110 | + | } | |
| 111 | + | run( | |
| 112 | + | arg0, | |
| 113 | + | ) | |
| 114 | + | }) | |
| 115 | + | return _c | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | func (_c *MockHandler_FDReady_Call) Return() *MockHandler_FDReady_Call { | |
| 119 | + | _c.Call.Return() | |
| 120 | + | return _c | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func (_c *MockHandler_FDReady_Call) RunAndReturn(run func(id uint64)) *MockHandler_FDReady_Call { | |
| 124 | + | _c.Run(run) | |
| 125 | + | return _c | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // OutputAdded provides a mock function for the type MockHandler | |
| 129 | + | func (_mock *MockHandler) OutputAdded(out *neferclient.Output) { | |
| 130 | + | _mock.Called(out) | |
| 131 | + | return | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // MockHandler_OutputAdded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputAdded' | |
| 135 | + | type MockHandler_OutputAdded_Call struct { | |
| 136 | + | *mock.Call | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // OutputAdded is a helper method to define mock.On call | |
| 140 | + | // - out *neferclient.Output | |
| 141 | + | func (_e *MockHandler_Expecter) OutputAdded(out any) *MockHandler_OutputAdded_Call { | |
| 142 | + | return &MockHandler_OutputAdded_Call{Call: _e.mock.On("OutputAdded", out)} | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func (_c *MockHandler_OutputAdded_Call) Run(run func(out *neferclient.Output)) *MockHandler_OutputAdded_Call { | |
| 146 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 147 | + | var arg0 *neferclient.Output | |
| 148 | + | if args[0] != nil { | |
| 149 | + | arg0 = args[0].(*neferclient.Output) | |
| 150 | + | } | |
| 151 | + | run( | |
| 152 | + | arg0, | |
| 153 | + | ) | |
| 154 | + | }) | |
| 155 | + | return _c | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | func (_c *MockHandler_OutputAdded_Call) Return() *MockHandler_OutputAdded_Call { | |
| 159 | + | _c.Call.Return() | |
| 160 | + | return _c | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func (_c *MockHandler_OutputAdded_Call) RunAndReturn(run func(out *neferclient.Output)) *MockHandler_OutputAdded_Call { | |
| 164 | + | _c.Run(run) | |
| 165 | + | return _c | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // OutputRemoved provides a mock function for the type MockHandler | |
| 169 | + | func (_mock *MockHandler) OutputRemoved(global uint32) { | |
| 170 | + | _mock.Called(global) | |
| 171 | + | return | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | // MockHandler_OutputRemoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputRemoved' | |
| 175 | + | type MockHandler_OutputRemoved_Call struct { | |
| 176 | + | *mock.Call | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // OutputRemoved is a helper method to define mock.On call | |
| 180 | + | // - global uint32 | |
| 181 | + | func (_e *MockHandler_Expecter) OutputRemoved(global any) *MockHandler_OutputRemoved_Call { | |
| 182 | + | return &MockHandler_OutputRemoved_Call{Call: _e.mock.On("OutputRemoved", global)} | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func (_c *MockHandler_OutputRemoved_Call) Run(run func(global uint32)) *MockHandler_OutputRemoved_Call { | |
| 186 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 187 | + | var arg0 uint32 | |
| 188 | + | if args[0] != nil { | |
| 189 | + | arg0 = args[0].(uint32) | |
| 190 | + | } | |
| 191 | + | run( | |
| 192 | + | arg0, | |
| 193 | + | ) | |
| 194 | + | }) | |
| 195 | + | return _c | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | func (_c *MockHandler_OutputRemoved_Call) Return() *MockHandler_OutputRemoved_Call { | |
| 199 | + | _c.Call.Return() | |
| 200 | + | return _c | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32)) *MockHandler_OutputRemoved_Call { | |
| 204 | + | _c.Run(run) | |
| 205 | + | return _c | |
| 206 | + | } | |
| @@ -0,0 +1,206 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package neferclientmocks | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "github.com/bnema/neferclient" | |||
| 9 | + | mock "github.com/stretchr/testify/mock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // NewMockHandler creates a new instance of MockHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 13 | + | // The first argument is typically a *testing.T value. | |||
| 14 | + | func NewMockHandler(t interface { | |||
| 15 | + | mock.TestingT | |||
| 16 | + | Cleanup(func()) | |||
| 17 | + | }) *MockHandler { | |||
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 19 | + | helper.Helper() | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | mock := &MockHandler{} | |||
| 23 | + | mock.Mock.Test(t) | |||
| 24 | + | ||||
| 25 | + | t.Cleanup(func() { | |||
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 27 | + | helper.Helper() | |||
| 28 | + | } | |||
| 29 | + | mock.AssertExpectations(t) | |||
| 30 | + | }) | |||
| 31 | + | ||||
| 32 | + | return mock | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // MockHandler is an autogenerated mock type for the Handler type | |||
| 36 | + | type MockHandler struct { | |||
| 37 | + | mock.Mock | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | type MockHandler_Expecter struct { | |||
| 41 | + | mock *mock.Mock | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func (_m *MockHandler) EXPECT() *MockHandler_Expecter { | |||
| 45 | + | return &MockHandler_Expecter{mock: &_m.Mock} | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // Error provides a mock function for the type MockHandler | |||
| 49 | + | func (_mock *MockHandler) Error(err error) { | |||
| 50 | + | _mock.Called(err) | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // MockHandler_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' | |||
| 55 | + | type MockHandler_Error_Call struct { | |||
| 56 | + | *mock.Call | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // Error is a helper method to define mock.On call | |||
| 60 | + | // - err error | |||
| 61 | + | func (_e *MockHandler_Expecter) Error(err any) *MockHandler_Error_Call { | |||
| 62 | + | return &MockHandler_Error_Call{Call: _e.mock.On("Error", err)} | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func (_c *MockHandler_Error_Call) Run(run func(err error)) *MockHandler_Error_Call { | |||
| 66 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 67 | + | var arg0 error | |||
| 68 | + | if args[0] != nil { | |||
| 69 | + | arg0 = args[0].(error) | |||
| 70 | + | } | |||
| 71 | + | run( | |||
| 72 | + | arg0, | |||
| 73 | + | ) | |||
| 74 | + | }) | |||
| 75 | + | return _c | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (_c *MockHandler_Error_Call) Return() *MockHandler_Error_Call { | |||
| 79 | + | _c.Call.Return() | |||
| 80 | + | return _c | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func (_c *MockHandler_Error_Call) RunAndReturn(run func(err error)) *MockHandler_Error_Call { | |||
| 84 | + | _c.Run(run) | |||
| 85 | + | return _c | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // FDReady provides a mock function for the type MockHandler | |||
| 89 | + | func (_mock *MockHandler) FDReady(id uint64) { | |||
| 90 | + | _mock.Called(id) | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // MockHandler_FDReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FDReady' | |||
| 95 | + | type MockHandler_FDReady_Call struct { | |||
| 96 | + | *mock.Call | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // FDReady is a helper method to define mock.On call | |||
| 100 | + | // - id uint64 | |||
| 101 | + | func (_e *MockHandler_Expecter) FDReady(id any) *MockHandler_FDReady_Call { | |||
| 102 | + | return &MockHandler_FDReady_Call{Call: _e.mock.On("FDReady", id)} | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func (_c *MockHandler_FDReady_Call) Run(run func(id uint64)) *MockHandler_FDReady_Call { | |||
| 106 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 107 | + | var arg0 uint64 | |||
| 108 | + | if args[0] != nil { | |||
| 109 | + | arg0 = args[0].(uint64) | |||
| 110 | + | } | |||
| 111 | + | run( | |||
| 112 | + | arg0, | |||
| 113 | + | ) | |||
| 114 | + | }) | |||
| 115 | + | return _c | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | func (_c *MockHandler_FDReady_Call) Return() *MockHandler_FDReady_Call { | |||
| 119 | + | _c.Call.Return() | |||
| 120 | + | return _c | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func (_c *MockHandler_FDReady_Call) RunAndReturn(run func(id uint64)) *MockHandler_FDReady_Call { | |||
| 124 | + | _c.Run(run) | |||
| 125 | + | return _c | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // OutputAdded provides a mock function for the type MockHandler | |||
| 129 | + | func (_mock *MockHandler) OutputAdded(out *neferclient.Output) { | |||
| 130 | + | _mock.Called(out) | |||
| 131 | + | return | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | // MockHandler_OutputAdded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputAdded' | |||
| 135 | + | type MockHandler_OutputAdded_Call struct { | |||
| 136 | + | *mock.Call | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | // OutputAdded is a helper method to define mock.On call | |||
| 140 | + | // - out *neferclient.Output | |||
| 141 | + | func (_e *MockHandler_Expecter) OutputAdded(out any) *MockHandler_OutputAdded_Call { | |||
| 142 | + | return &MockHandler_OutputAdded_Call{Call: _e.mock.On("OutputAdded", out)} | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | func (_c *MockHandler_OutputAdded_Call) Run(run func(out *neferclient.Output)) *MockHandler_OutputAdded_Call { | |||
| 146 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 147 | + | var arg0 *neferclient.Output | |||
| 148 | + | if args[0] != nil { | |||
| 149 | + | arg0 = args[0].(*neferclient.Output) | |||
| 150 | + | } | |||
| 151 | + | run( | |||
| 152 | + | arg0, | |||
| 153 | + | ) | |||
| 154 | + | }) | |||
| 155 | + | return _c | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | func (_c *MockHandler_OutputAdded_Call) Return() *MockHandler_OutputAdded_Call { | |||
| 159 | + | _c.Call.Return() | |||
| 160 | + | return _c | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | func (_c *MockHandler_OutputAdded_Call) RunAndReturn(run func(out *neferclient.Output)) *MockHandler_OutputAdded_Call { | |||
| 164 | + | _c.Run(run) | |||
| 165 | + | return _c | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // OutputRemoved provides a mock function for the type MockHandler | |||
| 169 | + | func (_mock *MockHandler) OutputRemoved(global uint32) { | |||
| 170 | + | _mock.Called(global) | |||
| 171 | + | return | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | // MockHandler_OutputRemoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputRemoved' | |||
| 175 | + | type MockHandler_OutputRemoved_Call struct { | |||
| 176 | + | *mock.Call | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // OutputRemoved is a helper method to define mock.On call | |||
| 180 | + | // - global uint32 | |||
| 181 | + | func (_e *MockHandler_Expecter) OutputRemoved(global any) *MockHandler_OutputRemoved_Call { | |||
| 182 | + | return &MockHandler_OutputRemoved_Call{Call: _e.mock.On("OutputRemoved", global)} | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | func (_c *MockHandler_OutputRemoved_Call) Run(run func(global uint32)) *MockHandler_OutputRemoved_Call { | |||
| 186 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 187 | + | var arg0 uint32 | |||
| 188 | + | if args[0] != nil { | |||
| 189 | + | arg0 = args[0].(uint32) | |||
| 190 | + | } | |||
| 191 | + | run( | |||
| 192 | + | arg0, | |||
| 193 | + | ) | |||
| 194 | + | }) | |||
| 195 | + | return _c | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | func (_c *MockHandler_OutputRemoved_Call) Return() *MockHandler_OutputRemoved_Call { | |||
| 199 | + | _c.Call.Return() | |||
| 200 | + | return _c | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32)) *MockHandler_OutputRemoved_Call { | |||
| 204 | + | _c.Run(run) | |||
| 205 | + | return _c | |||
| 206 | + | } | |||
A queue.go +242 -0
| @@ -0,0 +1,242 @@ | |||
| 1 | + | package neferclient | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | const ( | |
| 8 | + | ringSize = 256 // events queued between the reader and Dispatch | |
| 9 | + | maxNameLen = 64 // longest output name kept; longer names are truncated | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type eventKind uint8 | |
| 13 | + | ||
| 14 | + | const ( | |
| 15 | + | evGlobal eventKind = iota + 1 // registry global (wl_output only): global, version | |
| 16 | + | evGlobalRemove // registry global_remove: global | |
| 17 | + | evOutputName // name, global | |
| 18 | + | evOutputScale // a = factor, global | |
| 19 | + | evOutputMode // flags, a = width, b = height, global | |
| 20 | + | evOutputDone // global | |
| 21 | + | evFDReady // fd (epoll data) | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | // event is the only thing the reader and the epoll goroutine hand to the | |
| 25 | + | // owner. It holds no pointers except through fixed storage, so queuing one | |
| 26 | + | // never allocates. | |
| 27 | + | type event struct { | |
| 28 | + | kind eventKind | |
| 29 | + | nameLen uint8 | |
| 30 | + | global uint32 | |
| 31 | + | version uint32 | |
| 32 | + | flags uint32 | |
| 33 | + | fd int32 | |
| 34 | + | a, b int32 | |
| 35 | + | name [maxNameLen]byte | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func (e *event) setName(s string) { | |
| 39 | + | e.nameLen = uint8(copy(e.name[:], s)) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func (e *event) nameBytes() []byte { return e.name[:e.nameLen] } | |
| 43 | + | ||
| 44 | + | // queue is the bounded hand-off between producers (the reader goroutine and | |
| 45 | + | // the epoll goroutine) and the owner goroutine, plus the reader pause gate. | |
| 46 | + | // | |
| 47 | + | // A producer facing a full ring waits on cond; Dispatch releases it as it | |
| 48 | + | // drains. A pause request also releases it: while pausing, producers park | |
| 49 | + | // events in held instead of waiting, so neither side can block on the other. | |
| 50 | + | // Parked events are re-posted, in order, by the reader once it resumes. | |
| 51 | + | type queue struct { | |
| 52 | + | mu sync.Mutex | |
| 53 | + | cond sync.Cond // L = μ one cond for every state change, all are rare | |
| 54 | + | ||
| 55 | + | ring [ringSize]event | |
| 56 | + | head int | |
| 57 | + | n int | |
| 58 | + | ||
| 59 | + | pausing bool | |
| 60 | + | readerParked bool | |
| 61 | + | readerExit bool | |
| 62 | + | stopped bool | |
| 63 | + | held []event | |
| 64 | + | spare []event // capacity reused by the next held generation | |
| 65 | + | fatal error | |
| 66 | + | ||
| 67 | + | wake chan struct{} | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func newQueue(pausing bool) *queue { | |
| 71 | + | q := &queue{pausing: pausing, wake: make(chan struct{}, 1)} | |
| 72 | + | q.cond.L = &q.mu | |
| 73 | + | return q | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func (q *queue) signal() { | |
| 77 | + | select { | |
| 78 | + | case q.wake <- struct{}{}: | |
| 79 | + | default: | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // post queues ev. It blocks while the ring is full and not pausing. | |
| 84 | + | func (q *queue) post(ev *event) { | |
| 85 | + | q.mu.Lock() | |
| 86 | + | for { | |
| 87 | + | switch { | |
| 88 | + | case q.stopped: | |
| 89 | + | q.mu.Unlock() | |
| 90 | + | return | |
| 91 | + | case q.pausing: | |
| 92 | + | q.held = append(q.held, *ev) | |
| 93 | + | q.mu.Unlock() | |
| 94 | + | return | |
| 95 | + | case q.n < ringSize: | |
| 96 | + | q.ring[(q.head+q.n)%ringSize] = *ev | |
| 97 | + | q.n++ | |
| 98 | + | q.mu.Unlock() | |
| 99 | + | q.signal() | |
| 100 | + | return | |
| 101 | + | } | |
| 102 | + | q.cond.Wait() | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // pop moves the oldest queued event into ev. | |
| 107 | + | func (q *queue) pop(ev *event) bool { | |
| 108 | + | q.mu.Lock() | |
| 109 | + | if q.n == 0 { | |
| 110 | + | q.mu.Unlock() | |
| 111 | + | return false | |
| 112 | + | } | |
| 113 | + | *ev = q.ring[q.head] | |
| 114 | + | q.head = (q.head + 1) % ringSize | |
| 115 | + | full := q.n == ringSize | |
| 116 | + | q.n-- | |
| 117 | + | if full { | |
| 118 | + | q.cond.Broadcast() | |
| 119 | + | } | |
| 120 | + | q.mu.Unlock() | |
| 121 | + | return true | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // setFatal records the first transport failure; Dispatch returns it once the | |
| 125 | + | // events queued before it are drained. | |
| 126 | + | func (q *queue) setFatal(err error) { | |
| 127 | + | q.mu.Lock() | |
| 128 | + | if q.fatal == nil && !q.stopped { | |
| 129 | + | q.fatal = err | |
| 130 | + | } | |
| 131 | + | q.mu.Unlock() | |
| 132 | + | q.signal() | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func (q *queue) fatalErr() error { | |
| 136 | + | q.mu.Lock() | |
| 137 | + | err := q.fatal | |
| 138 | + | q.mu.Unlock() | |
| 139 | + | return err | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | func (q *queue) isStopped() bool { | |
| 143 | + | q.mu.Lock() | |
| 144 | + | s := q.stopped | |
| 145 | + | q.mu.Unlock() | |
| 146 | + | return s | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func (q *queue) stop() { | |
| 150 | + | q.mu.Lock() | |
| 151 | + | q.stopped = true | |
| 152 | + | q.cond.Broadcast() | |
| 153 | + | q.mu.Unlock() | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // setPausing starts or ends a pause. | |
| 157 | + | func (q *queue) setPausing(v bool) { | |
| 158 | + | q.mu.Lock() | |
| 159 | + | q.pausing = v | |
| 160 | + | q.cond.Broadcast() | |
| 161 | + | q.mu.Unlock() | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // awaitParked blocks until the reader is parked or gone. | |
| 165 | + | func (q *queue) awaitParked() { | |
| 166 | + | q.mu.Lock() | |
| 167 | + | for !q.readerParked && !q.readerExit { | |
| 168 | + | q.cond.Wait() | |
| 169 | + | } | |
| 170 | + | q.mu.Unlock() | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | // gate runs on the reader goroutine before every read. It parks the reader | |
| 174 | + | // while a pause is requested and re-posts what producers parked meanwhile. | |
| 175 | + | // It reports false when the connection is closing. | |
| 176 | + | func (q *queue) gate() bool { | |
| 177 | + | q.mu.Lock() | |
| 178 | + | for q.pausing && !q.stopped { | |
| 179 | + | if !q.readerParked { | |
| 180 | + | q.readerParked = true | |
| 181 | + | q.cond.Broadcast() | |
| 182 | + | } | |
| 183 | + | q.cond.Wait() | |
| 184 | + | } | |
| 185 | + | wasParked := q.readerParked | |
| 186 | + | q.readerParked = false | |
| 187 | + | stopped := q.stopped | |
| 188 | + | q.mu.Unlock() | |
| 189 | + | if stopped { | |
| 190 | + | return false | |
| 191 | + | } | |
| 192 | + | if wasParked { | |
| 193 | + | q.flushHeld() | |
| 194 | + | } | |
| 195 | + | return true | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | func (q *queue) flushHeld() { | |
| 199 | + | q.mu.Lock() | |
| 200 | + | held := q.held | |
| 201 | + | q.held = q.spare[:0] | |
| 202 | + | q.mu.Unlock() | |
| 203 | + | for i := range held { | |
| 204 | + | q.post(&held[i]) | |
| 205 | + | } | |
| 206 | + | q.mu.Lock() | |
| 207 | + | q.spare = held[:0] | |
| 208 | + | q.mu.Unlock() | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // takeHeld returns and clears the parked events. Only valid while no reader | |
| 212 | + | // runs (during setup). | |
| 213 | + | func (q *queue) takeHeld() []event { | |
| 214 | + | q.mu.Lock() | |
| 215 | + | held := q.held | |
| 216 | + | q.held = nil | |
| 217 | + | q.mu.Unlock() | |
| 218 | + | return held | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // length reports how many events are queued. | |
| 222 | + | func (q *queue) length() int { | |
| 223 | + | q.mu.Lock() | |
| 224 | + | n := q.n | |
| 225 | + | q.mu.Unlock() | |
| 226 | + | return n | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | // pending reports whether events are queued. | |
| 230 | + | func (q *queue) pending() bool { | |
| 231 | + | q.mu.Lock() | |
| 232 | + | n := q.n | |
| 233 | + | q.mu.Unlock() | |
| 234 | + | return n > 0 | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | func (q *queue) readerDone() { | |
| 238 | + | q.mu.Lock() | |
| 239 | + | q.readerExit = true | |
| 240 | + | q.cond.Broadcast() | |
| 241 | + | q.mu.Unlock() | |
| 242 | + | } | |
| @@ -0,0 +1,242 @@ | |||||
| 1 | + | package neferclient | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync" | |||
| 5 | + | ) | |||
| 6 | + | ||||
| 7 | + | const ( | |||
| 8 | + | ringSize = 256 // events queued between the reader and Dispatch | |||
| 9 | + | maxNameLen = 64 // longest output name kept; longer names are truncated | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type eventKind uint8 | |||
| 13 | + | ||||
| 14 | + | const ( | |||
| 15 | + | evGlobal eventKind = iota + 1 // registry global (wl_output only): global, version | |||
| 16 | + | evGlobalRemove // registry global_remove: global | |||
| 17 | + | evOutputName // name, global | |||
| 18 | + | evOutputScale // a = factor, global | |||
| 19 | + | evOutputMode // flags, a = width, b = height, global | |||
| 20 | + | evOutputDone // global | |||
| 21 | + | evFDReady // fd (epoll data) | |||
| 22 | + | ) | |||
| 23 | + | ||||
| 24 | + | // event is the only thing the reader and the epoll goroutine hand to the | |||
| 25 | + | // owner. It holds no pointers except through fixed storage, so queuing one | |||
| 26 | + | // never allocates. | |||
| 27 | + | type event struct { | |||
| 28 | + | kind eventKind | |||
| 29 | + | nameLen uint8 | |||
| 30 | + | global uint32 | |||
| 31 | + | version uint32 | |||
| 32 | + | flags uint32 | |||
| 33 | + | fd int32 | |||
| 34 | + | a, b int32 | |||
| 35 | + | name [maxNameLen]byte | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func (e *event) setName(s string) { | |||
| 39 | + | e.nameLen = uint8(copy(e.name[:], s)) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func (e *event) nameBytes() []byte { return e.name[:e.nameLen] } | |||
| 43 | + | ||||
| 44 | + | // queue is the bounded hand-off between producers (the reader goroutine and | |||
| 45 | + | // the epoll goroutine) and the owner goroutine, plus the reader pause gate. | |||
| 46 | + | // | |||
| 47 | + | // A producer facing a full ring waits on cond; Dispatch releases it as it | |||
| 48 | + | // drains. A pause request also releases it: while pausing, producers park | |||
| 49 | + | // events in held instead of waiting, so neither side can block on the other. | |||
| 50 | + | // Parked events are re-posted, in order, by the reader once it resumes. | |||
| 51 | + | type queue struct { | |||
| 52 | + | mu sync.Mutex | |||
| 53 | + | cond sync.Cond // L = μ one cond for every state change, all are rare | |||
| 54 | + | ||||
| 55 | + | ring [ringSize]event | |||
| 56 | + | head int | |||
| 57 | + | n int | |||
| 58 | + | ||||
| 59 | + | pausing bool | |||
| 60 | + | readerParked bool | |||
| 61 | + | readerExit bool | |||
| 62 | + | stopped bool | |||
| 63 | + | held []event | |||
| 64 | + | spare []event // capacity reused by the next held generation | |||
| 65 | + | fatal error | |||
| 66 | + | ||||
| 67 | + | wake chan struct{} | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func newQueue(pausing bool) *queue { | |||
| 71 | + | q := &queue{pausing: pausing, wake: make(chan struct{}, 1)} | |||
| 72 | + | q.cond.L = &q.mu | |||
| 73 | + | return q | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | func (q *queue) signal() { | |||
| 77 | + | select { | |||
| 78 | + | case q.wake <- struct{}{}: | |||
| 79 | + | default: | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // post queues ev. It blocks while the ring is full and not pausing. | |||
| 84 | + | func (q *queue) post(ev *event) { | |||
| 85 | + | q.mu.Lock() | |||
| 86 | + | for { | |||
| 87 | + | switch { | |||
| 88 | + | case q.stopped: | |||
| 89 | + | q.mu.Unlock() | |||
| 90 | + | return | |||
| 91 | + | case q.pausing: | |||
| 92 | + | q.held = append(q.held, *ev) | |||
| 93 | + | q.mu.Unlock() | |||
| 94 | + | return | |||
| 95 | + | case q.n < ringSize: | |||
| 96 | + | q.ring[(q.head+q.n)%ringSize] = *ev | |||
| 97 | + | q.n++ | |||
| 98 | + | q.mu.Unlock() | |||
| 99 | + | q.signal() | |||
| 100 | + | return | |||
| 101 | + | } | |||
| 102 | + | q.cond.Wait() | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // pop moves the oldest queued event into ev. | |||
| 107 | + | func (q *queue) pop(ev *event) bool { | |||
| 108 | + | q.mu.Lock() | |||
| 109 | + | if q.n == 0 { | |||
| 110 | + | q.mu.Unlock() | |||
| 111 | + | return false | |||
| 112 | + | } | |||
| 113 | + | *ev = q.ring[q.head] | |||
| 114 | + | q.head = (q.head + 1) % ringSize | |||
| 115 | + | full := q.n == ringSize | |||
| 116 | + | q.n-- | |||
| 117 | + | if full { | |||
| 118 | + | q.cond.Broadcast() | |||
| 119 | + | } | |||
| 120 | + | q.mu.Unlock() | |||
| 121 | + | return true | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // setFatal records the first transport failure; Dispatch returns it once the | |||
| 125 | + | // events queued before it are drained. | |||
| 126 | + | func (q *queue) setFatal(err error) { | |||
| 127 | + | q.mu.Lock() | |||
| 128 | + | if q.fatal == nil && !q.stopped { | |||
| 129 | + | q.fatal = err | |||
| 130 | + | } | |||
| 131 | + | q.mu.Unlock() | |||
| 132 | + | q.signal() | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func (q *queue) fatalErr() error { | |||
| 136 | + | q.mu.Lock() | |||
| 137 | + | err := q.fatal | |||
| 138 | + | q.mu.Unlock() | |||
| 139 | + | return err | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | func (q *queue) isStopped() bool { | |||
| 143 | + | q.mu.Lock() | |||
| 144 | + | s := q.stopped | |||
| 145 | + | q.mu.Unlock() | |||
| 146 | + | return s | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func (q *queue) stop() { | |||
| 150 | + | q.mu.Lock() | |||
| 151 | + | q.stopped = true | |||
| 152 | + | q.cond.Broadcast() | |||
| 153 | + | q.mu.Unlock() | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | // setPausing starts or ends a pause. | |||
| 157 | + | func (q *queue) setPausing(v bool) { | |||
| 158 | + | q.mu.Lock() | |||
| 159 | + | q.pausing = v | |||
| 160 | + | q.cond.Broadcast() | |||
| 161 | + | q.mu.Unlock() | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // awaitParked blocks until the reader is parked or gone. | |||
| 165 | + | func (q *queue) awaitParked() { | |||
| 166 | + | q.mu.Lock() | |||
| 167 | + | for !q.readerParked && !q.readerExit { | |||
| 168 | + | q.cond.Wait() | |||
| 169 | + | } | |||
| 170 | + | q.mu.Unlock() | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | // gate runs on the reader goroutine before every read. It parks the reader | |||
| 174 | + | // while a pause is requested and re-posts what producers parked meanwhile. | |||
| 175 | + | // It reports false when the connection is closing. | |||
| 176 | + | func (q *queue) gate() bool { | |||
| 177 | + | q.mu.Lock() | |||
| 178 | + | for q.pausing && !q.stopped { | |||
| 179 | + | if !q.readerParked { | |||
| 180 | + | q.readerParked = true | |||
| 181 | + | q.cond.Broadcast() | |||
| 182 | + | } | |||
| 183 | + | q.cond.Wait() | |||
| 184 | + | } | |||
| 185 | + | wasParked := q.readerParked | |||
| 186 | + | q.readerParked = false | |||
| 187 | + | stopped := q.stopped | |||
| 188 | + | q.mu.Unlock() | |||
| 189 | + | if stopped { | |||
| 190 | + | return false | |||
| 191 | + | } | |||
| 192 | + | if wasParked { | |||
| 193 | + | q.flushHeld() | |||
| 194 | + | } | |||
| 195 | + | return true | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | func (q *queue) flushHeld() { | |||
| 199 | + | q.mu.Lock() | |||
| 200 | + | held := q.held | |||
| 201 | + | q.held = q.spare[:0] | |||
| 202 | + | q.mu.Unlock() | |||
| 203 | + | for i := range held { | |||
| 204 | + | q.post(&held[i]) | |||
| 205 | + | } | |||
| 206 | + | q.mu.Lock() | |||
| 207 | + | q.spare = held[:0] | |||
| 208 | + | q.mu.Unlock() | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // takeHeld returns and clears the parked events. Only valid while no reader | |||
| 212 | + | // runs (during setup). | |||
| 213 | + | func (q *queue) takeHeld() []event { | |||
| 214 | + | q.mu.Lock() | |||
| 215 | + | held := q.held | |||
| 216 | + | q.held = nil | |||
| 217 | + | q.mu.Unlock() | |||
| 218 | + | return held | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // length reports how many events are queued. | |||
| 222 | + | func (q *queue) length() int { | |||
| 223 | + | q.mu.Lock() | |||
| 224 | + | n := q.n | |||
| 225 | + | q.mu.Unlock() | |||
| 226 | + | return n | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | // pending reports whether events are queued. | |||
| 230 | + | func (q *queue) pending() bool { | |||
| 231 | + | q.mu.Lock() | |||
| 232 | + | n := q.n | |||
| 233 | + | q.mu.Unlock() | |||
| 234 | + | return n > 0 | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | func (q *queue) readerDone() { | |||
| 238 | + | q.mu.Lock() | |||
| 239 | + | q.readerExit = true | |||
| 240 | + | q.cond.Broadcast() | |||
| 241 | + | q.mu.Unlock() | |||
| 242 | + | } | |||