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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +794 -278
M client.go +184 -223
| @@ -10,9 +10,6 @@ import ( | |||
| 10 | 10 | "errors" | |
| 11 | 11 | "fmt" | |
| 12 | 12 | "io" | |
| 13 | - | "log" | |
| 14 | - | ||
| 15 | - | // "log" | |
| 16 | 13 | "net" | |
| 17 | 14 | "os" | |
| 18 | 15 | "path/filepath" | |
| @@ -48,8 +45,8 @@ type Object interface { | |||
| 48 | 45 | // Display represents a connection to the Wayland display | |
| 49 | 46 | type Display struct { | |
| 50 | 47 | 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 | |
| 53 | 50 | nextID uint32 | |
| 54 | 51 | sendMu sync.Mutex | |
| 55 | 52 | recvMu sync.Mutex | |
| @@ -67,11 +64,38 @@ type Display struct { | |||
| 67 | 64 | lastErrorCode uint32 | |
| 68 | 65 | lastErrorObj uint32 | |
| 69 | 66 | ||
| 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 | + | } | |
| 72 | 76 | ||
| 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 | |
| 75 | 99 | } | |
| 76 | 100 | ||
| 77 | 101 | // Registry represents the global registry | |
| @@ -95,39 +119,10 @@ func (c *callbackObject) ID() uint32 { | |||
| 95 | 119 | ||
| 96 | 120 | // Dispatch handles callback events (opcode 0 = done) | |
| 97 | 121 | 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 | |
| 130 | 124 | } | |
| 125 | + | c.display.notifyListeners(c.id, 0, event.data) | |
| 131 | 126 | } | |
| 132 | 127 | ||
| 133 | 128 | // Global represents a global object | |
| @@ -140,7 +135,7 @@ type Global struct { | |||
| 140 | 135 | // GlobalHandler is called when a global is announced | |
| 141 | 136 | type GlobalHandler func(registry *Registry, name uint32, version uint32) | |
| 142 | 137 | ||
| 143 | - | // Connect connects to the Wayland display | |
| 138 | + | // Connect connects to the Wayland display and requests the registry. | |
| 144 | 139 | func Connect(socketPath string) (*Display, error) { | |
| 145 | 140 | if socketPath == "" { | |
| 146 | 141 | socketPath = os.Getenv("WAYLAND_DISPLAY") | |
| @@ -158,62 +153,37 @@ func Connect(socketPath string) (*Display, error) { | |||
| 158 | 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 | 156 | conn, err := net.Dial("unix", socketPath) | |
| 164 | 157 | if err != nil { | |
| 165 | 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) | |
| 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) | |
| 199 | 162 | ||
| 200 | 163 | // Get registry | |
| 201 | 164 | if err := d.getRegistry(); err != nil { | |
| 202 | - | _ = conn.Close() | |
| 165 | + | _ = d.Close() | |
| 203 | 166 | return nil, fmt.Errorf("failed to get registry: %w", err) | |
| 204 | 167 | } | |
| 205 | 168 | ||
| 206 | 169 | // 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 | 170 | return d, nil | |
| 210 | 171 | } | |
| 211 | 172 | ||
| 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. | |
| 213 | 175 | func (d *Display) Close() error { | |
| 176 | + | if !d.closed.CompareAndSwap(false, true) { | |
| 177 | + | return nil | |
| 178 | + | } | |
| 214 | 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 | 187 | // ID returns the display's object ID (always 1) | |
| 218 | 188 | func (d *Display) ID() uint32 { | |
| 219 | 189 | return 1 | |
| @@ -226,9 +196,7 @@ func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler E | |||
| 226 | 196 | ||
| 227 | 197 | // allocateID allocates a new object ID | |
| 228 | 198 | 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 | |
| 232 | 200 | } | |
| 233 | 201 | ||
| 234 | 202 | // AllocateID allocates a new object ID (public method) | |
| @@ -341,113 +309,151 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error { | |||
| 341 | 309 | return nil | |
| 342 | 310 | } | |
| 343 | 311 | ||
| 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. | |
| 345 | 319 | func (d *Display) Dispatch() error { | |
| 346 | 320 | d.recvMu.Lock() | |
| 347 | 321 | defer d.recvMu.Unlock() | |
| 348 | 322 | ||
| 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 | |
| 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 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 | |
| 370 | 330 | } | |
| 331 | + | return d.dispatchFrame(frame) | |
| 332 | + | } | |
| 371 | 333 | ||
| 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 | + | } | |
| 376 | 341 | ||
| 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 | |
| 383 | 362 | } | |
| 363 | + | } | |
| 364 | + | } | |
| 384 | 365 | ||
| 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 | |
| 388 | 389 | } | |
| 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 | |
| 395 | 393 | } | |
| 396 | - | fds = append(fds, moreFds...) | |
| 397 | 394 | } | |
| 395 | + | return nil | |
| 396 | + | } | |
| 398 | 397 | ||
| 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) | |
| 402 | 402 | } | |
| 403 | 403 | ||
| 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, | |
| 409 | 412 | } | |
| 413 | + | } | |
| 410 | 414 | ||
| 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) | |
| 415 | 417 | ||
| 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 | |
| 432 | 426 | } | |
| 433 | 427 | ||
| 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 | + | } | |
| 436 | 433 | ||
| 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) | |
| 447 | 455 | } | |
| 448 | 456 | } | |
| 449 | - | ||
| 450 | - | return nil | |
| 451 | 457 | } | |
| 452 | 458 | ||
| 453 | 459 | // handleDisplayEvent handles events on the display object | |
| @@ -455,60 +461,46 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error { | |||
| 455 | 461 | switch opcode { | |
| 456 | 462 | case 0: // error | |
| 457 | 463 | 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} | |
| 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 | 471 | d.lastErrorCode = code | |
| 476 | 472 | d.lastErrorObj = objectID | |
| 473 | + | d.lastError = &DisplayError{ObjectID: objectID, Code: code, Message: message} | |
| 477 | 474 | return d.lastError | |
| 478 | 475 | ||
| 479 | 476 | case 1: // delete_id | |
| 480 | 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 | 480 | id := binary.LittleEndian.Uint32(data[0:4]) | |
| 484 | 481 | d.objects.Delete(id) | |
| 485 | - | } | |
| 482 | + | return nil | |
| 486 | 483 | ||
| 487 | - | return nil | |
| 484 | + | default: | |
| 485 | + | return &ProtocolError{Kind: "unknown_opcode", Object: displayObjectID, Opcode: opcode, Err: ErrUnknownOpcode} | |
| 486 | + | } | |
| 488 | 487 | } | |
| 489 | 488 | ||
| 490 | 489 | // Roundtrip performs a synchronous roundtrip to the compositor | |
| 491 | 490 | func (d *Display) Roundtrip() error { | |
| 492 | - | log.Printf("wlclient: Roundtrip starting...") | |
| 493 | - | // Create callback | |
| 494 | 491 | callbackID := d.allocateID() | |
| 495 | 492 | done := make(chan error, 1) | |
| 496 | 493 | ||
| 497 | - | log.Printf("wlclient: Registering callback listener for ID %d", callbackID) | |
| 498 | - | // Register callback listener | |
| 499 | 494 | d.AddListener(callbackID, 0, func(_ []byte) { | |
| 500 | - | log.Printf("wlclient: Callback %d fired", callbackID) | |
| 501 | 495 | d.objects.Delete(callbackID) | |
| 502 | 496 | done <- nil | |
| 503 | 497 | }) | |
| 504 | 498 | ||
| 505 | 499 | // 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 { | |
| 508 | 501 | return err | |
| 509 | 502 | } | |
| 510 | 503 | ||
| 511 | - | // Store the callback object to track it | |
| 512 | 504 | d.objects.Store(callbackID, &callbackObject{ | |
| 513 | 505 | BaseProxy: BaseProxy{ | |
| 514 | 506 | context: d.Context(), | |
| @@ -517,22 +509,14 @@ func (d *Display) Roundtrip() error { | |||
| 517 | 509 | display: d, | |
| 518 | 510 | }) | |
| 519 | 511 | ||
| 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++ { | |
| 524 | 513 | if err := d.Dispatch(); err != nil { | |
| 525 | - | log.Printf("wlclient: Dispatch error: %v", err) | |
| 526 | 514 | return err | |
| 527 | 515 | } | |
| 528 | - | ||
| 529 | - | // Check if callback completed | |
| 530 | 516 | select { | |
| 531 | 517 | case err := <-done: | |
| 532 | - | log.Printf("wlclient: Roundtrip completed successfully") | |
| 533 | 518 | return err | |
| 534 | 519 | default: | |
| 535 | - | // Continue dispatching | |
| 536 | 520 | } | |
| 537 | 521 | } | |
| 538 | 522 | ||
| @@ -648,19 +632,15 @@ func (r *Registry) AddHandler(iface string, handler GlobalHandler) { | |||
| 648 | 632 | func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error { | |
| 649 | 633 | // Set the ID if not already set | |
| 650 | 634 | 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()) | |
| 654 | 636 | } | |
| 655 | 637 | ||
| 656 | 638 | // Ensure proxy has a context set - CRITICAL FIX | |
| 657 | 639 | if proxy.Context() == nil { | |
| 658 | 640 | if baseProxy, ok := proxy.(*BaseProxy); ok { | |
| 659 | 641 | baseProxy.SetContext(r.display.Context()) | |
| 660 | - | log.Printf("wlclient: Set context for proxy ID %d", proxy.ID()) | |
| 661 | 642 | } else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok { | |
| 662 | 643 | setter.SetContext(r.display.Context()) | |
| 663 | - | log.Printf("wlclient: Set context for proxy ID %d via interface", proxy.ID()) | |
| 664 | 644 | } else { | |
| 665 | 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 | 652 | // Also register directly in display objects map | |
| 673 | 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 | 655 | // Send bind request (opcode 0) with proper arguments | |
| 677 | 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 | 727 | switch opcode { | |
| 749 | 728 | case 0: // head event - creates new zwlr_output_head_v1 object | |
| 750 | 729 | if len(body) < 4 { | |
| 751 | - | log.Printf("wlclient: head event body too short: %d bytes", len(body)) | |
| 752 | 730 | return false | |
| 753 | 731 | } | |
| 754 | 732 | ||
| 755 | 733 | // Parse the new_id for the head object | |
| 756 | 734 | headID := binary.LittleEndian.Uint32(body[0:4]) | |
| 757 | - | log.Printf("wlclient: Creating server object (head) with ID %d", headID) | |
| 758 | 735 | ||
| 759 | 736 | // Create a proper OutputHead object | |
| 760 | 737 | headProxy := &OutputHead{ | |
| @@ -766,15 +743,12 @@ func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body | |||
| 766 | 743 | ||
| 767 | 744 | // Register the new head object | |
| 768 | 745 | d.objects.Store(headID, headProxy) | |
| 769 | - | log.Printf("wlclient: Registered server-created head object ID %d", headID) | |
| 770 | 746 | return true | |
| 771 | 747 | ||
| 772 | 748 | case 1: // done event | |
| 773 | - | log.Printf("wlclient: Output manager done event") | |
| 774 | 749 | return false | |
| 775 | 750 | ||
| 776 | 751 | case 2: // finished event | |
| 777 | - | log.Printf("wlclient: Output manager finished event") | |
| 778 | 752 | return false | |
| 779 | 753 | } | |
| 780 | 754 | ||
| @@ -792,21 +766,16 @@ type OutputHead struct { | |||
| 792 | 766 | ||
| 793 | 767 | // Dispatch handles head events | |
| 794 | 768 | func (h *OutputHead) Dispatch(event *Event) { | |
| 795 | - | log.Printf("wlclient: OutputHead.Dispatch ID %d, opcode %d", h.id, event.Opcode) | |
| 796 | 769 | switch event.Opcode { | |
| 797 | 770 | case 0: // name | |
| 798 | 771 | h.name = event.String() | |
| 799 | - | log.Printf("wlclient: Head %d name: %s", h.id, h.name) | |
| 800 | 772 | case 1: // description | |
| 801 | 773 | h.description = event.String() | |
| 802 | - | log.Printf("wlclient: Head %d description: %s", h.id, h.description) | |
| 803 | 774 | case 2: // physical_size | |
| 804 | 775 | h.width = event.Int32() | |
| 805 | 776 | h.height = event.Int32() | |
| 806 | - | log.Printf("wlclient: Head %d physical size: %dx%d mm", h.id, h.width, h.height) | |
| 807 | 777 | case 3: // mode (creates new mode object) | |
| 808 | 778 | modeID := event.Uint32() | |
| 809 | - | log.Printf("wlclient: Head %d mode object created: ID %d", h.id, modeID) | |
| 810 | 779 | // Create mode object | |
| 811 | 780 | mode := &OutputMode{ | |
| 812 | 781 | BaseProxy: BaseProxy{ | |
| @@ -816,10 +785,8 @@ func (h *OutputHead) Dispatch(event *Event) { | |||
| 816 | 785 | } | |
| 817 | 786 | h.context.display.objects.Store(modeID, mode) | |
| 818 | 787 | case 9: // finished | |
| 819 | - | log.Printf("wlclient: Head %d finished", h.id) | |
| 820 | 788 | h.context.Unregister(h) | |
| 821 | 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 | 801 | // Dispatch handles mode events | |
| 835 | 802 | func (m *OutputMode) Dispatch(event *Event) { | |
| 836 | - | log.Printf("wlclient: OutputMode.Dispatch ID %d, opcode %d", m.id, event.Opcode) | |
| 837 | 803 | switch event.Opcode { | |
| 838 | 804 | case 0: // size | |
| 839 | 805 | m.width = event.Int32() | |
| 840 | 806 | m.height = event.Int32() | |
| 841 | - | log.Printf("wlclient: Mode %d size: %dx%d", m.id, m.width, m.height) | |
| 842 | 807 | case 1: // refresh | |
| 843 | 808 | m.refresh = event.Int32() | |
| 844 | - | log.Printf("wlclient: Mode %d refresh: %d mHz", m.id, m.refresh) | |
| 845 | 809 | case 2: // preferred | |
| 846 | - | log.Printf("wlclient: Mode %d is preferred", m.id) | |
| 847 | 810 | case 3: // finished | |
| 848 | - | log.Printf("wlclient: Mode %d finished", m.id) | |
| 849 | 811 | m.context.Unregister(m) | |
| 850 | 812 | default: | |
| 851 | - | log.Printf("wlclient: Mode %d unhandled opcode %d", m.id, event.Opcode) | |
| 852 | 813 | } | |
| 853 | 814 | } | |
| @@ -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 display | 45 | // Display represents a connection to the Wayland display | ||
| 49 | type Display struct { | 46 | type Display struct { | ||
| 50 | conn net.Conn | 47 | conn net.Conn | ||
| 51 | - | fd int | 48 | + | unix *net.UnixConn // set when conn is a Unix socket, nil otherwise |
| 52 | - | objects sync.Map // map[uint32]Object | 49 | + | objects sync.Map // map[uint32]Object |
| 53 | nextID uint32 | 50 | nextID uint32 | ||
| 54 | sendMu sync.Mutex | 51 | sendMu sync.Mutex | ||
| 55 | recvMu sync.Mutex | 52 | recvMu sync.Mutex | ||
| @@ -67,11 +64,38 @@ type Display struct { | |||||
| 67 | lastErrorCode uint32 | 64 | lastErrorCode uint32 | ||
| 68 | lastErrorObj uint32 | 65 | lastErrorObj uint32 | ||
| 69 | 66 | ||||
| 70 | - | // Reusable read buffer for header | 67 | + | // Connection-local receive state. rbuf holds bytes read from the socket |
| 71 | - | headerBuf [8]byte | 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 | + | } | |||
| 72 | 76 | ||||
| 73 | - | // Pre-allocated buffer for event bodies (avoid allocations) | 77 | + | // newDisplay builds a Display over an established connection. |
| 74 | - | eventBodyBuf [4096]byte | 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 | |||
| 75 | } | 99 | } | ||
| 76 | 100 | ||||
| 77 | // Registry represents the global registry | 101 | // 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 event | 122 | + | 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 object | 128 | // Global represents a global object | ||
| @@ -140,7 +135,7 @@ type Global struct { | |||||
| 140 | // GlobalHandler is called when a global is announced | 135 | // 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 display | 138 | + | // 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 registry | 163 | // 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 handlers | 169 | // 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, nil | 170 | return d, nil | ||
| 210 | } | 171 | } | ||
| 211 | 172 | ||||
| 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. | |||
| 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 1 | 189 | return 1 | ||
| @@ -226,9 +196,7 @@ func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler E | |||||
| 226 | 196 | ||||
| 227 | // allocateID allocates a new object ID | 197 | // 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) - 1 | 199 | + | 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 nil | 309 | return nil | ||
| 342 | } | 310 | } | ||
| 343 | 311 | ||||
| 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. | |||
| 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 descriptors | 323 | + | 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 ones | 327 | + | 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 []byte | 335 | + | func (d *Display) maxMessageSize() uint32 { |
| 374 | - | if size > 8 { | 336 | + | if d.maxMsgLen == 0 { |
| 375 | - | bodySize := size - 8 | 337 | + | 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 buffer | 346 | + | 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 needed | 391 | + | 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 specially | 398 | + | // 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 object | 404 | + | 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 spam | 406 | + | 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 dispatching | 415 | + | // 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 method | 418 | + | 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 dispatcher | 428 | + | // 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 object | 459 | // 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: // error | 462 | 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 = code | 471 | d.lastErrorCode = code | ||
| 476 | d.lastErrorObj = objectID | 472 | d.lastErrorObj = objectID | ||
| 473 | + | d.lastError = &DisplayError{ObjectID: objectID, Code: code, Message: message} | |||
| 477 | return d.lastError | 474 | return d.lastError | ||
| 478 | 475 | ||||
| 479 | case 1: // delete_id | 476 | 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 nil | 484 | + | 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 compositor | 489 | // 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 <- nil | 496 | 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 err | 501 | 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 APPROACH | 512 | + | 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 err | 514 | 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 err | 518 | 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 set | 633 | // 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 FIX | 638 | // 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 map | 652 | // 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 arguments | 655 | // 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 object | 728 | 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 false | 730 | return false | ||
| 753 | } | 731 | } | ||
| 754 | 732 | ||||
| 755 | // Parse the new_id for the head object | 733 | // 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 object | 736 | // 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 object | 744 | // 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 true | 746 | return true | ||
| 771 | 747 | ||||
| 772 | case 1: // done event | 748 | case 1: // done event | ||
| 773 | - | log.Printf("wlclient: Output manager done event") | |||
| 774 | return false | 749 | return false | ||
| 775 | 750 | ||||
| 776 | case 2: // finished event | 751 | case 2: // finished event | ||
| 777 | - | log.Printf("wlclient: Output manager finished event") | |||
| 778 | return false | 752 | return false | ||
| 779 | } | 753 | } | ||
| 780 | 754 | ||||
| @@ -792,21 +766,16 @@ type OutputHead struct { | |||||
| 792 | 766 | ||||
| 793 | // Dispatch handles head events | 767 | // 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: // name | 770 | 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: // description | 772 | 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_size | 774 | 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 object | 779 | // 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: // finished | 787 | 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 events | 801 | // 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: // size | 804 | 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: // refresh | 807 | 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: // preferred | 809 | case 2: // preferred | ||
| 846 | - | log.Printf("wlclient: Mode %d is preferred", m.id) | |||
| 847 | case 3: // finished | 810 | 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | 5 | "encoding/binary" | |
| 6 | + | "errors" | |
| 7 | + | "net" | |
| 6 | 8 | "testing" | |
| 7 | 9 | ) | |
| 8 | 10 | ||
| @@ -47,7 +49,7 @@ func TestEventPool(t *testing.T) { | |||
| 47 | 49 | } | |
| 48 | 50 | ||
| 49 | 51 | // Set some data | |
| 50 | - | event.ProxyId = 123 | |
| 52 | + | event.ProxyID = 123 | |
| 51 | 53 | event.Opcode = 456 | |
| 52 | 54 | ||
| 53 | 55 | // Return to pool | |
| @@ -60,7 +62,7 @@ func TestEventPool(t *testing.T) { | |||
| 60 | 62 | } | |
| 61 | 63 | ||
| 62 | 64 | // 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 | 67 | // Clean up | |
| 66 | 68 | eventPool.Put(event2) | |
| @@ -72,8 +74,8 @@ func TestEventDispatcher(t *testing.T) { | |||
| 72 | 74 | called := false | |
| 73 | 75 | handler := func(event *Event) { | |
| 74 | 76 | 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) | |
| 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 | 277 | func TestAllocateID(t *testing.T) { | |
| 228 | 278 | d := &Display{ | |
| 229 | 279 | 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 data | 51 | // Set some data | ||
| 50 | - | event.ProxyId = 123 | 52 | + | event.ProxyID = 123 |
| 51 | event.Opcode = 456 | 53 | event.Opcode = 456 | ||
| 52 | 54 | ||||
| 53 | // Return to pool | 55 | // 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 valid | 64 | // 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 up | 67 | // Clean up | ||
| 66 | eventPool.Put(event2) | 68 | eventPool.Put(event2) | ||
| @@ -72,8 +74,8 @@ func TestEventDispatcher(t *testing.T) { | |||||
| 72 | called := false | 74 | called := false | ||
| 73 | handler := func(event *Event) { | 75 | handler := func(event *Event) { | ||
| 74 | called = true | 76 | 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 { | |||
| 36 | 36 | Opcode uint16 | |
| 37 | 37 | data []byte | |
| 38 | 38 | offset int | |
| 39 | + | display *Display | |
| 39 | 40 | } | |
| 40 | 41 | ||
| 41 | 42 | // Data returns the raw event data | |
| @@ -36,6 +36,7 @@ type Event struct { | |||||
| 36 | Opcode uint16 | 36 | Opcode uint16 | ||
| 37 | data []byte | 37 | data []byte | ||
| 38 | offset int | 38 | offset int | ||
| 39 | + | display *Display | |||
| 39 | } | 40 | } | ||
| 40 | 41 | ||||
| 41 | // Data returns the raw event data | 42 | // Data returns the raw event data | ||
M scm_linux.go +59 -51
| @@ -5,8 +5,8 @@ package wlturbo | |||
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | 7 | "fmt" | |
| 8 | - | "net" | |
| 9 | 8 | "golang.org/x/sys/unix" | |
| 9 | + | "net" | |
| 10 | 10 | "sync" | |
| 11 | 11 | "sync/atomic" | |
| 12 | 12 | "syscall" | |
| @@ -27,14 +27,14 @@ type fdItem struct { | |||
| 27 | 27 | var ( | |
| 28 | 28 | // Global FD queue for zero-allocation FD passing | |
| 29 | 29 | globalFDQueue = &fdQueue{} | |
| 30 | - | ||
| 30 | + | ||
| 31 | 31 | // Pre-allocated FD items pool | |
| 32 | 32 | fdItemPool = sync.Pool{ | |
| 33 | 33 | New: func() interface{} { | |
| 34 | 34 | return &fdItem{} | |
| 35 | 35 | }, | |
| 36 | 36 | } | |
| 37 | - | ||
| 37 | + | ||
| 38 | 38 | // Pre-allocated buffers for control messages | |
| 39 | 39 | controlBufferPool = sync.Pool{ | |
| 40 | 40 | New: func() interface{} { | |
| @@ -48,11 +48,11 @@ func (q *fdQueue) enqueueFD(fd int) { | |||
| 48 | 48 | item := fdItemPool.Get().(*fdItem) | |
| 49 | 49 | item.fd = fd | |
| 50 | 50 | item.next.Store(nil) | |
| 51 | - | ||
| 51 | + | ||
| 52 | 52 | for { | |
| 53 | 53 | tail := q.tail.Load() | |
| 54 | 54 | next := tail & 255 | |
| 55 | - | ||
| 55 | + | ||
| 56 | 56 | if q.items[next].CompareAndSwap(nil, item) { | |
| 57 | 57 | q.tail.Add(1) | |
| 58 | 58 | return | |
| @@ -66,18 +66,18 @@ func (q *fdQueue) dequeueFD() (int, bool) { | |||
| 66 | 66 | for { | |
| 67 | 67 | head := q.head.Load() | |
| 68 | 68 | tail := q.tail.Load() | |
| 69 | - | ||
| 69 | + | ||
| 70 | 70 | if head >= tail { | |
| 71 | 71 | return -1, false | |
| 72 | 72 | } | |
| 73 | - | ||
| 73 | + | ||
| 74 | 74 | next := head & 255 | |
| 75 | 75 | item := q.items[next].Load() | |
| 76 | - | ||
| 76 | + | ||
| 77 | 77 | if item == nil { | |
| 78 | 78 | continue | |
| 79 | 79 | } | |
| 80 | - | ||
| 80 | + | ||
| 81 | 81 | if q.head.CompareAndSwap(head, head+1) { | |
| 82 | 82 | fd := item.fd | |
| 83 | 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 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 | + | ||
| 93 | 102 | oob := controlBufferPool.Get().([]byte) | |
| 94 | 103 | 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 | |
| 100 | 108 | } | |
| 101 | - | ||
| 102 | - | // Parse control messages if any | |
| 103 | 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 | 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 | - | ||
| 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) | |
| 124 | 133 | } | |
| 125 | 134 | } | |
| 126 | - | ||
| 127 | - | return n, fds, nil | |
| 135 | + | return nil | |
| 128 | 136 | } | |
| 129 | 137 | ||
| 130 | 138 | // sendmsgWithFDs sends a message potentially containing file descriptors | |
| 131 | 139 | func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error { | |
| 132 | 140 | d.sendMu.Lock() | |
| 133 | 141 | defer d.sendMu.Unlock() | |
| 134 | - | ||
| 142 | + | ||
| 135 | 143 | if len(fds) == 0 { | |
| 136 | 144 | // Fast path: no FDs to send | |
| 137 | 145 | _, err := d.conn.Write(buf) | |
| 138 | 146 | return err | |
| 139 | 147 | } | |
| 140 | - | ||
| 148 | + | ||
| 141 | 149 | // Create control message with file descriptors | |
| 142 | 150 | oob := syscall.UnixRights(fds...) | |
| 143 | - | ||
| 151 | + | ||
| 144 | 152 | // Send message with control data | |
| 145 | 153 | _, _, err := d.conn.(*net.UnixConn).WriteMsgUnix(buf, oob, nil) | |
| 146 | 154 | return err | |
| @@ -164,18 +172,18 @@ func CreateAnonymousFile(size int64) (fd int, err error) { | |||
| 164 | 172 | _ = unix.Close(fd) | |
| 165 | 173 | return -1, err | |
| 166 | 174 | } | |
| 167 | - | ||
| 175 | + | ||
| 168 | 176 | // 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 | 178 | unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_SEAL) | |
| 171 | 179 | if err != nil { | |
| 172 | 180 | _ = unix.Close(fd) | |
| 173 | 181 | return -1, err | |
| 174 | 182 | } | |
| 175 | - | ||
| 183 | + | ||
| 176 | 184 | return fd, nil | |
| 177 | 185 | } | |
| 178 | - | ||
| 186 | + | ||
| 179 | 187 | // Fallback to O_TMPFILE if available | |
| 180 | 188 | fd, err = unix.Open("/dev/shm", unix.O_TMPFILE|unix.O_RDWR|unix.O_CLOEXEC, 0600) | |
| 181 | 189 | if err == nil { | |
| @@ -186,24 +194,24 @@ func CreateAnonymousFile(size int64) (fd int, err error) { | |||
| 186 | 194 | } | |
| 187 | 195 | return fd, nil | |
| 188 | 196 | } | |
| 189 | - | ||
| 197 | + | ||
| 190 | 198 | // Final fallback: create temp file and unlink | |
| 191 | 199 | name := fmt.Sprintf("/dev/shm/wlclient-%d", unix.Getpid()) | |
| 192 | 200 | fd, err = unix.Open(name, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC, 0600) | |
| 193 | 201 | if err != nil { | |
| 194 | 202 | return -1, err | |
| 195 | 203 | } | |
| 196 | - | ||
| 204 | + | ||
| 197 | 205 | // Unlink immediately | |
| 198 | 206 | _ = unix.Unlink(name) | |
| 199 | - | ||
| 207 | + | ||
| 200 | 208 | // Set size | |
| 201 | 209 | err = unix.Ftruncate(fd, size) | |
| 202 | 210 | if err != nil { | |
| 203 | 211 | unix.Close(fd) | |
| 204 | 212 | return -1, err | |
| 205 | 213 | } | |
| 206 | - | ||
| 214 | + | ||
| 207 | 215 | return fd, nil | |
| 208 | 216 | } | |
| 209 | 217 | ||
| @@ -215,4 +223,4 @@ func MapMemory(fd int, size int) ([]byte, error) { | |||
| 215 | 223 | // UnmapMemory unmaps memory | |
| 216 | 224 | func UnmapMemory(data []byte) error { | |
| 217 | 225 | 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 passing | 28 | // Global FD queue for zero-allocation FD passing | ||
| 29 | globalFDQueue = &fdQueue{} | 29 | globalFDQueue = &fdQueue{} | ||
| 30 | - | 30 | + | ||
| 31 | // Pre-allocated FD items pool | 31 | // 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 messages | 38 | // 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 = fd | 49 | 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 & 255 | 54 | 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 | return | 58 | 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, false | 71 | return -1, false | ||
| 72 | } | 72 | } | ||
| 73 | - | 73 | + | ||
| 74 | next := head & 255 | 74 | 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 | continue | 78 | continue | ||
| 79 | } | 79 | } | ||
| 80 | - | 80 | + | ||
| 81 | if q.head.CompareAndSwap(head, head+1) { | 81 | if q.head.CompareAndSwap(head, head+1) { | ||
| 82 | fd := item.fd | 82 | 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 descriptors | 90 | + | // 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 pool | 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 | + | ||||
| 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 data | 105 | + | 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 descriptors | 138 | // 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 send | 144 | // Fast path: no FDs to send | ||
| 137 | _, err := d.conn.Write(buf) | 145 | _, err := d.conn.Write(buf) | ||
| 138 | return err | 146 | return err | ||
| 139 | } | 147 | } | ||
| 140 | - | 148 | + | ||
| 141 | // Create control message with file descriptors | 149 | // Create control message with file descriptors | ||
| 142 | oob := syscall.UnixRights(fds...) | 150 | oob := syscall.UnixRights(fds...) | ||
| 143 | - | 151 | + | ||
| 144 | // Send message with control data | 152 | // 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 err | 154 | 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, err | 173 | return -1, err | ||
| 166 | } | 174 | } | ||
| 167 | - | 175 | + | ||
| 168 | // Add seals to prevent resizing | 176 | // 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, err | 181 | return -1, err | ||
| 174 | } | 182 | } | ||
| 175 | - | 183 | + | ||
| 176 | return fd, nil | 184 | return fd, nil | ||
| 177 | } | 185 | } | ||
| 178 | - | 186 | + | ||
| 179 | // Fallback to O_TMPFILE if available | 187 | // 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, nil | 195 | return fd, nil | ||
| 188 | } | 196 | } | ||
| 189 | - | 197 | + | ||
| 190 | // Final fallback: create temp file and unlink | 198 | // 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, err | 202 | return -1, err | ||
| 195 | } | 203 | } | ||
| 196 | - | 204 | + | ||
| 197 | // Unlink immediately | 205 | // Unlink immediately | ||
| 198 | _ = unix.Unlink(name) | 206 | _ = unix.Unlink(name) | ||
| 199 | - | 207 | + | ||
| 200 | // Set size | 208 | // 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, err | 212 | return -1, err | ||
| 205 | } | 213 | } | ||
| 206 | - | 214 | + | ||
| 207 | return fd, nil | 215 | return fd, nil | ||
| 208 | } | 216 | } | ||
| 209 | 217 | ||||
| @@ -215,4 +223,4 @@ func MapMemory(fd int, size int) ([]byte, error) { | |||||
| 215 | // UnmapMemory unmaps memory | 223 | // 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 | + | } | |||