detached 58d830c

fix: make Wayland stream framing deterministic

Read boundaries no longer define message boundaries: the display buffers
received bytes per connection and extracts complete frames from them with
io.ReadFull semantics, so fragmented headers, split bodies and coalesced
messages are all framed correctly.

Header validation now rejects sizes below the 8-byte header, misaligned
sizes and sizes above a configurable maximum, and an event for an unknown
object or an unknown wl_display opcode returns a typed ProtocolError
instead of logging and continuing. wl_display.error is reported as a
DisplayError carrying the offending object, code and message.

Close becomes idempotent and Dispatch after Close returns net.ErrClosed.
Unconditional hot-path logging was removed from dispatch, allocation,
roundtrip and output-manager paths, and the stale ProxyId assertions in
client_test.go were corrected to ProxyID.
commit
58d830c97457aa8149c8436636c1cdf1b2b9d7e4
author
brice · · 2026-09-13 06:16 UTC
parent
111649b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +794 -278
M client.go +184 -223
@@ -10,9 +10,6 @@ import (
1010 "errors"
1111 "fmt"
1212 "io"
13- "log"
14-
15- // "log"
1613 "net"
1714 "os"
1815 "path/filepath"
@@ -48,8 +45,8 @@ type Object interface {
4845 // Display represents a connection to the Wayland display
4946 type Display struct {
5047 conn net.Conn
51- fd int
52- objects sync.Map // map[uint32]Object
48+ unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
49+ objects sync.Map // map[uint32]Object
5350 nextID uint32
5451 sendMu sync.Mutex
5552 recvMu sync.Mutex
@@ -67,11 +64,38 @@ type Display struct {
6764 lastErrorCode uint32
6865 lastErrorObj uint32
6966
70- // Reusable read buffer for header
71- headerBuf [8]byte
67+ // Connection-local receive state. rbuf holds bytes read from the socket
68+ // that do not yet form a complete frame; recvBuf is the scratch buffer
69+ // used for a single read.
70+ rbuf []byte
71+ recvBuf []byte
72+ readErr error
73+ maxMsgLen uint32
74+ closed atomic.Bool
75+}
7276
73- // Pre-allocated buffer for event bodies (avoid allocations)
74- eventBodyBuf [4096]byte
77+// newDisplay builds a Display over an established connection.
78+func newDisplay(conn net.Conn) *Display {
79+ d := &Display{
80+ conn: conn,
81+ nextID: 2, // 1 is reserved for wl_display
82+ dispatcher: NewEventDispatcher(),
83+ }
84+ if uc, ok := conn.(*net.UnixConn); ok {
85+ d.unix = uc
86+ }
87+ d.context = NewContext(d)
88+ // Register display object (ID 1)
89+ d.objects.Store(uint32(displayObjectID), d)
90+ // Initialize registry
91+ d.registry = &Registry{
92+ id: d.allocateID(),
93+ display: d,
94+ globals: make(map[uint32]Global),
95+ handlers: make(map[string]GlobalHandler),
96+ }
97+ d.objects.Store(d.registry.id, d.registry)
98+ return d
7599 }
76100
77101 // Registry represents the global registry
@@ -95,39 +119,10 @@ func (c *callbackObject) ID() uint32 {
95119
96120 // Dispatch handles callback events (opcode 0 = done)
97121 func (c *callbackObject) Dispatch(event *Event) {
98- if event.Opcode == 0 { // done event
99- log.Printf("wlclient: callbackObject.Dispatch fired for ID %d", c.id)
100- // Trigger any listeners for this callback
101- if listeners, ok := c.display.listeners.Load(c.id); ok {
102- if opcodeMap, ok := listeners.(*sync.Map); ok {
103- if handlers, ok := opcodeMap.Load(uint16(0)); ok {
104- if handlerSlice, ok := handlers.(*[]func([]byte)); ok {
105- if handlerSlice == nil {
106- log.Printf("wlclient: WARNING: handlerSlice is nil for callback ID %d", c.id)
107- return
108- }
109- log.Printf("wlclient: Found %d handlers for callback ID %d", len(*handlerSlice), c.id)
110- for i, handler := range *handlerSlice {
111- if handler == nil {
112- log.Printf("wlclient: WARNING: handler %d is nil for callback ID %d", i, c.id)
113- continue
114- }
115- log.Printf("wlclient: Calling handler %d for callback ID %d", i, c.id)
116- handler(event.data)
117- }
118- } else {
119- log.Printf("wlclient: WARNING: handlers not a slice for callback ID %d", c.id)
120- }
121- } else {
122- log.Printf("wlclient: WARNING: no handlers found for opcode 0 on callback ID %d", c.id)
123- }
124- } else {
125- log.Printf("wlclient: WARNING: listeners not a sync.Map for callback ID %d", c.id)
126- }
127- } else {
128- log.Printf("wlclient: WARNING: no listeners found for callback ID %d", c.id)
129- }
122+ if event.Opcode != 0 { // done event
123+ return
130124 }
125+ c.display.notifyListeners(c.id, 0, event.data)
131126 }
132127
133128 // Global represents a global object
@@ -140,7 +135,7 @@ type Global struct {
140135 // GlobalHandler is called when a global is announced
141136 type GlobalHandler func(registry *Registry, name uint32, version uint32)
142137
143-// Connect connects to the Wayland display
138+// Connect connects to the Wayland display and requests the registry.
144139 func Connect(socketPath string) (*Display, error) {
145140 if socketPath == "" {
146141 socketPath = os.Getenv("WAYLAND_DISPLAY")
@@ -158,62 +153,37 @@ func Connect(socketPath string) (*Display, error) {
158153 socketPath = filepath.Join(runDir, socketPath)
159154 }
160155
161- // log.Printf("wlclient: Connecting to Wayland socket: %s", socketPath)
162- // Connect to socket
163156 conn, err := net.Dial("unix", socketPath)
164157 if err != nil {
165158 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
166159 }
167- // log.Printf("wlclient: Connected to Wayland socket successfully")
168-
169- // Get file descriptor for advanced operations
170- file, err := conn.(*net.UnixConn).File()
171- if err != nil {
172- _ = conn.Close()
173- return nil, fmt.Errorf("failed to get socket fd: %w", err)
174- }
175- fd := int(file.Fd())
176- _ = file.Close() // We only need the fd
177-
178- d := &Display{
179- conn: conn,
180- fd: fd,
181- nextID: 2, // 1 is reserved for wl_display
182- dispatcher: NewEventDispatcher(),
183- }
184-
185- // Create context once and store it
186- d.context = NewContext(d)
187160
188- // Register display object (ID 1)
189- d.objects.Store(uint32(1), d)
190-
191- // Initialize registry
192- d.registry = &Registry{
193- id: d.allocateID(),
194- display: d,
195- globals: make(map[uint32]Global),
196- handlers: make(map[string]GlobalHandler),
197- }
198- d.objects.Store(d.registry.id, d.registry)
161+ d := newDisplay(conn)
199162
200163 // Get registry
201164 if err := d.getRegistry(); err != nil {
202- _ = conn.Close()
165+ _ = d.Close()
203166 return nil, fmt.Errorf("failed to get registry: %w", err)
204167 }
205168
206169 // Don't do initial roundtrip here - let the caller do it after setting up handlers
207- // log.Printf("wlclient: display connected, registry ready")
208-
209170 return d, nil
210171 }
211172
212-// Close closes the display connection
173+// Close closes the display connection. It is safe to call more than once;
174+// later calls are no-ops. After Close, Dispatch returns net.ErrClosed.
213175 func (d *Display) Close() error {
176+ if !d.closed.CompareAndSwap(false, true) {
177+ return nil
178+ }
214179 return d.conn.Close()
215180 }
216181
182+// Closed reports whether the connection has been closed.
183+func (d *Display) Closed() bool {
184+ return d.closed.Load()
185+}
186+
217187 // ID returns the display's object ID (always 1)
218188 func (d *Display) ID() uint32 {
219189 return 1
@@ -226,9 +196,7 @@ func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler E
226196
227197 // allocateID allocates a new object ID
228198 func (d *Display) allocateID() uint32 {
229- id := atomic.AddUint32(&d.nextID, 1) - 1
230- log.Printf("wlclient: allocateID() generated ID %d", id)
231- return id
199+ return atomic.AddUint32(&d.nextID, 1) - 1
232200 }
233201
234202 // AllocateID allocates a new object ID (public method)
@@ -341,113 +309,151 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
341309 return nil
342310 }
343311
344-// Dispatch reads and dispatches events
312+// Dispatch reads one complete Wayland message and delivers it to the object
313+// that owns it.
314+//
315+// Read boundaries never define message boundaries: a header split across
316+// several reads and several messages arriving in one read are both handled.
317+// Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
318+// malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame.
345319 func (d *Display) Dispatch() error {
346320 d.recvMu.Lock()
347321 defer d.recvMu.Unlock()
348322
349- // Read header with potential file descriptors
350- n, fds, err := d.recvmsgWithFDs(d.headerBuf[:])
351- if err != nil {
352- return fmt.Errorf("failed to read header: %w", err)
323+ if d.closed.Load() {
324+ return net.ErrClosed
353325 }
354- if n < 8 {
355- return fmt.Errorf("incomplete header: got %d bytes", n)
356- }
357-
358- objectID := binary.LittleEndian.Uint32(d.headerBuf[0:4])
359- sizeOpcode := binary.LittleEndian.Uint32(d.headerBuf[4:8])
360- // Upper 16 bits = size (includes header), lower 16 bits = opcode
361- size := sizeOpcode >> 16
362- opcode := sizeOpcode & 0xffff
363326
364- // Only log suspicious events or important ones
365- // if objectID > 1000000 || objectID == 5 {
366- // log.Printf("wlclient: Dispatch received event: object=%d (0x%x), opcode=%d, size=%d", objectID, objectID, opcode, size)
367- // }
368- if opcode > 0xFFFF {
369- return fmt.Errorf("invalid opcode: %d", opcode)
327+ frame, err := d.nextFrame()
328+ if err != nil {
329+ return err
370330 }
331+ return d.dispatchFrame(frame)
332+}
371333
372- // Read message body (size includes 8-byte header)
373- var body []byte
374- if size > 8 {
375- bodySize := size - 8
334+// maxMessageSize reports the largest accepted message size.
335+func (d *Display) maxMessageSize() uint32 {
336+ if d.maxMsgLen == 0 {
337+ return DefaultMaxMessageSize
338+ }
339+ return d.maxMsgLen
340+}
376341
377- // Use pre-allocated buffer for small messages (zero allocation)
378- if bodySize <= uint32(len(d.eventBodyBuf)) {
379- body = d.eventBodyBuf[:bodySize]
380- } else {
381- // Large message, allocate new buffer
382- body = make([]byte, bodySize)
342+// nextFrame returns the next complete frame, reading more bytes as needed.
343+func (d *Display) nextFrame() (receivedFrame, error) {
344+ for {
345+ if len(d.rbuf) >= HeaderSize {
346+ object, opcode, size, err := parseHeader(d.rbuf[:HeaderSize], d.maxMessageSize())
347+ if err != nil {
348+ return receivedFrame{}, err
349+ }
350+ if uint32(len(d.rbuf)) >= size {
351+ frame := receivedFrame{object: object, opcode: opcode}
352+ if size > HeaderSize {
353+ frame.body = make([]byte, size-HeaderSize)
354+ copy(frame.body, d.rbuf[HeaderSize:size])
355+ }
356+ d.consume(size)
357+ return frame, nil
358+ }
359+ }
360+ if err := d.fill(); err != nil {
361+ return receivedFrame{}, err
383362 }
363+ }
364+}
384365
385- n, moreFds, err := d.recvmsgWithFDs(body)
386- if err != nil {
387- return fmt.Errorf("failed to read body: %w", err)
366+// consume removes the first n bytes from the receive buffer.
367+func (d *Display) consume(n uint32) {
368+ rest := copy(d.rbuf, d.rbuf[n:])
369+ d.rbuf = d.rbuf[:rest]
370+}
371+
372+// fill reads one chunk from the connection into the receive buffer. Bytes that
373+// arrive together with an error are kept so they are still parsed before the
374+// error surfaces.
375+func (d *Display) fill() error {
376+ if d.readErr != nil {
377+ return d.readErr
378+ }
379+ if d.recvBuf == nil {
380+ d.recvBuf = make([]byte, readChunkSize)
381+ }
382+ n, err := d.readChunk()
383+ if n > 0 {
384+ d.rbuf = append(d.rbuf, d.recvBuf[:n]...)
385+ }
386+ if err != nil {
387+ if errors.Is(err, io.EOF) {
388+ err = io.ErrUnexpectedEOF
388389 }
389- if n < int(bodySize) {
390- // Read remaining data if needed
391- remaining := body[n:]
392- if _, err := io.ReadFull(d.conn, remaining); err != nil {
393- return fmt.Errorf("failed to read remaining body: %w", err)
394- }
390+ d.readErr = err
391+ if n == 0 {
392+ return err
395393 }
396- fds = append(fds, moreFds...)
397394 }
395+ return nil
396+}
398397
399- // Handle display events specially
400- if objectID == 1 {
401- return d.handleDisplayEvent(uint16(opcode), body)
398+// dispatchFrame delivers one complete frame to the object that owns it.
399+func (d *Display) dispatchFrame(f receivedFrame) error {
400+ if f.object == displayObjectID {
401+ return d.handleDisplayEvent(f.opcode, f.body)
402402 }
403403
404- // Try to find the object
405- if obj, ok := d.objects.Load(objectID); ok {
406- // Only log for non-registry objects to reduce spam
407- if objectID != 2 {
408- log.Printf("wlclient: Found object for ID %d, type: %T", objectID, obj)
404+ obj, ok := d.objects.Load(f.object)
405+ if !ok {
406+ return &ProtocolError{
407+ Kind: "unknown_object",
408+ Object: f.object,
409+ Opcode: f.opcode,
410+ Size: uint32(len(f.body)) + HeaderSize,
411+ Err: ErrUnknownObject,
409412 }
413+ }
410414
411- // CRITICAL: Handle server object creation BEFORE dispatching
412- if d.handleServerObject(objectID, uint16(opcode), body) {
413- log.Printf("wlclient: Handled server object creation for ID %d, opcode %d", objectID, opcode)
414- }
415+ // Server-created objects are announced by an event on a known object.
416+ d.handleServerObject(f.object, f.opcode, f.body)
415417
416- // Check if it's a Proxy with Dispatch method
417- if proxy, ok := obj.(Proxy); ok && proxy != nil {
418- event := &Event{
419- ProxyID: objectID,
420- Opcode: uint16(opcode),
421- data: body,
422- offset: 0,
423- }
424- if objectID != 2 {
425- log.Printf("wlclient: Dispatching event to proxy ID %d, opcode %d", objectID, opcode)
426- }
427- proxy.Dispatch(event)
428- return nil // Event was handled
429- }
430- } else {
431- log.Printf("wlclient: WARNING: No object found for ID %d (0x%x)", objectID, objectID)
418+ if proxy, ok := obj.(Proxy); ok && proxy != nil {
419+ proxy.Dispatch(&Event{
420+ ProxyID: f.object,
421+ Opcode: f.opcode,
422+ data: f.body,
423+ display: d,
424+ })
425+ return nil
432426 }
433427
434- // Use high-performance dispatcher
435- d.dispatcher.Dispatch(objectID, uint16(opcode), body)
428+ // High-performance dispatcher, then compatibility listeners.
429+ d.dispatcher.Dispatch(f.object, f.opcode, f.body)
430+ d.notifyListeners(f.object, f.opcode, f.body)
431+ return nil
432+}
436433
437- // Also check listeners (for compatibility)
438- if listeners, ok := d.listeners.Load(objectID); ok {
439- if opcodeMap, ok := listeners.(*sync.Map); ok {
440- if handlers, ok := opcodeMap.Load(uint16(opcode)); ok {
441- if handlerSlice, ok := handlers.(*[]func([]byte)); ok {
442- for _, handler := range *handlerSlice {
443- handler(body)
444- }
445- }
446- }
434+// notifyListeners runs the listeners registered for an object and opcode.
435+func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
436+ listeners, ok := d.listeners.Load(objectID)
437+ if !ok {
438+ return
439+ }
440+ opcodeMap, ok := listeners.(*sync.Map)
441+ if !ok {
442+ return
443+ }
444+ handlers, ok := opcodeMap.Load(opcode)
445+ if !ok {
446+ return
447+ }
448+ handlerSlice, ok := handlers.(*[]func([]byte))
449+ if !ok || handlerSlice == nil {
450+ return
451+ }
452+ for _, handler := range *handlerSlice {
453+ if handler != nil {
454+ handler(body)
447455 }
448456 }
449-
450- return nil
451457 }
452458
453459 // handleDisplayEvent handles events on the display object
@@ -455,60 +461,46 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
455461 switch opcode {
456462 case 0: // error
457463 if len(data) < 8 {
458- return errors.New("invalid error event")
459- }
460- objectID := binary.LittleEndian.Uint32(data[0:4])
461- code := binary.LittleEndian.Uint32(data[4:8])
462-
463- var message string
464- if len(data) > 8 {
465- // Message is optional, parse if present
466- if len(data) >= 12 {
467- msgLen := binary.LittleEndian.Uint32(data[8:12])
468- if msgLen > 0 && len(data) >= 12+int(msgLen) {
469- message = string(data[12 : 12+msgLen-1]) // -1 to remove null terminator
470- }
471- }
464+ return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
472465 }
466+ event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
467+ objectID := event.Uint32()
468+ code := event.Uint32()
469+ message := event.String()
473470
474- d.lastError = fmt.Errorf("protocol error: object %d, code %d: %s", objectID, code, message)
475471 d.lastErrorCode = code
476472 d.lastErrorObj = objectID
473+ d.lastError = &DisplayError{ObjectID: objectID, Code: code, Message: message}
477474 return d.lastError
478475
479476 case 1: // delete_id
480477 if len(data) < 4 {
481- return errors.New("invalid delete_id event")
478+ return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
482479 }
483480 id := binary.LittleEndian.Uint32(data[0:4])
484481 d.objects.Delete(id)
485- }
482+ return nil
486483
487- return nil
484+ default:
485+ return &ProtocolError{Kind: "unknown_opcode", Object: displayObjectID, Opcode: opcode, Err: ErrUnknownOpcode}
486+ }
488487 }
489488
490489 // Roundtrip performs a synchronous roundtrip to the compositor
491490 func (d *Display) Roundtrip() error {
492- log.Printf("wlclient: Roundtrip starting...")
493- // Create callback
494491 callbackID := d.allocateID()
495492 done := make(chan error, 1)
496493
497- log.Printf("wlclient: Registering callback listener for ID %d", callbackID)
498- // Register callback listener
499494 d.AddListener(callbackID, 0, func(_ []byte) {
500- log.Printf("wlclient: Callback %d fired", callbackID)
501495 d.objects.Delete(callbackID)
502496 done <- nil
503497 })
504498
505499 // Send sync request (opcode 0)
506- log.Printf("wlclient: Sending sync request with callback ID %d", callbackID)
507- if err := d.SendRequest(1, 0, callbackID); err != nil {
500+ if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil {
508501 return err
509502 }
510503
511- // Store the callback object to track it
512504 d.objects.Store(callbackID, &callbackObject{
513505 BaseProxy: BaseProxy{
514506 context: d.Context(),
@@ -517,22 +509,14 @@ func (d *Display) Roundtrip() error {
517509 display: d,
518510 })
519511
520- // Process events until callback fires - SIMPLIFIED APPROACH
521- maxIterations := 1000 // Prevent infinite loops
522- for i := 0; i < maxIterations; i++ {
523- // Don't set read deadline for now - causes issues
512+ for i := 0; i < 1000; i++ {
524513 if err := d.Dispatch(); err != nil {
525- log.Printf("wlclient: Dispatch error: %v", err)
526514 return err
527515 }
528-
529- // Check if callback completed
530516 select {
531517 case err := <-done:
532- log.Printf("wlclient: Roundtrip completed successfully")
533518 return err
534519 default:
535- // Continue dispatching
536520 }
537521 }
538522
@@ -648,19 +632,15 @@ func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
648632 func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
649633 // Set the ID if not already set
650634 if proxy.ID() == 0 {
651- newID := r.display.allocateID()
652- log.Printf("wlclient: Allocating new ID %d for interface %s", newID, iface)
653- proxy.SetID(newID)
635+ proxy.SetID(r.display.allocateID())
654636 }
655637
656638 // Ensure proxy has a context set - CRITICAL FIX
657639 if proxy.Context() == nil {
658640 if baseProxy, ok := proxy.(*BaseProxy); ok {
659641 baseProxy.SetContext(r.display.Context())
660- log.Printf("wlclient: Set context for proxy ID %d", proxy.ID())
661642 } else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {
662643 setter.SetContext(r.display.Context())
663- log.Printf("wlclient: Set context for proxy ID %d via interface", proxy.ID())
664644 } else {
665645 return fmt.Errorf("proxy doesn't have context and can't set it")
666646 }
@@ -671,7 +651,6 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
671651
672652 // Also register directly in display objects map
673653 r.display.objects.Store(proxy.ID(), proxy)
674- log.Printf("wlclient: Registered proxy ID %d in display objects map", proxy.ID())
675654
676655 // Send bind request (opcode 0) with proper arguments
677656 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
@@ -748,13 +727,11 @@ func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body
748727 switch opcode {
749728 case 0: // head event - creates new zwlr_output_head_v1 object
750729 if len(body) < 4 {
751- log.Printf("wlclient: head event body too short: %d bytes", len(body))
752730 return false
753731 }
754732
755733 // Parse the new_id for the head object
756734 headID := binary.LittleEndian.Uint32(body[0:4])
757- log.Printf("wlclient: Creating server object (head) with ID %d", headID)
758735
759736 // Create a proper OutputHead object
760737 headProxy := &OutputHead{
@@ -766,15 +743,12 @@ func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body
766743
767744 // Register the new head object
768745 d.objects.Store(headID, headProxy)
769- log.Printf("wlclient: Registered server-created head object ID %d", headID)
770746 return true
771747
772748 case 1: // done event
773- log.Printf("wlclient: Output manager done event")
774749 return false
775750
776751 case 2: // finished event
777- log.Printf("wlclient: Output manager finished event")
778752 return false
779753 }
780754
@@ -792,21 +766,16 @@ type OutputHead struct {
792766
793767 // Dispatch handles head events
794768 func (h *OutputHead) Dispatch(event *Event) {
795- log.Printf("wlclient: OutputHead.Dispatch ID %d, opcode %d", h.id, event.Opcode)
796769 switch event.Opcode {
797770 case 0: // name
798771 h.name = event.String()
799- log.Printf("wlclient: Head %d name: %s", h.id, h.name)
800772 case 1: // description
801773 h.description = event.String()
802- log.Printf("wlclient: Head %d description: %s", h.id, h.description)
803774 case 2: // physical_size
804775 h.width = event.Int32()
805776 h.height = event.Int32()
806- log.Printf("wlclient: Head %d physical size: %dx%d mm", h.id, h.width, h.height)
807777 case 3: // mode (creates new mode object)
808778 modeID := event.Uint32()
809- log.Printf("wlclient: Head %d mode object created: ID %d", h.id, modeID)
810779 // Create mode object
811780 mode := &OutputMode{
812781 BaseProxy: BaseProxy{
@@ -816,10 +785,8 @@ func (h *OutputHead) Dispatch(event *Event) {
816785 }
817786 h.context.display.objects.Store(modeID, mode)
818787 case 9: // finished
819- log.Printf("wlclient: Head %d finished", h.id)
820788 h.context.Unregister(h)
821789 default:
822- log.Printf("wlclient: Head %d unhandled opcode %d", h.id, event.Opcode)
823790 }
824791 }
825792
@@ -833,21 +800,15 @@ type OutputMode struct {
833800
834801 // Dispatch handles mode events
835802 func (m *OutputMode) Dispatch(event *Event) {
836- log.Printf("wlclient: OutputMode.Dispatch ID %d, opcode %d", m.id, event.Opcode)
837803 switch event.Opcode {
838804 case 0: // size
839805 m.width = event.Int32()
840806 m.height = event.Int32()
841- log.Printf("wlclient: Mode %d size: %dx%d", m.id, m.width, m.height)
842807 case 1: // refresh
843808 m.refresh = event.Int32()
844- log.Printf("wlclient: Mode %d refresh: %d mHz", m.id, m.refresh)
845809 case 2: // preferred
846- log.Printf("wlclient: Mode %d is preferred", m.id)
847810 case 3: // finished
848- log.Printf("wlclient: Mode %d finished", m.id)
849811 m.context.Unregister(m)
850812 default:
851- log.Printf("wlclient: Mode %d unhandled opcode %d", m.id, event.Opcode)
852813 }
853814 }
@@ -10,9 +10,6 @@ import (
10 "errors"10 "errors"
11 "fmt"11 "fmt"
12 "io"12 "io"
13- "log"
14-
15- // "log"
16 "net"13 "net"
17 "os"14 "os"
18 "path/filepath"15 "path/filepath"
@@ -48,8 +45,8 @@ type Object interface {
48 // Display represents a connection to the Wayland display45 // Display represents a connection to the Wayland display
49 type Display struct {46 type Display struct {
50 conn net.Conn47 conn net.Conn
51- fd int48+ unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
52- objects sync.Map // map[uint32]Object49+ objects sync.Map // map[uint32]Object
53 nextID uint3250 nextID uint32
54 sendMu sync.Mutex51 sendMu sync.Mutex
55 recvMu sync.Mutex52 recvMu sync.Mutex
@@ -67,11 +64,38 @@ type Display struct {
67 lastErrorCode uint3264 lastErrorCode uint32
68 lastErrorObj uint3265 lastErrorObj uint32
69 66
70- // Reusable read buffer for header67+ // Connection-local receive state. rbuf holds bytes read from the socket
71- headerBuf [8]byte68+ // that do not yet form a complete frame; recvBuf is the scratch buffer
69+ // used for a single read.
70+ rbuf []byte
71+ recvBuf []byte
72+ readErr error
73+ maxMsgLen uint32
74+ closed atomic.Bool
75+}
72 76
73- // Pre-allocated buffer for event bodies (avoid allocations)77+// newDisplay builds a Display over an established connection.
74- eventBodyBuf [4096]byte78+func newDisplay(conn net.Conn) *Display {
79+ d := &Display{
80+ conn: conn,
81+ nextID: 2, // 1 is reserved for wl_display
82+ dispatcher: NewEventDispatcher(),
83+ }
84+ if uc, ok := conn.(*net.UnixConn); ok {
85+ d.unix = uc
86+ }
87+ d.context = NewContext(d)
88+ // Register display object (ID 1)
89+ d.objects.Store(uint32(displayObjectID), d)
90+ // Initialize registry
91+ d.registry = &Registry{
92+ id: d.allocateID(),
93+ display: d,
94+ globals: make(map[uint32]Global),
95+ handlers: make(map[string]GlobalHandler),
96+ }
97+ d.objects.Store(d.registry.id, d.registry)
98+ return d
75 }99 }
76 100
77 // Registry represents the global registry101 // Registry represents the global registry
@@ -95,39 +119,10 @@ func (c *callbackObject) ID() uint32 {
95 119
96 // Dispatch handles callback events (opcode 0 = done)120 // Dispatch handles callback events (opcode 0 = done)
97 func (c *callbackObject) Dispatch(event *Event) {121 func (c *callbackObject) Dispatch(event *Event) {
98- if event.Opcode == 0 { // done event122+ if event.Opcode != 0 { // done event
99- log.Printf("wlclient: callbackObject.Dispatch fired for ID %d", c.id)123+ return
100- // Trigger any listeners for this callback
101- if listeners, ok := c.display.listeners.Load(c.id); ok {
102- if opcodeMap, ok := listeners.(*sync.Map); ok {
103- if handlers, ok := opcodeMap.Load(uint16(0)); ok {
104- if handlerSlice, ok := handlers.(*[]func([]byte)); ok {
105- if handlerSlice == nil {
106- log.Printf("wlclient: WARNING: handlerSlice is nil for callback ID %d", c.id)
107- return
108- }
109- log.Printf("wlclient: Found %d handlers for callback ID %d", len(*handlerSlice), c.id)
110- for i, handler := range *handlerSlice {
111- if handler == nil {
112- log.Printf("wlclient: WARNING: handler %d is nil for callback ID %d", i, c.id)
113- continue
114- }
115- log.Printf("wlclient: Calling handler %d for callback ID %d", i, c.id)
116- handler(event.data)
117- }
118- } else {
119- log.Printf("wlclient: WARNING: handlers not a slice for callback ID %d", c.id)
120- }
121- } else {
122- log.Printf("wlclient: WARNING: no handlers found for opcode 0 on callback ID %d", c.id)
123- }
124- } else {
125- log.Printf("wlclient: WARNING: listeners not a sync.Map for callback ID %d", c.id)
126- }
127- } else {
128- log.Printf("wlclient: WARNING: no listeners found for callback ID %d", c.id)
129- }
130 }124 }
125+ c.display.notifyListeners(c.id, 0, event.data)
131 }126 }
132 127
133 // Global represents a global object128 // Global represents a global object
@@ -140,7 +135,7 @@ type Global struct {
140 // GlobalHandler is called when a global is announced135 // GlobalHandler is called when a global is announced
141 type GlobalHandler func(registry *Registry, name uint32, version uint32)136 type GlobalHandler func(registry *Registry, name uint32, version uint32)
142 137
143-// Connect connects to the Wayland display138+// Connect connects to the Wayland display and requests the registry.
144 func Connect(socketPath string) (*Display, error) {139 func Connect(socketPath string) (*Display, error) {
145 if socketPath == "" {140 if socketPath == "" {
146 socketPath = os.Getenv("WAYLAND_DISPLAY")141 socketPath = os.Getenv("WAYLAND_DISPLAY")
@@ -158,62 +153,37 @@ func Connect(socketPath string) (*Display, error) {
158 socketPath = filepath.Join(runDir, socketPath)153 socketPath = filepath.Join(runDir, socketPath)
159 }154 }
160 155
161- // log.Printf("wlclient: Connecting to Wayland socket: %s", socketPath)
162- // Connect to socket
163 conn, err := net.Dial("unix", socketPath)156 conn, err := net.Dial("unix", socketPath)
164 if err != nil {157 if err != nil {
165 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)158 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
166 }159 }
167- // log.Printf("wlclient: Connected to Wayland socket successfully")
168-
169- // Get file descriptor for advanced operations
170- file, err := conn.(*net.UnixConn).File()
171- if err != nil {
172- _ = conn.Close()
173- return nil, fmt.Errorf("failed to get socket fd: %w", err)
174- }
175- fd := int(file.Fd())
176- _ = file.Close() // We only need the fd
177-
178- d := &Display{
179- conn: conn,
180- fd: fd,
181- nextID: 2, // 1 is reserved for wl_display
182- dispatcher: NewEventDispatcher(),
183- }
184-
185- // Create context once and store it
186- d.context = NewContext(d)
187 160
188- // Register display object (ID 1)161+ d := newDisplay(conn)
189- d.objects.Store(uint32(1), d)
190-
191- // Initialize registry
192- d.registry = &Registry{
193- id: d.allocateID(),
194- display: d,
195- globals: make(map[uint32]Global),
196- handlers: make(map[string]GlobalHandler),
197- }
198- d.objects.Store(d.registry.id, d.registry)
199 162
200 // Get registry163 // Get registry
201 if err := d.getRegistry(); err != nil {164 if err := d.getRegistry(); err != nil {
202- _ = conn.Close()165+ _ = d.Close()
203 return nil, fmt.Errorf("failed to get registry: %w", err)166 return nil, fmt.Errorf("failed to get registry: %w", err)
204 }167 }
205 168
206 // Don't do initial roundtrip here - let the caller do it after setting up handlers169 // Don't do initial roundtrip here - let the caller do it after setting up handlers
207- // log.Printf("wlclient: display connected, registry ready")
208-
209 return d, nil170 return d, nil
210 }171 }
211 172
212-// Close closes the display connection173+// Close closes the display connection. It is safe to call more than once;
174+// later calls are no-ops. After Close, Dispatch returns net.ErrClosed.
213 func (d *Display) Close() error {175 func (d *Display) Close() error {
176+ if !d.closed.CompareAndSwap(false, true) {
177+ return nil
178+ }
214 return d.conn.Close()179 return d.conn.Close()
215 }180 }
216 181
182+// Closed reports whether the connection has been closed.
183+func (d *Display) Closed() bool {
184+ return d.closed.Load()
185+}
186+
217 // ID returns the display's object ID (always 1)187 // ID returns the display's object ID (always 1)
218 func (d *Display) ID() uint32 {188 func (d *Display) ID() uint32 {
219 return 1189 return 1
@@ -226,9 +196,7 @@ func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler E
226 196
227 // allocateID allocates a new object ID197 // allocateID allocates a new object ID
228 func (d *Display) allocateID() uint32 {198 func (d *Display) allocateID() uint32 {
229- id := atomic.AddUint32(&d.nextID, 1) - 1199+ return atomic.AddUint32(&d.nextID, 1) - 1
230- log.Printf("wlclient: allocateID() generated ID %d", id)
231- return id
232 }200 }
233 201
234 // AllocateID allocates a new object ID (public method)202 // AllocateID allocates a new object ID (public method)
@@ -341,113 +309,151 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
341 return nil309 return nil
342 }310 }
343 311
344-// Dispatch reads and dispatches events312+// Dispatch reads one complete Wayland message and delivers it to the object
313+// that owns it.
314+//
315+// Read boundaries never define message boundaries: a header split across
316+// several reads and several messages arriving in one read are both handled.
317+// Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
318+// malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame.
345 func (d *Display) Dispatch() error {319 func (d *Display) Dispatch() error {
346 d.recvMu.Lock()320 d.recvMu.Lock()
347 defer d.recvMu.Unlock()321 defer d.recvMu.Unlock()
348 322
349- // Read header with potential file descriptors323+ if d.closed.Load() {
350- n, fds, err := d.recvmsgWithFDs(d.headerBuf[:])324+ return net.ErrClosed
351- if err != nil {
352- return fmt.Errorf("failed to read header: %w", err)
353 }325 }
354- if n < 8 {
355- return fmt.Errorf("incomplete header: got %d bytes", n)
356- }
357-
358- objectID := binary.LittleEndian.Uint32(d.headerBuf[0:4])
359- sizeOpcode := binary.LittleEndian.Uint32(d.headerBuf[4:8])
360- // Upper 16 bits = size (includes header), lower 16 bits = opcode
361- size := sizeOpcode >> 16
362- opcode := sizeOpcode & 0xffff
363 326
364- // Only log suspicious events or important ones327+ frame, err := d.nextFrame()
365- // if objectID > 1000000 || objectID == 5 {328+ if err != nil {
366- // log.Printf("wlclient: Dispatch received event: object=%d (0x%x), opcode=%d, size=%d", objectID, objectID, opcode, size)329+ return err
367- // }
368- if opcode > 0xFFFF {
369- return fmt.Errorf("invalid opcode: %d", opcode)
370 }330 }
331+ return d.dispatchFrame(frame)
332+}
371 333
372- // Read message body (size includes 8-byte header)334+// maxMessageSize reports the largest accepted message size.
373- var body []byte335+func (d *Display) maxMessageSize() uint32 {
374- if size > 8 {336+ if d.maxMsgLen == 0 {
375- bodySize := size - 8337+ return DefaultMaxMessageSize
338+ }
339+ return d.maxMsgLen
340+}
376 341
377- // Use pre-allocated buffer for small messages (zero allocation)342+// nextFrame returns the next complete frame, reading more bytes as needed.
378- if bodySize <= uint32(len(d.eventBodyBuf)) {343+func (d *Display) nextFrame() (receivedFrame, error) {
379- body = d.eventBodyBuf[:bodySize]344+ for {
380- } else {345+ if len(d.rbuf) >= HeaderSize {
381- // Large message, allocate new buffer346+ object, opcode, size, err := parseHeader(d.rbuf[:HeaderSize], d.maxMessageSize())
382- body = make([]byte, bodySize)347+ if err != nil {
348+ return receivedFrame{}, err
349+ }
350+ if uint32(len(d.rbuf)) >= size {
351+ frame := receivedFrame{object: object, opcode: opcode}
352+ if size > HeaderSize {
353+ frame.body = make([]byte, size-HeaderSize)
354+ copy(frame.body, d.rbuf[HeaderSize:size])
355+ }
356+ d.consume(size)
357+ return frame, nil
358+ }
359+ }
360+ if err := d.fill(); err != nil {
361+ return receivedFrame{}, err
383 }362 }
363+ }
364+}
384 365
385- n, moreFds, err := d.recvmsgWithFDs(body)366+// consume removes the first n bytes from the receive buffer.
386- if err != nil {367+func (d *Display) consume(n uint32) {
387- return fmt.Errorf("failed to read body: %w", err)368+ rest := copy(d.rbuf, d.rbuf[n:])
369+ d.rbuf = d.rbuf[:rest]
370+}
371+
372+// fill reads one chunk from the connection into the receive buffer. Bytes that
373+// arrive together with an error are kept so they are still parsed before the
374+// error surfaces.
375+func (d *Display) fill() error {
376+ if d.readErr != nil {
377+ return d.readErr
378+ }
379+ if d.recvBuf == nil {
380+ d.recvBuf = make([]byte, readChunkSize)
381+ }
382+ n, err := d.readChunk()
383+ if n > 0 {
384+ d.rbuf = append(d.rbuf, d.recvBuf[:n]...)
385+ }
386+ if err != nil {
387+ if errors.Is(err, io.EOF) {
388+ err = io.ErrUnexpectedEOF
388 }389 }
389- if n < int(bodySize) {390+ d.readErr = err
390- // Read remaining data if needed391+ if n == 0 {
391- remaining := body[n:]392+ return err
392- if _, err := io.ReadFull(d.conn, remaining); err != nil {
393- return fmt.Errorf("failed to read remaining body: %w", err)
394- }
395 }393 }
396- fds = append(fds, moreFds...)
397 }394 }
395+ return nil
396+}
398 397
399- // Handle display events specially398+// dispatchFrame delivers one complete frame to the object that owns it.
400- if objectID == 1 {399+func (d *Display) dispatchFrame(f receivedFrame) error {
401- return d.handleDisplayEvent(uint16(opcode), body)400+ if f.object == displayObjectID {
401+ return d.handleDisplayEvent(f.opcode, f.body)
402 }402 }
403 403
404- // Try to find the object404+ obj, ok := d.objects.Load(f.object)
405- if obj, ok := d.objects.Load(objectID); ok {405+ if !ok {
406- // Only log for non-registry objects to reduce spam406+ return &ProtocolError{
407- if objectID != 2 {407+ Kind: "unknown_object",
408- log.Printf("wlclient: Found object for ID %d, type: %T", objectID, obj)408+ Object: f.object,
409+ Opcode: f.opcode,
410+ Size: uint32(len(f.body)) + HeaderSize,
411+ Err: ErrUnknownObject,
409 }412 }
413+ }
410 414
411- // CRITICAL: Handle server object creation BEFORE dispatching415+ // Server-created objects are announced by an event on a known object.
412- if d.handleServerObject(objectID, uint16(opcode), body) {416+ d.handleServerObject(f.object, f.opcode, f.body)
413- log.Printf("wlclient: Handled server object creation for ID %d, opcode %d", objectID, opcode)
414- }
415 417
416- // Check if it's a Proxy with Dispatch method418+ if proxy, ok := obj.(Proxy); ok && proxy != nil {
417- if proxy, ok := obj.(Proxy); ok && proxy != nil {419+ proxy.Dispatch(&Event{
418- event := &Event{420+ ProxyID: f.object,
419- ProxyID: objectID,421+ Opcode: f.opcode,
420- Opcode: uint16(opcode),422+ data: f.body,
421- data: body,423+ display: d,
422- offset: 0,424+ })
423- }425+ return nil
424- if objectID != 2 {
425- log.Printf("wlclient: Dispatching event to proxy ID %d, opcode %d", objectID, opcode)
426- }
427- proxy.Dispatch(event)
428- return nil // Event was handled
429- }
430- } else {
431- log.Printf("wlclient: WARNING: No object found for ID %d (0x%x)", objectID, objectID)
432 }426 }
433 427
434- // Use high-performance dispatcher428+ // High-performance dispatcher, then compatibility listeners.
435- d.dispatcher.Dispatch(objectID, uint16(opcode), body)429+ d.dispatcher.Dispatch(f.object, f.opcode, f.body)
430+ d.notifyListeners(f.object, f.opcode, f.body)
431+ return nil
432+}
436 433
437- // Also check listeners (for compatibility)434+// notifyListeners runs the listeners registered for an object and opcode.
438- if listeners, ok := d.listeners.Load(objectID); ok {435+func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
439- if opcodeMap, ok := listeners.(*sync.Map); ok {436+ listeners, ok := d.listeners.Load(objectID)
440- if handlers, ok := opcodeMap.Load(uint16(opcode)); ok {437+ if !ok {
441- if handlerSlice, ok := handlers.(*[]func([]byte)); ok {438+ return
442- for _, handler := range *handlerSlice {439+ }
443- handler(body)440+ opcodeMap, ok := listeners.(*sync.Map)
444- }441+ if !ok {
445- }442+ return
446- }443+ }
444+ handlers, ok := opcodeMap.Load(opcode)
445+ if !ok {
446+ return
447+ }
448+ handlerSlice, ok := handlers.(*[]func([]byte))
449+ if !ok || handlerSlice == nil {
450+ return
451+ }
452+ for _, handler := range *handlerSlice {
453+ if handler != nil {
454+ handler(body)
447 }455 }
448 }456 }
449-
450- return nil
451 }457 }
452 458
453 // handleDisplayEvent handles events on the display object459 // handleDisplayEvent handles events on the display object
@@ -455,60 +461,46 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
455 switch opcode {461 switch opcode {
456 case 0: // error462 case 0: // error
457 if len(data) < 8 {463 if len(data) < 8 {
458- return errors.New("invalid error event")464+ return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
459- }
460- objectID := binary.LittleEndian.Uint32(data[0:4])
461- code := binary.LittleEndian.Uint32(data[4:8])
462-
463- var message string
464- if len(data) > 8 {
465- // Message is optional, parse if present
466- if len(data) >= 12 {
467- msgLen := binary.LittleEndian.Uint32(data[8:12])
468- if msgLen > 0 && len(data) >= 12+int(msgLen) {
469- message = string(data[12 : 12+msgLen-1]) // -1 to remove null terminator
470- }
471- }
472 }465 }
466+ event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
467+ objectID := event.Uint32()
468+ code := event.Uint32()
469+ message := event.String()
473 470
474- d.lastError = fmt.Errorf("protocol error: object %d, code %d: %s", objectID, code, message)
475 d.lastErrorCode = code471 d.lastErrorCode = code
476 d.lastErrorObj = objectID472 d.lastErrorObj = objectID
473+ d.lastError = &DisplayError{ObjectID: objectID, Code: code, Message: message}
477 return d.lastError474 return d.lastError
478 475
479 case 1: // delete_id476 case 1: // delete_id
480 if len(data) < 4 {477 if len(data) < 4 {
481- return errors.New("invalid delete_id event")478+ return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
482 }479 }
483 id := binary.LittleEndian.Uint32(data[0:4])480 id := binary.LittleEndian.Uint32(data[0:4])
484 d.objects.Delete(id)481 d.objects.Delete(id)
485- }482+ return nil
486 483
487- return nil484+ default:
485+ return &ProtocolError{Kind: "unknown_opcode", Object: displayObjectID, Opcode: opcode, Err: ErrUnknownOpcode}
486+ }
488 }487 }
489 488
490 // Roundtrip performs a synchronous roundtrip to the compositor489 // Roundtrip performs a synchronous roundtrip to the compositor
491 func (d *Display) Roundtrip() error {490 func (d *Display) Roundtrip() error {
492- log.Printf("wlclient: Roundtrip starting...")
493- // Create callback
494 callbackID := d.allocateID()491 callbackID := d.allocateID()
495 done := make(chan error, 1)492 done := make(chan error, 1)
496 493
497- log.Printf("wlclient: Registering callback listener for ID %d", callbackID)
498- // Register callback listener
499 d.AddListener(callbackID, 0, func(_ []byte) {494 d.AddListener(callbackID, 0, func(_ []byte) {
500- log.Printf("wlclient: Callback %d fired", callbackID)
501 d.objects.Delete(callbackID)495 d.objects.Delete(callbackID)
502 done <- nil496 done <- nil
503 })497 })
504 498
505 // Send sync request (opcode 0)499 // Send sync request (opcode 0)
506- log.Printf("wlclient: Sending sync request with callback ID %d", callbackID)500+ if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil {
507- if err := d.SendRequest(1, 0, callbackID); err != nil {
508 return err501 return err
509 }502 }
510 503
511- // Store the callback object to track it
512 d.objects.Store(callbackID, &callbackObject{504 d.objects.Store(callbackID, &callbackObject{
513 BaseProxy: BaseProxy{505 BaseProxy: BaseProxy{
514 context: d.Context(),506 context: d.Context(),
@@ -517,22 +509,14 @@ func (d *Display) Roundtrip() error {
517 display: d,509 display: d,
518 })510 })
519 511
520- // Process events until callback fires - SIMPLIFIED APPROACH512+ for i := 0; i < 1000; i++ {
521- maxIterations := 1000 // Prevent infinite loops
522- for i := 0; i < maxIterations; i++ {
523- // Don't set read deadline for now - causes issues
524 if err := d.Dispatch(); err != nil {513 if err := d.Dispatch(); err != nil {
525- log.Printf("wlclient: Dispatch error: %v", err)
526 return err514 return err
527 }515 }
528-
529- // Check if callback completed
530 select {516 select {
531 case err := <-done:517 case err := <-done:
532- log.Printf("wlclient: Roundtrip completed successfully")
533 return err518 return err
534 default:519 default:
535- // Continue dispatching
536 }520 }
537 }521 }
538 522
@@ -648,19 +632,15 @@ func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
648 func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {632 func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
649 // Set the ID if not already set633 // Set the ID if not already set
650 if proxy.ID() == 0 {634 if proxy.ID() == 0 {
651- newID := r.display.allocateID()635+ proxy.SetID(r.display.allocateID())
652- log.Printf("wlclient: Allocating new ID %d for interface %s", newID, iface)
653- proxy.SetID(newID)
654 }636 }
655 637
656 // Ensure proxy has a context set - CRITICAL FIX638 // Ensure proxy has a context set - CRITICAL FIX
657 if proxy.Context() == nil {639 if proxy.Context() == nil {
658 if baseProxy, ok := proxy.(*BaseProxy); ok {640 if baseProxy, ok := proxy.(*BaseProxy); ok {
659 baseProxy.SetContext(r.display.Context())641 baseProxy.SetContext(r.display.Context())
660- log.Printf("wlclient: Set context for proxy ID %d", proxy.ID())
661 } else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {642 } else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {
662 setter.SetContext(r.display.Context())643 setter.SetContext(r.display.Context())
663- log.Printf("wlclient: Set context for proxy ID %d via interface", proxy.ID())
664 } else {644 } else {
665 return fmt.Errorf("proxy doesn't have context and can't set it")645 return fmt.Errorf("proxy doesn't have context and can't set it")
666 }646 }
@@ -671,7 +651,6 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
671 651
672 // Also register directly in display objects map652 // Also register directly in display objects map
673 r.display.objects.Store(proxy.ID(), proxy)653 r.display.objects.Store(proxy.ID(), proxy)
674- log.Printf("wlclient: Registered proxy ID %d in display objects map", proxy.ID())
675 654
676 // Send bind request (opcode 0) with proper arguments655 // Send bind request (opcode 0) with proper arguments
677 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {656 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
@@ -748,13 +727,11 @@ func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body
748 switch opcode {727 switch opcode {
749 case 0: // head event - creates new zwlr_output_head_v1 object728 case 0: // head event - creates new zwlr_output_head_v1 object
750 if len(body) < 4 {729 if len(body) < 4 {
751- log.Printf("wlclient: head event body too short: %d bytes", len(body))
752 return false730 return false
753 }731 }
754 732
755 // Parse the new_id for the head object733 // Parse the new_id for the head object
756 headID := binary.LittleEndian.Uint32(body[0:4])734 headID := binary.LittleEndian.Uint32(body[0:4])
757- log.Printf("wlclient: Creating server object (head) with ID %d", headID)
758 735
759 // Create a proper OutputHead object736 // Create a proper OutputHead object
760 headProxy := &OutputHead{737 headProxy := &OutputHead{
@@ -766,15 +743,12 @@ func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body
766 743
767 // Register the new head object744 // Register the new head object
768 d.objects.Store(headID, headProxy)745 d.objects.Store(headID, headProxy)
769- log.Printf("wlclient: Registered server-created head object ID %d", headID)
770 return true746 return true
771 747
772 case 1: // done event748 case 1: // done event
773- log.Printf("wlclient: Output manager done event")
774 return false749 return false
775 750
776 case 2: // finished event751 case 2: // finished event
777- log.Printf("wlclient: Output manager finished event")
778 return false752 return false
779 }753 }
780 754
@@ -792,21 +766,16 @@ type OutputHead struct {
792 766
793 // Dispatch handles head events767 // Dispatch handles head events
794 func (h *OutputHead) Dispatch(event *Event) {768 func (h *OutputHead) Dispatch(event *Event) {
795- log.Printf("wlclient: OutputHead.Dispatch ID %d, opcode %d", h.id, event.Opcode)
796 switch event.Opcode {769 switch event.Opcode {
797 case 0: // name770 case 0: // name
798 h.name = event.String()771 h.name = event.String()
799- log.Printf("wlclient: Head %d name: %s", h.id, h.name)
800 case 1: // description772 case 1: // description
801 h.description = event.String()773 h.description = event.String()
802- log.Printf("wlclient: Head %d description: %s", h.id, h.description)
803 case 2: // physical_size774 case 2: // physical_size
804 h.width = event.Int32()775 h.width = event.Int32()
805 h.height = event.Int32()776 h.height = event.Int32()
806- log.Printf("wlclient: Head %d physical size: %dx%d mm", h.id, h.width, h.height)
807 case 3: // mode (creates new mode object)777 case 3: // mode (creates new mode object)
808 modeID := event.Uint32()778 modeID := event.Uint32()
809- log.Printf("wlclient: Head %d mode object created: ID %d", h.id, modeID)
810 // Create mode object779 // Create mode object
811 mode := &OutputMode{780 mode := &OutputMode{
812 BaseProxy: BaseProxy{781 BaseProxy: BaseProxy{
@@ -816,10 +785,8 @@ func (h *OutputHead) Dispatch(event *Event) {
816 }785 }
817 h.context.display.objects.Store(modeID, mode)786 h.context.display.objects.Store(modeID, mode)
818 case 9: // finished787 case 9: // finished
819- log.Printf("wlclient: Head %d finished", h.id)
820 h.context.Unregister(h)788 h.context.Unregister(h)
821 default:789 default:
822- log.Printf("wlclient: Head %d unhandled opcode %d", h.id, event.Opcode)
823 }790 }
824 }791 }
825 792
@@ -833,21 +800,15 @@ type OutputMode struct {
833 800
834 // Dispatch handles mode events801 // Dispatch handles mode events
835 func (m *OutputMode) Dispatch(event *Event) {802 func (m *OutputMode) Dispatch(event *Event) {
836- log.Printf("wlclient: OutputMode.Dispatch ID %d, opcode %d", m.id, event.Opcode)
837 switch event.Opcode {803 switch event.Opcode {
838 case 0: // size804 case 0: // size
839 m.width = event.Int32()805 m.width = event.Int32()
840 m.height = event.Int32()806 m.height = event.Int32()
841- log.Printf("wlclient: Mode %d size: %dx%d", m.id, m.width, m.height)
842 case 1: // refresh807 case 1: // refresh
843 m.refresh = event.Int32()808 m.refresh = event.Int32()
844- log.Printf("wlclient: Mode %d refresh: %d mHz", m.id, m.refresh)
845 case 2: // preferred809 case 2: // preferred
846- log.Printf("wlclient: Mode %d is preferred", m.id)
847 case 3: // finished810 case 3: // finished
848- log.Printf("wlclient: Mode %d finished", m.id)
849 m.context.Unregister(m)811 m.context.Unregister(m)
850 default:812 default:
851- log.Printf("wlclient: Mode %d unhandled opcode %d", m.id, event.Opcode)
852 }813 }
853 }814 }
M client_test.go +54 -4
@@ -3,6 +3,8 @@ package wlturbo
33 import (
44 "bytes"
55 "encoding/binary"
6+ "errors"
7+ "net"
68 "testing"
79 )
810
@@ -47,7 +49,7 @@ func TestEventPool(t *testing.T) {
4749 }
4850
4951 // Set some data
50- event.ProxyId = 123
52+ event.ProxyID = 123
5153 event.Opcode = 456
5254
5355 // Return to pool
@@ -60,7 +62,7 @@ func TestEventPool(t *testing.T) {
6062 }
6163
6264 // The pool might return a clean event or the same one, both are valid
63- t.Logf("Event2: ProxyId=%d, Opcode=%d", event2.ProxyId, event2.Opcode)
65+ t.Logf("Event2: ProxyID=%d, Opcode=%d", event2.ProxyID, event2.Opcode)
6466
6567 // Clean up
6668 eventPool.Put(event2)
@@ -72,8 +74,8 @@ func TestEventDispatcher(t *testing.T) {
7274 called := false
7375 handler := func(event *Event) {
7476 called = true
75- if event.ProxyId != 123 || event.Opcode != 1 {
76- t.Errorf("Expected ProxyId=123, Opcode=1, got ProxyId=%d, Opcode=%d", event.ProxyId, event.Opcode)
77+ if event.ProxyID != 123 || event.Opcode != 1 {
78+ t.Errorf("Expected ProxyID=123, Opcode=1, got ProxyID=%d, Opcode=%d", event.ProxyID, event.Opcode)
7779 }
7880 }
7981
@@ -224,6 +226,54 @@ func TestMessageHeaderParsing(t *testing.T) {
224226 }
225227 }
226228
229+// Close must be safe to call repeatedly and must not report an error from the
230+// second call.
231+func TestDisplayCloseIsIdempotent(t *testing.T) {
232+ d := newDisplay(&chunkConn{})
233+
234+ if err := d.Close(); err != nil {
235+ t.Fatalf("first Close: %v", err)
236+ }
237+ if err := d.Close(); err != nil {
238+ t.Fatalf("second Close: %v", err)
239+ }
240+ if !d.Closed() {
241+ t.Error("Closed() = false after Close")
242+ }
243+}
244+
245+// A closed display rejects further dispatch instead of reading from a closed
246+// descriptor.
247+func TestDisplayDispatchAfterCloseReturnsErrClosed(t *testing.T) {
248+ d := newDisplay(&chunkConn{chunks: [][]byte{message(7, 3, nil)}})
249+
250+ if err := d.Close(); err != nil {
251+ t.Fatalf("Close: %v", err)
252+ }
253+ if err := d.Dispatch(); !errors.Is(err, net.ErrClosed) {
254+ t.Fatalf("Dispatch after Close = %v, want %v", err, net.ErrClosed)
255+ }
256+}
257+
258+// Binding registers the proxy for dispatch, and unregistering removes it.
259+func TestRegistryBindRegistration(t *testing.T) {
260+ d := newDisplay(&chunkConn{})
261+ records := []EventRecord{}
262+ proxy := &recordingProxy{BaseProxy: BaseProxy{id: 7, context: d.Context()}, records: &records}
263+
264+ if err := d.Registry().Bind(1, "wl_compositor", 4, proxy); err != nil {
265+ t.Fatalf("Bind: %v", err)
266+ }
267+ if _, ok := d.objects.Load(uint32(7)); !ok {
268+ t.Fatal("bound proxy is not registered for dispatch")
269+ }
270+
271+ d.Context().Unregister(proxy)
272+ if _, ok := d.objects.Load(uint32(7)); ok {
273+ t.Fatal("unregistered proxy is still registered for dispatch")
274+ }
275+}
276+
227277 func TestAllocateID(t *testing.T) {
228278 d := &Display{
229279 nextID: 2, // Start at 2 (1 is reserved for display)
@@ -3,6 +3,8 @@ package wlturbo
3 import (3 import (
4 "bytes"4 "bytes"
5 "encoding/binary"5 "encoding/binary"
6+ "errors"
7+ "net"
6 "testing"8 "testing"
7 )9 )
8 10
@@ -47,7 +49,7 @@ func TestEventPool(t *testing.T) {
47 }49 }
48 50
49 // Set some data51 // Set some data
50- event.ProxyId = 12352+ event.ProxyID = 123
51 event.Opcode = 45653 event.Opcode = 456
52 54
53 // Return to pool55 // Return to pool
@@ -60,7 +62,7 @@ func TestEventPool(t *testing.T) {
60 }62 }
61 63
62 // The pool might return a clean event or the same one, both are valid64 // The pool might return a clean event or the same one, both are valid
63- t.Logf("Event2: ProxyId=%d, Opcode=%d", event2.ProxyId, event2.Opcode)65+ t.Logf("Event2: ProxyID=%d, Opcode=%d", event2.ProxyID, event2.Opcode)
64 66
65 // Clean up67 // Clean up
66 eventPool.Put(event2)68 eventPool.Put(event2)
@@ -72,8 +74,8 @@ func TestEventDispatcher(t *testing.T) {
72 called := false74 called := false
73 handler := func(event *Event) {75 handler := func(event *Event) {
74 called = true76 called = true
75- if event.ProxyId != 123 || event.Opcode != 1 {77+ if event.ProxyID != 123 || event.Opcode != 1 {
76- t.Errorf("Expected ProxyId=123, Opcode=1, got ProxyId=%d, Opcode=%d", event.ProxyId, event.Opcode)78+ t.Errorf("Expected ProxyID=123, Opcode=1, got ProxyID=%d, Opcode=%d", event.ProxyID, event.Opcode)
77 }79 }
78 }80 }
79 81
@@ -224,6 +226,54 @@ func TestMessageHeaderParsing(t *testing.T) {
224 }226 }
225 }227 }
226 228
229+// Close must be safe to call repeatedly and must not report an error from the
230+// second call.
231+func TestDisplayCloseIsIdempotent(t *testing.T) {
232+ d := newDisplay(&chunkConn{})
233+
234+ if err := d.Close(); err != nil {
235+ t.Fatalf("first Close: %v", err)
236+ }
237+ if err := d.Close(); err != nil {
238+ t.Fatalf("second Close: %v", err)
239+ }
240+ if !d.Closed() {
241+ t.Error("Closed() = false after Close")
242+ }
243+}
244+
245+// A closed display rejects further dispatch instead of reading from a closed
246+// descriptor.
247+func TestDisplayDispatchAfterCloseReturnsErrClosed(t *testing.T) {
248+ d := newDisplay(&chunkConn{chunks: [][]byte{message(7, 3, nil)}})
249+
250+ if err := d.Close(); err != nil {
251+ t.Fatalf("Close: %v", err)
252+ }
253+ if err := d.Dispatch(); !errors.Is(err, net.ErrClosed) {
254+ t.Fatalf("Dispatch after Close = %v, want %v", err, net.ErrClosed)
255+ }
256+}
257+
258+// Binding registers the proxy for dispatch, and unregistering removes it.
259+func TestRegistryBindRegistration(t *testing.T) {
260+ d := newDisplay(&chunkConn{})
261+ records := []EventRecord{}
262+ proxy := &recordingProxy{BaseProxy: BaseProxy{id: 7, context: d.Context()}, records: &records}
263+
264+ if err := d.Registry().Bind(1, "wl_compositor", 4, proxy); err != nil {
265+ t.Fatalf("Bind: %v", err)
266+ }
267+ if _, ok := d.objects.Load(uint32(7)); !ok {
268+ t.Fatal("bound proxy is not registered for dispatch")
269+ }
270+
271+ d.Context().Unregister(proxy)
272+ if _, ok := d.objects.Load(uint32(7)); ok {
273+ t.Fatal("unregistered proxy is still registered for dispatch")
274+ }
275+}
276+
227 func TestAllocateID(t *testing.T) {277 func TestAllocateID(t *testing.T) {
228 d := &Display{278 d := &Display{
229 nextID: 2, // Start at 2 (1 is reserved for display)279 nextID: 2, // Start at 2 (1 is reserved for display)
M core.go +1 -0
@@ -36,6 +36,7 @@ type Event struct {
3636 Opcode uint16
3737 data []byte
3838 offset int
39+ display *Display
3940 }
4041
4142 // Data returns the raw event data
@@ -36,6 +36,7 @@ type Event struct {
36 Opcode uint1636 Opcode uint16
37 data []byte37 data []byte
38 offset int38 offset int
39+ display *Display
39 }40 }
40 41
41 // Data returns the raw event data42 // Data returns the raw event data
M scm_linux.go +59 -51
@@ -5,8 +5,8 @@ package wlturbo
55
66 import (
77 "fmt"
8- "net"
98 "golang.org/x/sys/unix"
9+ "net"
1010 "sync"
1111 "sync/atomic"
1212 "syscall"
@@ -27,14 +27,14 @@ type fdItem struct {
2727 var (
2828 // Global FD queue for zero-allocation FD passing
2929 globalFDQueue = &fdQueue{}
30-
30+
3131 // Pre-allocated FD items pool
3232 fdItemPool = sync.Pool{
3333 New: func() interface{} {
3434 return &fdItem{}
3535 },
3636 }
37-
37+
3838 // Pre-allocated buffers for control messages
3939 controlBufferPool = sync.Pool{
4040 New: func() interface{} {
@@ -48,11 +48,11 @@ func (q *fdQueue) enqueueFD(fd int) {
4848 item := fdItemPool.Get().(*fdItem)
4949 item.fd = fd
5050 item.next.Store(nil)
51-
51+
5252 for {
5353 tail := q.tail.Load()
5454 next := tail & 255
55-
55+
5656 if q.items[next].CompareAndSwap(nil, item) {
5757 q.tail.Add(1)
5858 return
@@ -66,18 +66,18 @@ func (q *fdQueue) dequeueFD() (int, bool) {
6666 for {
6767 head := q.head.Load()
6868 tail := q.tail.Load()
69-
69+
7070 if head >= tail {
7171 return -1, false
7272 }
73-
73+
7474 next := head & 255
7575 item := q.items[next].Load()
76-
76+
7777 if item == nil {
7878 continue
7979 }
80-
80+
8181 if q.head.CompareAndSwap(head, head+1) {
8282 fd := item.fd
8383 q.items[next].Store(nil)
@@ -87,60 +87,68 @@ func (q *fdQueue) dequeueFD() (int, bool) {
8787 }
8888 }
8989
90-// recvmsgWithFDs receives a message potentially containing file descriptors
91-func (d *Display) recvmsgWithFDs(buf []byte) (n int, fds []int, err error) {
92- // Get control buffer from pool
90+// readChunk reads one chunk of bytes from the display connection, collecting
91+// any ancillary file descriptors that arrive with it. A single read never
92+// defines a message boundary; callers frame the returned bytes themselves.
93+func (d *Display) readChunk() (int, error) {
94+ if d.unix == nil {
95+ n, err := d.conn.Read(d.recvBuf)
96+ if n < 0 {
97+ n = 0
98+ }
99+ return n, err
100+ }
101+
93102 oob := controlBufferPool.Get().([]byte)
94103 defer controlBufferPool.Put(oob)
95-
96- // Receive message with control data
97- n, oobn, _, _, err := d.conn.(*net.UnixConn).ReadMsgUnix(buf, oob)
98- if err != nil {
99- return 0, nil, err
104+
105+ n, oobn, _, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob)
106+ if n < 0 {
107+ n = 0
100108 }
101-
102- // Parse control messages if any
103109 if oobn > 0 {
104- scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
110+ if perr := globalFDQueue.parseControl(oob[:oobn]); perr != nil {
111+ return n, perr
112+ }
113+ }
114+ return n, err
115+}
116+
117+// parseControl queues the file descriptors carried by socket control messages.
118+func (q *fdQueue) parseControl(oob []byte) error {
119+ scms, err := syscall.ParseSocketControlMessage(oob)
120+ if err != nil {
121+ return fmt.Errorf("parse control message: %w", err)
122+ }
123+ for _, scm := range scms {
124+ if scm.Header.Type != syscall.SCM_RIGHTS {
125+ continue
126+ }
127+ parsedFDs, err := syscall.ParseUnixRights(&scm)
105128 if err != nil {
106- return n, nil, fmt.Errorf("parse control message: %w", err)
129+ return fmt.Errorf("parse unix rights: %w", err)
107130 }
108-
109- for _, scm := range scms {
110- if scm.Header.Type == syscall.SCM_RIGHTS {
111- // Parse unix rights (file descriptors)
112- parsedFDs, err := syscall.ParseUnixRights(&scm)
113- if err != nil {
114- return n, nil, fmt.Errorf("parse unix rights: %w", err)
115- }
116-
117- // Add FDs to our lock-free queue
118- for _, fd := range parsedFDs {
119- globalFDQueue.enqueueFD(fd)
120- }
121-
122- fds = append(fds, parsedFDs...)
123- }
131+ for _, fd := range parsedFDs {
132+ q.enqueueFD(fd)
124133 }
125134 }
126-
127- return n, fds, nil
135+ return nil
128136 }
129137
130138 // sendmsgWithFDs sends a message potentially containing file descriptors
131139 func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {
132140 d.sendMu.Lock()
133141 defer d.sendMu.Unlock()
134-
142+
135143 if len(fds) == 0 {
136144 // Fast path: no FDs to send
137145 _, err := d.conn.Write(buf)
138146 return err
139147 }
140-
148+
141149 // Create control message with file descriptors
142150 oob := syscall.UnixRights(fds...)
143-
151+
144152 // Send message with control data
145153 _, _, err := d.conn.(*net.UnixConn).WriteMsgUnix(buf, oob, nil)
146154 return err
@@ -164,18 +172,18 @@ func CreateAnonymousFile(size int64) (fd int, err error) {
164172 _ = unix.Close(fd)
165173 return -1, err
166174 }
167-
175+
168176 // Add seals to prevent resizing
169- _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS,
177+ _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS,
170178 unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_SEAL)
171179 if err != nil {
172180 _ = unix.Close(fd)
173181 return -1, err
174182 }
175-
183+
176184 return fd, nil
177185 }
178-
186+
179187 // Fallback to O_TMPFILE if available
180188 fd, err = unix.Open("/dev/shm", unix.O_TMPFILE|unix.O_RDWR|unix.O_CLOEXEC, 0600)
181189 if err == nil {
@@ -186,24 +194,24 @@ func CreateAnonymousFile(size int64) (fd int, err error) {
186194 }
187195 return fd, nil
188196 }
189-
197+
190198 // Final fallback: create temp file and unlink
191199 name := fmt.Sprintf("/dev/shm/wlclient-%d", unix.Getpid())
192200 fd, err = unix.Open(name, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC, 0600)
193201 if err != nil {
194202 return -1, err
195203 }
196-
204+
197205 // Unlink immediately
198206 _ = unix.Unlink(name)
199-
207+
200208 // Set size
201209 err = unix.Ftruncate(fd, size)
202210 if err != nil {
203211 unix.Close(fd)
204212 return -1, err
205213 }
206-
214+
207215 return fd, nil
208216 }
209217
@@ -215,4 +223,4 @@ func MapMemory(fd int, size int) ([]byte, error) {
215223 // UnmapMemory unmaps memory
216224 func UnmapMemory(data []byte) error {
217225 return unix.Munmap(data)
218-}⏎̸
226+}
@@ -5,8 +5,8 @@ package wlturbo
5 5
6 import (6 import (
7 "fmt"7 "fmt"
8- "net"
9 "golang.org/x/sys/unix"8 "golang.org/x/sys/unix"
9+ "net"
10 "sync"10 "sync"
11 "sync/atomic"11 "sync/atomic"
12 "syscall"12 "syscall"
@@ -27,14 +27,14 @@ type fdItem struct {
27 var (27 var (
28 // Global FD queue for zero-allocation FD passing28 // Global FD queue for zero-allocation FD passing
29 globalFDQueue = &fdQueue{}29 globalFDQueue = &fdQueue{}
30- 30+
31 // Pre-allocated FD items pool31 // Pre-allocated FD items pool
32 fdItemPool = sync.Pool{32 fdItemPool = sync.Pool{
33 New: func() interface{} {33 New: func() interface{} {
34 return &fdItem{}34 return &fdItem{}
35 },35 },
36 }36 }
37- 37+
38 // Pre-allocated buffers for control messages38 // Pre-allocated buffers for control messages
39 controlBufferPool = sync.Pool{39 controlBufferPool = sync.Pool{
40 New: func() interface{} {40 New: func() interface{} {
@@ -48,11 +48,11 @@ func (q *fdQueue) enqueueFD(fd int) {
48 item := fdItemPool.Get().(*fdItem)48 item := fdItemPool.Get().(*fdItem)
49 item.fd = fd49 item.fd = fd
50 item.next.Store(nil)50 item.next.Store(nil)
51- 51+
52 for {52 for {
53 tail := q.tail.Load()53 tail := q.tail.Load()
54 next := tail & 25554 next := tail & 255
55- 55+
56 if q.items[next].CompareAndSwap(nil, item) {56 if q.items[next].CompareAndSwap(nil, item) {
57 q.tail.Add(1)57 q.tail.Add(1)
58 return58 return
@@ -66,18 +66,18 @@ func (q *fdQueue) dequeueFD() (int, bool) {
66 for {66 for {
67 head := q.head.Load()67 head := q.head.Load()
68 tail := q.tail.Load()68 tail := q.tail.Load()
69- 69+
70 if head >= tail {70 if head >= tail {
71 return -1, false71 return -1, false
72 }72 }
73- 73+
74 next := head & 25574 next := head & 255
75 item := q.items[next].Load()75 item := q.items[next].Load()
76- 76+
77 if item == nil {77 if item == nil {
78 continue78 continue
79 }79 }
80- 80+
81 if q.head.CompareAndSwap(head, head+1) {81 if q.head.CompareAndSwap(head, head+1) {
82 fd := item.fd82 fd := item.fd
83 q.items[next].Store(nil)83 q.items[next].Store(nil)
@@ -87,60 +87,68 @@ func (q *fdQueue) dequeueFD() (int, bool) {
87 }87 }
88 }88 }
89 89
90-// recvmsgWithFDs receives a message potentially containing file descriptors90+// readChunk reads one chunk of bytes from the display connection, collecting
91-func (d *Display) recvmsgWithFDs(buf []byte) (n int, fds []int, err error) {91+// any ancillary file descriptors that arrive with it. A single read never
92- // Get control buffer from pool92+// defines a message boundary; callers frame the returned bytes themselves.
93+func (d *Display) readChunk() (int, error) {
94+ if d.unix == nil {
95+ n, err := d.conn.Read(d.recvBuf)
96+ if n < 0 {
97+ n = 0
98+ }
99+ return n, err
100+ }
101+
93 oob := controlBufferPool.Get().([]byte)102 oob := controlBufferPool.Get().([]byte)
94 defer controlBufferPool.Put(oob)103 defer controlBufferPool.Put(oob)
95- 104+
96- // Receive message with control data105+ n, oobn, _, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob)
97- n, oobn, _, _, err := d.conn.(*net.UnixConn).ReadMsgUnix(buf, oob)106+ if n < 0 {
98- if err != nil {107+ n = 0
99- return 0, nil, err
100 }108 }
101-
102- // Parse control messages if any
103 if oobn > 0 {109 if oobn > 0 {
104- scms, err := syscall.ParseSocketControlMessage(oob[:oobn])110+ if perr := globalFDQueue.parseControl(oob[:oobn]); perr != nil {
111+ return n, perr
112+ }
113+ }
114+ return n, err
115+}
116+
117+// parseControl queues the file descriptors carried by socket control messages.
118+func (q *fdQueue) parseControl(oob []byte) error {
119+ scms, err := syscall.ParseSocketControlMessage(oob)
120+ if err != nil {
121+ return fmt.Errorf("parse control message: %w", err)
122+ }
123+ for _, scm := range scms {
124+ if scm.Header.Type != syscall.SCM_RIGHTS {
125+ continue
126+ }
127+ parsedFDs, err := syscall.ParseUnixRights(&scm)
105 if err != nil {128 if err != nil {
106- return n, nil, fmt.Errorf("parse control message: %w", err)129+ return fmt.Errorf("parse unix rights: %w", err)
107 }130 }
108- 131+ for _, fd := range parsedFDs {
109- for _, scm := range scms {132+ q.enqueueFD(fd)
110- if scm.Header.Type == syscall.SCM_RIGHTS {
111- // Parse unix rights (file descriptors)
112- parsedFDs, err := syscall.ParseUnixRights(&scm)
113- if err != nil {
114- return n, nil, fmt.Errorf("parse unix rights: %w", err)
115- }
116-
117- // Add FDs to our lock-free queue
118- for _, fd := range parsedFDs {
119- globalFDQueue.enqueueFD(fd)
120- }
121-
122- fds = append(fds, parsedFDs...)
123- }
124 }133 }
125 }134 }
126- 135+ return nil
127- return n, fds, nil
128 }136 }
129 137
130 // sendmsgWithFDs sends a message potentially containing file descriptors138 // sendmsgWithFDs sends a message potentially containing file descriptors
131 func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {139 func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {
132 d.sendMu.Lock()140 d.sendMu.Lock()
133 defer d.sendMu.Unlock()141 defer d.sendMu.Unlock()
134- 142+
135 if len(fds) == 0 {143 if len(fds) == 0 {
136 // Fast path: no FDs to send144 // Fast path: no FDs to send
137 _, err := d.conn.Write(buf)145 _, err := d.conn.Write(buf)
138 return err146 return err
139 }147 }
140- 148+
141 // Create control message with file descriptors149 // Create control message with file descriptors
142 oob := syscall.UnixRights(fds...)150 oob := syscall.UnixRights(fds...)
143- 151+
144 // Send message with control data152 // Send message with control data
145 _, _, err := d.conn.(*net.UnixConn).WriteMsgUnix(buf, oob, nil)153 _, _, err := d.conn.(*net.UnixConn).WriteMsgUnix(buf, oob, nil)
146 return err154 return err
@@ -164,18 +172,18 @@ func CreateAnonymousFile(size int64) (fd int, err error) {
164 _ = unix.Close(fd)172 _ = unix.Close(fd)
165 return -1, err173 return -1, err
166 }174 }
167- 175+
168 // Add seals to prevent resizing176 // Add seals to prevent resizing
169- _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, 177+ _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS,
170 unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_SEAL)178 unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_SEAL)
171 if err != nil {179 if err != nil {
172 _ = unix.Close(fd)180 _ = unix.Close(fd)
173 return -1, err181 return -1, err
174 }182 }
175- 183+
176 return fd, nil184 return fd, nil
177 }185 }
178- 186+
179 // Fallback to O_TMPFILE if available187 // Fallback to O_TMPFILE if available
180 fd, err = unix.Open("/dev/shm", unix.O_TMPFILE|unix.O_RDWR|unix.O_CLOEXEC, 0600)188 fd, err = unix.Open("/dev/shm", unix.O_TMPFILE|unix.O_RDWR|unix.O_CLOEXEC, 0600)
181 if err == nil {189 if err == nil {
@@ -186,24 +194,24 @@ func CreateAnonymousFile(size int64) (fd int, err error) {
186 }194 }
187 return fd, nil195 return fd, nil
188 }196 }
189- 197+
190 // Final fallback: create temp file and unlink198 // Final fallback: create temp file and unlink
191 name := fmt.Sprintf("/dev/shm/wlclient-%d", unix.Getpid())199 name := fmt.Sprintf("/dev/shm/wlclient-%d", unix.Getpid())
192 fd, err = unix.Open(name, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC, 0600)200 fd, err = unix.Open(name, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC, 0600)
193 if err != nil {201 if err != nil {
194 return -1, err202 return -1, err
195 }203 }
196- 204+
197 // Unlink immediately205 // Unlink immediately
198 _ = unix.Unlink(name)206 _ = unix.Unlink(name)
199- 207+
200 // Set size208 // Set size
201 err = unix.Ftruncate(fd, size)209 err = unix.Ftruncate(fd, size)
202 if err != nil {210 if err != nil {
203 unix.Close(fd)211 unix.Close(fd)
204 return -1, err212 return -1, err
205 }213 }
206- 214+
207 return fd, nil215 return fd, nil
208 }216 }
209 217
@@ -215,4 +223,4 @@ func MapMemory(fd int, size int) ([]byte, error) {
215 // UnmapMemory unmaps memory223 // UnmapMemory unmaps memory
216 func UnmapMemory(data []byte) error {224 func UnmapMemory(data []byte) error {
217 return unix.Munmap(data)225 return unix.Munmap(data)
218-}⏎̸226+}
A transport_test.go +352 -0
@@ -0,0 +1,352 @@
1+package wlturbo
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "io"
7+ "net"
8+ "reflect"
9+ "testing"
10+ "time"
11+)
12+
13+// EventRecord is what a test observes for one dispatched event.
14+type EventRecord struct {
15+ Object uint32
16+ Opcode uint16
17+ Body []byte
18+}
19+
20+// recordingProxy records every dispatched event. It is a stand-in for a real
21+// protocol proxy so framing tests need no compositor.
22+type recordingProxy struct {
23+ BaseProxy
24+ records *[]EventRecord
25+}
26+
27+func (p *recordingProxy) Dispatch(event *Event) {
28+ *p.records = append(*p.records, EventRecord{
29+ Object: event.ProxyID,
30+ Opcode: event.Opcode,
31+ Body: append([]byte(nil), event.Data()...),
32+ })
33+}
34+
35+// chunkConn returns one chunk per Read, so a test controls exactly how bytes
36+// are split across read boundaries.
37+type chunkConn struct {
38+ chunks [][]byte
39+ idx int
40+}
41+
42+func (c *chunkConn) Read(p []byte) (int, error) {
43+ if c.idx >= len(c.chunks) {
44+ return 0, io.EOF
45+ }
46+ chunk := c.chunks[c.idx]
47+ n := copy(p, chunk)
48+ switch {
49+ case n == len(chunk):
50+ c.idx++
51+ case n < len(chunk):
52+ // Caller's buffer was smaller than the chunk: keep the remainder.
53+ c.chunks[c.idx] = chunk[n:]
54+ }
55+ return n, nil
56+}
57+
58+func (c *chunkConn) Write(p []byte) (int, error) { return len(p), nil }
59+func (c *chunkConn) Close() error { return nil }
60+func (c *chunkConn) LocalAddr() net.Addr { return testAddr{} }
61+func (c *chunkConn) RemoteAddr() net.Addr { return testAddr{} }
62+func (c *chunkConn) SetDeadline(time.Time) error { return nil }
63+func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
64+func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
65+
66+type testAddr struct{}
67+
68+func (testAddr) Network() string { return "test" }
69+func (testAddr) String() string { return "test" }
70+
71+// header builds a Wayland message header for the given object, opcode and
72+// total message size.
73+func header(object uint32, opcode uint16, size uint32) []byte {
74+ buf := make([]byte, HeaderSize)
75+ binary.LittleEndian.PutUint32(buf[0:4], object)
76+ binary.LittleEndian.PutUint32(buf[4:8], (size<<16)|uint32(opcode))
77+ return buf
78+}
79+
80+// message builds one complete 4-byte-aligned Wayland message.
81+func message(object uint32, opcode uint16, body []byte) []byte {
82+ msg := header(object, opcode, uint32(HeaderSize+len(body)))
83+ return append(msg, body...)
84+}
85+
86+// newChunkDisplay builds a Display over a connection that yields the given
87+// chunks, with a recording proxy registered at object ID 7.
88+func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) {
89+ d := newDisplay(&chunkConn{chunks: chunks})
90+ records := []EventRecord{}
91+ d.objects.Store(uint32(7), &recordingProxy{
92+ BaseProxy: BaseProxy{id: 7, context: d.context},
93+ records: &records,
94+ })
95+ return d, &records
96+}
97+
98+// dispatchUntilError dispatches at most max frames and returns the first error.
99+func dispatchUntilError(d *Display, max int) error {
100+ for i := 0; i < max; i++ {
101+ if err := d.Dispatch(); err != nil {
102+ return err
103+ }
104+ }
105+ return nil
106+}
107+
108+func TestDisplayDispatch_Fragmented(t *testing.T) {
109+ body := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
110+ msg := message(7, 3, body)
111+
112+ // Split the header across three reads and the body byte by byte.
113+ chunks := [][]byte{
114+ msg[0:3],
115+ msg[3:5],
116+ msg[5:HeaderSize],
117+ }
118+ for i := HeaderSize; i < len(msg); i++ {
119+ chunks = append(chunks, msg[i:i+1])
120+ }
121+
122+ d, records := newChunkDisplay(chunks)
123+ err := dispatchUntilError(d, 2)
124+ if !errors.Is(err, io.ErrUnexpectedEOF) {
125+ t.Fatalf("Dispatch after the message = %v, want %v", err, io.ErrUnexpectedEOF)
126+ }
127+
128+ want := []EventRecord{{Object: 7, Opcode: 3, Body: body}}
129+ if !reflect.DeepEqual(*records, want) {
130+ t.Fatalf("records = %#v, want %#v", *records, want)
131+ }
132+}
133+
134+func TestDisplayDispatch_Coalesced(t *testing.T) {
135+ first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14})
136+ second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
137+
138+ d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
139+
140+ // Two messages arrive in a single read; one Dispatch per message.
141+ if err := d.Dispatch(); err != nil {
142+ t.Fatalf("first Dispatch: %v", err)
143+ }
144+ if err := d.Dispatch(); err != nil {
145+ t.Fatalf("second Dispatch: %v", err)
146+ }
147+
148+ want := []EventRecord{
149+ {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}},
150+ {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
151+ }
152+ if !reflect.DeepEqual(*records, want) {
153+ t.Fatalf("records = %#v, want %#v", *records, want)
154+ }
155+}
156+
157+func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
158+ // A read boundary that ends between the header and the body of the second
159+ // message must not be mistaken for a frame boundary.
160+ whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}),
161+ message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
162+
163+ split := HeaderSize + 4 + 4 // mid-body of the second message
164+ d, records := newChunkDisplay([][]byte{whole[:split], whole[split:]})
165+
166+ if err := d.Dispatch(); err != nil {
167+ t.Fatalf("first Dispatch: %v", err)
168+ }
169+ if err := d.Dispatch(); err != nil {
170+ t.Fatalf("second Dispatch: %v", err)
171+ }
172+
173+ want := []EventRecord{
174+ {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}},
175+ {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
176+ }
177+ if !reflect.DeepEqual(*records, want) {
178+ t.Fatalf("records = %#v, want %#v", *records, want)
179+ }
180+}
181+
182+func TestDisplayDispatch_ProtocolError(t *testing.T) {
183+ cases := []struct {
184+ name string
185+ chunks [][]byte
186+ maxLen uint32
187+ wantErr error
188+ wantKind string
189+ }{
190+ {
191+ name: "size smaller than header",
192+ chunks: [][]byte{header(7, 3, 0)},
193+ wantErr: ErrMalformedFrame,
194+ wantKind: "header_size",
195+ },
196+ {
197+ name: "misaligned size",
198+ chunks: [][]byte{header(7, 3, 10)},
199+ wantErr: ErrMalformedFrame,
200+ wantKind: "misaligned_size",
201+ },
202+ {
203+ name: "size over configured maximum",
204+ chunks: [][]byte{message(7, 3, make([]byte, 32))},
205+ maxLen: 16,
206+ wantErr: ErrMalformedFrame,
207+ wantKind: "size_over_limit",
208+ },
209+ {
210+ name: "unknown object",
211+ chunks: [][]byte{message(9, 1, []byte{0x01, 0x02, 0x03, 0x04})},
212+ wantErr: ErrUnknownObject,
213+ wantKind: "unknown_object",
214+ },
215+ {
216+ name: "unknown opcode on display",
217+ chunks: [][]byte{message(displayObjectID, 9, nil)},
218+ wantErr: ErrUnknownOpcode,
219+ wantKind: "unknown_opcode",
220+ },
221+ }
222+
223+ for _, tc := range cases {
224+ t.Run(tc.name, func(t *testing.T) {
225+ d, records := newChunkDisplay(tc.chunks)
226+ if tc.maxLen != 0 {
227+ d.maxMsgLen = tc.maxLen
228+ }
229+
230+ err := d.Dispatch()
231+ if !errors.Is(err, tc.wantErr) {
232+ t.Fatalf("Dispatch error = %v, want %v", err, tc.wantErr)
233+ }
234+
235+ var perr *ProtocolError
236+ if !errors.As(err, &perr) {
237+ t.Fatalf("Dispatch error type = %T, want *ProtocolError", err)
238+ }
239+ if perr.Kind != tc.wantKind {
240+ t.Fatalf("error kind = %q, want %q", perr.Kind, tc.wantKind)
241+ }
242+ if len(*records) != 0 {
243+ t.Fatalf("records = %#v, want none", *records)
244+ }
245+ })
246+ }
247+}
248+
249+func TestDisplayDispatch_DisplayError(t *testing.T) {
250+ // wl_display.error: object id, code, message (padded to 4 bytes).
251+ msg := []byte("boom")
252+ body := make([]byte, 0, 8+4+8)
253+ body = binary.LittleEndian.AppendUint32(body, 7)
254+ body = binary.LittleEndian.AppendUint32(body, 3)
255+ body = binary.LittleEndian.AppendUint32(body, uint32(len(msg)+1))
256+ body = append(body, msg...)
257+ body = append(body, 0, 0, 0, 0)
258+
259+ d, _ := newChunkDisplay([][]byte{message(displayObjectID, 0, body)})
260+
261+ err := d.Dispatch()
262+ if !errors.Is(err, ErrDisplayError) {
263+ t.Fatalf("Dispatch error = %v, want %v", err, ErrDisplayError)
264+ }
265+
266+ var derr *DisplayError
267+ if !errors.As(err, &derr) {
268+ t.Fatalf("Dispatch error type = %T, want *DisplayError", err)
269+ }
270+ if derr.ObjectID != 7 || derr.Code != 3 || derr.Message != "boom" {
271+ t.Fatalf("DisplayError = %+v, want object=7 code=3 message=boom", derr)
272+ }
273+}
274+
275+func TestDisplayDispatch_DeleteID(t *testing.T) {
276+ d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})
277+
278+ if err := d.Dispatch(); err != nil {
279+ t.Fatalf("Dispatch: %v", err)
280+ }
281+ if _, ok := d.objects.Load(uint32(7)); ok {
282+ t.Fatal("delete_id did not remove object 7")
283+ }
284+ if len(*records) != 0 {
285+ t.Fatalf("records = %#v, want none", *records)
286+ }
287+}
288+
289+func TestDisplayDispatch_PeerClose(t *testing.T) {
290+ cases := []struct {
291+ name string
292+ chunks [][]byte
293+ }{
294+ {name: "closed before any byte"},
295+ {name: "closed mid header", chunks: [][]byte{{0x01, 0x02, 0x03}}},
296+ {
297+ name: "closed mid body",
298+ chunks: [][]byte{append(header(7, 3, 16), 0x01, 0x02, 0x03, 0x04)},
299+ },
300+ {
301+ name: "closed after a complete message",
302+ chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})},
303+ },
304+ }
305+
306+ for _, tc := range cases {
307+ t.Run(tc.name, func(t *testing.T) {
308+ d, _ := newChunkDisplay(tc.chunks)
309+
310+ var err error
311+ for i := 0; i < 4; i++ {
312+ if err = d.Dispatch(); err != nil {
313+ break
314+ }
315+ }
316+ if !errors.Is(err, io.ErrUnexpectedEOF) {
317+ t.Fatalf("Dispatch error = %v, want %v", err, io.ErrUnexpectedEOF)
318+ }
319+ })
320+ }
321+}
322+
323+func TestDisplayDispatch_ClosedReturnsErrClosed(t *testing.T) {
324+ d, _ := newChunkDisplay([][]byte{message(7, 3, nil)})
325+ if err := d.Close(); err != nil {
326+ t.Fatalf("Close: %v", err)
327+ }
328+ if err := d.Dispatch(); !errors.Is(err, net.ErrClosed) {
329+ t.Fatalf("Dispatch after Close = %v, want %v", err, net.ErrClosed)
330+ }
331+}
332+
333+func TestReadFrame(t *testing.T) {
334+ body := []byte{0x0a, 0x0b, 0x0c, 0x0d}
335+ object, opcode, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, body)}}, DefaultMaxMessageSize)
336+ if err != nil {
337+ t.Fatalf("readFrame: %v", err)
338+ }
339+ if object != 7 || opcode != 3 || !reflect.DeepEqual(got, body) {
340+ t.Fatalf("readFrame = (%d, %d, %x), want (7, 3, %x)", object, opcode, got, body)
341+ }
342+
343+ // A header-only message has an empty body.
344+ if _, _, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, nil)}}, 0); err != nil || len(got) != 0 {
345+ t.Fatalf("readFrame(header only) = (%x, %v), want (empty, nil)", got, err)
346+ }
347+
348+ // Truncated bodies are reported, never returned partially.
349+ if _, _, _, err := readFrame(&chunkConn{chunks: [][]byte{append(header(7, 3, 16), 0x01)}}, 0); !errors.Is(err, io.ErrUnexpectedEOF) {
350+ t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)
351+ }
352+}
@@ -0,0 +1,352 @@
1+package wlturbo
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "io"
7+ "net"
8+ "reflect"
9+ "testing"
10+ "time"
11+)
12+
13+// EventRecord is what a test observes for one dispatched event.
14+type EventRecord struct {
15+ Object uint32
16+ Opcode uint16
17+ Body []byte
18+}
19+
20+// recordingProxy records every dispatched event. It is a stand-in for a real
21+// protocol proxy so framing tests need no compositor.
22+type recordingProxy struct {
23+ BaseProxy
24+ records *[]EventRecord
25+}
26+
27+func (p *recordingProxy) Dispatch(event *Event) {
28+ *p.records = append(*p.records, EventRecord{
29+ Object: event.ProxyID,
30+ Opcode: event.Opcode,
31+ Body: append([]byte(nil), event.Data()...),
32+ })
33+}
34+
35+// chunkConn returns one chunk per Read, so a test controls exactly how bytes
36+// are split across read boundaries.
37+type chunkConn struct {
38+ chunks [][]byte
39+ idx int
40+}
41+
42+func (c *chunkConn) Read(p []byte) (int, error) {
43+ if c.idx >= len(c.chunks) {
44+ return 0, io.EOF
45+ }
46+ chunk := c.chunks[c.idx]
47+ n := copy(p, chunk)
48+ switch {
49+ case n == len(chunk):
50+ c.idx++
51+ case n < len(chunk):
52+ // Caller's buffer was smaller than the chunk: keep the remainder.
53+ c.chunks[c.idx] = chunk[n:]
54+ }
55+ return n, nil
56+}
57+
58+func (c *chunkConn) Write(p []byte) (int, error) { return len(p), nil }
59+func (c *chunkConn) Close() error { return nil }
60+func (c *chunkConn) LocalAddr() net.Addr { return testAddr{} }
61+func (c *chunkConn) RemoteAddr() net.Addr { return testAddr{} }
62+func (c *chunkConn) SetDeadline(time.Time) error { return nil }
63+func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
64+func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
65+
66+type testAddr struct{}
67+
68+func (testAddr) Network() string { return "test" }
69+func (testAddr) String() string { return "test" }
70+
71+// header builds a Wayland message header for the given object, opcode and
72+// total message size.
73+func header(object uint32, opcode uint16, size uint32) []byte {
74+ buf := make([]byte, HeaderSize)
75+ binary.LittleEndian.PutUint32(buf[0:4], object)
76+ binary.LittleEndian.PutUint32(buf[4:8], (size<<16)|uint32(opcode))
77+ return buf
78+}
79+
80+// message builds one complete 4-byte-aligned Wayland message.
81+func message(object uint32, opcode uint16, body []byte) []byte {
82+ msg := header(object, opcode, uint32(HeaderSize+len(body)))
83+ return append(msg, body...)
84+}
85+
86+// newChunkDisplay builds a Display over a connection that yields the given
87+// chunks, with a recording proxy registered at object ID 7.
88+func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) {
89+ d := newDisplay(&chunkConn{chunks: chunks})
90+ records := []EventRecord{}
91+ d.objects.Store(uint32(7), &recordingProxy{
92+ BaseProxy: BaseProxy{id: 7, context: d.context},
93+ records: &records,
94+ })
95+ return d, &records
96+}
97+
98+// dispatchUntilError dispatches at most max frames and returns the first error.
99+func dispatchUntilError(d *Display, max int) error {
100+ for i := 0; i < max; i++ {
101+ if err := d.Dispatch(); err != nil {
102+ return err
103+ }
104+ }
105+ return nil
106+}
107+
108+func TestDisplayDispatch_Fragmented(t *testing.T) {
109+ body := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
110+ msg := message(7, 3, body)
111+
112+ // Split the header across three reads and the body byte by byte.
113+ chunks := [][]byte{
114+ msg[0:3],
115+ msg[3:5],
116+ msg[5:HeaderSize],
117+ }
118+ for i := HeaderSize; i < len(msg); i++ {
119+ chunks = append(chunks, msg[i:i+1])
120+ }
121+
122+ d, records := newChunkDisplay(chunks)
123+ err := dispatchUntilError(d, 2)
124+ if !errors.Is(err, io.ErrUnexpectedEOF) {
125+ t.Fatalf("Dispatch after the message = %v, want %v", err, io.ErrUnexpectedEOF)
126+ }
127+
128+ want := []EventRecord{{Object: 7, Opcode: 3, Body: body}}
129+ if !reflect.DeepEqual(*records, want) {
130+ t.Fatalf("records = %#v, want %#v", *records, want)
131+ }
132+}
133+
134+func TestDisplayDispatch_Coalesced(t *testing.T) {
135+ first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14})
136+ second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
137+
138+ d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
139+
140+ // Two messages arrive in a single read; one Dispatch per message.
141+ if err := d.Dispatch(); err != nil {
142+ t.Fatalf("first Dispatch: %v", err)
143+ }
144+ if err := d.Dispatch(); err != nil {
145+ t.Fatalf("second Dispatch: %v", err)
146+ }
147+
148+ want := []EventRecord{
149+ {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}},
150+ {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
151+ }
152+ if !reflect.DeepEqual(*records, want) {
153+ t.Fatalf("records = %#v, want %#v", *records, want)
154+ }
155+}
156+
157+func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
158+ // A read boundary that ends between the header and the body of the second
159+ // message must not be mistaken for a frame boundary.
160+ whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}),
161+ message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
162+
163+ split := HeaderSize + 4 + 4 // mid-body of the second message
164+ d, records := newChunkDisplay([][]byte{whole[:split], whole[split:]})
165+
166+ if err := d.Dispatch(); err != nil {
167+ t.Fatalf("first Dispatch: %v", err)
168+ }
169+ if err := d.Dispatch(); err != nil {
170+ t.Fatalf("second Dispatch: %v", err)
171+ }
172+
173+ want := []EventRecord{
174+ {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}},
175+ {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
176+ }
177+ if !reflect.DeepEqual(*records, want) {
178+ t.Fatalf("records = %#v, want %#v", *records, want)
179+ }
180+}
181+
182+func TestDisplayDispatch_ProtocolError(t *testing.T) {
183+ cases := []struct {
184+ name string
185+ chunks [][]byte
186+ maxLen uint32
187+ wantErr error
188+ wantKind string
189+ }{
190+ {
191+ name: "size smaller than header",
192+ chunks: [][]byte{header(7, 3, 0)},
193+ wantErr: ErrMalformedFrame,
194+ wantKind: "header_size",
195+ },
196+ {
197+ name: "misaligned size",
198+ chunks: [][]byte{header(7, 3, 10)},
199+ wantErr: ErrMalformedFrame,
200+ wantKind: "misaligned_size",
201+ },
202+ {
203+ name: "size over configured maximum",
204+ chunks: [][]byte{message(7, 3, make([]byte, 32))},
205+ maxLen: 16,
206+ wantErr: ErrMalformedFrame,
207+ wantKind: "size_over_limit",
208+ },
209+ {
210+ name: "unknown object",
211+ chunks: [][]byte{message(9, 1, []byte{0x01, 0x02, 0x03, 0x04})},
212+ wantErr: ErrUnknownObject,
213+ wantKind: "unknown_object",
214+ },
215+ {
216+ name: "unknown opcode on display",
217+ chunks: [][]byte{message(displayObjectID, 9, nil)},
218+ wantErr: ErrUnknownOpcode,
219+ wantKind: "unknown_opcode",
220+ },
221+ }
222+
223+ for _, tc := range cases {
224+ t.Run(tc.name, func(t *testing.T) {
225+ d, records := newChunkDisplay(tc.chunks)
226+ if tc.maxLen != 0 {
227+ d.maxMsgLen = tc.maxLen
228+ }
229+
230+ err := d.Dispatch()
231+ if !errors.Is(err, tc.wantErr) {
232+ t.Fatalf("Dispatch error = %v, want %v", err, tc.wantErr)
233+ }
234+
235+ var perr *ProtocolError
236+ if !errors.As(err, &perr) {
237+ t.Fatalf("Dispatch error type = %T, want *ProtocolError", err)
238+ }
239+ if perr.Kind != tc.wantKind {
240+ t.Fatalf("error kind = %q, want %q", perr.Kind, tc.wantKind)
241+ }
242+ if len(*records) != 0 {
243+ t.Fatalf("records = %#v, want none", *records)
244+ }
245+ })
246+ }
247+}
248+
249+func TestDisplayDispatch_DisplayError(t *testing.T) {
250+ // wl_display.error: object id, code, message (padded to 4 bytes).
251+ msg := []byte("boom")
252+ body := make([]byte, 0, 8+4+8)
253+ body = binary.LittleEndian.AppendUint32(body, 7)
254+ body = binary.LittleEndian.AppendUint32(body, 3)
255+ body = binary.LittleEndian.AppendUint32(body, uint32(len(msg)+1))
256+ body = append(body, msg...)
257+ body = append(body, 0, 0, 0, 0)
258+
259+ d, _ := newChunkDisplay([][]byte{message(displayObjectID, 0, body)})
260+
261+ err := d.Dispatch()
262+ if !errors.Is(err, ErrDisplayError) {
263+ t.Fatalf("Dispatch error = %v, want %v", err, ErrDisplayError)
264+ }
265+
266+ var derr *DisplayError
267+ if !errors.As(err, &derr) {
268+ t.Fatalf("Dispatch error type = %T, want *DisplayError", err)
269+ }
270+ if derr.ObjectID != 7 || derr.Code != 3 || derr.Message != "boom" {
271+ t.Fatalf("DisplayError = %+v, want object=7 code=3 message=boom", derr)
272+ }
273+}
274+
275+func TestDisplayDispatch_DeleteID(t *testing.T) {
276+ d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})
277+
278+ if err := d.Dispatch(); err != nil {
279+ t.Fatalf("Dispatch: %v", err)
280+ }
281+ if _, ok := d.objects.Load(uint32(7)); ok {
282+ t.Fatal("delete_id did not remove object 7")
283+ }
284+ if len(*records) != 0 {
285+ t.Fatalf("records = %#v, want none", *records)
286+ }
287+}
288+
289+func TestDisplayDispatch_PeerClose(t *testing.T) {
290+ cases := []struct {
291+ name string
292+ chunks [][]byte
293+ }{
294+ {name: "closed before any byte"},
295+ {name: "closed mid header", chunks: [][]byte{{0x01, 0x02, 0x03}}},
296+ {
297+ name: "closed mid body",
298+ chunks: [][]byte{append(header(7, 3, 16), 0x01, 0x02, 0x03, 0x04)},
299+ },
300+ {
301+ name: "closed after a complete message",
302+ chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})},
303+ },
304+ }
305+
306+ for _, tc := range cases {
307+ t.Run(tc.name, func(t *testing.T) {
308+ d, _ := newChunkDisplay(tc.chunks)
309+
310+ var err error
311+ for i := 0; i < 4; i++ {
312+ if err = d.Dispatch(); err != nil {
313+ break
314+ }
315+ }
316+ if !errors.Is(err, io.ErrUnexpectedEOF) {
317+ t.Fatalf("Dispatch error = %v, want %v", err, io.ErrUnexpectedEOF)
318+ }
319+ })
320+ }
321+}
322+
323+func TestDisplayDispatch_ClosedReturnsErrClosed(t *testing.T) {
324+ d, _ := newChunkDisplay([][]byte{message(7, 3, nil)})
325+ if err := d.Close(); err != nil {
326+ t.Fatalf("Close: %v", err)
327+ }
328+ if err := d.Dispatch(); !errors.Is(err, net.ErrClosed) {
329+ t.Fatalf("Dispatch after Close = %v, want %v", err, net.ErrClosed)
330+ }
331+}
332+
333+func TestReadFrame(t *testing.T) {
334+ body := []byte{0x0a, 0x0b, 0x0c, 0x0d}
335+ object, opcode, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, body)}}, DefaultMaxMessageSize)
336+ if err != nil {
337+ t.Fatalf("readFrame: %v", err)
338+ }
339+ if object != 7 || opcode != 3 || !reflect.DeepEqual(got, body) {
340+ t.Fatalf("readFrame = (%d, %d, %x), want (7, 3, %x)", object, opcode, got, body)
341+ }
342+
343+ // A header-only message has an empty body.
344+ if _, _, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, nil)}}, 0); err != nil || len(got) != 0 {
345+ t.Fatalf("readFrame(header only) = (%x, %v), want (empty, nil)", got, err)
346+ }
347+
348+ // Truncated bodies are reported, never returned partially.
349+ if _, _, _, err := readFrame(&chunkConn{chunks: [][]byte{append(header(7, 3, 16), 0x01)}}, 0); !errors.Is(err, io.ErrUnexpectedEOF) {
350+ t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)
351+ }
352+}
A wire.go +144 -0
@@ -0,0 +1,144 @@
1+package wlturbo
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "fmt"
7+ "io"
8+)
9+
10+const (
11+ // HeaderSize is the fixed size of a Wayland message header in bytes.
12+ HeaderSize = 8
13+
14+ // displayObjectID is the well-known object ID of wl_display.
15+ displayObjectID = 1
16+
17+ // DefaultMaxMessageSize bounds a single Wayland message, including its
18+ // header. Larger messages are rejected instead of allocating.
19+ DefaultMaxMessageSize = 1 << 20
20+
21+ // readChunkSize is the size of one read from the display socket.
22+ readChunkSize = 64 * 1024
23+)
24+
25+// Sentinel errors reported by the transport. Wire-level failures wrap one of
26+// them so callers can classify with errors.Is, while ProtocolError carries the
27+// offending object, opcode and size.
28+var (
29+ // ErrMalformedFrame reports a message whose header violates the wire
30+ // protocol: too short, misaligned, or larger than the accepted maximum.
31+ ErrMalformedFrame = errors.New("malformed wayland frame")
32+
33+ // ErrUnknownObject reports an event for an object this client never
34+ // created or bound.
35+ ErrUnknownObject = errors.New("wayland event for unknown object")
36+
37+ // ErrUnknownOpcode reports an event opcode the target object cannot receive.
38+ ErrUnknownOpcode = errors.New("wayland event with unknown opcode")
39+
40+ // ErrDisplayError reports that the compositor sent wl_display.error.
41+ ErrDisplayError = errors.New("wayland display error")
42+)
43+
44+// ProtocolError describes a wire-level protocol violation.
45+type ProtocolError struct {
46+ Kind string
47+ Object uint32
48+ Opcode uint16
49+ Size uint32
50+ Err error
51+}
52+
53+func (e *ProtocolError) Error() string {
54+ msg := e.Kind
55+ if e.Object != 0 {
56+ msg += fmt.Sprintf(" object=%d", e.Object)
57+ }
58+ if e.Kind == "unknown_opcode" || e.Kind == "display_error" {
59+ msg += fmt.Sprintf(" opcode=%d", e.Opcode)
60+ }
61+ if e.Size != 0 {
62+ msg += fmt.Sprintf(" size=%d", e.Size)
63+ }
64+ if e.Err != nil {
65+ return fmt.Sprintf("wlturbo: %s: %v", msg, e.Err)
66+ }
67+ return "wlturbo: " + msg
68+}
69+
70+// Unwrap returns the sentinel error classifying this violation.
71+func (e *ProtocolError) Unwrap() error { return e.Err }
72+
73+// DisplayError is returned when the compositor reports wl_display.error.
74+type DisplayError struct {
75+ ObjectID uint32
76+ Code uint32
77+ Message string
78+}
79+
80+func (e *DisplayError) Error() string {
81+ if e.Message == "" {
82+ return fmt.Sprintf("wlturbo: %v: object=%d code=%d", ErrDisplayError, e.ObjectID, e.Code)
83+ }
84+ return fmt.Sprintf("wlturbo: %v: object=%d code=%d: %s", ErrDisplayError, e.ObjectID, e.Code, e.Message)
85+}
86+
87+// Unwrap reports DisplayError as ErrDisplayError.
88+func (e *DisplayError) Unwrap() error { return ErrDisplayError }
89+
90+// receivedFrame is one complete Wayland message read from the connection.
91+// fds holds the ancillary descriptors that arrived while the frame's bytes
92+// were read, in arrival order.
93+type receivedFrame struct {
94+ object uint32
95+ opcode uint16
96+ body []byte
97+ fds []int
98+}
99+
100+// parseHeader decodes and validates an 8-byte Wayland message header.
101+func parseHeader(hdr []byte, maxSize uint32) (object uint32, opcode uint16, size uint32, err error) {
102+ if len(hdr) < HeaderSize {
103+ return 0, 0, 0, &ProtocolError{Kind: "short_header", Err: ErrMalformedFrame}
104+ }
105+ object = binary.LittleEndian.Uint32(hdr[0:4])
106+ sizeOpcode := binary.LittleEndian.Uint32(hdr[4:8])
107+ size = sizeOpcode >> 16
108+ opcode = uint16(sizeOpcode & 0xffff)
109+
110+ switch {
111+ case size < HeaderSize:
112+ return 0, 0, 0, &ProtocolError{Kind: "header_size", Object: object, Opcode: opcode, Size: size, Err: ErrMalformedFrame}
113+ case size%4 != 0:
114+ return 0, 0, 0, &ProtocolError{Kind: "misaligned_size", Object: object, Opcode: opcode, Size: size, Err: ErrMalformedFrame}
115+ case maxSize > 0 && size > maxSize:
116+ return 0, 0, 0, &ProtocolError{Kind: "size_over_limit", Object: object, Opcode: opcode, Size: size, Err: ErrMalformedFrame}
117+ }
118+ return object, opcode, size, nil
119+}
120+
121+// readFrame reads exactly one Wayland message from r. A short read never
122+// yields a partial frame: the header and the declared body are both read in
123+// full before returning. A maxSize of 0 means DefaultMaxMessageSize.
124+func readFrame(r io.Reader, maxSize uint32) (object uint32, opcode uint16, body []byte, err error) {
125+ if maxSize == 0 {
126+ maxSize = DefaultMaxMessageSize
127+ }
128+ var hdr [HeaderSize]byte
129+ if _, err = io.ReadFull(r, hdr[:]); err != nil {
130+ return 0, 0, nil, err
131+ }
132+ object, opcode, size, err := parseHeader(hdr[:], maxSize)
133+ if err != nil {
134+ return 0, 0, nil, err
135+ }
136+ if size == HeaderSize {
137+ return object, opcode, nil, nil
138+ }
139+ body = make([]byte, size-HeaderSize)
140+ if _, err = io.ReadFull(r, body); err != nil {
141+ return 0, 0, nil, err
142+ }
143+ return object, opcode, body, nil
144+}
@@ -0,0 +1,144 @@
1+package wlturbo
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "fmt"
7+ "io"
8+)
9+
10+const (
11+ // HeaderSize is the fixed size of a Wayland message header in bytes.
12+ HeaderSize = 8
13+
14+ // displayObjectID is the well-known object ID of wl_display.
15+ displayObjectID = 1
16+
17+ // DefaultMaxMessageSize bounds a single Wayland message, including its
18+ // header. Larger messages are rejected instead of allocating.
19+ DefaultMaxMessageSize = 1 << 20
20+
21+ // readChunkSize is the size of one read from the display socket.
22+ readChunkSize = 64 * 1024
23+)
24+
25+// Sentinel errors reported by the transport. Wire-level failures wrap one of
26+// them so callers can classify with errors.Is, while ProtocolError carries the
27+// offending object, opcode and size.
28+var (
29+ // ErrMalformedFrame reports a message whose header violates the wire
30+ // protocol: too short, misaligned, or larger than the accepted maximum.
31+ ErrMalformedFrame = errors.New("malformed wayland frame")
32+
33+ // ErrUnknownObject reports an event for an object this client never
34+ // created or bound.
35+ ErrUnknownObject = errors.New("wayland event for unknown object")
36+
37+ // ErrUnknownOpcode reports an event opcode the target object cannot receive.
38+ ErrUnknownOpcode = errors.New("wayland event with unknown opcode")
39+
40+ // ErrDisplayError reports that the compositor sent wl_display.error.
41+ ErrDisplayError = errors.New("wayland display error")
42+)
43+
44+// ProtocolError describes a wire-level protocol violation.
45+type ProtocolError struct {
46+ Kind string
47+ Object uint32
48+ Opcode uint16
49+ Size uint32
50+ Err error
51+}
52+
53+func (e *ProtocolError) Error() string {
54+ msg := e.Kind
55+ if e.Object != 0 {
56+ msg += fmt.Sprintf(" object=%d", e.Object)
57+ }
58+ if e.Kind == "unknown_opcode" || e.Kind == "display_error" {
59+ msg += fmt.Sprintf(" opcode=%d", e.Opcode)
60+ }
61+ if e.Size != 0 {
62+ msg += fmt.Sprintf(" size=%d", e.Size)
63+ }
64+ if e.Err != nil {
65+ return fmt.Sprintf("wlturbo: %s: %v", msg, e.Err)
66+ }
67+ return "wlturbo: " + msg
68+}
69+
70+// Unwrap returns the sentinel error classifying this violation.
71+func (e *ProtocolError) Unwrap() error { return e.Err }
72+
73+// DisplayError is returned when the compositor reports wl_display.error.
74+type DisplayError struct {
75+ ObjectID uint32
76+ Code uint32
77+ Message string
78+}
79+
80+func (e *DisplayError) Error() string {
81+ if e.Message == "" {
82+ return fmt.Sprintf("wlturbo: %v: object=%d code=%d", ErrDisplayError, e.ObjectID, e.Code)
83+ }
84+ return fmt.Sprintf("wlturbo: %v: object=%d code=%d: %s", ErrDisplayError, e.ObjectID, e.Code, e.Message)
85+}
86+
87+// Unwrap reports DisplayError as ErrDisplayError.
88+func (e *DisplayError) Unwrap() error { return ErrDisplayError }
89+
90+// receivedFrame is one complete Wayland message read from the connection.
91+// fds holds the ancillary descriptors that arrived while the frame's bytes
92+// were read, in arrival order.
93+type receivedFrame struct {
94+ object uint32
95+ opcode uint16
96+ body []byte
97+ fds []int
98+}
99+
100+// parseHeader decodes and validates an 8-byte Wayland message header.
101+func parseHeader(hdr []byte, maxSize uint32) (object uint32, opcode uint16, size uint32, err error) {
102+ if len(hdr) < HeaderSize {
103+ return 0, 0, 0, &ProtocolError{Kind: "short_header", Err: ErrMalformedFrame}
104+ }
105+ object = binary.LittleEndian.Uint32(hdr[0:4])
106+ sizeOpcode := binary.LittleEndian.Uint32(hdr[4:8])
107+ size = sizeOpcode >> 16
108+ opcode = uint16(sizeOpcode & 0xffff)
109+
110+ switch {
111+ case size < HeaderSize:
112+ return 0, 0, 0, &ProtocolError{Kind: "header_size", Object: object, Opcode: opcode, Size: size, Err: ErrMalformedFrame}
113+ case size%4 != 0:
114+ return 0, 0, 0, &ProtocolError{Kind: "misaligned_size", Object: object, Opcode: opcode, Size: size, Err: ErrMalformedFrame}
115+ case maxSize > 0 && size > maxSize:
116+ return 0, 0, 0, &ProtocolError{Kind: "size_over_limit", Object: object, Opcode: opcode, Size: size, Err: ErrMalformedFrame}
117+ }
118+ return object, opcode, size, nil
119+}
120+
121+// readFrame reads exactly one Wayland message from r. A short read never
122+// yields a partial frame: the header and the declared body are both read in
123+// full before returning. A maxSize of 0 means DefaultMaxMessageSize.
124+func readFrame(r io.Reader, maxSize uint32) (object uint32, opcode uint16, body []byte, err error) {
125+ if maxSize == 0 {
126+ maxSize = DefaultMaxMessageSize
127+ }
128+ var hdr [HeaderSize]byte
129+ if _, err = io.ReadFull(r, hdr[:]); err != nil {
130+ return 0, 0, nil, err
131+ }
132+ object, opcode, size, err := parseHeader(hdr[:], maxSize)
133+ if err != nil {
134+ return 0, 0, nil, err
135+ }
136+ if size == HeaderSize {
137+ return object, opcode, nil, nil
138+ }
139+ body = make([]byte, size-HeaderSize)
140+ if _, err = io.ReadFull(r, body); err != nil {
141+ return 0, 0, nil, err
142+ }
143+ return object, opcode, body, nil
144+}

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