detached 7576621

feat(core): generate canonical core bindings and harden event transport

commit
75766210f33cf0f15489c80f4b602dfd3331388f
author
brice · · 2026-09-28 07:15 UTC
parent
d5c3b4c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +4780 -687
A benchmark_test.go +26 -0
@@ -0,0 +1,26 @@
1+package wlturbo
2+
3+import "testing"
4+
5+// BenchmarkWireDispatch isolates framed typed dispatch from socket scheduling.
6+func BenchmarkWireDispatch(b *testing.B) {
7+ d := newDisplay(&chunkConn{})
8+ p := &benchProxy{BaseProxy: BaseProxy{id: 7, context: d.context}}
9+ d.context.Register(p)
10+ f := receivedFrame{object: 7, opcode: 0, body: []byte{1, 0, 0, 0}}
11+ b.ReportAllocs()
12+ b.ResetTimer()
13+ for i := 0; i < b.N; i++ {
14+ if err := d.dispatchFrame(f); err != nil {
15+ b.Fatal(err)
16+ }
17+ }
18+}
19+
20+type benchProxy struct {
21+ BaseProxy
22+ value uint32
23+}
24+
25+func (p *benchProxy) EventSignature(op uint16) (string, bool) { return "uint,", op == 0 }
26+func (p *benchProxy) Dispatch(e *Event) { p.value = e.Uint32() }
@@ -0,0 +1,26 @@
1+package wlturbo
2+
3+import "testing"
4+
5+// BenchmarkWireDispatch isolates framed typed dispatch from socket scheduling.
6+func BenchmarkWireDispatch(b *testing.B) {
7+ d := newDisplay(&chunkConn{})
8+ p := &benchProxy{BaseProxy: BaseProxy{id: 7, context: d.context}}
9+ d.context.Register(p)
10+ f := receivedFrame{object: 7, opcode: 0, body: []byte{1, 0, 0, 0}}
11+ b.ReportAllocs()
12+ b.ResetTimer()
13+ for i := 0; i < b.N; i++ {
14+ if err := d.dispatchFrame(f); err != nil {
15+ b.Fatal(err)
16+ }
17+ }
18+}
19+
20+type benchProxy struct {
21+ BaseProxy
22+ value uint32
23+}
24+
25+func (p *benchProxy) EventSignature(op uint16) (string, bool) { return "uint,", op == 0 }
26+func (p *benchProxy) Dispatch(e *Event) { p.value = e.Uint32() }
M client.go +175 -224
@@ -13,6 +13,7 @@ import (
1313 "net"
1414 "os"
1515 "path/filepath"
16+ "strings"
1617 "sync"
1718 "sync/atomic"
1819 )
@@ -44,13 +45,15 @@ type Object interface {
4445
4546 // Display represents a connection to the Wayland display
4647 type Display struct {
47- conn net.Conn
48- unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
49- objects sync.Map // map[uint32]Object
50- nextID uint32
51- sendMu sync.Mutex
52- recvMu sync.Mutex
53- listeners sync.Map // map[uint32]map[uint16][]func([]byte)
48+ conn net.Conn
49+ unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
50+ objects sync.Map // map[uint32]Object
51+ nextID uint32
52+ sendMu sync.Mutex
53+ recvMu sync.Mutex
54+ signatures sync.Map
55+ listenerMu sync.Mutex
56+ listeners sync.Map // map[uint32]map[uint16][]func([]byte)
5457
5558 // High-performance event dispatcher
5659 dispatcher *EventDispatcher
@@ -73,7 +76,6 @@ type Display struct {
7376 readErr error
7477 maxMsgLen uint32
7578 closed atomic.Bool
76- fdMu sync.Mutex
7779 pendingFDs []int
7880 }
7981
@@ -103,11 +105,12 @@ func newDisplay(conn net.Conn) *Display {
103105
104106 // Registry represents the global registry
105107 type Registry struct {
106- id uint32
107- display *Display
108- globals map[uint32]Global
109- mu sync.RWMutex
110- handlers map[string]GlobalHandler
108+ id uint32
109+ display *Display
110+ globals map[uint32]Global
111+ mu sync.RWMutex
112+ handlers map[string]GlobalHandler
113+ removeHandlers []RegistryGlobalRemoveHandler
111114 }
112115
113116 // callbackObject represents a wl_callback object
@@ -119,6 +122,12 @@ type callbackObject struct {
119122 func (c *callbackObject) ID() uint32 {
120123 return c.id
121124 }
125+func (c *callbackObject) EventSignature(op uint16) (string, bool) {
126+ if op == 0 {
127+ return "uint,", true
128+ }
129+ return "", false
130+}
122131
123132 // Dispatch handles callback events (opcode 0 = done)
124133 func (c *callbackObject) Dispatch(event *Event) {
@@ -126,6 +135,7 @@ func (c *callbackObject) Dispatch(event *Event) {
126135 return
127136 }
128137 c.display.notifyListeners(c.id, 0, event.data)
138+ c.display.context.Unregister(c)
129139 }
130140
131141 // Global represents a global object
@@ -138,6 +148,16 @@ type Global struct {
138148 // GlobalHandler is called when a global is announced
139149 type GlobalHandler func(registry *Registry, name uint32, version uint32)
140150
151+// ConnectFromConn attaches to an established Wayland socket and requests its registry.
152+func ConnectFromConn(conn net.Conn) (*Display, error) {
153+ d := newDisplay(conn)
154+ if err := d.getRegistry(); err != nil {
155+ _ = d.Close()
156+ return nil, fmt.Errorf("get registry: %w", err)
157+ }
158+ return d, nil
159+}
160+
141161 // Connect connects to the Wayland display and requests the registry.
142162 func Connect(socketPath string) (*Display, error) {
143163 if socketPath == "" {
@@ -161,16 +181,7 @@ func Connect(socketPath string) (*Display, error) {
161181 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
162182 }
163183
164- d := newDisplay(conn)
165-
166- // Get registry
167- if err := d.getRegistry(); err != nil {
168- _ = d.Close()
169- return nil, fmt.Errorf("failed to get registry: %w", err)
170- }
171-
172- // Don't do initial roundtrip here - let the caller do it after setting up handlers
173- return d, nil
184+ return ConnectFromConn(conn)
174185 }
175186
176187 // Close closes the display connection. It is safe to call more than once;
@@ -179,8 +190,14 @@ func (d *Display) Close() error {
179190 if !d.closed.CompareAndSwap(false, true) {
180191 return nil
181192 }
193+ d.context.closed.Store(true)
194+ err := d.conn.Close()
195+ d.sendMu.Lock()
196+ d.sendMu.Unlock()
197+ d.recvMu.Lock()
182198 d.closePendingFDs()
183- return d.conn.Close()
199+ d.recvMu.Unlock()
200+ return err
184201 }
185202
186203 // Closed reports whether the connection has been closed.
@@ -235,8 +252,8 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int,
235252
236253 // Update header with actual size in bytes
237254 bufLen := buf.Len()
238- if bufLen > 0xFFFF {
239- return fmt.Errorf("message too large: %d bytes", bufLen)
255+ if bufLen > 0xFFFF || bufLen%4 != 0 {
256+ return fmt.Errorf("invalid message size: %d bytes", bufLen)
240257 }
241258 size := uint32(bufLen) // Safe: checked above
242259 binary.LittleEndian.PutUint32(header[0:4], objectID)
@@ -263,7 +280,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
263280 case string:
264281 // String format: length (including null) + string + null + padding
265282 strlen := len(v) + 1
266- if strlen > 0xFFFFFFFF {
283+ if strlen > 0xffff-HeaderSize {
267284 return fmt.Errorf("string too long: %d bytes", strlen)
268285 }
269286 if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above
@@ -279,7 +296,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
279296 case []byte:
280297 // Array format: length + data + padding
281298 arrlen := len(v)
282- if arrlen > 0xFFFFFFFF {
299+ if arrlen > 0xffff-HeaderSize {
283300 return fmt.Errorf("array too long: %d bytes", arrlen)
284301 }
285302 if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil {
@@ -329,6 +346,9 @@ func (d *Display) Dispatch() error {
329346 }
330347
331348 frame, err := d.nextFrame()
349+ if d.closed.Load() {
350+ return net.ErrClosed
351+ }
332352 if err != nil {
333353 return d.fail(err)
334354 }
@@ -346,6 +366,9 @@ func (d *Display) fail(err error) error {
346366 if errors.As(err, &perr) {
347367 d.closePendingFDs()
348368 }
369+ if errors.Is(err, io.ErrUnexpectedEOF) {
370+ d.closePendingFDs()
371+ }
349372 return err
350373 }
351374
@@ -376,6 +399,9 @@ func (d *Display) nextFrame() (receivedFrame, error) {
376399 }
377400 }
378401 if err := d.fill(); err != nil {
402+ if d.closed.Load() {
403+ return receivedFrame{}, net.ErrClosed
404+ }
379405 return receivedFrame{}, err
380406 }
381407 }
@@ -416,40 +442,85 @@ func (d *Display) fill() error {
416442 return nil
417443 }
418444
419-// dispatchFrame delivers one complete frame to the object that owns it.
445+// signatureProvider is implemented by generated objects; every opcode must
446+// have a known signature before any descriptor can be assigned or dispatched.
447+type signatureProvider interface{ EventSignature(uint16) (string, bool) }
448+
449+// dispatchFrame delivers one complete frame with precisely its signature's FDs.
420450 func (d *Display) dispatchFrame(f receivedFrame) error {
421451 if f.object == displayObjectID {
452+ if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
453+ return &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
454+ }
422455 return d.handleDisplayEvent(f.opcode, f.body)
423456 }
424-
425457 obj, ok := d.objects.Load(f.object)
426458 if !ok {
427- return &ProtocolError{
428- Kind: "unknown_object",
429- Object: f.object,
430- Opcode: f.opcode,
431- Size: uint32(len(f.body)) + HeaderSize,
432- Err: ErrUnknownObject,
459+ return &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
460+ }
461+ sig, valid := "", false
462+ p, isGenerated := obj.(signatureProvider)
463+ if isGenerated {
464+ sig, valid = p.EventSignature(f.opcode)
465+ }
466+ // Legacy custom proxies may declare their signatures with RegisterEventSignature.
467+ if !valid && !isGenerated {
468+ if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok {
469+ sig, valid = v.(string), true
433470 }
434471 }
472+ if !valid {
473+ return &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
474+ }
475+ if err := validateEventBody(sig, f.body); err != nil {
476+ return &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
477+ }
478+ count := strings.Count(sig, "fd,")
479+ if len(d.pendingFDs) < count {
480+ return &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
481+ }
482+ ev := eventPool.Get().(*Event)
483+ *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
484+ if count != 0 {
485+ ev.fds = make([]*OwnedFD, count)
486+ for i := range ev.fds {
487+ ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]}
488+ }
489+ d.pendingFDs = d.pendingFDs[count:]
490+ }
491+ defer func() {
492+ for _, fd := range ev.fds {
493+ _ = fd.Close()
494+ }
495+ *ev = Event{}
496+ eventPool.Put(ev)
497+ }()
498+ // FDs from a single recvmsg can accompany multiple coalesced frames. Once
499+ // all buffered bytes have been framed, surplus descriptors are not legal.
500+ if len(d.rbuf) == 0 && len(d.pendingFDs) != 0 {
501+ return &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
502+ }
503+ if proxy, ok := obj.(Proxy); ok {
504+ proxy.Dispatch(ev)
505+ } else {
506+ d.dispatcher.Dispatch(f.object, f.opcode, f.body)
507+ d.notifyListeners(f.object, f.opcode, f.body)
508+ }
509+ return nil
510+}
435511
436- // Server-created objects are announced by an event on a known object.
437- d.handleServerObject(f.object, f.opcode, f.body)
512+type signatureKey struct {
513+ object uint32
514+ opcode uint16
515+}
438516
439- if proxy, ok := obj.(Proxy); ok && proxy != nil {
440- proxy.Dispatch(&Event{
441- ProxyID: f.object,
442- Opcode: f.opcode,
443- data: f.body,
444- display: d,
445- })
446- return nil
517+// RegisterEventSignature defines a signature for a legacy proxy. Generated
518+// proxies supply it themselves and cannot be overridden.
519+func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) {
520+ if object == 0 || object == displayObjectID {
521+ return
447522 }
448-
449- // High-performance dispatcher, then compatibility listeners.
450- d.dispatcher.Dispatch(f.object, f.opcode, f.body)
451- d.notifyListeners(f.object, f.opcode, f.body)
452- return nil
523+ d.signatures.Store(signatureKey{object, opcode}, signature)
453524 }
454525
455526 // notifyListeners runs the listeners registered for an object and opcode.
@@ -466,11 +537,7 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
466537 if !ok {
467538 return
468539 }
469- handlerSlice, ok := handlers.(*[]func([]byte))
470- if !ok || handlerSlice == nil {
471- return
472- }
473- for _, handler := range *handlerSlice {
540+ for _, handler := range handlers.([]func([]byte)) {
474541 if handler != nil {
475542 handler(body)
476543 }
@@ -481,8 +548,8 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
481548 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
482549 switch opcode {
483550 case 0: // error
484- if len(data) < 8 {
485- return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
551+ if err := validateEventBody("object,uint,string,", data); err != nil {
552+ return &ProtocolError{Kind: "malformed_display_error", Err: err}
486553 }
487554 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
488555 objectID := event.Uint32()
@@ -495,11 +562,12 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
495562 return d.lastError
496563
497564 case 1: // delete_id
498- if len(data) < 4 {
565+ if len(data) != 4 {
499566 return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
500567 }
501568 id := binary.LittleEndian.Uint32(data[0:4])
502569 d.objects.Delete(id)
570+ d.context.proxies.Delete(id)
503571 return nil
504572
505573 default:
@@ -507,55 +575,42 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
507575 }
508576 }
509577
510-// Roundtrip performs a synchronous roundtrip to the compositor
578+// Roundtrip waits for one wl_display.sync callback. Concurrent event loops
579+// must not call Roundtrip while another goroutine dispatches this connection.
511580 func (d *Display) Roundtrip() error {
512- callbackID := d.allocateID()
513- done := make(chan error, 1)
514-
515- d.AddListener(callbackID, 0, func(_ []byte) {
516- d.objects.Delete(callbackID)
517- done <- nil
518- })
519-
520- // Send sync request (opcode 0)
521- if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil {
581+ callback, err := d.Sync()
582+ if err != nil {
522583 return err
523584 }
524-
525- d.objects.Store(callbackID, &callbackObject{
526- BaseProxy: BaseProxy{
527- context: d.Context(),
528- id: callbackID,
529- },
530- display: d,
531- })
532-
533- for i := 0; i < 1000; i++ {
534- if err := d.Dispatch(); err != nil {
585+ for {
586+ if err = d.Dispatch(); err != nil {
535587 return err
536588 }
537- select {
538- case err := <-done:
539- return err
540- default:
589+ if _, exists := d.objects.Load(callback.ID()); !exists {
590+ return nil
541591 }
542592 }
543-
544- return fmt.Errorf("roundtrip failed: max iterations reached")
545593 }
546594
547595 // AddListener adds an event listener for an object
548596 func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {
597+ if handler == nil {
598+ return
599+ }
549600 // Load or create listener map for object
550601 listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})
551602 opcodeMap := listeners.(*sync.Map)
552603
553- // Load or create handlers slice for opcode
554- handlers, _ := opcodeMap.LoadOrStore(opcode, &[]func([]byte){})
555- handlerSlice := handlers.(*[]func([]byte))
556-
557- // Add handler (thread-safe)
558- *handlerSlice = append(*handlerSlice, handler)
604+ d.listenerMu.Lock()
605+ defer d.listenerMu.Unlock()
606+ var handlers []func([]byte)
607+ if old, ok := opcodeMap.Load(opcode); ok {
608+ handlers = old.([]func([]byte))
609+ }
610+ next := make([]func([]byte), len(handlers)+1)
611+ copy(next, handlers)
612+ next[len(handlers)] = handler
613+ opcodeMap.Store(opcode, next)
559614 }
560615
561616 // getRegistry gets the global registry
@@ -581,15 +636,13 @@ func (r *Registry) ID() uint32 {
581636 // handleGlobal handles global announcements
582637 func (r *Registry) handleGlobal(data []byte) {
583638 if len(data) < 8 {
584- // log.Printf("wlclient: handleGlobal: data too short (%d bytes)", len(data))
585639 return
586640 }
587641
588642 name := binary.LittleEndian.Uint32(data[0:4])
589643 ifaceLen := binary.LittleEndian.Uint32(data[4:8])
590644
591- if len(data) < 8+int(ifaceLen)+4 {
592- // log.Printf("wlclient: handleGlobal: insufficient data for interface string (need %d, have %d)", 8+int(ifaceLen)+4, len(data))
645+ if ifaceLen == 0 || uint64(ifaceLen)+12 > uint64(len(data)) || data[8+ifaceLen-1] != 0 {
593646 return
594647 }
595648
@@ -600,15 +653,12 @@ func (r *Registry) handleGlobal(data []byte) {
600653 padding := (4 - (ifaceLen % 4)) % 4
601654 versionOffset := 8 + int(ifaceLen) + int(padding)
602655
603- if len(data) < versionOffset+4 {
604- // log.Printf("wlclient: handleGlobal: insufficient data for version (need %d, have %d)", versionOffset+4, len(data))
656+ if len(data) != versionOffset+4 {
605657 return
606658 }
607659
608660 version := binary.LittleEndian.Uint32(data[versionOffset:])
609661
610- // log.Printf("wlclient: global announced: %s v%d (name=%d)", iface, version, name)
611-
612662 // Store global
613663 r.mu.Lock()
614664 r.globals[name] = Global{
@@ -619,15 +669,19 @@ func (r *Registry) handleGlobal(data []byte) {
619669 r.mu.Unlock()
620670
621671 // Call specific handler if registered
622- if handler, ok := r.handlers[iface]; ok {
623- // log.Printf("wlclient: calling handler for interface %s", iface)
672+ r.mu.RLock()
673+ handler, specific := r.handlers[iface]
674+ wildcard, all := r.handlers["*"]
675+ r.mu.RUnlock()
676+ if specific {
677+
624678 handler(r, name, version)
625679 }
626680
627681 // Call wildcard handler if registered
628- if handler, ok := r.handlers["*"]; ok {
629- // log.Printf("wlclient: calling wildcard handler for interface %s", iface)
630- handler(r, name, version)
682+ if all {
683+
684+ wildcard(r, name, version)
631685 }
632686 }
633687
@@ -641,11 +695,17 @@ func (r *Registry) handleGlobalRemove(data []byte) {
641695
642696 r.mu.Lock()
643697 delete(r.globals, name)
698+ handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
644699 r.mu.Unlock()
700+ for _, handler := range handlers {
701+ handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name})
702+ }
645703 }
646704
647705 // AddHandler adds a handler for a specific interface
648706 func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
707+ r.mu.Lock()
708+ defer r.mu.Unlock()
649709 r.handlers[iface] = handler
650710 }
651711
@@ -656,7 +716,7 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
656716 proxy.SetID(r.display.allocateID())
657717 }
658718
659- // Ensure proxy has a context set - CRITICAL FIX
719+ // Bootstrap a generated proxy with this display's context.
660720 if proxy.Context() == nil {
661721 if baseProxy, ok := proxy.(*BaseProxy); ok {
662722 baseProxy.SetContext(r.display.Context())
@@ -667,12 +727,18 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
667727 }
668728 }
669729
730+ if proxy.Context() != r.display.Context() {
731+ return fmt.Errorf("proxy belongs to another display")
732+ }
733+ if proxy.ID() == displayObjectID || proxy.ID() == r.id {
734+ return fmt.Errorf("reserved object ID")
735+ }
736+ if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy {
737+ return fmt.Errorf("object ID %d already registered", proxy.ID())
738+ }
670739 // Register the proxy
671740 proxy.Context().Register(proxy)
672741
673- // Also register directly in display objects map
674- r.display.objects.Store(proxy.ID(), proxy)
675-
676742 // Send bind request (opcode 0) with proper arguments
677743 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
678744 proxy.Context().Unregister(proxy)
@@ -718,118 +784,3 @@ func (r *Registry) FindGlobal(iface string) (Global, bool) {
718784 }
719785 return Global{}, false
720786 }
721-
722-// handleServerObject creates and registers server-allocated objects from events
723-func (d *Display) handleServerObject(objectID uint32, opcode uint16, body []byte) bool {
724- // Handle specific known protocols that create server objects
725- obj, ok := d.objects.Load(objectID)
726- if !ok {
727- return false
728- }
729-
730- // Check if this is a known object type that creates server objects
731- switch obj.(type) {
732- case *Registry:
733- // Registry doesn't create server objects directly
734- return false
735-
736- default:
737- // Check if this might be an output manager (interface zwlr_output_manager_v1)
738- if objectID == 5 { // This is likely the output manager based on your logs
739- return d.handleOutputManagerEvent(objectID, opcode, body)
740- }
741- }
742-
743- return false
744-}
745-
746-// handleOutputManagerEvent handles events from zwlr_output_manager_v1
747-func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body []byte) bool {
748- switch opcode {
749- case 0: // head event - creates new zwlr_output_head_v1 object
750- if len(body) < 4 {
751- return false
752- }
753-
754- // Parse the new_id for the head object
755- headID := binary.LittleEndian.Uint32(body[0:4])
756-
757- // Create a proper OutputHead object
758- headProxy := &OutputHead{
759- BaseProxy: BaseProxy{
760- id: headID,
761- context: d.Context(),
762- },
763- }
764-
765- // Register the new head object
766- d.objects.Store(headID, headProxy)
767- return true
768-
769- case 1: // done event
770- return false
771-
772- case 2: // finished event
773- return false
774- }
775-
776- return false
777-}
778-
779-// OutputHead represents a zwlr_output_head_v1 object
780-type OutputHead struct {
781- BaseProxy
782- name string
783- description string
784- width int32
785- height int32
786-}
787-
788-// Dispatch handles head events
789-func (h *OutputHead) Dispatch(event *Event) {
790- switch event.Opcode {
791- case 0: // name
792- h.name = event.String()
793- case 1: // description
794- h.description = event.String()
795- case 2: // physical_size
796- h.width = event.Int32()
797- h.height = event.Int32()
798- case 3: // mode (creates new mode object)
799- modeID := event.Uint32()
800- // Create mode object
801- mode := &OutputMode{
802- BaseProxy: BaseProxy{
803- id: modeID,
804- context: h.context,
805- },
806- }
807- h.context.display.objects.Store(modeID, mode)
808- case 9: // finished
809- h.context.Unregister(h)
810- default:
811- }
812-}
813-
814-// OutputMode represents a zwlr_output_mode_v1 object
815-type OutputMode struct {
816- BaseProxy
817- width int32
818- height int32
819- refresh int32
820-}
821-
822-// Dispatch handles mode events
823-func (m *OutputMode) Dispatch(event *Event) {
824- switch event.Opcode {
825- case 0: // size
826- m.width = event.Int32()
827- m.height = event.Int32()
828- case 1: // refresh
829- m.refresh = event.Int32()
830- case 2: // preferred
831- case 3: // finished
832- m.context.Unregister(m)
833- default:
834- }
835-}
@@ -13,6 +13,7 @@ import (
13 "net"13 "net"
14 "os"14 "os"
15 "path/filepath"15 "path/filepath"
16+ "strings"
16 "sync"17 "sync"
17 "sync/atomic"18 "sync/atomic"
18 )19 )
@@ -44,13 +45,15 @@ type Object interface {
44 45
45 // Display represents a connection to the Wayland display46 // Display represents a connection to the Wayland display
46 type Display struct {47 type Display struct {
47- conn net.Conn48+ conn net.Conn
48- unix *net.UnixConn // set when conn is a Unix socket, nil otherwise49+ unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
49- objects sync.Map // map[uint32]Object50+ objects sync.Map // map[uint32]Object
50- nextID uint3251+ nextID uint32
51- sendMu sync.Mutex52+ sendMu sync.Mutex
52- recvMu sync.Mutex53+ recvMu sync.Mutex
53- listeners sync.Map // map[uint32]map[uint16][]func([]byte)54+ signatures sync.Map
55+ listenerMu sync.Mutex
56+ listeners sync.Map // map[uint32]map[uint16][]func([]byte)
54 57
55 // High-performance event dispatcher58 // High-performance event dispatcher
56 dispatcher *EventDispatcher59 dispatcher *EventDispatcher
@@ -73,7 +76,6 @@ type Display struct {
73 readErr error76 readErr error
74 maxMsgLen uint3277 maxMsgLen uint32
75 closed atomic.Bool78 closed atomic.Bool
76- fdMu sync.Mutex
77 pendingFDs []int79 pendingFDs []int
78 }80 }
79 81
@@ -103,11 +105,12 @@ func newDisplay(conn net.Conn) *Display {
103 105
104 // Registry represents the global registry106 // Registry represents the global registry
105 type Registry struct {107 type Registry struct {
106- id uint32108+ id uint32
107- display *Display109+ display *Display
108- globals map[uint32]Global110+ globals map[uint32]Global
109- mu sync.RWMutex111+ mu sync.RWMutex
110- handlers map[string]GlobalHandler112+ handlers map[string]GlobalHandler
113+ removeHandlers []RegistryGlobalRemoveHandler
111 }114 }
112 115
113 // callbackObject represents a wl_callback object116 // callbackObject represents a wl_callback object
@@ -119,6 +122,12 @@ type callbackObject struct {
119 func (c *callbackObject) ID() uint32 {122 func (c *callbackObject) ID() uint32 {
120 return c.id123 return c.id
121 }124 }
125+func (c *callbackObject) EventSignature(op uint16) (string, bool) {
126+ if op == 0 {
127+ return "uint,", true
128+ }
129+ return "", false
130+}
122 131
123 // Dispatch handles callback events (opcode 0 = done)132 // Dispatch handles callback events (opcode 0 = done)
124 func (c *callbackObject) Dispatch(event *Event) {133 func (c *callbackObject) Dispatch(event *Event) {
@@ -126,6 +135,7 @@ func (c *callbackObject) Dispatch(event *Event) {
126 return135 return
127 }136 }
128 c.display.notifyListeners(c.id, 0, event.data)137 c.display.notifyListeners(c.id, 0, event.data)
138+ c.display.context.Unregister(c)
129 }139 }
130 140
131 // Global represents a global object141 // Global represents a global object
@@ -138,6 +148,16 @@ type Global struct {
138 // GlobalHandler is called when a global is announced148 // GlobalHandler is called when a global is announced
139 type GlobalHandler func(registry *Registry, name uint32, version uint32)149 type GlobalHandler func(registry *Registry, name uint32, version uint32)
140 150
151+// ConnectFromConn attaches to an established Wayland socket and requests its registry.
152+func ConnectFromConn(conn net.Conn) (*Display, error) {
153+ d := newDisplay(conn)
154+ if err := d.getRegistry(); err != nil {
155+ _ = d.Close()
156+ return nil, fmt.Errorf("get registry: %w", err)
157+ }
158+ return d, nil
159+}
160+
141 // Connect connects to the Wayland display and requests the registry.161 // Connect connects to the Wayland display and requests the registry.
142 func Connect(socketPath string) (*Display, error) {162 func Connect(socketPath string) (*Display, error) {
143 if socketPath == "" {163 if socketPath == "" {
@@ -161,16 +181,7 @@ func Connect(socketPath string) (*Display, error) {
161 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)181 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
162 }182 }
163 183
164- d := newDisplay(conn)184+ return ConnectFromConn(conn)
165-
166- // Get registry
167- if err := d.getRegistry(); err != nil {
168- _ = d.Close()
169- return nil, fmt.Errorf("failed to get registry: %w", err)
170- }
171-
172- // Don't do initial roundtrip here - let the caller do it after setting up handlers
173- return d, nil
174 }185 }
175 186
176 // Close closes the display connection. It is safe to call more than once;187 // Close closes the display connection. It is safe to call more than once;
@@ -179,8 +190,14 @@ func (d *Display) Close() error {
179 if !d.closed.CompareAndSwap(false, true) {190 if !d.closed.CompareAndSwap(false, true) {
180 return nil191 return nil
181 }192 }
193+ d.context.closed.Store(true)
194+ err := d.conn.Close()
195+ d.sendMu.Lock()
196+ d.sendMu.Unlock()
197+ d.recvMu.Lock()
182 d.closePendingFDs()198 d.closePendingFDs()
183- return d.conn.Close()199+ d.recvMu.Unlock()
200+ return err
184 }201 }
185 202
186 // Closed reports whether the connection has been closed.203 // Closed reports whether the connection has been closed.
@@ -235,8 +252,8 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int,
235 252
236 // Update header with actual size in bytes253 // Update header with actual size in bytes
237 bufLen := buf.Len()254 bufLen := buf.Len()
238- if bufLen > 0xFFFF {255+ if bufLen > 0xFFFF || bufLen%4 != 0 {
239- return fmt.Errorf("message too large: %d bytes", bufLen)256+ return fmt.Errorf("invalid message size: %d bytes", bufLen)
240 }257 }
241 size := uint32(bufLen) // Safe: checked above258 size := uint32(bufLen) // Safe: checked above
242 binary.LittleEndian.PutUint32(header[0:4], objectID)259 binary.LittleEndian.PutUint32(header[0:4], objectID)
@@ -263,7 +280,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
263 case string:280 case string:
264 // String format: length (including null) + string + null + padding281 // String format: length (including null) + string + null + padding
265 strlen := len(v) + 1282 strlen := len(v) + 1
266- if strlen > 0xFFFFFFFF {283+ if strlen > 0xffff-HeaderSize {
267 return fmt.Errorf("string too long: %d bytes", strlen)284 return fmt.Errorf("string too long: %d bytes", strlen)
268 }285 }
269 if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above286 if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above
@@ -279,7 +296,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
279 case []byte:296 case []byte:
280 // Array format: length + data + padding297 // Array format: length + data + padding
281 arrlen := len(v)298 arrlen := len(v)
282- if arrlen > 0xFFFFFFFF {299+ if arrlen > 0xffff-HeaderSize {
283 return fmt.Errorf("array too long: %d bytes", arrlen)300 return fmt.Errorf("array too long: %d bytes", arrlen)
284 }301 }
285 if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil {302 if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil {
@@ -329,6 +346,9 @@ func (d *Display) Dispatch() error {
329 }346 }
330 347
331 frame, err := d.nextFrame()348 frame, err := d.nextFrame()
349+ if d.closed.Load() {
350+ return net.ErrClosed
351+ }
332 if err != nil {352 if err != nil {
333 return d.fail(err)353 return d.fail(err)
334 }354 }
@@ -346,6 +366,9 @@ func (d *Display) fail(err error) error {
346 if errors.As(err, &perr) {366 if errors.As(err, &perr) {
347 d.closePendingFDs()367 d.closePendingFDs()
348 }368 }
369+ if errors.Is(err, io.ErrUnexpectedEOF) {
370+ d.closePendingFDs()
371+ }
349 return err372 return err
350 }373 }
351 374
@@ -376,6 +399,9 @@ func (d *Display) nextFrame() (receivedFrame, error) {
376 }399 }
377 }400 }
378 if err := d.fill(); err != nil {401 if err := d.fill(); err != nil {
402+ if d.closed.Load() {
403+ return receivedFrame{}, net.ErrClosed
404+ }
379 return receivedFrame{}, err405 return receivedFrame{}, err
380 }406 }
381 }407 }
@@ -416,40 +442,85 @@ func (d *Display) fill() error {
416 return nil442 return nil
417 }443 }
418 444
419-// dispatchFrame delivers one complete frame to the object that owns it.445+// signatureProvider is implemented by generated objects; every opcode must
446+// have a known signature before any descriptor can be assigned or dispatched.
447+type signatureProvider interface{ EventSignature(uint16) (string, bool) }
448+
449+// dispatchFrame delivers one complete frame with precisely its signature's FDs.
420 func (d *Display) dispatchFrame(f receivedFrame) error {450 func (d *Display) dispatchFrame(f receivedFrame) error {
421 if f.object == displayObjectID {451 if f.object == displayObjectID {
452+ if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
453+ return &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
454+ }
422 return d.handleDisplayEvent(f.opcode, f.body)455 return d.handleDisplayEvent(f.opcode, f.body)
423 }456 }
424-
425 obj, ok := d.objects.Load(f.object)457 obj, ok := d.objects.Load(f.object)
426 if !ok {458 if !ok {
427- return &ProtocolError{459+ return &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
428- Kind: "unknown_object",460+ }
429- Object: f.object,461+ sig, valid := "", false
430- Opcode: f.opcode,462+ p, isGenerated := obj.(signatureProvider)
431- Size: uint32(len(f.body)) + HeaderSize,463+ if isGenerated {
432- Err: ErrUnknownObject,464+ sig, valid = p.EventSignature(f.opcode)
465+ }
466+ // Legacy custom proxies may declare their signatures with RegisterEventSignature.
467+ if !valid && !isGenerated {
468+ if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok {
469+ sig, valid = v.(string), true
433 }470 }
434 }471 }
472+ if !valid {
473+ return &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
474+ }
475+ if err := validateEventBody(sig, f.body); err != nil {
476+ return &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
477+ }
478+ count := strings.Count(sig, "fd,")
479+ if len(d.pendingFDs) < count {
480+ return &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
481+ }
482+ ev := eventPool.Get().(*Event)
483+ *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
484+ if count != 0 {
485+ ev.fds = make([]*OwnedFD, count)
486+ for i := range ev.fds {
487+ ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]}
488+ }
489+ d.pendingFDs = d.pendingFDs[count:]
490+ }
491+ defer func() {
492+ for _, fd := range ev.fds {
493+ _ = fd.Close()
494+ }
495+ *ev = Event{}
496+ eventPool.Put(ev)
497+ }()
498+ // FDs from a single recvmsg can accompany multiple coalesced frames. Once
499+ // all buffered bytes have been framed, surplus descriptors are not legal.
500+ if len(d.rbuf) == 0 && len(d.pendingFDs) != 0 {
501+ return &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
502+ }
503+ if proxy, ok := obj.(Proxy); ok {
504+ proxy.Dispatch(ev)
505+ } else {
506+ d.dispatcher.Dispatch(f.object, f.opcode, f.body)
507+ d.notifyListeners(f.object, f.opcode, f.body)
508+ }
509+ return nil
510+}
435 511
436- // Server-created objects are announced by an event on a known object.512+type signatureKey struct {
437- d.handleServerObject(f.object, f.opcode, f.body)513+ object uint32
514+ opcode uint16
515+}
438 516
439- if proxy, ok := obj.(Proxy); ok && proxy != nil {517+// RegisterEventSignature defines a signature for a legacy proxy. Generated
440- proxy.Dispatch(&Event{518+// proxies supply it themselves and cannot be overridden.
441- ProxyID: f.object,519+func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) {
442- Opcode: f.opcode,520+ if object == 0 || object == displayObjectID {
443- data: f.body,521+ return
444- display: d,
445- })
446- return nil
447 }522 }
448-523+ d.signatures.Store(signatureKey{object, opcode}, signature)
449- // High-performance dispatcher, then compatibility listeners.
450- d.dispatcher.Dispatch(f.object, f.opcode, f.body)
451- d.notifyListeners(f.object, f.opcode, f.body)
452- return nil
453 }524 }
454 525
455 // notifyListeners runs the listeners registered for an object and opcode.526 // notifyListeners runs the listeners registered for an object and opcode.
@@ -466,11 +537,7 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
466 if !ok {537 if !ok {
467 return538 return
468 }539 }
469- handlerSlice, ok := handlers.(*[]func([]byte))540+ for _, handler := range handlers.([]func([]byte)) {
470- if !ok || handlerSlice == nil {
471- return
472- }
473- for _, handler := range *handlerSlice {
474 if handler != nil {541 if handler != nil {
475 handler(body)542 handler(body)
476 }543 }
@@ -481,8 +548,8 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
481 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {548 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
482 switch opcode {549 switch opcode {
483 case 0: // error550 case 0: // error
484- if len(data) < 8 {551+ if err := validateEventBody("object,uint,string,", data); err != nil {
485- return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}552+ return &ProtocolError{Kind: "malformed_display_error", Err: err}
486 }553 }
487 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}554 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
488 objectID := event.Uint32()555 objectID := event.Uint32()
@@ -495,11 +562,12 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
495 return d.lastError562 return d.lastError
496 563
497 case 1: // delete_id564 case 1: // delete_id
498- if len(data) < 4 {565+ if len(data) != 4 {
499 return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}566 return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
500 }567 }
501 id := binary.LittleEndian.Uint32(data[0:4])568 id := binary.LittleEndian.Uint32(data[0:4])
502 d.objects.Delete(id)569 d.objects.Delete(id)
570+ d.context.proxies.Delete(id)
503 return nil571 return nil
504 572
505 default:573 default:
@@ -507,55 +575,42 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
507 }575 }
508 }576 }
509 577
510-// Roundtrip performs a synchronous roundtrip to the compositor578+// Roundtrip waits for one wl_display.sync callback. Concurrent event loops
579+// must not call Roundtrip while another goroutine dispatches this connection.
511 func (d *Display) Roundtrip() error {580 func (d *Display) Roundtrip() error {
512- callbackID := d.allocateID()581+ callback, err := d.Sync()
513- done := make(chan error, 1)582+ if err != nil {
514-
515- d.AddListener(callbackID, 0, func(_ []byte) {
516- d.objects.Delete(callbackID)
517- done <- nil
518- })
519-
520- // Send sync request (opcode 0)
521- if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil {
522 return err583 return err
523 }584 }
524-585+ for {
525- d.objects.Store(callbackID, &callbackObject{586+ if err = d.Dispatch(); err != nil {
526- BaseProxy: BaseProxy{
527- context: d.Context(),
528- id: callbackID,
529- },
530- display: d,
531- })
532-
533- for i := 0; i < 1000; i++ {
534- if err := d.Dispatch(); err != nil {
535 return err587 return err
536 }588 }
537- select {589+ if _, exists := d.objects.Load(callback.ID()); !exists {
538- case err := <-done:590+ return nil
539- return err
540- default:
541 }591 }
542 }592 }
543-
544- return fmt.Errorf("roundtrip failed: max iterations reached")
545 }593 }
546 594
547 // AddListener adds an event listener for an object595 // AddListener adds an event listener for an object
548 func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {596 func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {
597+ if handler == nil {
598+ return
599+ }
549 // Load or create listener map for object600 // Load or create listener map for object
550 listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})601 listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})
551 opcodeMap := listeners.(*sync.Map)602 opcodeMap := listeners.(*sync.Map)
552 603
553- // Load or create handlers slice for opcode604+ d.listenerMu.Lock()
554- handlers, _ := opcodeMap.LoadOrStore(opcode, &[]func([]byte){})605+ defer d.listenerMu.Unlock()
555- handlerSlice := handlers.(*[]func([]byte))606+ var handlers []func([]byte)
556-607+ if old, ok := opcodeMap.Load(opcode); ok {
557- // Add handler (thread-safe)608+ handlers = old.([]func([]byte))
558- *handlerSlice = append(*handlerSlice, handler)609+ }
610+ next := make([]func([]byte), len(handlers)+1)
611+ copy(next, handlers)
612+ next[len(handlers)] = handler
613+ opcodeMap.Store(opcode, next)
559 }614 }
560 615
561 // getRegistry gets the global registry616 // getRegistry gets the global registry
@@ -581,15 +636,13 @@ func (r *Registry) ID() uint32 {
581 // handleGlobal handles global announcements636 // handleGlobal handles global announcements
582 func (r *Registry) handleGlobal(data []byte) {637 func (r *Registry) handleGlobal(data []byte) {
583 if len(data) < 8 {638 if len(data) < 8 {
584- // log.Printf("wlclient: handleGlobal: data too short (%d bytes)", len(data))
585 return639 return
586 }640 }
587 641
588 name := binary.LittleEndian.Uint32(data[0:4])642 name := binary.LittleEndian.Uint32(data[0:4])
589 ifaceLen := binary.LittleEndian.Uint32(data[4:8])643 ifaceLen := binary.LittleEndian.Uint32(data[4:8])
590 644
591- if len(data) < 8+int(ifaceLen)+4 {645+ if ifaceLen == 0 || uint64(ifaceLen)+12 > uint64(len(data)) || data[8+ifaceLen-1] != 0 {
592- // log.Printf("wlclient: handleGlobal: insufficient data for interface string (need %d, have %d)", 8+int(ifaceLen)+4, len(data))
593 return646 return
594 }647 }
595 648
@@ -600,15 +653,12 @@ func (r *Registry) handleGlobal(data []byte) {
600 padding := (4 - (ifaceLen % 4)) % 4653 padding := (4 - (ifaceLen % 4)) % 4
601 versionOffset := 8 + int(ifaceLen) + int(padding)654 versionOffset := 8 + int(ifaceLen) + int(padding)
602 655
603- if len(data) < versionOffset+4 {656+ if len(data) != versionOffset+4 {
604- // log.Printf("wlclient: handleGlobal: insufficient data for version (need %d, have %d)", versionOffset+4, len(data))
605 return657 return
606 }658 }
607 659
608 version := binary.LittleEndian.Uint32(data[versionOffset:])660 version := binary.LittleEndian.Uint32(data[versionOffset:])
609 661
610- // log.Printf("wlclient: global announced: %s v%d (name=%d)", iface, version, name)
611-
612 // Store global662 // Store global
613 r.mu.Lock()663 r.mu.Lock()
614 r.globals[name] = Global{664 r.globals[name] = Global{
@@ -619,15 +669,19 @@ func (r *Registry) handleGlobal(data []byte) {
619 r.mu.Unlock()669 r.mu.Unlock()
620 670
621 // Call specific handler if registered671 // Call specific handler if registered
622- if handler, ok := r.handlers[iface]; ok {672+ r.mu.RLock()
623- // log.Printf("wlclient: calling handler for interface %s", iface)673+ handler, specific := r.handlers[iface]
674+ wildcard, all := r.handlers["*"]
675+ r.mu.RUnlock()
676+ if specific {
677+
624 handler(r, name, version)678 handler(r, name, version)
625 }679 }
626 680
627 // Call wildcard handler if registered681 // Call wildcard handler if registered
628- if handler, ok := r.handlers["*"]; ok {682+ if all {
629- // log.Printf("wlclient: calling wildcard handler for interface %s", iface)683+
630- handler(r, name, version)684+ wildcard(r, name, version)
631 }685 }
632 }686 }
633 687
@@ -641,11 +695,17 @@ func (r *Registry) handleGlobalRemove(data []byte) {
641 695
642 r.mu.Lock()696 r.mu.Lock()
643 delete(r.globals, name)697 delete(r.globals, name)
698+ handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
644 r.mu.Unlock()699 r.mu.Unlock()
700+ for _, handler := range handlers {
701+ handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name})
702+ }
645 }703 }
646 704
647 // AddHandler adds a handler for a specific interface705 // AddHandler adds a handler for a specific interface
648 func (r *Registry) AddHandler(iface string, handler GlobalHandler) {706 func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
707+ r.mu.Lock()
708+ defer r.mu.Unlock()
649 r.handlers[iface] = handler709 r.handlers[iface] = handler
650 }710 }
651 711
@@ -656,7 +716,7 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
656 proxy.SetID(r.display.allocateID())716 proxy.SetID(r.display.allocateID())
657 }717 }
658 718
659- // Ensure proxy has a context set - CRITICAL FIX719+ // Bootstrap a generated proxy with this display's context.
660 if proxy.Context() == nil {720 if proxy.Context() == nil {
661 if baseProxy, ok := proxy.(*BaseProxy); ok {721 if baseProxy, ok := proxy.(*BaseProxy); ok {
662 baseProxy.SetContext(r.display.Context())722 baseProxy.SetContext(r.display.Context())
@@ -667,12 +727,18 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
667 }727 }
668 }728 }
669 729
730+ if proxy.Context() != r.display.Context() {
731+ return fmt.Errorf("proxy belongs to another display")
732+ }
733+ if proxy.ID() == displayObjectID || proxy.ID() == r.id {
734+ return fmt.Errorf("reserved object ID")
735+ }
736+ if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy {
737+ return fmt.Errorf("object ID %d already registered", proxy.ID())
738+ }
670 // Register the proxy739 // Register the proxy
671 proxy.Context().Register(proxy)740 proxy.Context().Register(proxy)
672 741
673- // Also register directly in display objects map
674- r.display.objects.Store(proxy.ID(), proxy)
675-
676 // Send bind request (opcode 0) with proper arguments742 // Send bind request (opcode 0) with proper arguments
677 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {743 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
678 proxy.Context().Unregister(proxy)744 proxy.Context().Unregister(proxy)
@@ -718,118 +784,3 @@ func (r *Registry) FindGlobal(iface string) (Global, bool) {
718 }784 }
719 return Global{}, false785 return Global{}, false
720 }786 }
721-
722-// handleServerObject creates and registers server-allocated objects from events
723-func (d *Display) handleServerObject(objectID uint32, opcode uint16, body []byte) bool {
724- // Handle specific known protocols that create server objects
725- obj, ok := d.objects.Load(objectID)
726- if !ok {
727- return false
728- }
729-
730- // Check if this is a known object type that creates server objects
731- switch obj.(type) {
732- case *Registry:
733- // Registry doesn't create server objects directly
734- return false
735-
736- default:
737- // Check if this might be an output manager (interface zwlr_output_manager_v1)
738- if objectID == 5 { // This is likely the output manager based on your logs
739- return d.handleOutputManagerEvent(objectID, opcode, body)
740- }
741- }
742-
743- return false
744-}
745-
746-// handleOutputManagerEvent handles events from zwlr_output_manager_v1
747-func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body []byte) bool {
748- switch opcode {
749- case 0: // head event - creates new zwlr_output_head_v1 object
750- if len(body) < 4 {
751- return false
752- }
753-
754- // Parse the new_id for the head object
755- headID := binary.LittleEndian.Uint32(body[0:4])
756-
757- // Create a proper OutputHead object
758- headProxy := &OutputHead{
759- BaseProxy: BaseProxy{
760- id: headID,
761- context: d.Context(),
762- },
763- }
764-
765- // Register the new head object
766- d.objects.Store(headID, headProxy)
767- return true
768-
769- case 1: // done event
770- return false
771-
772- case 2: // finished event
773- return false
774- }
775-
776- return false
777-}
778-
779-// OutputHead represents a zwlr_output_head_v1 object
780-type OutputHead struct {
781- BaseProxy
782- name string
783- description string
784- width int32
785- height int32
786-}
787-
788-// Dispatch handles head events
789-func (h *OutputHead) Dispatch(event *Event) {
790- switch event.Opcode {
791- case 0: // name
792- h.name = event.String()
793- case 1: // description
794- h.description = event.String()
795- case 2: // physical_size
796- h.width = event.Int32()
797- h.height = event.Int32()
798- case 3: // mode (creates new mode object)
799- modeID := event.Uint32()
800- // Create mode object
801- mode := &OutputMode{
802- BaseProxy: BaseProxy{
803- id: modeID,
804- context: h.context,
805- },
806- }
807- h.context.display.objects.Store(modeID, mode)
808- case 9: // finished
809- h.context.Unregister(h)
810- default:
811- }
812-}
813-
814-// OutputMode represents a zwlr_output_mode_v1 object
815-type OutputMode struct {
816- BaseProxy
817- width int32
818- height int32
819- refresh int32
820-}
821-
822-// Dispatch handles mode events
823-func (m *OutputMode) Dispatch(event *Event) {
824- switch event.Opcode {
825- case 0: // size
826- m.width = event.Int32()
827- m.height = event.Int32()
828- case 1: // refresh
829- m.refresh = event.Int32()
830- case 2: // preferred
831- case 3: // finished
832- m.context.Unregister(m)
833- default:
834- }
835-}
M core.go +75 -372
@@ -3,10 +3,10 @@ package wlturbo
33 import (
44 "encoding/binary"
55 "errors"
6-
7- // "log"
86 "sync"
97 "sync/atomic"
8+
9+ "golang.org/x/sys/unix"
1010 )
1111
1212 // Context provides a compatibility layer for wl.Context
@@ -37,6 +37,8 @@ type Event struct {
3737 data []byte
3838 offset int
3939 display *Display
40+ fds []*OwnedFD
41+ fdIndex int
4042 }
4143
4244 // Data returns the raw event data
@@ -74,15 +76,12 @@ func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args
7476
7577 // Register registers a proxy object
7678 func (c *Context) Register(proxy Proxy) {
77- if proxy != nil && proxy.ID() != 0 {
79+ if proxy != nil && proxy.ID() != 0 && !c.closed.Load() {
80+ if proxy.Context() != c {
81+ return
82+ }
7883 c.proxies.Store(proxy.ID(), proxy)
7984 c.display.objects.Store(proxy.ID(), proxy)
80- // Debug large IDs
81- // if proxy.ID() > 1000000 {
82- // log.Printf("wlclient: WARNING: Registering proxy with suspicious ID %d (0x%x)", proxy.ID(), proxy.ID())
83- // } else if proxy.ID() == 5 {
84- // log.Printf("wlclient: Registered output manager proxy with ID 5")
85- // }
8685 }
8786 }
8887
@@ -221,19 +220,61 @@ func (e *Event) Array() []byte {
221220 return arr
222221 }
223222
224-// Fd reads a file descriptor that was delivered out-of-band for this event.
225-// Descriptors are queued on the connection in arrival order, as the Wayland
226-// wire protocol requires. The returned descriptor is owned by the caller,
227-// which must close it. It returns 0 when the event carried no descriptor.
223+// OwnedFD owns one received descriptor. Close is idempotent; Take transfers
224+// ownership to the caller (who must close it). Descriptor zero is valid.
225+type OwnedFD struct {
226+ mu sync.Mutex
227+ fd int
228+}
229+
230+func (f *OwnedFD) Take() (int, error) {
231+ if f == nil {
232+ return -1, errors.New("missing descriptor")
233+ }
234+ f.mu.Lock()
235+ defer f.mu.Unlock()
236+ if f.fd < 0 {
237+ return -1, errors.New("descriptor already taken or closed")
238+ }
239+ n := f.fd
240+ f.fd = -1
241+ return n, nil
242+}
243+func (f *OwnedFD) Close() error {
244+ if f == nil {
245+ return nil
246+ }
247+ f.mu.Lock()
248+ defer f.mu.Unlock()
249+ if f.fd < 0 {
250+ return nil
251+ }
252+ n := f.fd
253+ f.fd = -1
254+ return unix.Close(n)
255+}
256+
257+// FD transfers ownership of the next descriptor assigned to this event.
258+// An unclaimed descriptor is closed when dispatch completes.
259+func (e *Event) FD() *OwnedFD {
260+ if e.fdIndex >= len(e.fds) {
261+ return nil
262+ }
263+ fd := e.fds[e.fdIndex]
264+ e.fds[e.fdIndex] = nil
265+ e.fdIndex++
266+ return fd
267+}
268+
269+// Fd is the legacy raw descriptor API; prefer FD for explicit ownership.
228270 func (e *Event) Fd() uintptr {
229- if e.display != nil {
230- if fd, ok := e.display.nextFD(); ok {
231- return uintptr(fd)
232- }
271+ fd := e.FD()
272+ if fd == nil {
273+ _ = e.Uint32()
274+ return 0
233275 }
234- // No descriptor was delivered: consume the placeholder argument.
235- _ = e.Uint32()
236- return 0
276+ n, _ := fd.Take()
277+ return uintptr(n)
237278 }
238279
239280 // NewId reads a new object ID from the event
@@ -278,32 +319,24 @@ type RegistryGlobalRemoveEvent struct {
278319
279320 // AddGlobalHandler adds a global handler to the registry
280321 func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {
281- // Store the handler to be called for ALL globals
282- r.mu.Lock()
283- defer r.mu.Unlock()
284-
285- // Initialize handlers map if needed
286- if r.handlers == nil {
287- r.handlers = make(map[string]GlobalHandler)
322+ if handler == nil {
323+ return
288324 }
289-
290325 r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {
291- global, ok := r.FindGlobalByName(name)
292- if ok {
293- event := RegistryGlobalEvent{
294- Registry: r,
295- Name: name,
296- Interface: global.Interface,
297- Version: version,
298- }
299- handler.HandleRegistryGlobal(event)
326+ if global, ok := r.FindGlobalByName(name); ok {
327+ handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version})
300328 }
301329 })
302330 }
303331
304-// AddGlobalRemoveHandler adds a global remove handler (placeholder)
332+// AddGlobalRemoveHandler registers a handler for registry removals.
305333 func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {
306- // Would need to implement removal handling
334+ if handler == nil {
335+ return
336+ }
337+ r.mu.Lock()
338+ r.removeHandlers = append(r.removeHandlers, handler)
339+ r.mu.Unlock()
307340 }
308341
309342 // FindGlobalByName finds a global by its name ID
@@ -340,342 +373,12 @@ func (d *Display) Sync() (Object, error) {
340373 display: d,
341374 }
342375
343- // Send sync request (opcode 0)
376+ // Store before sending: the compositor may reply immediately.
377+ d.context.Register(callback)
344378 if err := d.SendRequest(1, 0, callbackID); err != nil {
379+ d.context.Unregister(callback)
345380 return nil, err
346381 }
347382
348- // Store the callback object
349- d.objects.Store(callbackID, callback)
350-
351383 return callback, nil
352384 }
353-
354-// Additional protocol object types needed for compatibility
355-
356-// Seat capability constants
357-const (
358- SeatCapabilityPointer = 1
359- SeatCapabilityKeyboard = 2
360- SeatCapabilityTouch = 4
361-)
362-
363-// Seat represents a wl_seat
364-type Seat struct {
365- BaseProxy
366- capabilities uint32
367- name string
368-}
369-
370-// SeatCapabilitiesHandler handles seat capabilities events
371-type SeatCapabilitiesHandler interface {
372- HandleSeatCapabilities(seat *Seat, capabilities uint32)
373-}
374-
375-// SeatNameHandler handles seat name events
376-type SeatNameHandler interface {
377- HandleSeatName(seat *Seat, name string)
378-}
379-
380-// NewSeat creates a new seat proxy
381-func NewSeat(ctx *Context) *Seat {
382- return &Seat{
383- BaseProxy: BaseProxy{
384- context: ctx,
385- },
386- }
387-}
388-
389-// GetPointer gets the pointer device
390-func (s *Seat) GetPointer() (*Pointer, error) {
391- pointer := &Pointer{
392- BaseProxy: BaseProxy{
393- context: s.context,
394- id: s.context.display.allocateID(),
395- },
396- }
397-
398- // Register pointer before sending request
399- s.context.Register(pointer)
400-
401- // Send get_pointer request (opcode 0)
402- err := s.context.SendRequest(s, 0, pointer.id)
403- if err != nil {
404- s.context.Unregister(pointer)
405- return nil, err
406- }
407-
408- return pointer, nil
409-}
410-
411-// GetKeyboard gets the keyboard device
412-func (s *Seat) GetKeyboard() (*Keyboard, error) {
413- keyboard := &Keyboard{
414- BaseProxy: BaseProxy{
415- context: s.context,
416- id: s.context.display.allocateID(),
417- },
418- }
419-
420- // Register keyboard before sending request
421- s.context.Register(keyboard)
422-
423- // Send get_keyboard request (opcode 1)
424- err := s.context.SendRequest(s, 1, keyboard.id)
425- if err != nil {
426- s.context.Unregister(keyboard)
427- return nil, err
428- }
429-
430- return keyboard, nil
431-}
432-
433-// GetTouch gets the touch device
434-func (s *Seat) GetTouch() (*Touch, error) {
435- touch := &Touch{
436- BaseProxy: BaseProxy{
437- context: s.context,
438- id: s.context.display.allocateID(),
439- },
440- }
441-
442- // Register touch before sending request
443- s.context.Register(touch)
444-
445- // Send get_touch request (opcode 2)
446- err := s.context.SendRequest(s, 2, touch.id)
447- if err != nil {
448- s.context.Unregister(touch)
449- return nil, err
450- }
451-
452- return touch, nil
453-}
454-
455-// Release releases the seat
456-func (s *Seat) Release() error {
457- err := s.context.SendRequest(s, 3) // opcode 3
458- if err == nil {
459- s.context.Unregister(s)
460- }
461- return err
462-}
463-
464-// Capabilities returns the seat capabilities
465-func (s *Seat) Capabilities() uint32 {
466- return s.capabilities
467-}
468-
469-// Name returns the seat name
470-func (s *Seat) Name() string {
471- return s.name
472-}
473-
474-// Dispatch handles events for the seat
475-func (s *Seat) Dispatch(event *Event) {
476- // Handle seat events
477- switch event.Opcode {
478- case 0: // capabilities
479- s.capabilities = event.Uint32()
480- // TODO: Call registered handlers
481- case 1: // name
482- s.name = event.String()
483- // TODO: Call registered handlers
484- }
485-}
486-
487-// Surface represents a wl_surface
488-type Surface struct {
489- BaseProxy
490-}
491-
492-// NewSurface creates a new surface proxy
493-func NewSurface(ctx *Context) *Surface {
494- return &Surface{
495- BaseProxy: BaseProxy{
496- context: ctx,
497- },
498- }
499-}
500-
501-// Destroy destroys the surface
502-func (s *Surface) Destroy() error {
503- err := s.context.SendRequest(s, 0) // opcode 0
504- if err == nil {
505- s.context.Unregister(s)
506- }
507- return err
508-}
509-
510-// Attach attaches a buffer to the surface
511-func (s *Surface) Attach(buffer Object, x, y int32) error {
512- return s.context.SendRequest(s, 1, buffer, x, y) // opcode 1
513-}
514-
515-// Damage marks a region of the surface as damaged
516-func (s *Surface) Damage(x, y, width, height int32) error {
517- return s.context.SendRequest(s, 2, x, y, width, height) // opcode 2
518-}
519-
520-// Frame requests a frame callback
521-func (s *Surface) Frame() (Object, error) {
522- callback := &callbackObject{
523- BaseProxy: BaseProxy{
524- context: s.context,
525- id: s.context.display.allocateID(),
526- },
527- display: s.context.display,
528- }
529- s.context.Register(callback)
530-
531- err := s.context.SendRequest(s, 3, callback.id) // opcode 3
532- if err != nil {
533- s.context.Unregister(callback)
534- return nil, err
535- }
536-
537- return callback, nil
538-}
539-
540-// SetOpaqueRegion sets the opaque region
541-func (s *Surface) SetOpaqueRegion(region *Region) error {
542- return s.context.SendRequest(s, 4, region) // opcode 4
543-}
544-
545-// SetInputRegion sets the input region
546-func (s *Surface) SetInputRegion(region *Region) error {
547- return s.context.SendRequest(s, 5, region) // opcode 5
548-}
549-
550-// Commit commits pending surface state
551-func (s *Surface) Commit() error {
552- return s.context.SendRequest(s, 6) // opcode 6
553-}
554-
555-// SetBufferTransform sets the buffer transform
556-func (s *Surface) SetBufferTransform(transform int32) error {
557- return s.context.SendRequest(s, 7, transform) // opcode 7
558-}
559-
560-// SetBufferScale sets the buffer scale
561-func (s *Surface) SetBufferScale(scale int32) error {
562- return s.context.SendRequest(s, 8, scale) // opcode 8
563-}
564-
565-// DamageBuffer marks a region of the buffer as damaged
566-func (s *Surface) DamageBuffer(x, y, width, height int32) error {
567- return s.context.SendRequest(s, 9, x, y, width, height) // opcode 9
568-}
569-
570-// Offset sets the buffer offset
571-func (s *Surface) Offset(x, y int32) error {
572- return s.context.SendRequest(s, 10, x, y) // opcode 10
573-}
574-
575-// Dispatch handles surface events
576-func (s *Surface) Dispatch(event *Event) {
577- // Surface can receive enter/leave/preferred_buffer_scale/preferred_buffer_transform events
578- // But for now we don't handle them
579-}
580-
581-// Pointer represents a wl_pointer
582-type Pointer struct {
583- BaseProxy
584-}
585-
586-// Keyboard represents a wl_keyboard
587-type Keyboard struct {
588- BaseProxy
589-}
590-
591-// Touch represents a wl_touch
592-type Touch struct {
593- BaseProxy
594-}
595-
596-// Output represents a wl_output
597-type Output struct {
598- BaseProxy
599-}
600-
601-// Region represents a wl_region
602-type Region struct {
603- BaseProxy
604-}
605-
606-// Add adds a rectangle to the region
607-func (r *Region) Add(x, y, width, height int32) error {
608- return r.context.SendRequest(r, 0, x, y, width, height) // opcode 0
609-}
610-
611-// Subtract subtracts a rectangle from the region
612-func (r *Region) Subtract(x, y, width, height int32) error {
613- return r.context.SendRequest(r, 1, x, y, width, height) // opcode 1
614-}
615-
616-// Destroy destroys the region
617-func (r *Region) Destroy() error {
618- err := r.context.SendRequest(r, 2) // opcode 2
619- if err == nil {
620- r.context.Unregister(r)
621- }
622- return err
623-}
624-
625-// Compositor represents a wl_compositor
626-type Compositor struct {
627- BaseProxy
628-}
629-
630-// NewCompositor creates a new compositor proxy
631-func NewCompositor(ctx *Context) *Compositor {
632- return &Compositor{
633- BaseProxy: BaseProxy{
634- context: ctx,
635- },
636- }
637-}
638-
639-// CreateSurface creates a new surface
640-func (c *Compositor) CreateSurface() (*Surface, error) {
641- surface := &Surface{
642- BaseProxy: BaseProxy{
643- context: c.context,
644- id: c.context.display.allocateID(),
645- },
646- }
647-
648- // Register surface before sending request
649- c.context.Register(surface)
650-
651- // Send create_surface request (opcode 0)
652- err := c.context.SendRequest(c, 0, surface.id)
653- if err != nil {
654- c.context.Unregister(surface)
655- return nil, err
656- }
657-
658- return surface, nil
659-}
660-
661-// CreateRegion creates a new region
662-func (c *Compositor) CreateRegion() (*Region, error) {
663- region := &Region{
664- BaseProxy: BaseProxy{
665- context: c.context,
666- id: c.context.display.allocateID(),
667- },
668- }
669-
670- // Register region before sending request
671- c.context.Register(region)
672-
673- // Send create_region request (opcode 1)
674- err := c.context.SendRequest(c, 1, region.id)
675- if err != nil {
676- c.context.Unregister(region)
677- return nil, err
678- }
679-
680- return region, nil
681-}
@@ -3,10 +3,10 @@ package wlturbo
3 import (3 import (
4 "encoding/binary"4 "encoding/binary"
5 "errors"5 "errors"
6-
7- // "log"
8 "sync"6 "sync"
9 "sync/atomic"7 "sync/atomic"
8+
9+ "golang.org/x/sys/unix"
10 )10 )
11 11
12 // Context provides a compatibility layer for wl.Context12 // Context provides a compatibility layer for wl.Context
@@ -37,6 +37,8 @@ type Event struct {
37 data []byte37 data []byte
38 offset int38 offset int
39 display *Display39 display *Display
40+ fds []*OwnedFD
41+ fdIndex int
40 }42 }
41 43
42 // Data returns the raw event data44 // Data returns the raw event data
@@ -74,15 +76,12 @@ func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args
74 76
75 // Register registers a proxy object77 // Register registers a proxy object
76 func (c *Context) Register(proxy Proxy) {78 func (c *Context) Register(proxy Proxy) {
77- if proxy != nil && proxy.ID() != 0 {79+ if proxy != nil && proxy.ID() != 0 && !c.closed.Load() {
80+ if proxy.Context() != c {
81+ return
82+ }
78 c.proxies.Store(proxy.ID(), proxy)83 c.proxies.Store(proxy.ID(), proxy)
79 c.display.objects.Store(proxy.ID(), proxy)84 c.display.objects.Store(proxy.ID(), proxy)
80- // Debug large IDs
81- // if proxy.ID() > 1000000 {
82- // log.Printf("wlclient: WARNING: Registering proxy with suspicious ID %d (0x%x)", proxy.ID(), proxy.ID())
83- // } else if proxy.ID() == 5 {
84- // log.Printf("wlclient: Registered output manager proxy with ID 5")
85- // }
86 }85 }
87 }86 }
88 87
@@ -221,19 +220,61 @@ func (e *Event) Array() []byte {
221 return arr220 return arr
222 }221 }
223 222
224-// Fd reads a file descriptor that was delivered out-of-band for this event.223+// OwnedFD owns one received descriptor. Close is idempotent; Take transfers
225-// Descriptors are queued on the connection in arrival order, as the Wayland224+// ownership to the caller (who must close it). Descriptor zero is valid.
226-// wire protocol requires. The returned descriptor is owned by the caller,225+type OwnedFD struct {
227-// which must close it. It returns 0 when the event carried no descriptor.226+ mu sync.Mutex
227+ fd int
228+}
229+
230+func (f *OwnedFD) Take() (int, error) {
231+ if f == nil {
232+ return -1, errors.New("missing descriptor")
233+ }
234+ f.mu.Lock()
235+ defer f.mu.Unlock()
236+ if f.fd < 0 {
237+ return -1, errors.New("descriptor already taken or closed")
238+ }
239+ n := f.fd
240+ f.fd = -1
241+ return n, nil
242+}
243+func (f *OwnedFD) Close() error {
244+ if f == nil {
245+ return nil
246+ }
247+ f.mu.Lock()
248+ defer f.mu.Unlock()
249+ if f.fd < 0 {
250+ return nil
251+ }
252+ n := f.fd
253+ f.fd = -1
254+ return unix.Close(n)
255+}
256+
257+// FD transfers ownership of the next descriptor assigned to this event.
258+// An unclaimed descriptor is closed when dispatch completes.
259+func (e *Event) FD() *OwnedFD {
260+ if e.fdIndex >= len(e.fds) {
261+ return nil
262+ }
263+ fd := e.fds[e.fdIndex]
264+ e.fds[e.fdIndex] = nil
265+ e.fdIndex++
266+ return fd
267+}
268+
269+// Fd is the legacy raw descriptor API; prefer FD for explicit ownership.
228 func (e *Event) Fd() uintptr {270 func (e *Event) Fd() uintptr {
229- if e.display != nil {271+ fd := e.FD()
230- if fd, ok := e.display.nextFD(); ok {272+ if fd == nil {
231- return uintptr(fd)273+ _ = e.Uint32()
232- }274+ return 0
233 }275 }
234- // No descriptor was delivered: consume the placeholder argument.276+ n, _ := fd.Take()
235- _ = e.Uint32()277+ return uintptr(n)
236- return 0
237 }278 }
238 279
239 // NewId reads a new object ID from the event280 // NewId reads a new object ID from the event
@@ -278,32 +319,24 @@ type RegistryGlobalRemoveEvent struct {
278 319
279 // AddGlobalHandler adds a global handler to the registry320 // AddGlobalHandler adds a global handler to the registry
280 func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {321 func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {
281- // Store the handler to be called for ALL globals322+ if handler == nil {
282- r.mu.Lock()323+ return
283- defer r.mu.Unlock()
284-
285- // Initialize handlers map if needed
286- if r.handlers == nil {
287- r.handlers = make(map[string]GlobalHandler)
288 }324 }
289-
290 r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {325 r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {
291- global, ok := r.FindGlobalByName(name)326+ if global, ok := r.FindGlobalByName(name); ok {
292- if ok {327+ handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version})
293- event := RegistryGlobalEvent{
294- Registry: r,
295- Name: name,
296- Interface: global.Interface,
297- Version: version,
298- }
299- handler.HandleRegistryGlobal(event)
300 }328 }
301 })329 })
302 }330 }
303 331
304-// AddGlobalRemoveHandler adds a global remove handler (placeholder)332+// AddGlobalRemoveHandler registers a handler for registry removals.
305 func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {333 func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {
306- // Would need to implement removal handling334+ if handler == nil {
335+ return
336+ }
337+ r.mu.Lock()
338+ r.removeHandlers = append(r.removeHandlers, handler)
339+ r.mu.Unlock()
307 }340 }
308 341
309 // FindGlobalByName finds a global by its name ID342 // FindGlobalByName finds a global by its name ID
@@ -340,342 +373,12 @@ func (d *Display) Sync() (Object, error) {
340 display: d,373 display: d,
341 }374 }
342 375
343- // Send sync request (opcode 0)376+ // Store before sending: the compositor may reply immediately.
377+ d.context.Register(callback)
344 if err := d.SendRequest(1, 0, callbackID); err != nil {378 if err := d.SendRequest(1, 0, callbackID); err != nil {
379+ d.context.Unregister(callback)
345 return nil, err380 return nil, err
346 }381 }
347 382
348- // Store the callback object
349- d.objects.Store(callbackID, callback)
350-
351 return callback, nil383 return callback, nil
352 }384 }
353-
354-// Additional protocol object types needed for compatibility
355-
356-// Seat capability constants
357-const (
358- SeatCapabilityPointer = 1
359- SeatCapabilityKeyboard = 2
360- SeatCapabilityTouch = 4
361-)
362-
363-// Seat represents a wl_seat
364-type Seat struct {
365- BaseProxy
366- capabilities uint32
367- name string
368-}
369-
370-// SeatCapabilitiesHandler handles seat capabilities events
371-type SeatCapabilitiesHandler interface {
372- HandleSeatCapabilities(seat *Seat, capabilities uint32)
373-}
374-
375-// SeatNameHandler handles seat name events
376-type SeatNameHandler interface {
377- HandleSeatName(seat *Seat, name string)
378-}
379-
380-// NewSeat creates a new seat proxy
381-func NewSeat(ctx *Context) *Seat {
382- return &Seat{
383- BaseProxy: BaseProxy{
384- context: ctx,
385- },
386- }
387-}
388-
389-// GetPointer gets the pointer device
390-func (s *Seat) GetPointer() (*Pointer, error) {
391- pointer := &Pointer{
392- BaseProxy: BaseProxy{
393- context: s.context,
394- id: s.context.display.allocateID(),
395- },
396- }
397-
398- // Register pointer before sending request
399- s.context.Register(pointer)
400-
401- // Send get_pointer request (opcode 0)
402- err := s.context.SendRequest(s, 0, pointer.id)
403- if err != nil {
404- s.context.Unregister(pointer)
405- return nil, err
406- }
407-
408- return pointer, nil
409-}
410-
411-// GetKeyboard gets the keyboard device
412-func (s *Seat) GetKeyboard() (*Keyboard, error) {
413- keyboard := &Keyboard{
414- BaseProxy: BaseProxy{
415- context: s.context,
416- id: s.context.display.allocateID(),
417- },
418- }
419-
420- // Register keyboard before sending request
421- s.context.Register(keyboard)
422-
423- // Send get_keyboard request (opcode 1)
424- err := s.context.SendRequest(s, 1, keyboard.id)
425- if err != nil {
426- s.context.Unregister(keyboard)
427- return nil, err
428- }
429-
430- return keyboard, nil
431-}
432-
433-// GetTouch gets the touch device
434-func (s *Seat) GetTouch() (*Touch, error) {
435- touch := &Touch{
436- BaseProxy: BaseProxy{
437- context: s.context,
438- id: s.context.display.allocateID(),
439- },
440- }
441-
442- // Register touch before sending request
443- s.context.Register(touch)
444-
445- // Send get_touch request (opcode 2)
446- err := s.context.SendRequest(s, 2, touch.id)
447- if err != nil {
448- s.context.Unregister(touch)
449- return nil, err
450- }
451-
452- return touch, nil
453-}
454-
455-// Release releases the seat
456-func (s *Seat) Release() error {
457- err := s.context.SendRequest(s, 3) // opcode 3
458- if err == nil {
459- s.context.Unregister(s)
460- }
461- return err
462-}
463-
464-// Capabilities returns the seat capabilities
465-func (s *Seat) Capabilities() uint32 {
466- return s.capabilities
467-}
468-
469-// Name returns the seat name
470-func (s *Seat) Name() string {
471- return s.name
472-}
473-
474-// Dispatch handles events for the seat
475-func (s *Seat) Dispatch(event *Event) {
476- // Handle seat events
477- switch event.Opcode {
478- case 0: // capabilities
479- s.capabilities = event.Uint32()
480- // TODO: Call registered handlers
481- case 1: // name
482- s.name = event.String()
483- // TODO: Call registered handlers
484- }
485-}
486-
487-// Surface represents a wl_surface
488-type Surface struct {
489- BaseProxy
490-}
491-
492-// NewSurface creates a new surface proxy
493-func NewSurface(ctx *Context) *Surface {
494- return &Surface{
495- BaseProxy: BaseProxy{
496- context: ctx,
497- },
498- }
499-}
500-
501-// Destroy destroys the surface
502-func (s *Surface) Destroy() error {
503- err := s.context.SendRequest(s, 0) // opcode 0
504- if err == nil {
505- s.context.Unregister(s)
506- }
507- return err
508-}
509-
510-// Attach attaches a buffer to the surface
511-func (s *Surface) Attach(buffer Object, x, y int32) error {
512- return s.context.SendRequest(s, 1, buffer, x, y) // opcode 1
513-}
514-
515-// Damage marks a region of the surface as damaged
516-func (s *Surface) Damage(x, y, width, height int32) error {
517- return s.context.SendRequest(s, 2, x, y, width, height) // opcode 2
518-}
519-
520-// Frame requests a frame callback
521-func (s *Surface) Frame() (Object, error) {
522- callback := &callbackObject{
523- BaseProxy: BaseProxy{
524- context: s.context,
525- id: s.context.display.allocateID(),
526- },
527- display: s.context.display,
528- }
529- s.context.Register(callback)
530-
531- err := s.context.SendRequest(s, 3, callback.id) // opcode 3
532- if err != nil {
533- s.context.Unregister(callback)
534- return nil, err
535- }
536-
537- return callback, nil
538-}
539-
540-// SetOpaqueRegion sets the opaque region
541-func (s *Surface) SetOpaqueRegion(region *Region) error {
542- return s.context.SendRequest(s, 4, region) // opcode 4
543-}
544-
545-// SetInputRegion sets the input region
546-func (s *Surface) SetInputRegion(region *Region) error {
547- return s.context.SendRequest(s, 5, region) // opcode 5
548-}
549-
550-// Commit commits pending surface state
551-func (s *Surface) Commit() error {
552- return s.context.SendRequest(s, 6) // opcode 6
553-}
554-
555-// SetBufferTransform sets the buffer transform
556-func (s *Surface) SetBufferTransform(transform int32) error {
557- return s.context.SendRequest(s, 7, transform) // opcode 7
558-}
559-
560-// SetBufferScale sets the buffer scale
561-func (s *Surface) SetBufferScale(scale int32) error {
562- return s.context.SendRequest(s, 8, scale) // opcode 8
563-}
564-
565-// DamageBuffer marks a region of the buffer as damaged
566-func (s *Surface) DamageBuffer(x, y, width, height int32) error {
567- return s.context.SendRequest(s, 9, x, y, width, height) // opcode 9
568-}
569-
570-// Offset sets the buffer offset
571-func (s *Surface) Offset(x, y int32) error {
572- return s.context.SendRequest(s, 10, x, y) // opcode 10
573-}
574-
575-// Dispatch handles surface events
576-func (s *Surface) Dispatch(event *Event) {
577- // Surface can receive enter/leave/preferred_buffer_scale/preferred_buffer_transform events
578- // But for now we don't handle them
579-}
580-
581-// Pointer represents a wl_pointer
582-type Pointer struct {
583- BaseProxy
584-}
585-
586-// Keyboard represents a wl_keyboard
587-type Keyboard struct {
588- BaseProxy
589-}
590-
591-// Touch represents a wl_touch
592-type Touch struct {
593- BaseProxy
594-}
595-
596-// Output represents a wl_output
597-type Output struct {
598- BaseProxy
599-}
600-
601-// Region represents a wl_region
602-type Region struct {
603- BaseProxy
604-}
605-
606-// Add adds a rectangle to the region
607-func (r *Region) Add(x, y, width, height int32) error {
608- return r.context.SendRequest(r, 0, x, y, width, height) // opcode 0
609-}
610-
611-// Subtract subtracts a rectangle from the region
612-func (r *Region) Subtract(x, y, width, height int32) error {
613- return r.context.SendRequest(r, 1, x, y, width, height) // opcode 1
614-}
615-
616-// Destroy destroys the region
617-func (r *Region) Destroy() error {
618- err := r.context.SendRequest(r, 2) // opcode 2
619- if err == nil {
620- r.context.Unregister(r)
621- }
622- return err
623-}
624-
625-// Compositor represents a wl_compositor
626-type Compositor struct {
627- BaseProxy
628-}
629-
630-// NewCompositor creates a new compositor proxy
631-func NewCompositor(ctx *Context) *Compositor {
632- return &Compositor{
633- BaseProxy: BaseProxy{
634- context: ctx,
635- },
636- }
637-}
638-
639-// CreateSurface creates a new surface
640-func (c *Compositor) CreateSurface() (*Surface, error) {
641- surface := &Surface{
642- BaseProxy: BaseProxy{
643- context: c.context,
644- id: c.context.display.allocateID(),
645- },
646- }
647-
648- // Register surface before sending request
649- c.context.Register(surface)
650-
651- // Send create_surface request (opcode 0)
652- err := c.context.SendRequest(c, 0, surface.id)
653- if err != nil {
654- c.context.Unregister(surface)
655- return nil, err
656- }
657-
658- return surface, nil
659-}
660-
661-// CreateRegion creates a new region
662-func (c *Compositor) CreateRegion() (*Region, error) {
663- region := &Region{
664- BaseProxy: BaseProxy{
665- context: c.context,
666- id: c.context.display.allocateID(),
667- },
668- }
669-
670- // Register region before sending request
671- c.context.Register(region)
672-
673- // Send create_region request (opcode 1)
674- err := c.context.SendRequest(c, 1, region.id)
675- if err != nil {
676- c.context.Unregister(region)
677- return nil, err
678- }
679-
680- return region, nil
681-}
A docs/protocol-migration.md +29 -0
@@ -0,0 +1,29 @@
1+# Canonical core / migration (v0.x)
2+
3+`protocol/core` replaces root-package and `wl` shim core object types. Bootstrap
4+`Display`, `Registry`, `Context`, and `BaseProxy` remain in the root/`wl`
5+transport. Bind globals using `core.NewCompositor(display.Context())`,
6+`core.NewSeat(display.Context())`, etc. and `display.Registry().Bind(name,
7+core.CompositorInterface, version, compositor)`. Requests create typed registered
8+children (`seat.GetKeyboard()`, `surface.Frame()`, `shm.CreatePool()`) and
9+server-side `new_id` events register concrete objects before invoking handlers.
10+Do not overwrite registered objects with a second wrapper.
11+
12+Breaking changes: `wlturbo`/`wl` `Seat`, `Surface`, `Pointer`, `Keyboard`,
13+`Touch`, `Output`, `Region`, `Compositor`, their constructors, and seat
14+capability aliases have moved to `protocol/core`. Generated methods accept
15+concrete core objects and return concrete children, not `wl.Object`. Legacy
16+raw `Event.Fd()` is not for generated events; generated FD handlers receive
17+`*wlturbo.OwnedFD` with `Take() (int,error)` or `Close()`; second and later
18+handlers receive nil FD. On a successful full send a generated FD request
19+closes the local descriptor; on error the caller keeps it. `Registry.BindID`
20+remains for legacy callers but cannot register typed event dispatch: prefer
21+`Bind` and generated constructors. Unrecognized custom-proxy event opcodes
22+now fail unless declared via `Display.RegisterEventSignature`.
23+
24+The scanner accepts `-import wl_surface=github.com/bnema/wlturbo/protocol/core`
25+(repeat for each external type); local XML definitions take precedence, then
26+explicit external mapping, then only bootstrap display/registry. Generate with
27+`GOWORK=off go generate ./...`; metadata and XML license are in
28+`protocol/SOURCE.md`. P3 extensions will use the same scanner and public core
29+mappings; no generator-owned compositor policy is introduced here.
@@ -0,0 +1,29 @@
1+# Canonical core / migration (v0.x)
2+
3+`protocol/core` replaces root-package and `wl` shim core object types. Bootstrap
4+`Display`, `Registry`, `Context`, and `BaseProxy` remain in the root/`wl`
5+transport. Bind globals using `core.NewCompositor(display.Context())`,
6+`core.NewSeat(display.Context())`, etc. and `display.Registry().Bind(name,
7+core.CompositorInterface, version, compositor)`. Requests create typed registered
8+children (`seat.GetKeyboard()`, `surface.Frame()`, `shm.CreatePool()`) and
9+server-side `new_id` events register concrete objects before invoking handlers.
10+Do not overwrite registered objects with a second wrapper.
11+
12+Breaking changes: `wlturbo`/`wl` `Seat`, `Surface`, `Pointer`, `Keyboard`,
13+`Touch`, `Output`, `Region`, `Compositor`, their constructors, and seat
14+capability aliases have moved to `protocol/core`. Generated methods accept
15+concrete core objects and return concrete children, not `wl.Object`. Legacy
16+raw `Event.Fd()` is not for generated events; generated FD handlers receive
17+`*wlturbo.OwnedFD` with `Take() (int,error)` or `Close()`; second and later
18+handlers receive nil FD. On a successful full send a generated FD request
19+closes the local descriptor; on error the caller keeps it. `Registry.BindID`
20+remains for legacy callers but cannot register typed event dispatch: prefer
21+`Bind` and generated constructors. Unrecognized custom-proxy event opcodes
22+now fail unless declared via `Display.RegisterEventSignature`.
23+
24+The scanner accepts `-import wl_surface=github.com/bnema/wlturbo/protocol/core`
25+(repeat for each external type); local XML definitions take precedence, then
26+explicit external mapping, then only bootstrap display/registry. Generate with
27+`GOWORK=off go generate ./...`; metadata and XML license are in
28+`protocol/SOURCE.md`. P3 extensions will use the same scanner and public core
29+mappings; no generator-owned compositor policy is introduced here.
M fd_test.go +23 -48
@@ -90,6 +90,8 @@ func newFDDisplay(t *testing.T, client *net.UnixConn) (*Display, *[]int) {
9090
9191 d := newDisplay(client)
9292 fds := []int{}
93+ d.RegisterEventSignature(7, 3, "uint,fd,")
94+ d.RegisterEventSignature(7, 4, "uint,")
9395 d.objects.Store(uint32(7), &fdRecordingProxy{
9496 BaseProxy: BaseProxy{id: 7, context: d.context},
9597 fds: &fds,
@@ -146,49 +148,19 @@ func TestDisplayFD_Single(t *testing.T) {
146148 readSentinel(t, r, "single-fd-sentinel")
147149 }
148150
149-func TestDisplayFD_MultipleInOneControlMessage(t *testing.T) {
151+func TestDisplayFD_ExtraDescriptorRejected(t *testing.T) {
150152 client, peer := unixSocketPair(t)
151- d, received := newFDDisplay(t, client)
152-
153- firstR, firstW, err := os.Pipe()
154- if err != nil {
155- t.Fatalf("pipe: %v", err)
156- }
157- defer firstR.Close()
158- secondR, secondW, err := os.Pipe()
153+ d, _ := newFDDisplay(t, client)
154+ r, w, err := os.Pipe()
159155 if err != nil {
160- t.Fatalf("pipe: %v", err)
156+ t.Fatal(err)
161157 }
162- defer secondR.Close()
163-
164- sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}),
165- []int{int(firstW.Fd()), int(secondW.Fd())})
166- firstW.Close()
167- secondW.Close()
168-
169- if err := d.Dispatch(); err != nil {
170- t.Fatalf("Dispatch: %v", err)
171- }
172-
173- // A handler consumes one descriptor per fd argument, so the test drains
174- // whatever the connection still holds, in arrival order.
175- for {
176- fd, ok := d.nextFD()
177- if !ok {
178- break
179- }
180- *received = append(*received, fd)
181- }
182-
183- if len(*received) != 2 {
184- t.Fatalf("received %d descriptors, want 2", len(*received))
158+ defer r.Close()
159+ defer w.Close()
160+ sendMessageWithFDs(t, peer, message(7, 3, nil), []int{int(w.Fd()), int(w.Fd())})
161+ if err := d.Dispatch(); !errors.Is(err, ErrMalformedFrame) {
162+ t.Fatalf("extra FD: %v", err)
185163 }
186-
187- writeSentinel(t, uintptr((*received)[0]), "first-pipe")
188- writeSentinel(t, uintptr((*received)[1]), "second-pipe")
189-
190- readSentinel(t, firstR, "first-pipe")
191- readSentinel(t, secondR, "second-pipe")
192164 }
193165
194166 func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) {
@@ -304,9 +276,9 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) {
304276 }
305277
306278 // A truncated control message must not leave descriptors queued.
307- d.fdMu.Lock()
279+ d.recvMu.Lock()
308280 pending := len(d.pendingFDs)
309- d.fdMu.Unlock()
281+ d.recvMu.Unlock()
310282 if pending != 0 {
311283 t.Fatalf("%d descriptors still queued after a truncated control message", pending)
312284 }
@@ -315,6 +287,7 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) {
315287 func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
316288 client, peer := unixSocketPair(t)
317289 d := newDisplay(client)
290+ d.RegisterEventSignature(7, 3, "uint,fd,")
318291 d.objects.Store(uint32(7), &fdIgnoringProxy{
319292 BaseProxy: BaseProxy{id: 7, context: d.context},
320293 })
@@ -333,12 +306,12 @@ func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
333306 t.Fatalf("Dispatch: %v", err)
334307 }
335308
336- // While the display still holds the descriptor the pipe stays open.
337- if err := r.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
338- t.Fatalf("SetReadDeadline: %v", err)
309+ // An ignored FD is closed at the end of the event, not at shutdown.
310+ if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
311+ t.Fatal(err)
339312 }
340- if _, err := r.Read(make([]byte, 1)); !errors.Is(err, os.ErrDeadlineExceeded) {
341- t.Fatalf("read with the display holding the descriptor = %v, want %v", err, os.ErrDeadlineExceeded)
313+ if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
314+ t.Fatalf("ignored FD: %v", err)
342315 }
343316
344317 // Closing the display must release it, so the read end sees EOF.
@@ -421,6 +394,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) {
421394
422395 client, peer := rawSocketPair(t)
423396 d := newDisplay(client)
397+ d.RegisterEventSignature(7, 3, "uint,fd,")
424398 d.objects.Store(uint32(7), &fdIgnoringProxy{
425399 BaseProxy: BaseProxy{id: 7, context: d.context},
426400 })
@@ -448,6 +422,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) {
448422 func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
449423 client, peer := unixSocketPair(t)
450424 d := newDisplay(client)
425+ d.RegisterEventSignature(7, 3, "uint,fd,")
451426 d.objects.Store(uint32(7), &fdIgnoringProxy{
452427 BaseProxy: BaseProxy{id: 7, context: d.context},
453428 })
@@ -467,9 +442,9 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
467442 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)
468443 }
469444
470- d.fdMu.Lock()
445+ d.recvMu.Lock()
471446 pending := len(d.pendingFDs)
472- d.fdMu.Unlock()
447+ d.recvMu.Unlock()
473448 if pending != 0 {
474449 t.Fatalf("%d descriptors still queued after a malformed frame", pending)
475450 }
@@ -90,6 +90,8 @@ func newFDDisplay(t *testing.T, client *net.UnixConn) (*Display, *[]int) {
90 90
91 d := newDisplay(client)91 d := newDisplay(client)
92 fds := []int{}92 fds := []int{}
93+ d.RegisterEventSignature(7, 3, "uint,fd,")
94+ d.RegisterEventSignature(7, 4, "uint,")
93 d.objects.Store(uint32(7), &fdRecordingProxy{95 d.objects.Store(uint32(7), &fdRecordingProxy{
94 BaseProxy: BaseProxy{id: 7, context: d.context},96 BaseProxy: BaseProxy{id: 7, context: d.context},
95 fds: &fds,97 fds: &fds,
@@ -146,49 +148,19 @@ func TestDisplayFD_Single(t *testing.T) {
146 readSentinel(t, r, "single-fd-sentinel")148 readSentinel(t, r, "single-fd-sentinel")
147 }149 }
148 150
149-func TestDisplayFD_MultipleInOneControlMessage(t *testing.T) {151+func TestDisplayFD_ExtraDescriptorRejected(t *testing.T) {
150 client, peer := unixSocketPair(t)152 client, peer := unixSocketPair(t)
151- d, received := newFDDisplay(t, client)153+ d, _ := newFDDisplay(t, client)
152-154+ r, w, err := os.Pipe()
153- firstR, firstW, err := os.Pipe()
154- if err != nil {
155- t.Fatalf("pipe: %v", err)
156- }
157- defer firstR.Close()
158- secondR, secondW, err := os.Pipe()
159 if err != nil {155 if err != nil {
160- t.Fatalf("pipe: %v", err)156+ t.Fatal(err)
161 }157 }
162- defer secondR.Close()158+ defer r.Close()
163-159+ defer w.Close()
164- sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}),160+ sendMessageWithFDs(t, peer, message(7, 3, nil), []int{int(w.Fd()), int(w.Fd())})
165- []int{int(firstW.Fd()), int(secondW.Fd())})161+ if err := d.Dispatch(); !errors.Is(err, ErrMalformedFrame) {
166- firstW.Close()162+ t.Fatalf("extra FD: %v", err)
167- secondW.Close()
168-
169- if err := d.Dispatch(); err != nil {
170- t.Fatalf("Dispatch: %v", err)
171- }
172-
173- // A handler consumes one descriptor per fd argument, so the test drains
174- // whatever the connection still holds, in arrival order.
175- for {
176- fd, ok := d.nextFD()
177- if !ok {
178- break
179- }
180- *received = append(*received, fd)
181- }
182-
183- if len(*received) != 2 {
184- t.Fatalf("received %d descriptors, want 2", len(*received))
185 }163 }
186-
187- writeSentinel(t, uintptr((*received)[0]), "first-pipe")
188- writeSentinel(t, uintptr((*received)[1]), "second-pipe")
189-
190- readSentinel(t, firstR, "first-pipe")
191- readSentinel(t, secondR, "second-pipe")
192 }164 }
193 165
194 func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) {166 func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) {
@@ -304,9 +276,9 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) {
304 }276 }
305 277
306 // A truncated control message must not leave descriptors queued.278 // A truncated control message must not leave descriptors queued.
307- d.fdMu.Lock()279+ d.recvMu.Lock()
308 pending := len(d.pendingFDs)280 pending := len(d.pendingFDs)
309- d.fdMu.Unlock()281+ d.recvMu.Unlock()
310 if pending != 0 {282 if pending != 0 {
311 t.Fatalf("%d descriptors still queued after a truncated control message", pending)283 t.Fatalf("%d descriptors still queued after a truncated control message", pending)
312 }284 }
@@ -315,6 +287,7 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) {
315 func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {287 func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
316 client, peer := unixSocketPair(t)288 client, peer := unixSocketPair(t)
317 d := newDisplay(client)289 d := newDisplay(client)
290+ d.RegisterEventSignature(7, 3, "uint,fd,")
318 d.objects.Store(uint32(7), &fdIgnoringProxy{291 d.objects.Store(uint32(7), &fdIgnoringProxy{
319 BaseProxy: BaseProxy{id: 7, context: d.context},292 BaseProxy: BaseProxy{id: 7, context: d.context},
320 })293 })
@@ -333,12 +306,12 @@ func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
333 t.Fatalf("Dispatch: %v", err)306 t.Fatalf("Dispatch: %v", err)
334 }307 }
335 308
336- // While the display still holds the descriptor the pipe stays open.309+ // An ignored FD is closed at the end of the event, not at shutdown.
337- if err := r.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {310+ if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
338- t.Fatalf("SetReadDeadline: %v", err)311+ t.Fatal(err)
339 }312 }
340- if _, err := r.Read(make([]byte, 1)); !errors.Is(err, os.ErrDeadlineExceeded) {313+ if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
341- t.Fatalf("read with the display holding the descriptor = %v, want %v", err, os.ErrDeadlineExceeded)314+ t.Fatalf("ignored FD: %v", err)
342 }315 }
343 316
344 // Closing the display must release it, so the read end sees EOF.317 // Closing the display must release it, so the read end sees EOF.
@@ -421,6 +394,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) {
421 394
422 client, peer := rawSocketPair(t)395 client, peer := rawSocketPair(t)
423 d := newDisplay(client)396 d := newDisplay(client)
397+ d.RegisterEventSignature(7, 3, "uint,fd,")
424 d.objects.Store(uint32(7), &fdIgnoringProxy{398 d.objects.Store(uint32(7), &fdIgnoringProxy{
425 BaseProxy: BaseProxy{id: 7, context: d.context},399 BaseProxy: BaseProxy{id: 7, context: d.context},
426 })400 })
@@ -448,6 +422,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) {
448 func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {422 func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
449 client, peer := unixSocketPair(t)423 client, peer := unixSocketPair(t)
450 d := newDisplay(client)424 d := newDisplay(client)
425+ d.RegisterEventSignature(7, 3, "uint,fd,")
451 d.objects.Store(uint32(7), &fdIgnoringProxy{426 d.objects.Store(uint32(7), &fdIgnoringProxy{
452 BaseProxy: BaseProxy{id: 7, context: d.context},427 BaseProxy: BaseProxy{id: 7, context: d.context},
453 })428 })
@@ -467,9 +442,9 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
467 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)442 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)
468 }443 }
469 444
470- d.fdMu.Lock()445+ d.recvMu.Lock()
471 pending := len(d.pendingFDs)446 pending := len(d.pendingFDs)
472- d.fdMu.Unlock()447+ d.recvMu.Unlock()
473 if pending != 0 {448 if pending != 0 {
474 t.Fatalf("%d descriptors still queued after a malformed frame", pending)449 t.Fatalf("%d descriptors still queued after a malformed frame", pending)
475 }450 }
A fd_zero_test.go +14 -0
@@ -0,0 +1,14 @@
1+package wlturbo
2+
3+import "testing"
4+
5+func TestOwnedFDZero(t *testing.T) {
6+ fd := &OwnedFD{fd: 0}
7+ n, e := fd.Take()
8+ if e != nil || n != 0 {
9+ t.Fatalf("Take fd zero = %d %v", n, e)
10+ }
11+ if e := fd.Close(); e != nil {
12+ t.Fatal(e)
13+ }
14+}
@@ -0,0 +1,14 @@
1+package wlturbo
2+
3+import "testing"
4+
5+func TestOwnedFDZero(t *testing.T) {
6+ fd := &OwnedFD{fd: 0}
7+ n, e := fd.Take()
8+ if e != nil || n != 0 {
9+ t.Fatalf("Take fd zero = %d %v", n, e)
10+ }
11+ if e := fd.Close(); e != nil {
12+ t.Fatal(e)
13+ }
14+}
A protocol/core/core_test.go +324 -0
@@ -0,0 +1,324 @@
1+//go:build linux
2+
3+package core_test
4+
5+import (
6+ "encoding/binary"
7+ "errors"
8+ "io"
9+ "net"
10+ "os"
11+ "testing"
12+ "time"
13+
14+ "github.com/bnema/wlturbo"
15+ "github.com/bnema/wlturbo/protocol/core"
16+ "golang.org/x/sys/unix"
17+)
18+
19+func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
20+ t.Helper()
21+ f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ a := os.NewFile(uintptr(f[0]), "a")
26+ b := os.NewFile(uintptr(f[1]), "b")
27+ ca, e := net.FileConn(a)
28+ a.Close()
29+ if e != nil {
30+ t.Fatal(e)
31+ }
32+ cb, e := net.FileConn(b)
33+ b.Close()
34+ if e != nil {
35+ t.Fatal(e)
36+ }
37+ t.Cleanup(func() { ca.Close(); cb.Close() })
38+ return ca.(*net.UnixConn), cb.(*net.UnixConn)
39+}
40+func frame(id uint32, op uint16, body ...uint32) []byte {
41+ p := make([]byte, 8+4*len(body))
42+ binary.LittleEndian.PutUint32(p, id)
43+ binary.LittleEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
44+ for i, v := range body {
45+ binary.LittleEndian.PutUint32(p[8+i*4:], v)
46+ }
47+ return p
48+}
49+func receiveRequest(t *testing.T, p *net.UnixConn) {
50+ t.Helper()
51+ p.SetReadDeadline(testDeadline())
52+ b := make([]byte, 256)
53+ if _, e := p.Read(b); e != nil {
54+ t.Fatal(e)
55+ }
56+}
57+func testDeadline() (d time.Time) { return time.Now().Add(2 * time.Second) }
58+func send(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) {
59+ t.Helper()
60+ var oob []byte
61+ if len(fd) > 0 {
62+ oob = unix.UnixRights(fd...)
63+ }
64+ if _, _, e := p.WriteMsgUnix(msg, oob, nil); e != nil {
65+ t.Fatal(e)
66+ }
67+}
68+func TestGeneratedCoreOverSocketpair(t *testing.T) {
69+ c, p := pair(t)
70+ d, err := wlturbo.ConnectFromConn(c)
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ defer d.Close()
75+ receiveRequest(t, p) // get_registry
76+ seat := core.NewSeat(d.Context())
77+ if err := d.Registry().Bind(1, core.SeatInterface, 1, seat); err != nil {
78+ t.Fatal(err)
79+ }
80+ receiveRequest(t, p)
81+ kb, err := seat.GetKeyboard()
82+ if err != nil {
83+ t.Fatal(err)
84+ }
85+ receiveRequest(t, p)
86+ var key uint32
87+ kb.OnKey(func(_, _, k, _ uint32) { key = k })
88+ send(t, p, frame(kb.ID(), 3, 1, 2, 42, 1))
89+ if err := d.Dispatch(); err != nil {
90+ t.Fatal(err)
91+ }
92+ if key != 42 {
93+ t.Fatalf("key=%d", key)
94+ }
95+ compositor := core.NewCompositor(d.Context())
96+ if err := d.Registry().Bind(2, core.CompositorInterface, 1, compositor); err != nil {
97+ t.Fatal(err)
98+ }
99+ receiveRequest(t, p)
100+ surface, err := compositor.CreateSurface()
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ receiveRequest(t, p)
105+ callback, err := surface.Frame()
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ receiveRequest(t, p)
110+ var done uint32
111+ callback.OnDone(func(ms uint32) { done = ms })
112+ send(t, p, frame(callback.ID(), 0, 123))
113+ if err := d.Dispatch(); err != nil {
114+ t.Fatal(err)
115+ }
116+ if done != 123 {
117+ t.Fatalf("done=%d", done)
118+ }
119+ send(t, p, frame(callback.ID(), 0, 124))
120+ if err := d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
121+ t.Fatalf("destroyed callback: %v", err)
122+ }
123+}
124+func TestKeymapFDNoHandlerAndMultipleHandlers(t *testing.T) {
125+ c, p := pair(t)
126+ d, e := wlturbo.ConnectFromConn(c)
127+ if e != nil {
128+ t.Fatal(e)
129+ }
130+ defer d.Close()
131+ receiveRequest(t, p)
132+ kb := core.NewKeyboard(d.Context())
133+ kb.SetID(d.AllocateID())
134+ d.Context().Register(kb)
135+ r, w, e := os.Pipe()
136+ if e != nil {
137+ t.Fatal(e)
138+ }
139+ defer r.Close()
140+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
141+ w.Close()
142+ if e := d.Dispatch(); e != nil {
143+ t.Fatal(e)
144+ }
145+ buf := make([]byte, 1)
146+ if n, e := r.Read(buf); n != 0 || e != io.EOF {
147+ t.Fatalf("unclaimed FD n=%d err=%v", n, e)
148+ }
149+ r2, w2, e := os.Pipe()
150+ if e != nil {
151+ t.Fatal(e)
152+ }
153+ defer r2.Close()
154+ received := 0
155+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
156+ if fd == nil {
157+ t.Fatal("first FD missing")
158+ }
159+ n, e := fd.Take()
160+ if e != nil {
161+ t.Fatal(e)
162+ }
163+ received++
164+ unix.Close(n)
165+ })
166+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
167+ if fd != nil {
168+ t.Fatal("duplicated owner")
169+ }
170+ received++
171+ })
172+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w2.Fd()))
173+ w2.Close()
174+ if e := d.Dispatch(); e != nil {
175+ t.Fatal(e)
176+ }
177+ if received != 2 {
178+ t.Fatalf("handlers=%d", received)
179+ }
180+}
181+func TestBufferReleaseAndOfferChild(t *testing.T) {
182+ c, p := pair(t)
183+ d, e := wlturbo.ConnectFromConn(c)
184+ if e != nil {
185+ t.Fatal(e)
186+ }
187+ defer d.Close()
188+ receiveRequest(t, p)
189+ b := core.NewBuffer(d.Context())
190+ b.SetID(d.AllocateID())
191+ d.Context().Register(b)
192+ releases := 0
193+ b.OnRelease(func() { releases++ })
194+ send(t, p, frame(b.ID(), 0))
195+ if e := d.Dispatch(); e != nil {
196+ t.Fatal(e)
197+ }
198+ if releases != 1 {
199+ t.Fatal("release not dispatched")
200+ }
201+ device := core.NewDataDevice(d.Context())
202+ device.SetID(d.AllocateID())
203+ d.Context().Register(device)
204+ var offer *core.DataOffer
205+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
206+ send(t, p, frame(device.ID(), 0, 0xff000001))
207+ if e := d.Dispatch(); e != nil {
208+ t.Fatal(e)
209+ }
210+ if offer == nil || offer.ID() != 0xff000001 {
211+ t.Fatalf("offer=%v", offer)
212+ }
213+}
214+
215+func TestMissingKeymapDescriptor(t *testing.T) {
216+ c, p := pair(t)
217+ d, e := wlturbo.ConnectFromConn(c)
218+ if e != nil {
219+ t.Fatal(e)
220+ }
221+ defer d.Close()
222+ receiveRequest(t, p)
223+ kb := core.NewKeyboard(d.Context())
224+ kb.SetID(d.AllocateID())
225+ d.Context().Register(kb)
226+ send(t, p, frame(kb.ID(), 0, 1, 8))
227+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
228+ t.Fatalf("missing FD: %v", e)
229+ }
230+}
231+
232+func TestInterleavedFDAndPlainMessages(t *testing.T) {
233+ c, p := pair(t)
234+ d, e := wlturbo.ConnectFromConn(c)
235+ if e != nil {
236+ t.Fatal(e)
237+ }
238+ defer d.Close()
239+ receiveRequest(t, p)
240+ kb := core.NewKeyboard(d.Context())
241+ kb.SetID(d.AllocateID())
242+ d.Context().Register(kb)
243+ calls := 0
244+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
245+ if fd == nil {
246+ t.Fatal("FD not assigned")
247+ }
248+ _ = fd.Close()
249+ calls++
250+ })
251+ kb.OnKey(func(_, _, key, _ uint32) {
252+ if key != 33 {
253+ t.Errorf("key=%d", key)
254+ }
255+ calls++
256+ })
257+ r, w, e := os.Pipe()
258+ if e != nil {
259+ t.Fatal(e)
260+ }
261+ defer r.Close()
262+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
263+ w.Close()
264+ send(t, p, frame(kb.ID(), 3, 1, 2, 33, 1))
265+ if e := d.Dispatch(); e != nil {
266+ t.Fatal(e)
267+ }
268+ if e := d.Dispatch(); e != nil {
269+ t.Fatal(e)
270+ }
271+ if calls != 2 {
272+ t.Fatalf("calls=%d", calls)
273+ }
274+}
275+func TestMalformedGeneratedPayload(t *testing.T) {
276+ c, p := pair(t)
277+ d, e := wlturbo.ConnectFromConn(c)
278+ if e != nil {
279+ t.Fatal(e)
280+ }
281+ defer d.Close()
282+ receiveRequest(t, p)
283+ kb := core.NewKeyboard(d.Context())
284+ kb.SetID(d.AllocateID())
285+ d.Context().Register(kb)
286+ send(t, p, frame(kb.ID(), 3, 1, 2))
287+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
288+ t.Fatalf("truncated key: %v", e)
289+ }
290+}
291+
292+func TestServerChildEventsAfterOffer(t *testing.T) {
293+ c, p := pair(t)
294+ d, e := wlturbo.ConnectFromConn(c)
295+ if e != nil {
296+ t.Fatal(e)
297+ }
298+ defer d.Close()
299+ receiveRequest(t, p)
300+ device := core.NewDataDevice(d.Context())
301+ device.SetID(d.AllocateID())
302+ d.Context().Register(device)
303+ const offerID = 0xff000011
304+ var offer *core.DataOffer
305+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
306+ send(t, p, frame(device.ID(), 0, offerID))
307+ if e := d.Dispatch(); e != nil {
308+ t.Fatal(e)
309+ }
310+ if offer == nil {
311+ t.Fatal("missing concrete child")
312+ }
313+ got := ""
314+ offer.OnOffer(func(mime string) { got = mime })
315+ payload := append(frame(offerID, 0), 4, 0, 0, 0, 'a', 'b', 'c', 0)
316+ binary.LittleEndian.PutUint32(payload[4:], uint32(len(payload))<<16)
317+ send(t, p, payload)
318+ if e := d.Dispatch(); e != nil {
319+ t.Fatal(e)
320+ }
321+ if got != "abc" {
322+ t.Fatalf("typed child event: %q", got)
323+ }
324+}
@@ -0,0 +1,324 @@
1+//go:build linux
2+
3+package core_test
4+
5+import (
6+ "encoding/binary"
7+ "errors"
8+ "io"
9+ "net"
10+ "os"
11+ "testing"
12+ "time"
13+
14+ "github.com/bnema/wlturbo"
15+ "github.com/bnema/wlturbo/protocol/core"
16+ "golang.org/x/sys/unix"
17+)
18+
19+func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
20+ t.Helper()
21+ f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ a := os.NewFile(uintptr(f[0]), "a")
26+ b := os.NewFile(uintptr(f[1]), "b")
27+ ca, e := net.FileConn(a)
28+ a.Close()
29+ if e != nil {
30+ t.Fatal(e)
31+ }
32+ cb, e := net.FileConn(b)
33+ b.Close()
34+ if e != nil {
35+ t.Fatal(e)
36+ }
37+ t.Cleanup(func() { ca.Close(); cb.Close() })
38+ return ca.(*net.UnixConn), cb.(*net.UnixConn)
39+}
40+func frame(id uint32, op uint16, body ...uint32) []byte {
41+ p := make([]byte, 8+4*len(body))
42+ binary.LittleEndian.PutUint32(p, id)
43+ binary.LittleEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
44+ for i, v := range body {
45+ binary.LittleEndian.PutUint32(p[8+i*4:], v)
46+ }
47+ return p
48+}
49+func receiveRequest(t *testing.T, p *net.UnixConn) {
50+ t.Helper()
51+ p.SetReadDeadline(testDeadline())
52+ b := make([]byte, 256)
53+ if _, e := p.Read(b); e != nil {
54+ t.Fatal(e)
55+ }
56+}
57+func testDeadline() (d time.Time) { return time.Now().Add(2 * time.Second) }
58+func send(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) {
59+ t.Helper()
60+ var oob []byte
61+ if len(fd) > 0 {
62+ oob = unix.UnixRights(fd...)
63+ }
64+ if _, _, e := p.WriteMsgUnix(msg, oob, nil); e != nil {
65+ t.Fatal(e)
66+ }
67+}
68+func TestGeneratedCoreOverSocketpair(t *testing.T) {
69+ c, p := pair(t)
70+ d, err := wlturbo.ConnectFromConn(c)
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ defer d.Close()
75+ receiveRequest(t, p) // get_registry
76+ seat := core.NewSeat(d.Context())
77+ if err := d.Registry().Bind(1, core.SeatInterface, 1, seat); err != nil {
78+ t.Fatal(err)
79+ }
80+ receiveRequest(t, p)
81+ kb, err := seat.GetKeyboard()
82+ if err != nil {
83+ t.Fatal(err)
84+ }
85+ receiveRequest(t, p)
86+ var key uint32
87+ kb.OnKey(func(_, _, k, _ uint32) { key = k })
88+ send(t, p, frame(kb.ID(), 3, 1, 2, 42, 1))
89+ if err := d.Dispatch(); err != nil {
90+ t.Fatal(err)
91+ }
92+ if key != 42 {
93+ t.Fatalf("key=%d", key)
94+ }
95+ compositor := core.NewCompositor(d.Context())
96+ if err := d.Registry().Bind(2, core.CompositorInterface, 1, compositor); err != nil {
97+ t.Fatal(err)
98+ }
99+ receiveRequest(t, p)
100+ surface, err := compositor.CreateSurface()
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ receiveRequest(t, p)
105+ callback, err := surface.Frame()
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ receiveRequest(t, p)
110+ var done uint32
111+ callback.OnDone(func(ms uint32) { done = ms })
112+ send(t, p, frame(callback.ID(), 0, 123))
113+ if err := d.Dispatch(); err != nil {
114+ t.Fatal(err)
115+ }
116+ if done != 123 {
117+ t.Fatalf("done=%d", done)
118+ }
119+ send(t, p, frame(callback.ID(), 0, 124))
120+ if err := d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
121+ t.Fatalf("destroyed callback: %v", err)
122+ }
123+}
124+func TestKeymapFDNoHandlerAndMultipleHandlers(t *testing.T) {
125+ c, p := pair(t)
126+ d, e := wlturbo.ConnectFromConn(c)
127+ if e != nil {
128+ t.Fatal(e)
129+ }
130+ defer d.Close()
131+ receiveRequest(t, p)
132+ kb := core.NewKeyboard(d.Context())
133+ kb.SetID(d.AllocateID())
134+ d.Context().Register(kb)
135+ r, w, e := os.Pipe()
136+ if e != nil {
137+ t.Fatal(e)
138+ }
139+ defer r.Close()
140+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
141+ w.Close()
142+ if e := d.Dispatch(); e != nil {
143+ t.Fatal(e)
144+ }
145+ buf := make([]byte, 1)
146+ if n, e := r.Read(buf); n != 0 || e != io.EOF {
147+ t.Fatalf("unclaimed FD n=%d err=%v", n, e)
148+ }
149+ r2, w2, e := os.Pipe()
150+ if e != nil {
151+ t.Fatal(e)
152+ }
153+ defer r2.Close()
154+ received := 0
155+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
156+ if fd == nil {
157+ t.Fatal("first FD missing")
158+ }
159+ n, e := fd.Take()
160+ if e != nil {
161+ t.Fatal(e)
162+ }
163+ received++
164+ unix.Close(n)
165+ })
166+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
167+ if fd != nil {
168+ t.Fatal("duplicated owner")
169+ }
170+ received++
171+ })
172+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w2.Fd()))
173+ w2.Close()
174+ if e := d.Dispatch(); e != nil {
175+ t.Fatal(e)
176+ }
177+ if received != 2 {
178+ t.Fatalf("handlers=%d", received)
179+ }
180+}
181+func TestBufferReleaseAndOfferChild(t *testing.T) {
182+ c, p := pair(t)
183+ d, e := wlturbo.ConnectFromConn(c)
184+ if e != nil {
185+ t.Fatal(e)
186+ }
187+ defer d.Close()
188+ receiveRequest(t, p)
189+ b := core.NewBuffer(d.Context())
190+ b.SetID(d.AllocateID())
191+ d.Context().Register(b)
192+ releases := 0
193+ b.OnRelease(func() { releases++ })
194+ send(t, p, frame(b.ID(), 0))
195+ if e := d.Dispatch(); e != nil {
196+ t.Fatal(e)
197+ }
198+ if releases != 1 {
199+ t.Fatal("release not dispatched")
200+ }
201+ device := core.NewDataDevice(d.Context())
202+ device.SetID(d.AllocateID())
203+ d.Context().Register(device)
204+ var offer *core.DataOffer
205+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
206+ send(t, p, frame(device.ID(), 0, 0xff000001))
207+ if e := d.Dispatch(); e != nil {
208+ t.Fatal(e)
209+ }
210+ if offer == nil || offer.ID() != 0xff000001 {
211+ t.Fatalf("offer=%v", offer)
212+ }
213+}
214+
215+func TestMissingKeymapDescriptor(t *testing.T) {
216+ c, p := pair(t)
217+ d, e := wlturbo.ConnectFromConn(c)
218+ if e != nil {
219+ t.Fatal(e)
220+ }
221+ defer d.Close()
222+ receiveRequest(t, p)
223+ kb := core.NewKeyboard(d.Context())
224+ kb.SetID(d.AllocateID())
225+ d.Context().Register(kb)
226+ send(t, p, frame(kb.ID(), 0, 1, 8))
227+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
228+ t.Fatalf("missing FD: %v", e)
229+ }
230+}
231+
232+func TestInterleavedFDAndPlainMessages(t *testing.T) {
233+ c, p := pair(t)
234+ d, e := wlturbo.ConnectFromConn(c)
235+ if e != nil {
236+ t.Fatal(e)
237+ }
238+ defer d.Close()
239+ receiveRequest(t, p)
240+ kb := core.NewKeyboard(d.Context())
241+ kb.SetID(d.AllocateID())
242+ d.Context().Register(kb)
243+ calls := 0
244+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
245+ if fd == nil {
246+ t.Fatal("FD not assigned")
247+ }
248+ _ = fd.Close()
249+ calls++
250+ })
251+ kb.OnKey(func(_, _, key, _ uint32) {
252+ if key != 33 {
253+ t.Errorf("key=%d", key)
254+ }
255+ calls++
256+ })
257+ r, w, e := os.Pipe()
258+ if e != nil {
259+ t.Fatal(e)
260+ }
261+ defer r.Close()
262+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
263+ w.Close()
264+ send(t, p, frame(kb.ID(), 3, 1, 2, 33, 1))
265+ if e := d.Dispatch(); e != nil {
266+ t.Fatal(e)
267+ }
268+ if e := d.Dispatch(); e != nil {
269+ t.Fatal(e)
270+ }
271+ if calls != 2 {
272+ t.Fatalf("calls=%d", calls)
273+ }
274+}
275+func TestMalformedGeneratedPayload(t *testing.T) {
276+ c, p := pair(t)
277+ d, e := wlturbo.ConnectFromConn(c)
278+ if e != nil {
279+ t.Fatal(e)
280+ }
281+ defer d.Close()
282+ receiveRequest(t, p)
283+ kb := core.NewKeyboard(d.Context())
284+ kb.SetID(d.AllocateID())
285+ d.Context().Register(kb)
286+ send(t, p, frame(kb.ID(), 3, 1, 2))
287+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
288+ t.Fatalf("truncated key: %v", e)
289+ }
290+}
291+
292+func TestServerChildEventsAfterOffer(t *testing.T) {
293+ c, p := pair(t)
294+ d, e := wlturbo.ConnectFromConn(c)
295+ if e != nil {
296+ t.Fatal(e)
297+ }
298+ defer d.Close()
299+ receiveRequest(t, p)
300+ device := core.NewDataDevice(d.Context())
301+ device.SetID(d.AllocateID())
302+ d.Context().Register(device)
303+ const offerID = 0xff000011
304+ var offer *core.DataOffer
305+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
306+ send(t, p, frame(device.ID(), 0, offerID))
307+ if e := d.Dispatch(); e != nil {
308+ t.Fatal(e)
309+ }
310+ if offer == nil {
311+ t.Fatal("missing concrete child")
312+ }
313+ got := ""
314+ offer.OnOffer(func(mime string) { got = mime })
315+ payload := append(frame(offerID, 0), 4, 0, 0, 0, 'a', 'b', 'c', 0)
316+ binary.LittleEndian.PutUint32(payload[4:], uint32(len(payload))<<16)
317+ send(t, p, payload)
318+ if e := d.Dispatch(); e != nil {
319+ t.Fatal(e)
320+ }
321+ if got != "abc" {
322+ t.Fatalf("typed child event: %q", got)
323+ }
324+}
A protocol/core/doc.go +8 -0
@@ -0,0 +1,8 @@
1+// Package core contains canonical Wayland core protocol bindings generated from
2+// pinned wayland.xml. Bind globals with display.Registry().Bind and the
3+// corresponding New* constructor. Client-created and server-created children
4+// are registered with their concrete generated types; never replace them with
5+// transport wrappers. On* handlers receive typed values. Received OwnedFDs
6+// are closed after the event unless a handler calls Take; request FDs are
7+// transferred (and closed locally) only after a complete successful send.
8+package core
@@ -0,0 +1,8 @@
1+// Package core contains canonical Wayland core protocol bindings generated from
2+// pinned wayland.xml. Bind globals with display.Registry().Bind and the
3+// corresponding New* constructor. Client-created and server-created children
4+// are registered with their concrete generated types; never replace them with
5+// transport wrappers. On* handlers receive typed values. Received OwnedFDs
6+// are closed after the event unless a handler calls Take; request FDs are
7+// transferred (and closed locally) only after a complete successful send.
8+package core
A protocol/core/wayland_generated.go +4023 -0 large diff (4023 lines changed): collapsed
M scm_linux.go +21 -20
@@ -6,9 +6,12 @@ package wlturbo
66 import (
77 "errors"
88 "fmt"
9- "golang.org/x/sys/unix"
9+ "io"
10+ "net"
1011 "sync"
1112 "syscall"
13+
14+ "golang.org/x/sys/unix"
1215 )
1316
1417 // maxRecvFDs bounds how many ancillary file descriptors a single read accepts.
@@ -88,20 +91,9 @@ func parseFDs(oob []byte) ([]int, error) {
8891 return fds, nil
8992 }
9093
91-// queueFDs appends descriptors to this connection's pending list.
92-func (d *Display) queueFDs(fds []int) {
93- if len(fds) == 0 {
94- return
95- }
96- d.fdMu.Lock()
97- d.pendingFDs = append(d.pendingFDs, fds...)
98- d.fdMu.Unlock()
99-}
100-
101-// nextFD removes and returns the oldest pending descriptor.
94+// queueFDs is called only by the active Dispatch reader.
95+func (d *Display) queueFDs(fds []int) { d.pendingFDs = append(d.pendingFDs, fds...) }
10296 func (d *Display) nextFD() (int, bool) {
103- d.fdMu.Lock()
104- defer d.fdMu.Unlock()
10597 if len(d.pendingFDs) == 0 {
10698 return -1, false
10799 }
@@ -110,13 +102,10 @@ func (d *Display) nextFD() (int, bool) {
110102 return fd, true
111103 }
112104
113-// closePendingFDs closes every descriptor this connection still holds.
105+// closePendingFDs is called under recvMu (including during shutdown).
114106 func (d *Display) closePendingFDs() {
115- d.fdMu.Lock()
116107 fds := d.pendingFDs
117108 d.pendingFDs = nil
118- d.fdMu.Unlock()
119-
120109 for _, fd := range fds {
121110 if fd >= 0 {
122111 _ = unix.Close(fd)
@@ -124,20 +113,32 @@ func (d *Display) closePendingFDs() {
124113 }
125114 }
126115
116+// CloseSentFD releases a descriptor after a complete successful Wayland send.
117+func CloseSentFD(fd int) error { return unix.Close(fd) }
118+
127119 // sendmsgWithFDs sends a message, attaching file descriptors when present.
128120 func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {
129121 d.sendMu.Lock()
130122 defer d.sendMu.Unlock()
131123
124+ if d.closed.Load() {
125+ return net.ErrClosed
126+ }
132127 if len(fds) == 0 {
133- _, err := d.conn.Write(buf)
128+ n, err := d.conn.Write(buf)
129+ if err == nil && n != len(buf) {
130+ return io.ErrShortWrite
131+ }
134132 return err
135133 }
136134 if d.unix == nil {
137135 return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection")
138136 }
139137
140- _, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)
138+ n, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)
139+ if err == nil && n != len(buf) {
140+ return io.ErrShortWrite
141+ }
141142 return err
142143 }
143144
@@ -6,9 +6,12 @@ package wlturbo
6 import (6 import (
7 "errors"7 "errors"
8 "fmt"8 "fmt"
9- "golang.org/x/sys/unix"9+ "io"
10+ "net"
10 "sync"11 "sync"
11 "syscall"12 "syscall"
13+
14+ "golang.org/x/sys/unix"
12 )15 )
13 16
14 // maxRecvFDs bounds how many ancillary file descriptors a single read accepts.17 // maxRecvFDs bounds how many ancillary file descriptors a single read accepts.
@@ -88,20 +91,9 @@ func parseFDs(oob []byte) ([]int, error) {
88 return fds, nil91 return fds, nil
89 }92 }
90 93
91-// queueFDs appends descriptors to this connection's pending list.94+// queueFDs is called only by the active Dispatch reader.
92-func (d *Display) queueFDs(fds []int) {95+func (d *Display) queueFDs(fds []int) { d.pendingFDs = append(d.pendingFDs, fds...) }
93- if len(fds) == 0 {
94- return
95- }
96- d.fdMu.Lock()
97- d.pendingFDs = append(d.pendingFDs, fds...)
98- d.fdMu.Unlock()
99-}
100-
101-// nextFD removes and returns the oldest pending descriptor.
102 func (d *Display) nextFD() (int, bool) {96 func (d *Display) nextFD() (int, bool) {
103- d.fdMu.Lock()
104- defer d.fdMu.Unlock()
105 if len(d.pendingFDs) == 0 {97 if len(d.pendingFDs) == 0 {
106 return -1, false98 return -1, false
107 }99 }
@@ -110,13 +102,10 @@ func (d *Display) nextFD() (int, bool) {
110 return fd, true102 return fd, true
111 }103 }
112 104
113-// closePendingFDs closes every descriptor this connection still holds.105+// closePendingFDs is called under recvMu (including during shutdown).
114 func (d *Display) closePendingFDs() {106 func (d *Display) closePendingFDs() {
115- d.fdMu.Lock()
116 fds := d.pendingFDs107 fds := d.pendingFDs
117 d.pendingFDs = nil108 d.pendingFDs = nil
118- d.fdMu.Unlock()
119-
120 for _, fd := range fds {109 for _, fd := range fds {
121 if fd >= 0 {110 if fd >= 0 {
122 _ = unix.Close(fd)111 _ = unix.Close(fd)
@@ -124,20 +113,32 @@ func (d *Display) closePendingFDs() {
124 }113 }
125 }114 }
126 115
116+// CloseSentFD releases a descriptor after a complete successful Wayland send.
117+func CloseSentFD(fd int) error { return unix.Close(fd) }
118+
127 // sendmsgWithFDs sends a message, attaching file descriptors when present.119 // sendmsgWithFDs sends a message, attaching file descriptors when present.
128 func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {120 func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {
129 d.sendMu.Lock()121 d.sendMu.Lock()
130 defer d.sendMu.Unlock()122 defer d.sendMu.Unlock()
131 123
124+ if d.closed.Load() {
125+ return net.ErrClosed
126+ }
132 if len(fds) == 0 {127 if len(fds) == 0 {
133- _, err := d.conn.Write(buf)128+ n, err := d.conn.Write(buf)
129+ if err == nil && n != len(buf) {
130+ return io.ErrShortWrite
131+ }
134 return err132 return err
135 }133 }
136 if d.unix == nil {134 if d.unix == nil {
137 return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection")135 return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection")
138 }136 }
139 137
140- _, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)138+ n, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)
139+ if err == nil && n != len(buf) {
140+ return io.ErrShortWrite
141+ }
141 return err142 return err
142 }143 }
143 144
A signature.go +50 -0
@@ -0,0 +1,50 @@
1+package wlturbo
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+)
7+
8+// validateEventBody rejects truncated, unterminated and trailing wire values
9+// before generated code decodes or registers any children.
10+func validateEventBody(sig string, body []byte) error {
11+ pos := 0
12+ for len(sig) > 0 {
13+ end := 0
14+ for end < len(sig) && sig[end] != ',' {
15+ end++
16+ }
17+ kind := sig[:end]
18+ if end == len(sig) {
19+ return fmt.Errorf("invalid signature %q", sig)
20+ }
21+ sig = sig[end+1:]
22+ if kind == "fd" {
23+ continue
24+ }
25+ if len(body)-pos < 4 {
26+ return ErrMalformedFrame
27+ }
28+ switch kind {
29+ case "int", "uint", "fixed", "object", "new_id":
30+ pos += 4
31+ case "string", "array":
32+ n := uint64(binary.LittleEndian.Uint32(body[pos:]))
33+ pos += 4
34+ padded := (n + 3) &^ uint64(3)
35+ if padded > uint64(len(body)-pos) {
36+ return ErrMalformedFrame
37+ }
38+ if kind == "string" && n > 0 && body[pos+int(n)-1] != 0 {
39+ return ErrMalformedFrame
40+ }
41+ pos += int(padded)
42+ default:
43+ return fmt.Errorf("invalid argument kind %q: %w", kind, ErrMalformedFrame)
44+ }
45+ }
46+ if pos != len(body) {
47+ return ErrMalformedFrame
48+ }
49+ return nil
50+}
@@ -0,0 +1,50 @@
1+package wlturbo
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+)
7+
8+// validateEventBody rejects truncated, unterminated and trailing wire values
9+// before generated code decodes or registers any children.
10+func validateEventBody(sig string, body []byte) error {
11+ pos := 0
12+ for len(sig) > 0 {
13+ end := 0
14+ for end < len(sig) && sig[end] != ',' {
15+ end++
16+ }
17+ kind := sig[:end]
18+ if end == len(sig) {
19+ return fmt.Errorf("invalid signature %q", sig)
20+ }
21+ sig = sig[end+1:]
22+ if kind == "fd" {
23+ continue
24+ }
25+ if len(body)-pos < 4 {
26+ return ErrMalformedFrame
27+ }
28+ switch kind {
29+ case "int", "uint", "fixed", "object", "new_id":
30+ pos += 4
31+ case "string", "array":
32+ n := uint64(binary.LittleEndian.Uint32(body[pos:]))
33+ pos += 4
34+ padded := (n + 3) &^ uint64(3)
35+ if padded > uint64(len(body)-pos) {
36+ return ErrMalformedFrame
37+ }
38+ if kind == "string" && n > 0 && body[pos+int(n)-1] != 0 {
39+ return ErrMalformedFrame
40+ }
41+ pos += int(padded)
42+ default:
43+ return fmt.Errorf("invalid argument kind %q: %w", kind, ErrMalformedFrame)
44+ }
45+ }
46+ if pos != len(body) {
47+ return ErrMalformedFrame
48+ }
49+ return nil
50+}
M transport_test.go +10 -5
@@ -92,6 +92,11 @@ func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) {
9292 BaseProxy: BaseProxy{id: 7, context: d.context},
9393 records: &records,
9494 })
95+ for _, opcode := range []uint16{3, 4, 5} {
96+ signature := "uint,"
97+ if opcode == 3 { signature = "uint,uint," }
98+ d.RegisterEventSignature(7, opcode, signature)
99+ }
95100 return d, &records
96101 }
97102
@@ -132,7 +137,7 @@ func TestDisplayDispatch_Fragmented(t *testing.T) {
132137 }
133138
134139 func TestDisplayDispatch_Coalesced(t *testing.T) {
135- first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14})
140+ first := message(7, 4, []byte{0x11, 0x12, 0x13, 0x14})
136141 second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
137142
138143 d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
@@ -146,7 +151,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) {
146151 }
147152
148153 want := []EventRecord{
149- {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}},
154+ {Object: 7, Opcode: 4, Body: []byte{0x11, 0x12, 0x13, 0x14}},
150155 {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
151156 }
152157 if !reflect.DeepEqual(*records, want) {
@@ -157,7 +162,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) {
157162 func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
158163 // A read boundary that ends between the header and the body of the second
159164 // message must not be mistaken for a frame boundary.
160- whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}),
165+ whole := append(message(7, 4, []byte{0x01, 0x02, 0x03, 0x04}),
161166 message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
162167
163168 split := HeaderSize + 4 + 4 // mid-body of the second message
@@ -171,7 +176,7 @@ func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
171176 }
172177
173178 want := []EventRecord{
174- {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}},
179+ {Object: 7, Opcode: 4, Body: []byte{0x01, 0x02, 0x03, 0x04}},
175180 {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
176181 }
177182 if !reflect.DeepEqual(*records, want) {
@@ -299,7 +304,7 @@ func TestDisplayDispatch_PeerClose(t *testing.T) {
299304 },
300305 {
301306 name: "closed after a complete message",
302- chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})},
307+ chunks: [][]byte{message(7, 4, []byte{0x01, 0x02, 0x03, 0x04})},
303308 },
304309 }
305310
@@ -92,6 +92,11 @@ func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) {
92 BaseProxy: BaseProxy{id: 7, context: d.context},92 BaseProxy: BaseProxy{id: 7, context: d.context},
93 records: &records,93 records: &records,
94 })94 })
95+ for _, opcode := range []uint16{3, 4, 5} {
96+ signature := "uint,"
97+ if opcode == 3 { signature = "uint,uint," }
98+ d.RegisterEventSignature(7, opcode, signature)
99+ }
95 return d, &records100 return d, &records
96 }101 }
97 102
@@ -132,7 +137,7 @@ func TestDisplayDispatch_Fragmented(t *testing.T) {
132 }137 }
133 138
134 func TestDisplayDispatch_Coalesced(t *testing.T) {139 func TestDisplayDispatch_Coalesced(t *testing.T) {
135- first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14})140+ first := message(7, 4, []byte{0x11, 0x12, 0x13, 0x14})
136 second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})141 second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
137 142
138 d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})143 d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
@@ -146,7 +151,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) {
146 }151 }
147 152
148 want := []EventRecord{153 want := []EventRecord{
149- {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}},154+ {Object: 7, Opcode: 4, Body: []byte{0x11, 0x12, 0x13, 0x14}},
150 {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},155 {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
151 }156 }
152 if !reflect.DeepEqual(*records, want) {157 if !reflect.DeepEqual(*records, want) {
@@ -157,7 +162,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) {
157 func TestDisplayDispatch_HalfCoalesced(t *testing.T) {162 func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
158 // A read boundary that ends between the header and the body of the second163 // A read boundary that ends between the header and the body of the second
159 // message must not be mistaken for a frame boundary.164 // message must not be mistaken for a frame boundary.
160- whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}),165+ whole := append(message(7, 4, []byte{0x01, 0x02, 0x03, 0x04}),
161 message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)166 message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
162 167
163 split := HeaderSize + 4 + 4 // mid-body of the second message168 split := HeaderSize + 4 + 4 // mid-body of the second message
@@ -171,7 +176,7 @@ func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
171 }176 }
172 177
173 want := []EventRecord{178 want := []EventRecord{
174- {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}},179+ {Object: 7, Opcode: 4, Body: []byte{0x01, 0x02, 0x03, 0x04}},
175 {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},180 {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
176 }181 }
177 if !reflect.DeepEqual(*records, want) {182 if !reflect.DeepEqual(*records, want) {
@@ -299,7 +304,7 @@ func TestDisplayDispatch_PeerClose(t *testing.T) {
299 },304 },
300 {305 {
301 name: "closed after a complete message",306 name: "closed after a complete message",
302- chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})},307+ chunks: [][]byte{message(7, 4, []byte{0x01, 0x02, 0x03, 0x04})},
303 },308 },
304 }309 }
305 310
M wl/wl.go +2 -18
@@ -21,14 +21,7 @@ type (
2121 RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler
2222 RegistryGlobalEvent = wlturbo.RegistryGlobalEvent
2323 RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent
24- Seat = wlturbo.Seat
25- Surface = wlturbo.Surface
26- Pointer = wlturbo.Pointer
27- Keyboard = wlturbo.Keyboard
28- Touch = wlturbo.Touch
29- Output = wlturbo.Output
30- Region = wlturbo.Region
31- Compositor = wlturbo.Compositor
24+ OwnedFD = wlturbo.OwnedFD
3225
3326 // Transport failures. Without these, a caller that imports this shim has to
3427 // import the root package as well just to classify an error.
@@ -40,10 +33,8 @@ type (
4033 var (
4134 Connect = wlturbo.Connect
4235 NewFixed = wlturbo.NewFixed
43- NewSeat = wlturbo.NewSeat
4436 NewContext = wlturbo.NewContext
45- NewSurface = wlturbo.NewSurface
46- NewCompositor = wlturbo.NewCompositor
37+ CloseSentFD = wlturbo.CloseSentFD
4738 CreateAnonymousFile = wlturbo.CreateAnonymousFile
4839 MapMemory = wlturbo.MapMemory
4940 UnmapMemory = wlturbo.UnmapMemory
@@ -57,10 +48,3 @@ var (
5748 ErrUnknownOpcode = wlturbo.ErrUnknownOpcode
5849 ErrDisplayError = wlturbo.ErrDisplayError
5950 )
60-
61-// Seat capability constants
62-const (
63- SeatCapabilityPointer = wlturbo.SeatCapabilityPointer
64- SeatCapabilityKeyboard = wlturbo.SeatCapabilityKeyboard
65- SeatCapabilityTouch = wlturbo.SeatCapabilityTouch
66-)
@@ -21,14 +21,7 @@ type (
21 RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler21 RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler
22 RegistryGlobalEvent = wlturbo.RegistryGlobalEvent22 RegistryGlobalEvent = wlturbo.RegistryGlobalEvent
23 RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent23 RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent
24- Seat = wlturbo.Seat24+ OwnedFD = wlturbo.OwnedFD
25- Surface = wlturbo.Surface
26- Pointer = wlturbo.Pointer
27- Keyboard = wlturbo.Keyboard
28- Touch = wlturbo.Touch
29- Output = wlturbo.Output
30- Region = wlturbo.Region
31- Compositor = wlturbo.Compositor
32 25
33 // Transport failures. Without these, a caller that imports this shim has to26 // Transport failures. Without these, a caller that imports this shim has to
34 // import the root package as well just to classify an error.27 // import the root package as well just to classify an error.
@@ -40,10 +33,8 @@ type (
40 var (33 var (
41 Connect = wlturbo.Connect34 Connect = wlturbo.Connect
42 NewFixed = wlturbo.NewFixed35 NewFixed = wlturbo.NewFixed
43- NewSeat = wlturbo.NewSeat
44 NewContext = wlturbo.NewContext36 NewContext = wlturbo.NewContext
45- NewSurface = wlturbo.NewSurface37+ CloseSentFD = wlturbo.CloseSentFD
46- NewCompositor = wlturbo.NewCompositor
47 CreateAnonymousFile = wlturbo.CreateAnonymousFile38 CreateAnonymousFile = wlturbo.CreateAnonymousFile
48 MapMemory = wlturbo.MapMemory39 MapMemory = wlturbo.MapMemory
49 UnmapMemory = wlturbo.UnmapMemory40 UnmapMemory = wlturbo.UnmapMemory
@@ -57,10 +48,3 @@ var (
57 ErrUnknownOpcode = wlturbo.ErrUnknownOpcode48 ErrUnknownOpcode = wlturbo.ErrUnknownOpcode
58 ErrDisplayError = wlturbo.ErrDisplayError49 ErrDisplayError = wlturbo.ErrDisplayError
59 )50 )
60-
61-// Seat capability constants
62-const (
63- SeatCapabilityPointer = wlturbo.SeatCapabilityPointer
64- SeatCapabilityKeyboard = wlturbo.SeatCapabilityKeyboard
65- SeatCapabilityTouch = wlturbo.SeatCapabilityTouch
66-)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close