1//go:build linux
2
3package wayland
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "net"
10 "os"
11 "sort"
12 "sync"
13 "sync/atomic"
14 "time"
15
16 "github.com/bnema/wlturbo"
17 "github.com/bnema/wlturbo/protocol/core"
18 "github.com/bnema/wlturbo/protocol/extsessionlock"
19)
20
21// Connection owns one socket, one reader and one seat. All NewWindow, Apply,
22// Outputs, AcquireLock and Close calls belong to the same UI owner goroutine.
23// Reader callbacks post immutable events; they never mutate owner window state.
24type Connection struct {
25 Display *wlturbo.Display
26 events chan Event
27 stop chan struct{}
28 done chan struct{}
29 once sync.Once
30 started atomic.Bool
31 windows map[uint32]*Window
32 routes sync.Map // immutable surface->window identities published by owner
33 outputs map[uint32]Output
34 seat *Window // transport-only seat host, never a rendering window
35 lockManager *extsessionlock.ExtSessionLockManager
36 lock *extsessionlock.ExtSessionLock
37 lockFinish bool // finished event applied
38 lockDone bool // locked event applied
39 held []Event // lock/output events kept by drainStartup(true); owner only
40 closed bool
41
42 // Reader pause gate. The owner pauses the reader to register handlers on
43 // freshly created objects (whose first events may arrive immediately) or to
44 // run its own roundtrip. While paused, post parks events instead of
45 // blocking, so neither side can wait on the bounded queue.
46 socket net.Conn
47 gate sync.Mutex
48 pausing bool
49 pauseSig chan struct{} // closed when a pause is requested
50 parked []Event
51 pauseDepth int // owner goroutine only
52 pauseAck chan struct{}
53 resume chan struct{}
54}
55
56// Output identifies the registry lifetime, not merely a reusable monitor name.
57type Output struct {
58 Global uint32
59 Name string
60 Scale int32
61 proxy *core.Output
62}
63
64func ConnectConnection(ctx context.Context, name string) (res *Connection, err error) {
65 if ctx == nil {
66 return nil, errors.New("nil context")
67 }
68 if err := ctx.Err(); err != nil {
69 return nil, err
70 }
71 path, err := waylandSocketPath(name)
72 if err != nil {
73 return nil, err
74 }
75 socket, err := (&net.Dialer{Timeout: dialTimeout}).DialContext(ctx, "unix", path)
76 if err != nil {
77 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
78 }
79 display, err := wlturbo.ConnectFromConn(socket)
80 if err != nil {
81 return nil, err
82 }
83 c := &Connection{Display: display, socket: socket, events: make(chan Event, 256), stop: make(chan struct{}), windows: make(map[uint32]*Window), outputs: make(map[uint32]Output), pauseSig: make(chan struct{}), pauseAck: make(chan struct{}, 1), resume: make(chan struct{}, 1)}
84 watch := context.AfterFunc(ctx, func() { _ = display.Close() })
85 defer func() {
86 if !watch() && err == nil {
87 err = ctx.Err()
88 }
89 if err != nil {
90 _ = c.Close()
91 res = nil
92 if ctx.Err() != nil {
93 err = errors.Join(ctx.Err(), err)
94 }
95 }
96 }()
97 if err = display.Roundtrip(); err != nil {
98 return nil, err
99 }
100 c.seat = &Window{Display: display, connection: c, events: c.events, readerStop: c.stop}
101 if err = c.seat.BindSeat(); err != nil {
102 return nil, err
103 }
104 display.Registry().AddGlobalHandler(c)
105 display.Registry().AddGlobalRemoveHandler(c)
106 for name, g := range display.Registry().GetGlobals() {
107 if g.Interface == core.OutputInterface {
108 if err = c.bindOutput(name, g.Version); err != nil {
109 return nil, err
110 }
111 }
112 }
113 if err = display.Roundtrip(); err != nil {
114 return nil, err
115 }
116 if err = c.drainStartup(false); err != nil {
117 return nil, err
118 }
119 return c, nil
120}
121
122func (c *Connection) bindOutput(global, version uint32) error {
123 if version < 4 {
124 return &CapabilityError{Name: core.OutputInterface, Cause: fmt.Errorf("output names require version 4, announced %d", version)}
125 }
126 out := core.NewOutput(c.Display.Context())
127 if err := c.Display.Registry().Bind(global, core.OutputInterface, 4, out); err != nil {
128 c.Display.Context().Unregister(out)
129 return err
130 }
131 c.outputs[global] = Output{Global: global, Scale: 1, proxy: out}
132 out.OnName(func(name string) { c.post(Event{Kind: OutputNamed, Output: Output{Global: global, Name: name}}) })
133 out.OnScale(func(scale int32) { c.post(Event{Kind: OutputScale, Output: Output{Global: global, Scale: scale}}) })
134 out.OnDone(func() { c.post(Event{Kind: OutputAdded, Output: Output{Global: global}}) })
135 return nil
136}
137
138func (c *Connection) HandleRegistryGlobal(e wlturbo.RegistryGlobalEvent) {
139 if e.Interface == core.OutputInterface {
140 c.post(Event{Kind: OutputGlobal, Output: Output{Global: e.Name}, Version: e.Version})
141 }
142}
143func (c *Connection) HandleRegistryGlobalRemove(e wlturbo.RegistryGlobalRemoveEvent) {
144 c.post(Event{Kind: OutputRemoved, Output: Output{Global: e.Name}})
145}
146func (c *Connection) post(ev Event) {
147 c.gate.Lock()
148 if c.pausing {
149 c.parked = append(c.parked, ev)
150 c.gate.Unlock()
151 return
152 }
153 sig := c.pauseSig
154 c.gate.Unlock()
155 select {
156 case c.events <- ev:
157 case <-c.stop:
158 CloseEventFD(ev)
159 case <-sig:
160 c.gate.Lock()
161 c.parked = append(c.parked, ev)
162 c.gate.Unlock()
163 }
164}
165
166// pauseReader stops the reader between two Dispatch calls and returns once it
167// is parked. The nested calls of one owner goroutine share the first pause;
168// the returned func ends it. Without a running reader it does nothing.
169func (c *Connection) pauseReader() (resume func()) {
170 if !c.started.Load() || c.done == nil {
171 return func() {}
172 }
173 c.pauseDepth++
174 if c.pauseDepth > 1 {
175 return func() { c.pauseDepth-- }
176 }
177 c.gate.Lock()
178 c.pausing = true
179 close(c.pauseSig)
180 c.gate.Unlock()
181 if c.socket != nil {
182 _ = c.socket.SetReadDeadline(time.Unix(1, 0)) // wakes a blocked read
183 }
184 select {
185 case <-c.pauseAck:
186 case <-c.done:
187 }
188 if c.socket != nil {
189 _ = c.socket.SetReadDeadline(time.Time{})
190 }
191 return func() {
192 c.pauseDepth--
193 if c.pauseDepth > 0 {
194 return
195 }
196 c.gate.Lock()
197 c.pausing = false
198 c.pauseSig = make(chan struct{})
199 c.gate.Unlock()
200 select {
201 case c.resume <- struct{}{}:
202 default:
203 }
204 }
205}
206
207// flushParked re-posts events held while paused; reader goroutine only.
208func (c *Connection) flushParked() {
209 c.gate.Lock()
210 held := c.parked
211 c.parked = nil
212 c.gate.Unlock()
213 for _, ev := range held {
214 c.post(ev)
215 }
216}
217
218// ErrLockFinished is wrapped by platform errors when the compositor refused or
219// ended the session lock (ext_session_lock_v1.finished).
220var ErrLockFinished = errors.New("wayland: session lock finished")
221
222// holdDuringSetup reports events the lock owner must see through its own event
223// handling: lock state and output lifetimes. They are never applied by the
224// startup drain, which would hide them from the owner.
225func holdDuringSetup(k EventKind) bool {
226 switch k {
227 case LockAcquired, LockFinished, OutputGlobal, OutputNamed, OutputScale, OutputAdded, OutputRemoved:
228 return true
229 }
230 return false
231}
232
233// drainStartup applies queued events before the reader starts. With hold set
234// (window creation after the lock exists) lock and output events are kept, in
235// order, for TakeHeld instead of being applied; a held `finished` fails setup.
236func (c *Connection) drainStartup(hold bool) error {
237 for {
238 select {
239 case ev := <-c.events:
240 if hold && ev.Window == nil && holdDuringSetup(ev.Kind) {
241 c.held = append(c.held, ev)
242 if ev.Kind == LockFinished {
243 c.lockFinish = true // Close then sends the plain destroy
244 return ErrLockFinished
245 }
246 continue
247 }
248 if err := c.Apply(ev); err != nil {
249 return err
250 }
251 default:
252 return nil
253 }
254 }
255}
256
257// TakeHeld returns the lock and output events kept during window setup, oldest
258// first. The owner feeds them to its normal event handling right after it
259// starts the reader, so they precede everything the reader posts later.
260func (c *Connection) TakeHeld() []Event {
261 held := c.held
262 c.held = nil
263 return held
264}
265
266// Outputs returns a copy of owner-applied output lifetimes. Call after applying
267// OutputAdded/Removed events; names and scales are never read from the reader.
268func (c *Connection) Outputs() []Output {
269 result := make([]Output, 0, len(c.outputs))
270 for _, out := range c.outputs {
271 if out.Name != "" {
272 result = append(result, out)
273 }
274 }
275 sort.Slice(result, func(i, j int) bool { return result[i].Global < result[j].Global })
276 return result
277}
278
279// AcquireLock creates the sole lock object for this connection. Locked and
280// Finished are transport events, not an authentication or unlock policy.
281func (c *Connection) AcquireLock() error {
282 if c.closed || c.lock != nil {
283 return errors.New("connection closed or lock already acquired")
284 }
285 defer c.pauseReader()() // locked/finished may follow the request immediately
286 if c.lockManager == nil {
287 manager := extsessionlock.NewExtSessionLockManager(c.Display.Context())
288 if _, err := c.Display.Registry().BindNegotiated(extsessionlock.ExtSessionLockManagerInterface, 1, manager); err != nil {
289 c.Display.Context().Unregister(manager)
290 return &CapabilityError{Name: extsessionlock.ExtSessionLockManagerInterface, Cause: err}
291 }
292 c.lockManager = manager
293 }
294 lock, err := c.lockManager.Lock()
295 if err != nil {
296 return err
297 }
298 c.lock = lock
299 lock.OnLocked(func() { c.post(Event{Kind: LockAcquired}) })
300 lock.OnFinished(func() { c.post(Event{Kind: LockFinished}) })
301 if !c.started.Load() {
302 // The compositor answers with locked or finished immediately; queue it
303 // now so window setup can hold it for the owner (see drainStartup).
304 return c.Display.Roundtrip()
305 }
306 return nil
307}
308
309// Locked and Finished report the owner-applied lock state.
310func (c *Connection) Locked() bool { return c.lockDone && !c.lockFinish }
311func (c *Connection) Finished() bool { return c.lockFinish }
312
313// UnlockAndDestroy sends unlock_and_destroy and roundtrips so the compositor
314// has processed it before the caller closes anything. It is only valid after
315// the locked event; the platform supplies no authorization policy. The reader
316// is paused for the roundtrip, so events it posts are parked, never lost.
317func (c *Connection) UnlockAndDestroy() error {
318 if c.lock == nil {
319 return errors.New("no session lock")
320 }
321 if !c.Locked() {
322 return errors.New("unlock before the locked event")
323 }
324 defer c.pauseReader()()
325 if err := c.lock.UnlockAndDestroy(); err != nil {
326 return err
327 }
328 c.lock, c.lockDone = nil, false
329 return c.Display.Roundtrip()
330}
331
332func (c *Connection) StartReader() <-chan Event {
333 c.once.Do(func() {
334 c.started.Store(true)
335 for _, w := range c.windows {
336 w.readerStarted.Store(true)
337 }
338 c.done = make(chan struct{})
339 go func() {
340 defer close(c.done)
341 for {
342 select {
343 case <-c.stop:
344 return
345 default:
346 }
347 c.gate.Lock()
348 paused := c.pausing
349 c.gate.Unlock()
350 if paused {
351 c.pauseAck <- struct{}{}
352 select {
353 case <-c.resume:
354 c.flushParked()
355 continue
356 case <-c.stop:
357 return
358 }
359 }
360 err := c.Display.Dispatch()
361 if errors.Is(err, os.ErrDeadlineExceeded) {
362 continue // a pause request interrupted the read
363 }
364 if err != nil {
365 c.post(Event{Kind: TransportError, Err: err})
366 return
367 }
368 }
369 }()
370 })
371 return c.events
372}
373func (c *Connection) Events() <-chan Event { return c.events }
374
375// Apply handles connection events or routes a tagged event to its window.
376func (c *Connection) Apply(ev Event) error {
377 if ev.Window != nil {
378 return ev.Window.Apply(ev)
379 }
380 switch ev.Kind {
381 case OutputGlobal:
382 if _, exists := c.outputs[ev.Output.Global]; !exists {
383 defer c.pauseReader()() // name/scale/done follow the bind
384 return c.bindOutput(ev.Output.Global, ev.Version)
385 }
386 case OutputNamed, OutputScale:
387 out, ok := c.outputs[ev.Output.Global]
388 if !ok {
389 return nil
390 }
391 if ev.Kind == OutputNamed {
392 out.Name = ev.Output.Name
393 } else if ev.Output.Scale > 0 {
394 out.Scale = ev.Output.Scale
395 }
396 c.outputs[out.Global] = out
397 case OutputAdded:
398 case OutputRemoved:
399 if out, ok := c.outputs[ev.Output.Global]; ok {
400 _ = out.proxy.Release()
401 delete(c.outputs, ev.Output.Global)
402 }
403 case SeatCapabilities, SeatRemoved:
404 oldPointer := c.seat.Pointer
405 defer c.pauseReader()() // keymap/capability events follow get_keyboard
406 if ev.Kind == SeatRemoved || (ev.Capabilities&1 == 0 && oldPointer != nil) {
407 // Shape devices die before the wl_pointer they were created from
408 // is released; their enter serials belonged to that pointer.
409 for _, w := range c.windows {
410 w.dropCursorDevice()
411 }
412 }
413 if err := c.seat.ApplySeat(ev); err != nil {
414 return err
415 }
416 for _, w := range c.windows {
417 if oldPointer != c.seat.Pointer {
418 w.dropCursorDevice()
419 w.queueInput([]Input{{Kind: "leave"}})
420 }
421 w.Seat, w.Pointer, w.WLKeyboard = c.seat.Seat, c.seat.Pointer, c.seat.WLKeyboard
422 if w.LockSurface != nil {
423 w.Seat = nil
424 }
425 w.pointerGen = c.seat.pointerGen
426 if ev.Kind == SeatRemoved || ev.Capabilities&2 == 0 {
427 w.ownerFocusOut()
428 }
429 }
430 case InputFocusOut:
431 for _, w := range c.windows {
432 w.ownerFocusOut()
433 }
434 case TransportError:
435 return ev.Err
436 case LockAcquired:
437 c.lockDone = true
438 case LockFinished:
439 c.lockFinish = true
440 default:
441 return fmt.Errorf("unrouted connection event %d", ev.Kind)
442 }
443 return nil
444}
445
446func (c *Connection) Close() error {
447 if c == nil || c.closed {
448 return nil
449 }
450 c.closed = true
451 close(c.stop)
452 for _, w := range c.windows {
453 _ = w.Close()
454 }
455 if c.lock != nil && c.lockFinish {
456 // Refused, or ended after locked: the compositor ended the lock, so a
457 // plain destroy is valid (unlock_and_destroy already cleared c.lock).
458 _ = c.lock.Destroy()
459 }
460 if c.lockManager != nil {
461 _ = c.lockManager.Destroy()
462 }
463 err := c.Display.Close() // closing an acquired owner never requests unlock
464 if c.done != nil {
465 <-c.done
466 }
467 if c.seat != nil {
468 _ = c.seat.releaseSeat()
469 }
470 for len(c.events) > 0 {
471 CloseEventFD(<-c.events)
472 }
473 for _, ev := range c.held {
474 CloseEventFD(ev)
475 }
476 c.held = nil
477 c.gate.Lock()
478 for _, ev := range c.parked {
479 CloseEventFD(ev)
480 }
481 c.parked = nil
482 c.gate.Unlock()
483 return err
484}