merge: generated core bindings and hardened transport (Veya P2)
24 files · +10254 -762
A benchmark_test.go +26 -0
| @@ -0,0 +1,26 @@ | |||
| 1 | + | package wlturbo | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | // BenchmarkWireDispatch isolates framed typed dispatch from socket scheduling. | |
| 6 | + | func BenchmarkWireDispatch(b *testing.B) { | |
| 7 | + | d := newDisplay(&chunkConn{}) | |
| 8 | + | p := &benchProxy{BaseProxy: BaseProxy{id: 7, context: d.context}} | |
| 9 | + | d.context.Register(p) | |
| 10 | + | f := receivedFrame{object: 7, opcode: 0, body: []byte{1, 0, 0, 0}} | |
| 11 | + | b.ReportAllocs() | |
| 12 | + | b.ResetTimer() | |
| 13 | + | for i := 0; i < b.N; i++ { | |
| 14 | + | if err := d.dispatchFrame(f); err != nil { | |
| 15 | + | b.Fatal(err) | |
| 16 | + | } | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | type benchProxy struct { | |
| 21 | + | BaseProxy | |
| 22 | + | value uint32 | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func (p *benchProxy) EventSignature(op uint16) (string, bool) { return "uint,", op == 0 } | |
| 26 | + | func (p *benchProxy) Dispatch(e *Event) { p.value = e.Uint32() } | |
| @@ -0,0 +1,26 @@ | |||||
| 1 | + | package wlturbo | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | // BenchmarkWireDispatch isolates framed typed dispatch from socket scheduling. | |||
| 6 | + | func BenchmarkWireDispatch(b *testing.B) { | |||
| 7 | + | d := newDisplay(&chunkConn{}) | |||
| 8 | + | p := &benchProxy{BaseProxy: BaseProxy{id: 7, context: d.context}} | |||
| 9 | + | d.context.Register(p) | |||
| 10 | + | f := receivedFrame{object: 7, opcode: 0, body: []byte{1, 0, 0, 0}} | |||
| 11 | + | b.ReportAllocs() | |||
| 12 | + | b.ResetTimer() | |||
| 13 | + | for i := 0; i < b.N; i++ { | |||
| 14 | + | if err := d.dispatchFrame(f); err != nil { | |||
| 15 | + | b.Fatal(err) | |||
| 16 | + | } | |||
| 17 | + | } | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | type benchProxy struct { | |||
| 21 | + | BaseProxy | |||
| 22 | + | value uint32 | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func (p *benchProxy) EventSignature(op uint16) (string, bool) { return "uint,", op == 0 } | |||
| 26 | + | func (p *benchProxy) Dispatch(e *Event) { p.value = e.Uint32() } | |||
M client.go +259 -228
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "net" | |
| 14 | 14 | "os" | |
| 15 | 15 | "path/filepath" | |
| 16 | + | "strings" | |
| 16 | 17 | "sync" | |
| 17 | 18 | "sync/atomic" | |
| 18 | 19 | ) | |
| @@ -44,13 +45,19 @@ type Object interface { | |||
| 44 | 45 | ||
| 45 | 46 | // Display represents a connection to the Wayland display | |
| 46 | 47 | type Display struct { | |
| 47 | - | conn net.Conn | |
| 48 | - | unix *net.UnixConn // set when conn is a Unix socket, nil otherwise | |
| 49 | - | objects sync.Map // map[uint32]Object | |
| 50 | - | nextID uint32 | |
| 51 | - | sendMu sync.Mutex | |
| 52 | - | recvMu sync.Mutex | |
| 53 | - | listeners sync.Map // map[uint32]map[uint16][]func([]byte) | |
| 48 | + | conn net.Conn | |
| 49 | + | unix *net.UnixConn // set when conn is a Unix socket, nil otherwise | |
| 50 | + | objects sync.Map // map[uint32]Object | |
| 51 | + | nextID uint32 | |
| 52 | + | idMu sync.Mutex | |
| 53 | + | freeIDs []uint32 | |
| 54 | + | retiredIDs map[uint32]bool | |
| 55 | + | sendMu sync.Mutex | |
| 56 | + | recvMu sync.Mutex | |
| 57 | + | dispatchMu sync.Mutex // serialize readers without holding receive state across handlers | |
| 58 | + | signatures sync.Map | |
| 59 | + | listenerMu sync.Mutex | |
| 60 | + | listeners sync.Map // map[uint32]map[uint16][]func([]byte) | |
| 54 | 61 | ||
| 55 | 62 | // High-performance event dispatcher | |
| 56 | 63 | dispatcher *EventDispatcher | |
| @@ -73,7 +80,6 @@ type Display struct { | |||
| 73 | 80 | readErr error | |
| 74 | 81 | maxMsgLen uint32 | |
| 75 | 82 | closed atomic.Bool | |
| 76 | - | fdMu sync.Mutex | |
| 77 | 83 | pendingFDs []int | |
| 78 | 84 | } | |
| 79 | 85 | ||
| @@ -103,11 +109,12 @@ func newDisplay(conn net.Conn) *Display { | |||
| 103 | 109 | ||
| 104 | 110 | // Registry represents the global registry | |
| 105 | 111 | type Registry struct { | |
| 106 | - | id uint32 | |
| 107 | - | display *Display | |
| 108 | - | globals map[uint32]Global | |
| 109 | - | mu sync.RWMutex | |
| 110 | - | handlers map[string]GlobalHandler | |
| 112 | + | id uint32 | |
| 113 | + | display *Display | |
| 114 | + | globals map[uint32]Global | |
| 115 | + | mu sync.RWMutex | |
| 116 | + | handlers map[string]GlobalHandler | |
| 117 | + | removeHandlers []RegistryGlobalRemoveHandler | |
| 111 | 118 | } | |
| 112 | 119 | ||
| 113 | 120 | // callbackObject represents a wl_callback object | |
| @@ -119,6 +126,12 @@ type callbackObject struct { | |||
| 119 | 126 | func (c *callbackObject) ID() uint32 { | |
| 120 | 127 | return c.id | |
| 121 | 128 | } | |
| 129 | + | func (c *callbackObject) EventSignature(op uint16) (string, bool) { | |
| 130 | + | if op == 0 { | |
| 131 | + | return "uint,", true | |
| 132 | + | } | |
| 133 | + | return "", false | |
| 134 | + | } | |
| 122 | 135 | ||
| 123 | 136 | // Dispatch handles callback events (opcode 0 = done) | |
| 124 | 137 | func (c *callbackObject) Dispatch(event *Event) { | |
| @@ -126,6 +139,7 @@ func (c *callbackObject) Dispatch(event *Event) { | |||
| 126 | 139 | return | |
| 127 | 140 | } | |
| 128 | 141 | c.display.notifyListeners(c.id, 0, event.data) | |
| 142 | + | c.display.context.Unregister(c) | |
| 129 | 143 | } | |
| 130 | 144 | ||
| 131 | 145 | // Global represents a global object | |
| @@ -138,6 +152,16 @@ type Global struct { | |||
| 138 | 152 | // GlobalHandler is called when a global is announced | |
| 139 | 153 | type GlobalHandler func(registry *Registry, name uint32, version uint32) | |
| 140 | 154 | ||
| 155 | + | // ConnectFromConn attaches to an established Wayland socket and requests its registry. | |
| 156 | + | func ConnectFromConn(conn net.Conn) (*Display, error) { | |
| 157 | + | d := newDisplay(conn) | |
| 158 | + | if err := d.getRegistry(); err != nil { | |
| 159 | + | _ = d.Close() | |
| 160 | + | return nil, fmt.Errorf("get registry: %w", err) | |
| 161 | + | } | |
| 162 | + | return d, nil | |
| 163 | + | } | |
| 164 | + | ||
| 141 | 165 | // Connect connects to the Wayland display and requests the registry. | |
| 142 | 166 | func Connect(socketPath string) (*Display, error) { | |
| 143 | 167 | if socketPath == "" { | |
| @@ -161,16 +185,7 @@ func Connect(socketPath string) (*Display, error) { | |||
| 161 | 185 | return nil, fmt.Errorf("failed to connect to Wayland: %w", err) | |
| 162 | 186 | } | |
| 163 | 187 | ||
| 164 | - | d := newDisplay(conn) | |
| 165 | - | ||
| 166 | - | // Get registry | |
| 167 | - | if err := d.getRegistry(); err != nil { | |
| 168 | - | _ = d.Close() | |
| 169 | - | return nil, fmt.Errorf("failed to get registry: %w", err) | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | // Don't do initial roundtrip here - let the caller do it after setting up handlers | |
| 173 | - | return d, nil | |
| 188 | + | return ConnectFromConn(conn) | |
| 174 | 189 | } | |
| 175 | 190 | ||
| 176 | 191 | // Close closes the display connection. It is safe to call more than once; | |
| @@ -179,8 +194,12 @@ func (d *Display) Close() error { | |||
| 179 | 194 | if !d.closed.CompareAndSwap(false, true) { | |
| 180 | 195 | return nil | |
| 181 | 196 | } | |
| 197 | + | d.context.closed.Store(true) | |
| 198 | + | err := d.conn.Close() | |
| 199 | + | d.recvMu.Lock() | |
| 182 | 200 | d.closePendingFDs() | |
| 183 | - | return d.conn.Close() | |
| 201 | + | d.recvMu.Unlock() | |
| 202 | + | return err | |
| 184 | 203 | } | |
| 185 | 204 | ||
| 186 | 205 | // Closed reports whether the connection has been closed. | |
| @@ -200,7 +219,20 @@ func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler E | |||
| 200 | 219 | ||
| 201 | 220 | // allocateID allocates a new object ID | |
| 202 | 221 | func (d *Display) allocateID() uint32 { | |
| 203 | - | return atomic.AddUint32(&d.nextID, 1) - 1 | |
| 222 | + | d.idMu.Lock() | |
| 223 | + | defer d.idMu.Unlock() | |
| 224 | + | if n := len(d.freeIDs); n != 0 { | |
| 225 | + | id := d.freeIDs[n-1] | |
| 226 | + | d.freeIDs = d.freeIDs[:n-1] | |
| 227 | + | delete(d.retiredIDs, id) | |
| 228 | + | return id | |
| 229 | + | } | |
| 230 | + | if d.nextID == 0 || d.nextID >= 0xff000000 { | |
| 231 | + | return 0 // exhausted; never allocate a reserved server ID | |
| 232 | + | } | |
| 233 | + | id := d.nextID | |
| 234 | + | d.nextID++ | |
| 235 | + | return id | |
| 204 | 236 | } | |
| 205 | 237 | ||
| 206 | 238 | // AllocateID allocates a new object ID (public method) | |
| @@ -215,6 +247,12 @@ func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{ | |||
| 215 | 247 | ||
| 216 | 248 | // SendRequestWithFDs sends a request with file descriptors | |
| 217 | 249 | func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error { | |
| 250 | + | return d.sendRequest(objectID, opcode, fds, nil, args) | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // sendRequest marshals a request, then runs guard and writes the message | |
| 254 | + | // under the send lock. Guard failures and marshaling errors write nothing. | |
| 255 | + | func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}) error { | |
| 218 | 256 | // Get buffer from pool | |
| 219 | 257 | buf := bufferPool.Get().(*bytes.Buffer) | |
| 220 | 258 | defer func() { | |
| @@ -235,8 +273,8 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, | |||
| 235 | 273 | ||
| 236 | 274 | // Update header with actual size in bytes | |
| 237 | 275 | bufLen := buf.Len() | |
| 238 | - | if bufLen > 0xFFFF { | |
| 239 | - | return fmt.Errorf("message too large: %d bytes", bufLen) | |
| 276 | + | if bufLen > 0xFFFF || bufLen%4 != 0 { | |
| 277 | + | return fmt.Errorf("invalid message size: %d bytes", bufLen) | |
| 240 | 278 | } | |
| 241 | 279 | size := uint32(bufLen) // Safe: checked above | |
| 242 | 280 | binary.LittleEndian.PutUint32(header[0:4], objectID) | |
| @@ -248,7 +286,7 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, | |||
| 248 | 286 | copy(data[0:8], header) | |
| 249 | 287 | ||
| 250 | 288 | // Send message with optional file descriptors | |
| 251 | - | return d.sendmsgWithFDs(data, fds) | |
| 289 | + | return d.sendmsgWithFDs(data, fds, guard) | |
| 252 | 290 | } | |
| 253 | 291 | ||
| 254 | 292 | // marshalArg marshals a single argument | |
| @@ -263,7 +301,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error { | |||
| 263 | 301 | case string: | |
| 264 | 302 | // String format: length (including null) + string + null + padding | |
| 265 | 303 | strlen := len(v) + 1 | |
| 266 | - | if strlen > 0xFFFFFFFF { | |
| 304 | + | if strlen > 0xffff-HeaderSize { | |
| 267 | 305 | return fmt.Errorf("string too long: %d bytes", strlen) | |
| 268 | 306 | } | |
| 269 | 307 | if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above | |
| @@ -279,7 +317,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error { | |||
| 279 | 317 | case []byte: | |
| 280 | 318 | // Array format: length + data + padding | |
| 281 | 319 | arrlen := len(v) | |
| 282 | - | if arrlen > 0xFFFFFFFF { | |
| 320 | + | if arrlen > 0xffff-HeaderSize { | |
| 283 | 321 | return fmt.Errorf("array too long: %d bytes", arrlen) | |
| 284 | 322 | } | |
| 285 | 323 | if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil { | |
| @@ -321,14 +359,20 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error { | |||
| 321 | 359 | // Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a | |
| 322 | 360 | // malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame. | |
| 323 | 361 | func (d *Display) Dispatch() error { | |
| 362 | + | d.dispatchMu.Lock() | |
| 363 | + | defer d.dispatchMu.Unlock() | |
| 324 | 364 | d.recvMu.Lock() | |
| 325 | - | defer d.recvMu.Unlock() | |
| 326 | 365 | ||
| 327 | 366 | if d.closed.Load() { | |
| 367 | + | d.recvMu.Unlock() | |
| 328 | 368 | return net.ErrClosed | |
| 329 | 369 | } | |
| 330 | 370 | ||
| 331 | 371 | frame, err := d.nextFrame() | |
| 372 | + | d.recvMu.Unlock() | |
| 373 | + | if d.closed.Load() { | |
| 374 | + | return net.ErrClosed | |
| 375 | + | } | |
| 332 | 376 | if err != nil { | |
| 333 | 377 | return d.fail(err) | |
| 334 | 378 | } | |
| @@ -342,10 +386,15 @@ func (d *Display) fail(err error) error { | |||
| 342 | 386 | if err == nil { | |
| 343 | 387 | return nil | |
| 344 | 388 | } | |
| 389 | + | d.recvMu.Lock() | |
| 390 | + | defer d.recvMu.Unlock() | |
| 345 | 391 | var perr *ProtocolError | |
| 346 | 392 | if errors.As(err, &perr) { | |
| 347 | 393 | d.closePendingFDs() | |
| 348 | 394 | } | |
| 395 | + | if errors.Is(err, io.ErrUnexpectedEOF) { | |
| 396 | + | d.closePendingFDs() | |
| 397 | + | } | |
| 349 | 398 | return err | |
| 350 | 399 | } | |
| 351 | 400 | ||
| @@ -376,6 +425,9 @@ func (d *Display) nextFrame() (receivedFrame, error) { | |||
| 376 | 425 | } | |
| 377 | 426 | } | |
| 378 | 427 | if err := d.fill(); err != nil { | |
| 428 | + | if d.closed.Load() { | |
| 429 | + | return receivedFrame{}, net.ErrClosed | |
| 430 | + | } | |
| 379 | 431 | return receivedFrame{}, err | |
| 380 | 432 | } | |
| 381 | 433 | } | |
| @@ -416,40 +468,129 @@ func (d *Display) fill() error { | |||
| 416 | 468 | return nil | |
| 417 | 469 | } | |
| 418 | 470 | ||
| 419 | - | // dispatchFrame delivers one complete frame to the object that owns it. | |
| 471 | + | // signatureProvider is implemented by generated objects; every opcode must | |
| 472 | + | // have a known signature before any descriptor can be assigned or dispatched. | |
| 473 | + | type signatureProvider interface{ EventSignature(uint16) (string, bool) } | |
| 474 | + | ||
| 475 | + | // dispatchFrame delivers one complete frame with precisely its signature's FDs. | |
| 420 | 476 | func (d *Display) dispatchFrame(f receivedFrame) error { | |
| 421 | - | if f.object == displayObjectID { | |
| 477 | + | d.recvMu.Lock() | |
| 478 | + | obj, ev, kind, err := d.prepareFrame(f) | |
| 479 | + | d.recvMu.Unlock() | |
| 480 | + | if err != nil { | |
| 481 | + | return err | |
| 482 | + | } | |
| 483 | + | switch kind { | |
| 484 | + | case 1: | |
| 422 | 485 | return d.handleDisplayEvent(f.opcode, f.body) | |
| 486 | + | case 2: | |
| 487 | + | d.notifyListeners(f.object, f.opcode, f.body) | |
| 488 | + | return nil | |
| 489 | + | } | |
| 490 | + | defer func() { | |
| 491 | + | for _, fd := range ev.fds { | |
| 492 | + | _ = fd.Close() | |
| 493 | + | } | |
| 494 | + | *ev = Event{} | |
| 495 | + | eventPool.Put(ev) | |
| 496 | + | }() | |
| 497 | + | if proxy, ok := obj.(Proxy); ok { | |
| 498 | + | proxy.Dispatch(ev) | |
| 499 | + | } else { | |
| 500 | + | d.dispatcher.Dispatch(f.object, f.opcode, f.body) | |
| 501 | + | d.notifyListeners(f.object, f.opcode, f.body) | |
| 423 | 502 | } | |
| 503 | + | return nil | |
| 504 | + | } | |
| 424 | 505 | ||
| 506 | + | // prepareFrame assigns owned descriptors under recvMu, but never calls user code. | |
| 507 | + | func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) { | |
| 508 | + | if d.closed.Load() { | |
| 509 | + | return nil, nil, 0, net.ErrClosed | |
| 510 | + | } | |
| 511 | + | if f.object == displayObjectID { | |
| 512 | + | if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 { | |
| 513 | + | return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame} | |
| 514 | + | } | |
| 515 | + | return nil, nil, 1, nil | |
| 516 | + | } | |
| 517 | + | if f.object == d.registry.id { | |
| 518 | + | var sig string | |
| 519 | + | switch f.opcode { | |
| 520 | + | case 0: | |
| 521 | + | sig = "uint,string,uint," | |
| 522 | + | case 1: | |
| 523 | + | sig = "uint," | |
| 524 | + | default: | |
| 525 | + | return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode} | |
| 526 | + | } | |
| 527 | + | if err := validateEventBody(sig, f.body); err != nil { | |
| 528 | + | return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err} | |
| 529 | + | } | |
| 530 | + | if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 { | |
| 531 | + | return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame} | |
| 532 | + | } | |
| 533 | + | return nil, nil, 2, nil | |
| 534 | + | } | |
| 425 | 535 | obj, ok := d.objects.Load(f.object) | |
| 426 | 536 | if !ok { | |
| 427 | - | return &ProtocolError{ | |
| 428 | - | Kind: "unknown_object", | |
| 429 | - | Object: f.object, | |
| 430 | - | Opcode: f.opcode, | |
| 431 | - | Size: uint32(len(f.body)) + HeaderSize, | |
| 432 | - | Err: ErrUnknownObject, | |
| 537 | + | return nil, nil, 0, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject} | |
| 538 | + | } | |
| 539 | + | sig, valid := "", false | |
| 540 | + | p, isGenerated := obj.(signatureProvider) | |
| 541 | + | if isGenerated { | |
| 542 | + | sig, valid = p.EventSignature(f.opcode) | |
| 543 | + | } | |
| 544 | + | // Legacy custom proxies may declare their signatures with RegisterEventSignature. | |
| 545 | + | if !valid && !isGenerated { | |
| 546 | + | if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok { | |
| 547 | + | sig, valid = v.(string), true | |
| 548 | + | } | |
| 549 | + | } | |
| 550 | + | if !valid { | |
| 551 | + | return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode} | |
| 552 | + | } | |
| 553 | + | if err := validateEventBody(sig, f.body); err != nil { | |
| 554 | + | return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err} | |
| 555 | + | } | |
| 556 | + | count := strings.Count(sig, "fd,") | |
| 557 | + | if len(d.pendingFDs) < count { | |
| 558 | + | return nil, nil, 0, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame} | |
| 559 | + | } | |
| 560 | + | ev := eventPool.Get().(*Event) | |
| 561 | + | *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d} | |
| 562 | + | if count != 0 { | |
| 563 | + | ev.fds = make([]*OwnedFD, count) | |
| 564 | + | for i := range ev.fds { | |
| 565 | + | ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]} | |
| 566 | + | } | |
| 567 | + | d.pendingFDs = d.pendingFDs[count:] | |
| 568 | + | } | |
| 569 | + | // FDs from a single recvmsg can accompany multiple coalesced frames. Once | |
| 570 | + | // all buffered bytes have been framed, surplus descriptors are not legal. | |
| 571 | + | if len(d.rbuf) == 0 && len(d.pendingFDs) != 0 { | |
| 572 | + | for _, fd := range ev.fds { | |
| 573 | + | _ = fd.Close() | |
| 433 | 574 | } | |
| 575 | + | *ev = Event{} | |
| 576 | + | eventPool.Put(ev) | |
| 577 | + | return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame} | |
| 434 | 578 | } | |
| 579 | + | return obj.(Object), ev, 0, nil | |
| 580 | + | } | |
| 435 | 581 | ||
| 436 | - | // Server-created objects are announced by an event on a known object. | |
| 437 | - | d.handleServerObject(f.object, f.opcode, f.body) | |
| 582 | + | type signatureKey struct { | |
| 583 | + | object uint32 | |
| 584 | + | opcode uint16 | |
| 585 | + | } | |
| 438 | 586 | ||
| 439 | - | if proxy, ok := obj.(Proxy); ok && proxy != nil { | |
| 440 | - | proxy.Dispatch(&Event{ | |
| 441 | - | ProxyID: f.object, | |
| 442 | - | Opcode: f.opcode, | |
| 443 | - | data: f.body, | |
| 444 | - | display: d, | |
| 445 | - | }) | |
| 446 | - | return nil | |
| 587 | + | // RegisterEventSignature defines a signature for a legacy proxy. Generated | |
| 588 | + | // proxies supply it themselves and cannot be overridden. | |
| 589 | + | func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) { | |
| 590 | + | if object == 0 || object == displayObjectID { | |
| 591 | + | return | |
| 447 | 592 | } | |
| 448 | - | ||
| 449 | - | // High-performance dispatcher, then compatibility listeners. | |
| 450 | - | d.dispatcher.Dispatch(f.object, f.opcode, f.body) | |
| 451 | - | d.notifyListeners(f.object, f.opcode, f.body) | |
| 452 | - | return nil | |
| 593 | + | d.signatures.Store(signatureKey{object, opcode}, signature) | |
| 453 | 594 | } | |
| 454 | 595 | ||
| 455 | 596 | // notifyListeners runs the listeners registered for an object and opcode. | |
| @@ -466,11 +607,7 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) { | |||
| 466 | 607 | if !ok { | |
| 467 | 608 | return | |
| 468 | 609 | } | |
| 469 | - | handlerSlice, ok := handlers.(*[]func([]byte)) | |
| 470 | - | if !ok || handlerSlice == nil { | |
| 471 | - | return | |
| 472 | - | } | |
| 473 | - | for _, handler := range *handlerSlice { | |
| 610 | + | for _, handler := range handlers.([]func([]byte)) { | |
| 474 | 611 | if handler != nil { | |
| 475 | 612 | handler(body) | |
| 476 | 613 | } | |
| @@ -481,8 +618,8 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) { | |||
| 481 | 618 | func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error { | |
| 482 | 619 | switch opcode { | |
| 483 | 620 | case 0: // error | |
| 484 | - | if len(data) < 8 { | |
| 485 | - | return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame} | |
| 621 | + | if err := validateEventBody("object,uint,string,", data); err != nil { | |
| 622 | + | return &ProtocolError{Kind: "malformed_display_error", Err: err} | |
| 486 | 623 | } | |
| 487 | 624 | event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d} | |
| 488 | 625 | objectID := event.Uint32() | |
| @@ -495,11 +632,22 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error { | |||
| 495 | 632 | return d.lastError | |
| 496 | 633 | ||
| 497 | 634 | case 1: // delete_id | |
| 498 | - | if len(data) < 4 { | |
| 635 | + | if len(data) != 4 { | |
| 499 | 636 | return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame} | |
| 500 | 637 | } | |
| 501 | 638 | id := binary.LittleEndian.Uint32(data[0:4]) | |
| 502 | - | d.objects.Delete(id) | |
| 639 | + | d.idMu.Lock() | |
| 640 | + | _, live := d.objects.Load(id) | |
| 641 | + | if id < 2 || id >= 0xff000000 || id >= d.nextID || live || d.retiredIDs[id] { | |
| 642 | + | d.idMu.Unlock() | |
| 643 | + | return &ProtocolError{Kind: "invalid_delete_id", Object: id, Err: ErrMalformedFrame} | |
| 644 | + | } | |
| 645 | + | if d.retiredIDs == nil { | |
| 646 | + | d.retiredIDs = make(map[uint32]bool) | |
| 647 | + | } | |
| 648 | + | d.retiredIDs[id] = true | |
| 649 | + | d.freeIDs = append(d.freeIDs, id) | |
| 650 | + | d.idMu.Unlock() | |
| 503 | 651 | return nil | |
| 504 | 652 | ||
| 505 | 653 | default: | |
| @@ -507,55 +655,42 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error { | |||
| 507 | 655 | } | |
| 508 | 656 | } | |
| 509 | 657 | ||
| 510 | - | // Roundtrip performs a synchronous roundtrip to the compositor | |
| 658 | + | // Roundtrip waits for one wl_display.sync callback. Concurrent event loops | |
| 659 | + | // must not call Roundtrip while another goroutine dispatches this connection. | |
| 511 | 660 | func (d *Display) Roundtrip() error { | |
| 512 | - | callbackID := d.allocateID() | |
| 513 | - | done := make(chan error, 1) | |
| 514 | - | ||
| 515 | - | d.AddListener(callbackID, 0, func(_ []byte) { | |
| 516 | - | d.objects.Delete(callbackID) | |
| 517 | - | done <- nil | |
| 518 | - | }) | |
| 519 | - | ||
| 520 | - | // Send sync request (opcode 0) | |
| 521 | - | if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil { | |
| 661 | + | callback, err := d.Sync() | |
| 662 | + | if err != nil { | |
| 522 | 663 | return err | |
| 523 | 664 | } | |
| 524 | - | ||
| 525 | - | d.objects.Store(callbackID, &callbackObject{ | |
| 526 | - | BaseProxy: BaseProxy{ | |
| 527 | - | context: d.Context(), | |
| 528 | - | id: callbackID, | |
| 529 | - | }, | |
| 530 | - | display: d, | |
| 531 | - | }) | |
| 532 | - | ||
| 533 | - | for i := 0; i < 1000; i++ { | |
| 534 | - | if err := d.Dispatch(); err != nil { | |
| 665 | + | for { | |
| 666 | + | if err = d.Dispatch(); err != nil { | |
| 535 | 667 | return err | |
| 536 | 668 | } | |
| 537 | - | select { | |
| 538 | - | case err := <-done: | |
| 539 | - | return err | |
| 540 | - | default: | |
| 669 | + | if _, exists := d.objects.Load(callback.ID()); !exists { | |
| 670 | + | return nil | |
| 541 | 671 | } | |
| 542 | 672 | } | |
| 543 | - | ||
| 544 | - | return fmt.Errorf("roundtrip failed: max iterations reached") | |
| 545 | 673 | } | |
| 546 | 674 | ||
| 547 | 675 | // AddListener adds an event listener for an object | |
| 548 | 676 | func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) { | |
| 677 | + | if handler == nil { | |
| 678 | + | return | |
| 679 | + | } | |
| 549 | 680 | // Load or create listener map for object | |
| 550 | 681 | listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{}) | |
| 551 | 682 | opcodeMap := listeners.(*sync.Map) | |
| 552 | 683 | ||
| 553 | - | // Load or create handlers slice for opcode | |
| 554 | - | handlers, _ := opcodeMap.LoadOrStore(opcode, &[]func([]byte){}) | |
| 555 | - | handlerSlice := handlers.(*[]func([]byte)) | |
| 556 | - | ||
| 557 | - | // Add handler (thread-safe) | |
| 558 | - | *handlerSlice = append(*handlerSlice, handler) | |
| 684 | + | d.listenerMu.Lock() | |
| 685 | + | defer d.listenerMu.Unlock() | |
| 686 | + | var handlers []func([]byte) | |
| 687 | + | if old, ok := opcodeMap.Load(opcode); ok { | |
| 688 | + | handlers = old.([]func([]byte)) | |
| 689 | + | } | |
| 690 | + | next := make([]func([]byte), len(handlers)+1) | |
| 691 | + | copy(next, handlers) | |
| 692 | + | next[len(handlers)] = handler | |
| 693 | + | opcodeMap.Store(opcode, next) | |
| 559 | 694 | } | |
| 560 | 695 | ||
| 561 | 696 | // getRegistry gets the global registry | |
| @@ -581,15 +716,13 @@ func (r *Registry) ID() uint32 { | |||
| 581 | 716 | // handleGlobal handles global announcements | |
| 582 | 717 | func (r *Registry) handleGlobal(data []byte) { | |
| 583 | 718 | if len(data) < 8 { | |
| 584 | - | // log.Printf("wlclient: handleGlobal: data too short (%d bytes)", len(data)) | |
| 585 | 719 | return | |
| 586 | 720 | } | |
| 587 | 721 | ||
| 588 | 722 | name := binary.LittleEndian.Uint32(data[0:4]) | |
| 589 | 723 | ifaceLen := binary.LittleEndian.Uint32(data[4:8]) | |
| 590 | 724 | ||
| 591 | - | if len(data) < 8+int(ifaceLen)+4 { | |
| 592 | - | // log.Printf("wlclient: handleGlobal: insufficient data for interface string (need %d, have %d)", 8+int(ifaceLen)+4, len(data)) | |
| 725 | + | if ifaceLen == 0 || uint64(ifaceLen)+12 > uint64(len(data)) || data[8+ifaceLen-1] != 0 { | |
| 593 | 726 | return | |
| 594 | 727 | } | |
| 595 | 728 | ||
| @@ -600,15 +733,12 @@ func (r *Registry) handleGlobal(data []byte) { | |||
| 600 | 733 | padding := (4 - (ifaceLen % 4)) % 4 | |
| 601 | 734 | versionOffset := 8 + int(ifaceLen) + int(padding) | |
| 602 | 735 | ||
| 603 | - | if len(data) < versionOffset+4 { | |
| 604 | - | // log.Printf("wlclient: handleGlobal: insufficient data for version (need %d, have %d)", versionOffset+4, len(data)) | |
| 736 | + | if len(data) != versionOffset+4 { | |
| 605 | 737 | return | |
| 606 | 738 | } | |
| 607 | 739 | ||
| 608 | 740 | version := binary.LittleEndian.Uint32(data[versionOffset:]) | |
| 609 | 741 | ||
| 610 | - | // log.Printf("wlclient: global announced: %s v%d (name=%d)", iface, version, name) | |
| 611 | - | ||
| 612 | 742 | // Store global | |
| 613 | 743 | r.mu.Lock() | |
| 614 | 744 | r.globals[name] = Global{ | |
| @@ -619,15 +749,19 @@ func (r *Registry) handleGlobal(data []byte) { | |||
| 619 | 749 | r.mu.Unlock() | |
| 620 | 750 | ||
| 621 | 751 | // Call specific handler if registered | |
| 622 | - | if handler, ok := r.handlers[iface]; ok { | |
| 623 | - | // log.Printf("wlclient: calling handler for interface %s", iface) | |
| 752 | + | r.mu.RLock() | |
| 753 | + | handler, specific := r.handlers[iface] | |
| 754 | + | wildcard, all := r.handlers["*"] | |
| 755 | + | r.mu.RUnlock() | |
| 756 | + | if specific { | |
| 757 | + | ||
| 624 | 758 | handler(r, name, version) | |
| 625 | 759 | } | |
| 626 | 760 | ||
| 627 | 761 | // Call wildcard handler if registered | |
| 628 | - | if handler, ok := r.handlers["*"]; ok { | |
| 629 | - | // log.Printf("wlclient: calling wildcard handler for interface %s", iface) | |
| 630 | - | handler(r, name, version) | |
| 762 | + | if all { | |
| 763 | + | ||
| 764 | + | wildcard(r, name, version) | |
| 631 | 765 | } | |
| 632 | 766 | } | |
| 633 | 767 | ||
| @@ -641,11 +775,17 @@ func (r *Registry) handleGlobalRemove(data []byte) { | |||
| 641 | 775 | ||
| 642 | 776 | r.mu.Lock() | |
| 643 | 777 | delete(r.globals, name) | |
| 778 | + | handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...) | |
| 644 | 779 | r.mu.Unlock() | |
| 780 | + | for _, handler := range handlers { | |
| 781 | + | handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name}) | |
| 782 | + | } | |
| 645 | 783 | } | |
| 646 | 784 | ||
| 647 | 785 | // AddHandler adds a handler for a specific interface | |
| 648 | 786 | func (r *Registry) AddHandler(iface string, handler GlobalHandler) { | |
| 787 | + | r.mu.Lock() | |
| 788 | + | defer r.mu.Unlock() | |
| 649 | 789 | r.handlers[iface] = handler | |
| 650 | 790 | } | |
| 651 | 791 | ||
| @@ -656,7 +796,7 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) | |||
| 656 | 796 | proxy.SetID(r.display.allocateID()) | |
| 657 | 797 | } | |
| 658 | 798 | ||
| 659 | - | // Ensure proxy has a context set - CRITICAL FIX | |
| 799 | + | // Bootstrap a generated proxy with this display's context. | |
| 660 | 800 | if proxy.Context() == nil { | |
| 661 | 801 | if baseProxy, ok := proxy.(*BaseProxy); ok { | |
| 662 | 802 | baseProxy.SetContext(r.display.Context()) | |
| @@ -667,12 +807,18 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) | |||
| 667 | 807 | } | |
| 668 | 808 | } | |
| 669 | 809 | ||
| 810 | + | if proxy.Context() != r.display.Context() { | |
| 811 | + | return fmt.Errorf("proxy belongs to another display") | |
| 812 | + | } | |
| 813 | + | if proxy.ID() == displayObjectID || proxy.ID() == r.id { | |
| 814 | + | return fmt.Errorf("reserved object ID") | |
| 815 | + | } | |
| 816 | + | if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy { | |
| 817 | + | return fmt.Errorf("object ID %d already registered", proxy.ID()) | |
| 818 | + | } | |
| 670 | 819 | // Register the proxy | |
| 671 | 820 | proxy.Context().Register(proxy) | |
| 672 | 821 | ||
| 673 | - | // Also register directly in display objects map | |
| 674 | - | r.display.objects.Store(proxy.ID(), proxy) | |
| 675 | - | ||
| 676 | 822 | // Send bind request (opcode 0) with proper arguments | |
| 677 | 823 | if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil { | |
| 678 | 824 | proxy.Context().Unregister(proxy) | |
| @@ -718,118 +864,3 @@ func (r *Registry) FindGlobal(iface string) (Global, bool) { | |||
| 718 | 864 | } | |
| 719 | 865 | return Global{}, false | |
| 720 | 866 | } | |
| 721 | - | ||
| 722 | - | // handleServerObject creates and registers server-allocated objects from events | |
| 723 | - | func (d *Display) handleServerObject(objectID uint32, opcode uint16, body []byte) bool { | |
| 724 | - | // Handle specific known protocols that create server objects | |
| 725 | - | obj, ok := d.objects.Load(objectID) | |
| 726 | - | if !ok { | |
| 727 | - | return false | |
| 728 | - | } | |
| 729 | - | ||
| 730 | - | // Check if this is a known object type that creates server objects | |
| 731 | - | switch obj.(type) { | |
| 732 | - | case *Registry: | |
| 733 | - | // Registry doesn't create server objects directly | |
| 734 | - | return false | |
| 735 | - | ||
| 736 | - | default: | |
| 737 | - | // Check if this might be an output manager (interface zwlr_output_manager_v1) | |
| 738 | - | if objectID == 5 { // This is likely the output manager based on your logs | |
| 739 | - | return d.handleOutputManagerEvent(objectID, opcode, body) | |
| 740 | - | } | |
| 741 | - | } | |
| 742 | - | ||
| 743 | - | return false | |
| 744 | - | } | |
| 745 | - | ||
| 746 | - | // handleOutputManagerEvent handles events from zwlr_output_manager_v1 | |
| 747 | - | func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body []byte) bool { | |
| 748 | - | switch opcode { | |
| 749 | - | case 0: // head event - creates new zwlr_output_head_v1 object | |
| 750 | - | if len(body) < 4 { | |
| 751 | - | return false | |
| 752 | - | } | |
| 753 | - | ||
| 754 | - | // Parse the new_id for the head object | |
| 755 | - | headID := binary.LittleEndian.Uint32(body[0:4]) | |
| 756 | - | ||
| 757 | - | // Create a proper OutputHead object | |
| 758 | - | headProxy := &OutputHead{ | |
| 759 | - | BaseProxy: BaseProxy{ | |
| 760 | - | id: headID, | |
| 761 | - | context: d.Context(), | |
| 762 | - | }, | |
| 763 | - | } | |
| 764 | - | ||
| 765 | - | // Register the new head object | |
| 766 | - | d.objects.Store(headID, headProxy) | |
| 767 | - | return true | |
| 768 | - | ||
| 769 | - | case 1: // done event | |
| 770 | - | return false | |
| 771 | - | ||
| 772 | - | case 2: // finished event | |
| 773 | - | return false | |
| 774 | - | } | |
| 775 | - | ||
| 776 | - | return false | |
| 777 | - | } | |
| 778 | - | ||
| 779 | - | // OutputHead represents a zwlr_output_head_v1 object | |
| 780 | - | type OutputHead struct { | |
| 781 | - | BaseProxy | |
| 782 | - | name string | |
| 783 | - | description string | |
| 784 | - | width int32 | |
| 785 | - | height int32 | |
| 786 | - | } | |
| 787 | - | ||
| 788 | - | // Dispatch handles head events | |
| 789 | - | func (h *OutputHead) Dispatch(event *Event) { | |
| 790 | - | switch event.Opcode { | |
| 791 | - | case 0: // name | |
| 792 | - | h.name = event.String() | |
| 793 | - | case 1: // description | |
| 794 | - | h.description = event.String() | |
| 795 | - | case 2: // physical_size | |
| 796 | - | h.width = event.Int32() | |
| 797 | - | h.height = event.Int32() | |
| 798 | - | case 3: // mode (creates new mode object) | |
| 799 | - | modeID := event.Uint32() | |
| 800 | - | // Create mode object | |
| 801 | - | mode := &OutputMode{ | |
| 802 | - | BaseProxy: BaseProxy{ | |
| 803 | - | id: modeID, | |
| 804 | - | context: h.context, | |
| 805 | - | }, | |
| 806 | - | } | |
| 807 | - | h.context.display.objects.Store(modeID, mode) | |
| 808 | - | case 9: // finished | |
| 809 | - | h.context.Unregister(h) | |
| 810 | - | default: | |
| 811 | - | } | |
| 812 | - | } | |
| 813 | - | ||
| 814 | - | // OutputMode represents a zwlr_output_mode_v1 object | |
| 815 | - | type OutputMode struct { | |
| 816 | - | BaseProxy | |
| 817 | - | width int32 | |
| 818 | - | height int32 | |
| 819 | - | refresh int32 | |
| 820 | - | } | |
| 821 | - | ||
| 822 | - | // Dispatch handles mode events | |
| 823 | - | func (m *OutputMode) Dispatch(event *Event) { | |
| 824 | - | switch event.Opcode { | |
| 825 | - | case 0: // size | |
| 826 | - | m.width = event.Int32() | |
| 827 | - | m.height = event.Int32() | |
| 828 | - | case 1: // refresh | |
| 829 | - | m.refresh = event.Int32() | |
| 830 | - | case 2: // preferred | |
| 831 | - | case 3: // finished | |
| 832 | - | m.context.Unregister(m) | |
| 833 | - | default: | |
| 834 | - | } | |
| 835 | - | } | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "net" | 13 | "net" | ||
| 14 | "os" | 14 | "os" | ||
| 15 | "path/filepath" | 15 | "path/filepath" | ||
| 16 | + | "strings" | |||
| 16 | "sync" | 17 | "sync" | ||
| 17 | "sync/atomic" | 18 | "sync/atomic" | ||
| 18 | ) | 19 | ) | ||
| @@ -44,13 +45,19 @@ type Object interface { | |||||
| 44 | 45 | ||||
| 45 | // Display represents a connection to the Wayland display | 46 | // Display represents a connection to the Wayland display | ||
| 46 | type Display struct { | 47 | type Display struct { | ||
| 47 | - | conn net.Conn | 48 | + | conn net.Conn |
| 48 | - | unix *net.UnixConn // set when conn is a Unix socket, nil otherwise | 49 | + | unix *net.UnixConn // set when conn is a Unix socket, nil otherwise |
| 49 | - | objects sync.Map // map[uint32]Object | 50 | + | objects sync.Map // map[uint32]Object |
| 50 | - | nextID uint32 | 51 | + | nextID uint32 |
| 51 | - | sendMu sync.Mutex | 52 | + | idMu sync.Mutex |
| 52 | - | recvMu sync.Mutex | 53 | + | freeIDs []uint32 |
| 53 | - | listeners sync.Map // map[uint32]map[uint16][]func([]byte) | 54 | + | retiredIDs map[uint32]bool |
| 55 | + | sendMu sync.Mutex | |||
| 56 | + | recvMu sync.Mutex | |||
| 57 | + | dispatchMu sync.Mutex // serialize readers without holding receive state across handlers | |||
| 58 | + | signatures sync.Map | |||
| 59 | + | listenerMu sync.Mutex | |||
| 60 | + | listeners sync.Map // map[uint32]map[uint16][]func([]byte) | |||
| 54 | 61 | ||||
| 55 | // High-performance event dispatcher | 62 | // High-performance event dispatcher | ||
| 56 | dispatcher *EventDispatcher | 63 | dispatcher *EventDispatcher | ||
| @@ -73,7 +80,6 @@ type Display struct { | |||||
| 73 | readErr error | 80 | readErr error | ||
| 74 | maxMsgLen uint32 | 81 | maxMsgLen uint32 | ||
| 75 | closed atomic.Bool | 82 | closed atomic.Bool | ||
| 76 | - | fdMu sync.Mutex | |||
| 77 | pendingFDs []int | 83 | pendingFDs []int | ||
| 78 | } | 84 | } | ||
| 79 | 85 | ||||
| @@ -103,11 +109,12 @@ func newDisplay(conn net.Conn) *Display { | |||||
| 103 | 109 | ||||
| 104 | // Registry represents the global registry | 110 | // Registry represents the global registry | ||
| 105 | type Registry struct { | 111 | type Registry struct { | ||
| 106 | - | id uint32 | 112 | + | id uint32 |
| 107 | - | display *Display | 113 | + | display *Display |
| 108 | - | globals map[uint32]Global | 114 | + | globals map[uint32]Global |
| 109 | - | mu sync.RWMutex | 115 | + | mu sync.RWMutex |
| 110 | - | handlers map[string]GlobalHandler | 116 | + | handlers map[string]GlobalHandler |
| 117 | + | removeHandlers []RegistryGlobalRemoveHandler | |||
| 111 | } | 118 | } | ||
| 112 | 119 | ||||
| 113 | // callbackObject represents a wl_callback object | 120 | // callbackObject represents a wl_callback object | ||
| @@ -119,6 +126,12 @@ type callbackObject struct { | |||||
| 119 | func (c *callbackObject) ID() uint32 { | 126 | func (c *callbackObject) ID() uint32 { | ||
| 120 | return c.id | 127 | return c.id | ||
| 121 | } | 128 | } | ||
| 129 | + | func (c *callbackObject) EventSignature(op uint16) (string, bool) { | |||
| 130 | + | if op == 0 { | |||
| 131 | + | return "uint,", true | |||
| 132 | + | } | |||
| 133 | + | return "", false | |||
| 134 | + | } | |||
| 122 | 135 | ||||
| 123 | // Dispatch handles callback events (opcode 0 = done) | 136 | // Dispatch handles callback events (opcode 0 = done) | ||
| 124 | func (c *callbackObject) Dispatch(event *Event) { | 137 | func (c *callbackObject) Dispatch(event *Event) { | ||
| @@ -126,6 +139,7 @@ func (c *callbackObject) Dispatch(event *Event) { | |||||
| 126 | return | 139 | return | ||
| 127 | } | 140 | } | ||
| 128 | c.display.notifyListeners(c.id, 0, event.data) | 141 | c.display.notifyListeners(c.id, 0, event.data) | ||
| 142 | + | c.display.context.Unregister(c) | |||
| 129 | } | 143 | } | ||
| 130 | 144 | ||||
| 131 | // Global represents a global object | 145 | // Global represents a global object | ||
| @@ -138,6 +152,16 @@ type Global struct { | |||||
| 138 | // GlobalHandler is called when a global is announced | 152 | // GlobalHandler is called when a global is announced | ||
| 139 | type GlobalHandler func(registry *Registry, name uint32, version uint32) | 153 | type GlobalHandler func(registry *Registry, name uint32, version uint32) | ||
| 140 | 154 | ||||
| 155 | + | // ConnectFromConn attaches to an established Wayland socket and requests its registry. | |||
| 156 | + | func ConnectFromConn(conn net.Conn) (*Display, error) { | |||
| 157 | + | d := newDisplay(conn) | |||
| 158 | + | if err := d.getRegistry(); err != nil { | |||
| 159 | + | _ = d.Close() | |||
| 160 | + | return nil, fmt.Errorf("get registry: %w", err) | |||
| 161 | + | } | |||
| 162 | + | return d, nil | |||
| 163 | + | } | |||
| 164 | + | ||||
| 141 | // Connect connects to the Wayland display and requests the registry. | 165 | // Connect connects to the Wayland display and requests the registry. | ||
| 142 | func Connect(socketPath string) (*Display, error) { | 166 | func Connect(socketPath string) (*Display, error) { | ||
| 143 | if socketPath == "" { | 167 | if socketPath == "" { | ||
| @@ -161,16 +185,7 @@ func Connect(socketPath string) (*Display, error) { | |||||
| 161 | return nil, fmt.Errorf("failed to connect to Wayland: %w", err) | 185 | return nil, fmt.Errorf("failed to connect to Wayland: %w", err) | ||
| 162 | } | 186 | } | ||
| 163 | 187 | ||||
| 164 | - | d := newDisplay(conn) | 188 | + | return ConnectFromConn(conn) |
| 165 | - | ||||
| 166 | - | // Get registry | |||
| 167 | - | if err := d.getRegistry(); err != nil { | |||
| 168 | - | _ = d.Close() | |||
| 169 | - | return nil, fmt.Errorf("failed to get registry: %w", err) | |||
| 170 | - | } | |||
| 171 | - | ||||
| 172 | - | // Don't do initial roundtrip here - let the caller do it after setting up handlers | |||
| 173 | - | return d, nil | |||
| 174 | } | 189 | } | ||
| 175 | 190 | ||||
| 176 | // Close closes the display connection. It is safe to call more than once; | 191 | // Close closes the display connection. It is safe to call more than once; | ||
| @@ -179,8 +194,12 @@ func (d *Display) Close() error { | |||||
| 179 | if !d.closed.CompareAndSwap(false, true) { | 194 | if !d.closed.CompareAndSwap(false, true) { | ||
| 180 | return nil | 195 | return nil | ||
| 181 | } | 196 | } | ||
| 197 | + | d.context.closed.Store(true) | |||
| 198 | + | err := d.conn.Close() | |||
| 199 | + | d.recvMu.Lock() | |||
| 182 | d.closePendingFDs() | 200 | d.closePendingFDs() | ||
| 183 | - | return d.conn.Close() | 201 | + | d.recvMu.Unlock() |
| 202 | + | return err | |||
| 184 | } | 203 | } | ||
| 185 | 204 | ||||
| 186 | // Closed reports whether the connection has been closed. | 205 | // Closed reports whether the connection has been closed. | ||
| @@ -200,7 +219,20 @@ func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler E | |||||
| 200 | 219 | ||||
| 201 | // allocateID allocates a new object ID | 220 | // allocateID allocates a new object ID | ||
| 202 | func (d *Display) allocateID() uint32 { | 221 | func (d *Display) allocateID() uint32 { | ||
| 203 | - | return atomic.AddUint32(&d.nextID, 1) - 1 | 222 | + | d.idMu.Lock() |
| 223 | + | defer d.idMu.Unlock() | |||
| 224 | + | if n := len(d.freeIDs); n != 0 { | |||
| 225 | + | id := d.freeIDs[n-1] | |||
| 226 | + | d.freeIDs = d.freeIDs[:n-1] | |||
| 227 | + | delete(d.retiredIDs, id) | |||
| 228 | + | return id | |||
| 229 | + | } | |||
| 230 | + | if d.nextID == 0 || d.nextID >= 0xff000000 { | |||
| 231 | + | return 0 // exhausted; never allocate a reserved server ID | |||
| 232 | + | } | |||
| 233 | + | id := d.nextID | |||
| 234 | + | d.nextID++ | |||
| 235 | + | return id | |||
| 204 | } | 236 | } | ||
| 205 | 237 | ||||
| 206 | // AllocateID allocates a new object ID (public method) | 238 | // AllocateID allocates a new object ID (public method) | ||
| @@ -215,6 +247,12 @@ func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{ | |||||
| 215 | 247 | ||||
| 216 | // SendRequestWithFDs sends a request with file descriptors | 248 | // SendRequestWithFDs sends a request with file descriptors | ||
| 217 | func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error { | 249 | func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error { | ||
| 250 | + | return d.sendRequest(objectID, opcode, fds, nil, args) | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | // sendRequest marshals a request, then runs guard and writes the message | |||
| 254 | + | // under the send lock. Guard failures and marshaling errors write nothing. | |||
| 255 | + | func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}) error { | |||
| 218 | // Get buffer from pool | 256 | // Get buffer from pool | ||
| 219 | buf := bufferPool.Get().(*bytes.Buffer) | 257 | buf := bufferPool.Get().(*bytes.Buffer) | ||
| 220 | defer func() { | 258 | defer func() { | ||
| @@ -235,8 +273,8 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, | |||||
| 235 | 273 | ||||
| 236 | // Update header with actual size in bytes | 274 | // Update header with actual size in bytes | ||
| 237 | bufLen := buf.Len() | 275 | bufLen := buf.Len() | ||
| 238 | - | if bufLen > 0xFFFF { | 276 | + | if bufLen > 0xFFFF || bufLen%4 != 0 { |
| 239 | - | return fmt.Errorf("message too large: %d bytes", bufLen) | 277 | + | return fmt.Errorf("invalid message size: %d bytes", bufLen) |
| 240 | } | 278 | } | ||
| 241 | size := uint32(bufLen) // Safe: checked above | 279 | size := uint32(bufLen) // Safe: checked above | ||
| 242 | binary.LittleEndian.PutUint32(header[0:4], objectID) | 280 | binary.LittleEndian.PutUint32(header[0:4], objectID) | ||
| @@ -248,7 +286,7 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, | |||||
| 248 | copy(data[0:8], header) | 286 | copy(data[0:8], header) | ||
| 249 | 287 | ||||
| 250 | // Send message with optional file descriptors | 288 | // Send message with optional file descriptors | ||
| 251 | - | return d.sendmsgWithFDs(data, fds) | 289 | + | return d.sendmsgWithFDs(data, fds, guard) |
| 252 | } | 290 | } | ||
| 253 | 291 | ||||
| 254 | // marshalArg marshals a single argument | 292 | // marshalArg marshals a single argument | ||
| @@ -263,7 +301,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error { | |||||
| 263 | case string: | 301 | case string: | ||
| 264 | // String format: length (including null) + string + null + padding | 302 | // String format: length (including null) + string + null + padding | ||
| 265 | strlen := len(v) + 1 | 303 | strlen := len(v) + 1 | ||
| 266 | - | if strlen > 0xFFFFFFFF { | 304 | + | if strlen > 0xffff-HeaderSize { |
| 267 | return fmt.Errorf("string too long: %d bytes", strlen) | 305 | return fmt.Errorf("string too long: %d bytes", strlen) | ||
| 268 | } | 306 | } | ||
| 269 | if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above | 307 | if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above | ||
| @@ -279,7 +317,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error { | |||||
| 279 | case []byte: | 317 | case []byte: | ||
| 280 | // Array format: length + data + padding | 318 | // Array format: length + data + padding | ||
| 281 | arrlen := len(v) | 319 | arrlen := len(v) | ||
| 282 | - | if arrlen > 0xFFFFFFFF { | 320 | + | if arrlen > 0xffff-HeaderSize { |
| 283 | return fmt.Errorf("array too long: %d bytes", arrlen) | 321 | return fmt.Errorf("array too long: %d bytes", arrlen) | ||
| 284 | } | 322 | } | ||
| 285 | if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil { | 323 | if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil { | ||
| @@ -321,14 +359,20 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error { | |||||
| 321 | // Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a | 359 | // Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a | ||
| 322 | // malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame. | 360 | // malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame. | ||
| 323 | func (d *Display) Dispatch() error { | 361 | func (d *Display) Dispatch() error { | ||
| 362 | + | d.dispatchMu.Lock() | |||
| 363 | + | defer d.dispatchMu.Unlock() | |||
| 324 | d.recvMu.Lock() | 364 | d.recvMu.Lock() | ||
| 325 | - | defer d.recvMu.Unlock() | |||
| 326 | 365 | ||||
| 327 | if d.closed.Load() { | 366 | if d.closed.Load() { | ||
| 367 | + | d.recvMu.Unlock() | |||
| 328 | return net.ErrClosed | 368 | return net.ErrClosed | ||
| 329 | } | 369 | } | ||
| 330 | 370 | ||||
| 331 | frame, err := d.nextFrame() | 371 | frame, err := d.nextFrame() | ||
| 372 | + | d.recvMu.Unlock() | |||
| 373 | + | if d.closed.Load() { | |||
| 374 | + | return net.ErrClosed | |||
| 375 | + | } | |||
| 332 | if err != nil { | 376 | if err != nil { | ||
| 333 | return d.fail(err) | 377 | return d.fail(err) | ||
| 334 | } | 378 | } | ||
| @@ -342,10 +386,15 @@ func (d *Display) fail(err error) error { | |||||
| 342 | if err == nil { | 386 | if err == nil { | ||
| 343 | return nil | 387 | return nil | ||
| 344 | } | 388 | } | ||
| 389 | + | d.recvMu.Lock() | |||
| 390 | + | defer d.recvMu.Unlock() | |||
| 345 | var perr *ProtocolError | 391 | var perr *ProtocolError | ||
| 346 | if errors.As(err, &perr) { | 392 | if errors.As(err, &perr) { | ||
| 347 | d.closePendingFDs() | 393 | d.closePendingFDs() | ||
| 348 | } | 394 | } | ||
| 395 | + | if errors.Is(err, io.ErrUnexpectedEOF) { | |||
| 396 | + | d.closePendingFDs() | |||
| 397 | + | } | |||
| 349 | return err | 398 | return err | ||
| 350 | } | 399 | } | ||
| 351 | 400 | ||||
| @@ -376,6 +425,9 @@ func (d *Display) nextFrame() (receivedFrame, error) { | |||||
| 376 | } | 425 | } | ||
| 377 | } | 426 | } | ||
| 378 | if err := d.fill(); err != nil { | 427 | if err := d.fill(); err != nil { | ||
| 428 | + | if d.closed.Load() { | |||
| 429 | + | return receivedFrame{}, net.ErrClosed | |||
| 430 | + | } | |||
| 379 | return receivedFrame{}, err | 431 | return receivedFrame{}, err | ||
| 380 | } | 432 | } | ||
| 381 | } | 433 | } | ||
| @@ -416,40 +468,129 @@ func (d *Display) fill() error { | |||||
| 416 | return nil | 468 | return nil | ||
| 417 | } | 469 | } | ||
| 418 | 470 | ||||
| 419 | - | // dispatchFrame delivers one complete frame to the object that owns it. | 471 | + | // signatureProvider is implemented by generated objects; every opcode must |
| 472 | + | // have a known signature before any descriptor can be assigned or dispatched. | |||
| 473 | + | type signatureProvider interface{ EventSignature(uint16) (string, bool) } | |||
| 474 | + | ||||
| 475 | + | // dispatchFrame delivers one complete frame with precisely its signature's FDs. | |||
| 420 | func (d *Display) dispatchFrame(f receivedFrame) error { | 476 | func (d *Display) dispatchFrame(f receivedFrame) error { | ||
| 421 | - | if f.object == displayObjectID { | 477 | + | d.recvMu.Lock() |
| 478 | + | obj, ev, kind, err := d.prepareFrame(f) | |||
| 479 | + | d.recvMu.Unlock() | |||
| 480 | + | if err != nil { | |||
| 481 | + | return err | |||
| 482 | + | } | |||
| 483 | + | switch kind { | |||
| 484 | + | case 1: | |||
| 422 | return d.handleDisplayEvent(f.opcode, f.body) | 485 | return d.handleDisplayEvent(f.opcode, f.body) | ||
| 486 | + | case 2: | |||
| 487 | + | d.notifyListeners(f.object, f.opcode, f.body) | |||
| 488 | + | return nil | |||
| 489 | + | } | |||
| 490 | + | defer func() { | |||
| 491 | + | for _, fd := range ev.fds { | |||
| 492 | + | _ = fd.Close() | |||
| 493 | + | } | |||
| 494 | + | *ev = Event{} | |||
| 495 | + | eventPool.Put(ev) | |||
| 496 | + | }() | |||
| 497 | + | if proxy, ok := obj.(Proxy); ok { | |||
| 498 | + | proxy.Dispatch(ev) | |||
| 499 | + | } else { | |||
| 500 | + | d.dispatcher.Dispatch(f.object, f.opcode, f.body) | |||
| 501 | + | d.notifyListeners(f.object, f.opcode, f.body) | |||
| 423 | } | 502 | } | ||
| 503 | + | return nil | |||
| 504 | + | } | |||
| 424 | 505 | ||||
| 506 | + | // prepareFrame assigns owned descriptors under recvMu, but never calls user code. | |||
| 507 | + | func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) { | |||
| 508 | + | if d.closed.Load() { | |||
| 509 | + | return nil, nil, 0, net.ErrClosed | |||
| 510 | + | } | |||
| 511 | + | if f.object == displayObjectID { | |||
| 512 | + | if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 { | |||
| 513 | + | return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame} | |||
| 514 | + | } | |||
| 515 | + | return nil, nil, 1, nil | |||
| 516 | + | } | |||
| 517 | + | if f.object == d.registry.id { | |||
| 518 | + | var sig string | |||
| 519 | + | switch f.opcode { | |||
| 520 | + | case 0: | |||
| 521 | + | sig = "uint,string,uint," | |||
| 522 | + | case 1: | |||
| 523 | + | sig = "uint," | |||
| 524 | + | default: | |||
| 525 | + | return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode} | |||
| 526 | + | } | |||
| 527 | + | if err := validateEventBody(sig, f.body); err != nil { | |||
| 528 | + | return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err} | |||
| 529 | + | } | |||
| 530 | + | if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 { | |||
| 531 | + | return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame} | |||
| 532 | + | } | |||
| 533 | + | return nil, nil, 2, nil | |||
| 534 | + | } | |||
| 425 | obj, ok := d.objects.Load(f.object) | 535 | obj, ok := d.objects.Load(f.object) | ||
| 426 | if !ok { | 536 | if !ok { | ||
| 427 | - | return &ProtocolError{ | 537 | + | return nil, nil, 0, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject} |
| 428 | - | Kind: "unknown_object", | 538 | + | } |
| 429 | - | Object: f.object, | 539 | + | sig, valid := "", false |
| 430 | - | Opcode: f.opcode, | 540 | + | p, isGenerated := obj.(signatureProvider) |
| 431 | - | Size: uint32(len(f.body)) + HeaderSize, | 541 | + | if isGenerated { |
| 432 | - | Err: ErrUnknownObject, | 542 | + | sig, valid = p.EventSignature(f.opcode) |
| 543 | + | } | |||
| 544 | + | // Legacy custom proxies may declare their signatures with RegisterEventSignature. | |||
| 545 | + | if !valid && !isGenerated { | |||
| 546 | + | if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok { | |||
| 547 | + | sig, valid = v.(string), true | |||
| 548 | + | } | |||
| 549 | + | } | |||
| 550 | + | if !valid { | |||
| 551 | + | return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode} | |||
| 552 | + | } | |||
| 553 | + | if err := validateEventBody(sig, f.body); err != nil { | |||
| 554 | + | return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err} | |||
| 555 | + | } | |||
| 556 | + | count := strings.Count(sig, "fd,") | |||
| 557 | + | if len(d.pendingFDs) < count { | |||
| 558 | + | return nil, nil, 0, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame} | |||
| 559 | + | } | |||
| 560 | + | ev := eventPool.Get().(*Event) | |||
| 561 | + | *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d} | |||
| 562 | + | if count != 0 { | |||
| 563 | + | ev.fds = make([]*OwnedFD, count) | |||
| 564 | + | for i := range ev.fds { | |||
| 565 | + | ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]} | |||
| 566 | + | } | |||
| 567 | + | d.pendingFDs = d.pendingFDs[count:] | |||
| 568 | + | } | |||
| 569 | + | // FDs from a single recvmsg can accompany multiple coalesced frames. Once | |||
| 570 | + | // all buffered bytes have been framed, surplus descriptors are not legal. | |||
| 571 | + | if len(d.rbuf) == 0 && len(d.pendingFDs) != 0 { | |||
| 572 | + | for _, fd := range ev.fds { | |||
| 573 | + | _ = fd.Close() | |||
| 433 | } | 574 | } | ||
| 575 | + | *ev = Event{} | |||
| 576 | + | eventPool.Put(ev) | |||
| 577 | + | return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame} | |||
| 434 | } | 578 | } | ||
| 579 | + | return obj.(Object), ev, 0, nil | |||
| 580 | + | } | |||
| 435 | 581 | ||||
| 436 | - | // Server-created objects are announced by an event on a known object. | 582 | + | type signatureKey struct { |
| 437 | - | d.handleServerObject(f.object, f.opcode, f.body) | 583 | + | object uint32 |
| 584 | + | opcode uint16 | |||
| 585 | + | } | |||
| 438 | 586 | ||||
| 439 | - | if proxy, ok := obj.(Proxy); ok && proxy != nil { | 587 | + | // RegisterEventSignature defines a signature for a legacy proxy. Generated |
| 440 | - | proxy.Dispatch(&Event{ | 588 | + | // proxies supply it themselves and cannot be overridden. |
| 441 | - | ProxyID: f.object, | 589 | + | func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) { |
| 442 | - | Opcode: f.opcode, | 590 | + | if object == 0 || object == displayObjectID { |
| 443 | - | data: f.body, | 591 | + | return |
| 444 | - | display: d, | |||
| 445 | - | }) | |||
| 446 | - | return nil | |||
| 447 | } | 592 | } | ||
| 448 | - | 593 | + | d.signatures.Store(signatureKey{object, opcode}, signature) | |
| 449 | - | // High-performance dispatcher, then compatibility listeners. | |||
| 450 | - | d.dispatcher.Dispatch(f.object, f.opcode, f.body) | |||
| 451 | - | d.notifyListeners(f.object, f.opcode, f.body) | |||
| 452 | - | return nil | |||
| 453 | } | 594 | } | ||
| 454 | 595 | ||||
| 455 | // notifyListeners runs the listeners registered for an object and opcode. | 596 | // notifyListeners runs the listeners registered for an object and opcode. | ||
| @@ -466,11 +607,7 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) { | |||||
| 466 | if !ok { | 607 | if !ok { | ||
| 467 | return | 608 | return | ||
| 468 | } | 609 | } | ||
| 469 | - | handlerSlice, ok := handlers.(*[]func([]byte)) | 610 | + | for _, handler := range handlers.([]func([]byte)) { |
| 470 | - | if !ok || handlerSlice == nil { | |||
| 471 | - | return | |||
| 472 | - | } | |||
| 473 | - | for _, handler := range *handlerSlice { | |||
| 474 | if handler != nil { | 611 | if handler != nil { | ||
| 475 | handler(body) | 612 | handler(body) | ||
| 476 | } | 613 | } | ||
| @@ -481,8 +618,8 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) { | |||||
| 481 | func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error { | 618 | func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error { | ||
| 482 | switch opcode { | 619 | switch opcode { | ||
| 483 | case 0: // error | 620 | case 0: // error | ||
| 484 | - | if len(data) < 8 { | 621 | + | if err := validateEventBody("object,uint,string,", data); err != nil { |
| 485 | - | return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame} | 622 | + | return &ProtocolError{Kind: "malformed_display_error", Err: err} |
| 486 | } | 623 | } | ||
| 487 | event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d} | 624 | event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d} | ||
| 488 | objectID := event.Uint32() | 625 | objectID := event.Uint32() | ||
| @@ -495,11 +632,22 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error { | |||||
| 495 | return d.lastError | 632 | return d.lastError | ||
| 496 | 633 | ||||
| 497 | case 1: // delete_id | 634 | case 1: // delete_id | ||
| 498 | - | if len(data) < 4 { | 635 | + | if len(data) != 4 { |
| 499 | return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame} | 636 | return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame} | ||
| 500 | } | 637 | } | ||
| 501 | id := binary.LittleEndian.Uint32(data[0:4]) | 638 | id := binary.LittleEndian.Uint32(data[0:4]) | ||
| 502 | - | d.objects.Delete(id) | 639 | + | d.idMu.Lock() |
| 640 | + | _, live := d.objects.Load(id) | |||
| 641 | + | if id < 2 || id >= 0xff000000 || id >= d.nextID || live || d.retiredIDs[id] { | |||
| 642 | + | d.idMu.Unlock() | |||
| 643 | + | return &ProtocolError{Kind: "invalid_delete_id", Object: id, Err: ErrMalformedFrame} | |||
| 644 | + | } | |||
| 645 | + | if d.retiredIDs == nil { | |||
| 646 | + | d.retiredIDs = make(map[uint32]bool) | |||
| 647 | + | } | |||
| 648 | + | d.retiredIDs[id] = true | |||
| 649 | + | d.freeIDs = append(d.freeIDs, id) | |||
| 650 | + | d.idMu.Unlock() | |||
| 503 | return nil | 651 | return nil | ||
| 504 | 652 | ||||
| 505 | default: | 653 | default: | ||
| @@ -507,55 +655,42 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error { | |||||
| 507 | } | 655 | } | ||
| 508 | } | 656 | } | ||
| 509 | 657 | ||||
| 510 | - | // Roundtrip performs a synchronous roundtrip to the compositor | 658 | + | // Roundtrip waits for one wl_display.sync callback. Concurrent event loops |
| 659 | + | // must not call Roundtrip while another goroutine dispatches this connection. | |||
| 511 | func (d *Display) Roundtrip() error { | 660 | func (d *Display) Roundtrip() error { | ||
| 512 | - | callbackID := d.allocateID() | 661 | + | callback, err := d.Sync() |
| 513 | - | done := make(chan error, 1) | 662 | + | if err != nil { |
| 514 | - | ||||
| 515 | - | d.AddListener(callbackID, 0, func(_ []byte) { | |||
| 516 | - | d.objects.Delete(callbackID) | |||
| 517 | - | done <- nil | |||
| 518 | - | }) | |||
| 519 | - | ||||
| 520 | - | // Send sync request (opcode 0) | |||
| 521 | - | if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil { | |||
| 522 | return err | 663 | return err | ||
| 523 | } | 664 | } | ||
| 524 | - | 665 | + | for { | |
| 525 | - | d.objects.Store(callbackID, &callbackObject{ | 666 | + | if err = d.Dispatch(); err != nil { |
| 526 | - | BaseProxy: BaseProxy{ | |||
| 527 | - | context: d.Context(), | |||
| 528 | - | id: callbackID, | |||
| 529 | - | }, | |||
| 530 | - | display: d, | |||
| 531 | - | }) | |||
| 532 | - | ||||
| 533 | - | for i := 0; i < 1000; i++ { | |||
| 534 | - | if err := d.Dispatch(); err != nil { | |||
| 535 | return err | 667 | return err | ||
| 536 | } | 668 | } | ||
| 537 | - | select { | 669 | + | if _, exists := d.objects.Load(callback.ID()); !exists { |
| 538 | - | case err := <-done: | 670 | + | return nil |
| 539 | - | return err | |||
| 540 | - | default: | |||
| 541 | } | 671 | } | ||
| 542 | } | 672 | } | ||
| 543 | - | ||||
| 544 | - | return fmt.Errorf("roundtrip failed: max iterations reached") | |||
| 545 | } | 673 | } | ||
| 546 | 674 | ||||
| 547 | // AddListener adds an event listener for an object | 675 | // AddListener adds an event listener for an object | ||
| 548 | func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) { | 676 | func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) { | ||
| 677 | + | if handler == nil { | |||
| 678 | + | return | |||
| 679 | + | } | |||
| 549 | // Load or create listener map for object | 680 | // Load or create listener map for object | ||
| 550 | listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{}) | 681 | listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{}) | ||
| 551 | opcodeMap := listeners.(*sync.Map) | 682 | opcodeMap := listeners.(*sync.Map) | ||
| 552 | 683 | ||||
| 553 | - | // Load or create handlers slice for opcode | 684 | + | d.listenerMu.Lock() |
| 554 | - | handlers, _ := opcodeMap.LoadOrStore(opcode, &[]func([]byte){}) | 685 | + | defer d.listenerMu.Unlock() |
| 555 | - | handlerSlice := handlers.(*[]func([]byte)) | 686 | + | var handlers []func([]byte) |
| 556 | - | 687 | + | if old, ok := opcodeMap.Load(opcode); ok { | |
| 557 | - | // Add handler (thread-safe) | 688 | + | handlers = old.([]func([]byte)) |
| 558 | - | *handlerSlice = append(*handlerSlice, handler) | 689 | + | } |
| 690 | + | next := make([]func([]byte), len(handlers)+1) | |||
| 691 | + | copy(next, handlers) | |||
| 692 | + | next[len(handlers)] = handler | |||
| 693 | + | opcodeMap.Store(opcode, next) | |||
| 559 | } | 694 | } | ||
| 560 | 695 | ||||
| 561 | // getRegistry gets the global registry | 696 | // getRegistry gets the global registry | ||
| @@ -581,15 +716,13 @@ func (r *Registry) ID() uint32 { | |||||
| 581 | // handleGlobal handles global announcements | 716 | // handleGlobal handles global announcements | ||
| 582 | func (r *Registry) handleGlobal(data []byte) { | 717 | func (r *Registry) handleGlobal(data []byte) { | ||
| 583 | if len(data) < 8 { | 718 | if len(data) < 8 { | ||
| 584 | - | // log.Printf("wlclient: handleGlobal: data too short (%d bytes)", len(data)) | |||
| 585 | return | 719 | return | ||
| 586 | } | 720 | } | ||
| 587 | 721 | ||||
| 588 | name := binary.LittleEndian.Uint32(data[0:4]) | 722 | name := binary.LittleEndian.Uint32(data[0:4]) | ||
| 589 | ifaceLen := binary.LittleEndian.Uint32(data[4:8]) | 723 | ifaceLen := binary.LittleEndian.Uint32(data[4:8]) | ||
| 590 | 724 | ||||
| 591 | - | if len(data) < 8+int(ifaceLen)+4 { | 725 | + | if ifaceLen == 0 || uint64(ifaceLen)+12 > uint64(len(data)) || data[8+ifaceLen-1] != 0 { |
| 592 | - | // log.Printf("wlclient: handleGlobal: insufficient data for interface string (need %d, have %d)", 8+int(ifaceLen)+4, len(data)) | |||
| 593 | return | 726 | return | ||
| 594 | } | 727 | } | ||
| 595 | 728 | ||||
| @@ -600,15 +733,12 @@ func (r *Registry) handleGlobal(data []byte) { | |||||
| 600 | padding := (4 - (ifaceLen % 4)) % 4 | 733 | padding := (4 - (ifaceLen % 4)) % 4 | ||
| 601 | versionOffset := 8 + int(ifaceLen) + int(padding) | 734 | versionOffset := 8 + int(ifaceLen) + int(padding) | ||
| 602 | 735 | ||||
| 603 | - | if len(data) < versionOffset+4 { | 736 | + | if len(data) != versionOffset+4 { |
| 604 | - | // log.Printf("wlclient: handleGlobal: insufficient data for version (need %d, have %d)", versionOffset+4, len(data)) | |||
| 605 | return | 737 | return | ||
| 606 | } | 738 | } | ||
| 607 | 739 | ||||
| 608 | version := binary.LittleEndian.Uint32(data[versionOffset:]) | 740 | version := binary.LittleEndian.Uint32(data[versionOffset:]) | ||
| 609 | 741 | ||||
| 610 | - | // log.Printf("wlclient: global announced: %s v%d (name=%d)", iface, version, name) | |||
| 611 | - | ||||
| 612 | // Store global | 742 | // Store global | ||
| 613 | r.mu.Lock() | 743 | r.mu.Lock() | ||
| 614 | r.globals[name] = Global{ | 744 | r.globals[name] = Global{ | ||
| @@ -619,15 +749,19 @@ func (r *Registry) handleGlobal(data []byte) { | |||||
| 619 | r.mu.Unlock() | 749 | r.mu.Unlock() | ||
| 620 | 750 | ||||
| 621 | // Call specific handler if registered | 751 | // Call specific handler if registered | ||
| 622 | - | if handler, ok := r.handlers[iface]; ok { | 752 | + | r.mu.RLock() |
| 623 | - | // log.Printf("wlclient: calling handler for interface %s", iface) | 753 | + | handler, specific := r.handlers[iface] |
| 754 | + | wildcard, all := r.handlers["*"] | |||
| 755 | + | r.mu.RUnlock() | |||
| 756 | + | if specific { | |||
| 757 | + | ||||
| 624 | handler(r, name, version) | 758 | handler(r, name, version) | ||
| 625 | } | 759 | } | ||
| 626 | 760 | ||||
| 627 | // Call wildcard handler if registered | 761 | // Call wildcard handler if registered | ||
| 628 | - | if handler, ok := r.handlers["*"]; ok { | 762 | + | if all { |
| 629 | - | // log.Printf("wlclient: calling wildcard handler for interface %s", iface) | 763 | + | |
| 630 | - | handler(r, name, version) | 764 | + | wildcard(r, name, version) |
| 631 | } | 765 | } | ||
| 632 | } | 766 | } | ||
| 633 | 767 | ||||
| @@ -641,11 +775,17 @@ func (r *Registry) handleGlobalRemove(data []byte) { | |||||
| 641 | 775 | ||||
| 642 | r.mu.Lock() | 776 | r.mu.Lock() | ||
| 643 | delete(r.globals, name) | 777 | delete(r.globals, name) | ||
| 778 | + | handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...) | |||
| 644 | r.mu.Unlock() | 779 | r.mu.Unlock() | ||
| 780 | + | for _, handler := range handlers { | |||
| 781 | + | handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name}) | |||
| 782 | + | } | |||
| 645 | } | 783 | } | ||
| 646 | 784 | ||||
| 647 | // AddHandler adds a handler for a specific interface | 785 | // AddHandler adds a handler for a specific interface | ||
| 648 | func (r *Registry) AddHandler(iface string, handler GlobalHandler) { | 786 | func (r *Registry) AddHandler(iface string, handler GlobalHandler) { | ||
| 787 | + | r.mu.Lock() | |||
| 788 | + | defer r.mu.Unlock() | |||
| 649 | r.handlers[iface] = handler | 789 | r.handlers[iface] = handler | ||
| 650 | } | 790 | } | ||
| 651 | 791 | ||||
| @@ -656,7 +796,7 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) | |||||
| 656 | proxy.SetID(r.display.allocateID()) | 796 | proxy.SetID(r.display.allocateID()) | ||
| 657 | } | 797 | } | ||
| 658 | 798 | ||||
| 659 | - | // Ensure proxy has a context set - CRITICAL FIX | 799 | + | // Bootstrap a generated proxy with this display's context. |
| 660 | if proxy.Context() == nil { | 800 | if proxy.Context() == nil { | ||
| 661 | if baseProxy, ok := proxy.(*BaseProxy); ok { | 801 | if baseProxy, ok := proxy.(*BaseProxy); ok { | ||
| 662 | baseProxy.SetContext(r.display.Context()) | 802 | baseProxy.SetContext(r.display.Context()) | ||
| @@ -667,12 +807,18 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) | |||||
| 667 | } | 807 | } | ||
| 668 | } | 808 | } | ||
| 669 | 809 | ||||
| 810 | + | if proxy.Context() != r.display.Context() { | |||
| 811 | + | return fmt.Errorf("proxy belongs to another display") | |||
| 812 | + | } | |||
| 813 | + | if proxy.ID() == displayObjectID || proxy.ID() == r.id { | |||
| 814 | + | return fmt.Errorf("reserved object ID") | |||
| 815 | + | } | |||
| 816 | + | if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy { | |||
| 817 | + | return fmt.Errorf("object ID %d already registered", proxy.ID()) | |||
| 818 | + | } | |||
| 670 | // Register the proxy | 819 | // Register the proxy | ||
| 671 | proxy.Context().Register(proxy) | 820 | proxy.Context().Register(proxy) | ||
| 672 | 821 | ||||
| 673 | - | // Also register directly in display objects map | |||
| 674 | - | r.display.objects.Store(proxy.ID(), proxy) | |||
| 675 | - | ||||
| 676 | // Send bind request (opcode 0) with proper arguments | 822 | // Send bind request (opcode 0) with proper arguments | ||
| 677 | if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil { | 823 | if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil { | ||
| 678 | proxy.Context().Unregister(proxy) | 824 | proxy.Context().Unregister(proxy) | ||
| @@ -718,118 +864,3 @@ func (r *Registry) FindGlobal(iface string) (Global, bool) { | |||||
| 718 | } | 864 | } | ||
| 719 | return Global{}, false | 865 | return Global{}, false | ||
| 720 | } | 866 | } | ||
| 721 | - | ||||
| 722 | - | // handleServerObject creates and registers server-allocated objects from events | |||
| 723 | - | func (d *Display) handleServerObject(objectID uint32, opcode uint16, body []byte) bool { | |||
| 724 | - | // Handle specific known protocols that create server objects | |||
| 725 | - | obj, ok := d.objects.Load(objectID) | |||
| 726 | - | if !ok { | |||
| 727 | - | return false | |||
| 728 | - | } | |||
| 729 | - | ||||
| 730 | - | // Check if this is a known object type that creates server objects | |||
| 731 | - | switch obj.(type) { | |||
| 732 | - | case *Registry: | |||
| 733 | - | // Registry doesn't create server objects directly | |||
| 734 | - | return false | |||
| 735 | - | ||||
| 736 | - | default: | |||
| 737 | - | // Check if this might be an output manager (interface zwlr_output_manager_v1) | |||
| 738 | - | if objectID == 5 { // This is likely the output manager based on your logs | |||
| 739 | - | return d.handleOutputManagerEvent(objectID, opcode, body) | |||
| 740 | - | } | |||
| 741 | - | } | |||
| 742 | - | ||||
| 743 | - | return false | |||
| 744 | - | } | |||
| 745 | - | ||||
| 746 | - | // handleOutputManagerEvent handles events from zwlr_output_manager_v1 | |||
| 747 | - | func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body []byte) bool { | |||
| 748 | - | switch opcode { | |||
| 749 | - | case 0: // head event - creates new zwlr_output_head_v1 object | |||
| 750 | - | if len(body) < 4 { | |||
| 751 | - | return false | |||
| 752 | - | } | |||
| 753 | - | ||||
| 754 | - | // Parse the new_id for the head object | |||
| 755 | - | headID := binary.LittleEndian.Uint32(body[0:4]) | |||
| 756 | - | ||||
| 757 | - | // Create a proper OutputHead object | |||
| 758 | - | headProxy := &OutputHead{ | |||
| 759 | - | BaseProxy: BaseProxy{ | |||
| 760 | - | id: headID, | |||
| 761 | - | context: d.Context(), | |||
| 762 | - | }, | |||
| 763 | - | } | |||
| 764 | - | ||||
| 765 | - | // Register the new head object | |||
| 766 | - | d.objects.Store(headID, headProxy) | |||
| 767 | - | return true | |||
| 768 | - | ||||
| 769 | - | case 1: // done event | |||
| 770 | - | return false | |||
| 771 | - | ||||
| 772 | - | case 2: // finished event | |||
| 773 | - | return false | |||
| 774 | - | } | |||
| 775 | - | ||||
| 776 | - | return false | |||
| 777 | - | } | |||
| 778 | - | ||||
| 779 | - | // OutputHead represents a zwlr_output_head_v1 object | |||
| 780 | - | type OutputHead struct { | |||
| 781 | - | BaseProxy | |||
| 782 | - | name string | |||
| 783 | - | description string | |||
| 784 | - | width int32 | |||
| 785 | - | height int32 | |||
| 786 | - | } | |||
| 787 | - | ||||
| 788 | - | // Dispatch handles head events | |||
| 789 | - | func (h *OutputHead) Dispatch(event *Event) { | |||
| 790 | - | switch event.Opcode { | |||
| 791 | - | case 0: // name | |||
| 792 | - | h.name = event.String() | |||
| 793 | - | case 1: // description | |||
| 794 | - | h.description = event.String() | |||
| 795 | - | case 2: // physical_size | |||
| 796 | - | h.width = event.Int32() | |||
| 797 | - | h.height = event.Int32() | |||
| 798 | - | case 3: // mode (creates new mode object) | |||
| 799 | - | modeID := event.Uint32() | |||
| 800 | - | // Create mode object | |||
| 801 | - | mode := &OutputMode{ | |||
| 802 | - | BaseProxy: BaseProxy{ | |||
| 803 | - | id: modeID, | |||
| 804 | - | context: h.context, | |||
| 805 | - | }, | |||
| 806 | - | } | |||
| 807 | - | h.context.display.objects.Store(modeID, mode) | |||
| 808 | - | case 9: // finished | |||
| 809 | - | h.context.Unregister(h) | |||
| 810 | - | default: | |||
| 811 | - | } | |||
| 812 | - | } | |||
| 813 | - | ||||
| 814 | - | // OutputMode represents a zwlr_output_mode_v1 object | |||
| 815 | - | type OutputMode struct { | |||
| 816 | - | BaseProxy | |||
| 817 | - | width int32 | |||
| 818 | - | height int32 | |||
| 819 | - | refresh int32 | |||
| 820 | - | } | |||
| 821 | - | ||||
| 822 | - | // Dispatch handles mode events | |||
| 823 | - | func (m *OutputMode) Dispatch(event *Event) { | |||
| 824 | - | switch event.Opcode { | |||
| 825 | - | case 0: // size | |||
| 826 | - | m.width = event.Int32() | |||
| 827 | - | m.height = event.Int32() | |||
| 828 | - | case 1: // refresh | |||
| 829 | - | m.refresh = event.Int32() | |||
| 830 | - | case 2: // preferred | |||
| 831 | - | case 3: // finished | |||
| 832 | - | m.context.Unregister(m) | |||
| 833 | - | default: | |||
| 834 | - | } | |||
| 835 | - | } | |||
A cmd/wlturbo-scanner/main.go +132 -0
| @@ -0,0 +1,132 @@ | |||
| 1 | + | // Command wlturbo-scanner generates Go bindings from Wayland protocol XML files. | |
| 2 | + | // | |
| 3 | + | // Usage: | |
| 4 | + | // | |
| 5 | + | // wlturbo-scanner [flags] protocol.xml | |
| 6 | + | // | |
| 7 | + | // Flags: | |
| 8 | + | // | |
| 9 | + | // -o, --output Output file path (default: <protocol>_generated.go) | |
| 10 | + | // -p, --package Go package name (default: derived from protocol name) | |
| 11 | + | // -h, --help Show help | |
| 12 | + | // | |
| 13 | + | // Examples: | |
| 14 | + | // | |
| 15 | + | // # Generate bindings for virtual pointer protocol | |
| 16 | + | // wlturbo-scanner -o virtual_pointer.go ../wlr-protocols/unstable/wlr-virtual-pointer-unstable-v1.xml | |
| 17 | + | // | |
| 18 | + | // # Generate with custom package name | |
| 19 | + | // wlturbo-scanner -p vpointer -o vpointer.go protocol.xml | |
| 20 | + | package main | |
| 21 | + | ||
| 22 | + | import ( | |
| 23 | + | "flag" | |
| 24 | + | "fmt" | |
| 25 | + | "log" | |
| 26 | + | "os" | |
| 27 | + | "path/filepath" | |
| 28 | + | "strings" | |
| 29 | + | ||
| 30 | + | "github.com/bnema/wlturbo/internal/scanner" | |
| 31 | + | ) | |
| 32 | + | ||
| 33 | + | func main() { | |
| 34 | + | var ( | |
| 35 | + | outputPath string | |
| 36 | + | packageName string | |
| 37 | + | showHelp bool | |
| 38 | + | crossImports crossFlags | |
| 39 | + | ) | |
| 40 | + | ||
| 41 | + | flag.StringVar(&outputPath, "o", "", "Output file path") | |
| 42 | + | flag.StringVar(&outputPath, "output", "", "Output file path") | |
| 43 | + | flag.StringVar(&packageName, "p", "", "Go package name") | |
| 44 | + | flag.StringVar(&packageName, "package", "", "Go package name") | |
| 45 | + | flag.Var(&crossImports, "import", "Interface=Go import path (repeatable)") | |
| 46 | + | flag.BoolVar(&showHelp, "h", false, "Show help") | |
| 47 | + | flag.BoolVar(&showHelp, "help", false, "Show help") | |
| 48 | + | ||
| 49 | + | flag.Usage = func() { | |
| 50 | + | fmt.Fprintf(os.Stderr, "Usage: %s [flags] protocol.xml\n\n", os.Args[0]) | |
| 51 | + | fmt.Fprintf(os.Stderr, "Generates Go bindings from Wayland protocol XML files.\n\n") | |
| 52 | + | fmt.Fprintf(os.Stderr, "Flags:\n") | |
| 53 | + | flag.PrintDefaults() | |
| 54 | + | fmt.Fprintf(os.Stderr, "\nExamples:\n") | |
| 55 | + | fmt.Fprintf(os.Stderr, " # Generate bindings for virtual pointer protocol\n") | |
| 56 | + | fmt.Fprintf(os.Stderr, " %s -o virtual_pointer.go wlr-virtual-pointer-unstable-v1.xml\n\n", os.Args[0]) | |
| 57 | + | fmt.Fprintf(os.Stderr, " # Generate with custom package name\n") | |
| 58 | + | fmt.Fprintf(os.Stderr, " %s -p vpointer -o vpointer.go protocol.xml\n", os.Args[0]) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | flag.Parse() | |
| 62 | + | ||
| 63 | + | if showHelp || flag.NArg() == 0 { | |
| 64 | + | flag.Usage() | |
| 65 | + | if showHelp { | |
| 66 | + | os.Exit(0) | |
| 67 | + | } | |
| 68 | + | os.Exit(1) | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | xmlPath := flag.Arg(0) | |
| 72 | + | ||
| 73 | + | // Derive output path if not specified | |
| 74 | + | if outputPath == "" { | |
| 75 | + | base := filepath.Base(xmlPath) | |
| 76 | + | base = strings.TrimSuffix(base, filepath.Ext(base)) | |
| 77 | + | outputPath = base + "_generated.go" | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // Derive package name if not specified | |
| 81 | + | if packageName == "" { | |
| 82 | + | base := filepath.Base(xmlPath) | |
| 83 | + | base = strings.TrimSuffix(base, filepath.Ext(base)) | |
| 84 | + | ||
| 85 | + | // Clean up common prefixes/suffixes | |
| 86 | + | base = strings.TrimPrefix(base, "wlr-") | |
| 87 | + | base = strings.TrimPrefix(base, "zwlr-") | |
| 88 | + | base = strings.TrimSuffix(base, "-unstable-v1") | |
| 89 | + | base = strings.TrimSuffix(base, "-unstable-v2") | |
| 90 | + | base = strings.TrimSuffix(base, "-stable-v1") | |
| 91 | + | ||
| 92 | + | // Convert to valid Go package name | |
| 93 | + | packageName = strings.ReplaceAll(base, "-", "_") | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // Create scanner | |
| 97 | + | s := scanner.NewScanner() | |
| 98 | + | s.CrossPackage = crossImports | |
| 99 | + | ||
| 100 | + | // Parse XML | |
| 101 | + | if err := s.ParseXML(xmlPath); err != nil { | |
| 102 | + | log.Fatalf("Failed to parse XML: %v", err) | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // Generate code | |
| 106 | + | code, err := s.Generate(packageName) | |
| 107 | + | if err != nil { | |
| 108 | + | log.Fatalf("Failed to generate code: %v", err) | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | // Write output with secure permissions | |
| 112 | + | if err := os.WriteFile(outputPath, code, 0600); err != nil { | |
| 113 | + | log.Fatalf("Failed to write output: %v", err) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | fmt.Printf("Generated %s (package %s)\n", outputPath, packageName) | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | type crossFlags map[string]string | |
| 120 | + | ||
| 121 | + | func (f *crossFlags) String() string { return fmt.Sprint(map[string]string(*f)) } | |
| 122 | + | func (f *crossFlags) Set(v string) error { | |
| 123 | + | parts := strings.SplitN(v, "=", 2) | |
| 124 | + | if len(parts) != 2 || parts[0] == "" || parts[1] == "" { | |
| 125 | + | return fmt.Errorf("-import must be interface=GoImportPath") | |
| 126 | + | } | |
| 127 | + | if *f == nil { | |
| 128 | + | *f = make(map[string]string) | |
| 129 | + | } | |
| 130 | + | (*f)[parts[0]] = parts[1] | |
| 131 | + | return nil | |
| 132 | + | } | |
| @@ -0,0 +1,132 @@ | |||||
| 1 | + | // Command wlturbo-scanner generates Go bindings from Wayland protocol XML files. | |||
| 2 | + | // | |||
| 3 | + | // Usage: | |||
| 4 | + | // | |||
| 5 | + | // wlturbo-scanner [flags] protocol.xml | |||
| 6 | + | // | |||
| 7 | + | // Flags: | |||
| 8 | + | // | |||
| 9 | + | // -o, --output Output file path (default: <protocol>_generated.go) | |||
| 10 | + | // -p, --package Go package name (default: derived from protocol name) | |||
| 11 | + | // -h, --help Show help | |||
| 12 | + | // | |||
| 13 | + | // Examples: | |||
| 14 | + | // | |||
| 15 | + | // # Generate bindings for virtual pointer protocol | |||
| 16 | + | // wlturbo-scanner -o virtual_pointer.go ../wlr-protocols/unstable/wlr-virtual-pointer-unstable-v1.xml | |||
| 17 | + | // | |||
| 18 | + | // # Generate with custom package name | |||
| 19 | + | // wlturbo-scanner -p vpointer -o vpointer.go protocol.xml | |||
| 20 | + | package main | |||
| 21 | + | ||||
| 22 | + | import ( | |||
| 23 | + | "flag" | |||
| 24 | + | "fmt" | |||
| 25 | + | "log" | |||
| 26 | + | "os" | |||
| 27 | + | "path/filepath" | |||
| 28 | + | "strings" | |||
| 29 | + | ||||
| 30 | + | "github.com/bnema/wlturbo/internal/scanner" | |||
| 31 | + | ) | |||
| 32 | + | ||||
| 33 | + | func main() { | |||
| 34 | + | var ( | |||
| 35 | + | outputPath string | |||
| 36 | + | packageName string | |||
| 37 | + | showHelp bool | |||
| 38 | + | crossImports crossFlags | |||
| 39 | + | ) | |||
| 40 | + | ||||
| 41 | + | flag.StringVar(&outputPath, "o", "", "Output file path") | |||
| 42 | + | flag.StringVar(&outputPath, "output", "", "Output file path") | |||
| 43 | + | flag.StringVar(&packageName, "p", "", "Go package name") | |||
| 44 | + | flag.StringVar(&packageName, "package", "", "Go package name") | |||
| 45 | + | flag.Var(&crossImports, "import", "Interface=Go import path (repeatable)") | |||
| 46 | + | flag.BoolVar(&showHelp, "h", false, "Show help") | |||
| 47 | + | flag.BoolVar(&showHelp, "help", false, "Show help") | |||
| 48 | + | ||||
| 49 | + | flag.Usage = func() { | |||
| 50 | + | fmt.Fprintf(os.Stderr, "Usage: %s [flags] protocol.xml\n\n", os.Args[0]) | |||
| 51 | + | fmt.Fprintf(os.Stderr, "Generates Go bindings from Wayland protocol XML files.\n\n") | |||
| 52 | + | fmt.Fprintf(os.Stderr, "Flags:\n") | |||
| 53 | + | flag.PrintDefaults() | |||
| 54 | + | fmt.Fprintf(os.Stderr, "\nExamples:\n") | |||
| 55 | + | fmt.Fprintf(os.Stderr, " # Generate bindings for virtual pointer protocol\n") | |||
| 56 | + | fmt.Fprintf(os.Stderr, " %s -o virtual_pointer.go wlr-virtual-pointer-unstable-v1.xml\n\n", os.Args[0]) | |||
| 57 | + | fmt.Fprintf(os.Stderr, " # Generate with custom package name\n") | |||
| 58 | + | fmt.Fprintf(os.Stderr, " %s -p vpointer -o vpointer.go protocol.xml\n", os.Args[0]) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | flag.Parse() | |||
| 62 | + | ||||
| 63 | + | if showHelp || flag.NArg() == 0 { | |||
| 64 | + | flag.Usage() | |||
| 65 | + | if showHelp { | |||
| 66 | + | os.Exit(0) | |||
| 67 | + | } | |||
| 68 | + | os.Exit(1) | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | xmlPath := flag.Arg(0) | |||
| 72 | + | ||||
| 73 | + | // Derive output path if not specified | |||
| 74 | + | if outputPath == "" { | |||
| 75 | + | base := filepath.Base(xmlPath) | |||
| 76 | + | base = strings.TrimSuffix(base, filepath.Ext(base)) | |||
| 77 | + | outputPath = base + "_generated.go" | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // Derive package name if not specified | |||
| 81 | + | if packageName == "" { | |||
| 82 | + | base := filepath.Base(xmlPath) | |||
| 83 | + | base = strings.TrimSuffix(base, filepath.Ext(base)) | |||
| 84 | + | ||||
| 85 | + | // Clean up common prefixes/suffixes | |||
| 86 | + | base = strings.TrimPrefix(base, "wlr-") | |||
| 87 | + | base = strings.TrimPrefix(base, "zwlr-") | |||
| 88 | + | base = strings.TrimSuffix(base, "-unstable-v1") | |||
| 89 | + | base = strings.TrimSuffix(base, "-unstable-v2") | |||
| 90 | + | base = strings.TrimSuffix(base, "-stable-v1") | |||
| 91 | + | ||||
| 92 | + | // Convert to valid Go package name | |||
| 93 | + | packageName = strings.ReplaceAll(base, "-", "_") | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // Create scanner | |||
| 97 | + | s := scanner.NewScanner() | |||
| 98 | + | s.CrossPackage = crossImports | |||
| 99 | + | ||||
| 100 | + | // Parse XML | |||
| 101 | + | if err := s.ParseXML(xmlPath); err != nil { | |||
| 102 | + | log.Fatalf("Failed to parse XML: %v", err) | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // Generate code | |||
| 106 | + | code, err := s.Generate(packageName) | |||
| 107 | + | if err != nil { | |||
| 108 | + | log.Fatalf("Failed to generate code: %v", err) | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | // Write output with secure permissions | |||
| 112 | + | if err := os.WriteFile(outputPath, code, 0600); err != nil { | |||
| 113 | + | log.Fatalf("Failed to write output: %v", err) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | fmt.Printf("Generated %s (package %s)\n", outputPath, packageName) | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | type crossFlags map[string]string | |||
| 120 | + | ||||
| 121 | + | func (f *crossFlags) String() string { return fmt.Sprint(map[string]string(*f)) } | |||
| 122 | + | func (f *crossFlags) Set(v string) error { | |||
| 123 | + | parts := strings.SplitN(v, "=", 2) | |||
| 124 | + | if len(parts) != 2 || parts[0] == "" || parts[1] == "" { | |||
| 125 | + | return fmt.Errorf("-import must be interface=GoImportPath") | |||
| 126 | + | } | |||
| 127 | + | if *f == nil { | |||
| 128 | + | *f = make(map[string]string) | |||
| 129 | + | } | |||
| 130 | + | (*f)[parts[0]] = parts[1] | |||
| 131 | + | return nil | |||
| 132 | + | } | |||
A consumer_test.go +44 -0
| @@ -0,0 +1,44 @@ | |||
| 1 | + | //go:build linux | |
| 2 | + | ||
| 3 | + | package wlturbo | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "os" | |
| 7 | + | "os/exec" | |
| 8 | + | "path/filepath" | |
| 9 | + | "testing" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // Run the public socketpair integration against a separate module, with no | |
| 13 | + | // imports from internal/ and no worktree replacement outside its temp go.mod. | |
| 14 | + | func TestExternalConsumer(t *testing.T) { | |
| 15 | + | goBin, err := exec.LookPath("go") | |
| 16 | + | if err != nil { | |
| 17 | + | t.Skip("go executable unavailable: external module integration requires go") | |
| 18 | + | } | |
| 19 | + | root, err := os.Getwd() | |
| 20 | + | if err != nil { | |
| 21 | + | t.Fatal(err) | |
| 22 | + | } | |
| 23 | + | temp := t.TempDir() | |
| 24 | + | mod := "module example.com/wlturbo-consumer\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => " + root + "\n" | |
| 25 | + | if err := os.WriteFile(filepath.Join(temp, "go.mod"), []byte(mod), 0600); err != nil { | |
| 26 | + | t.Fatal(err) | |
| 27 | + | } | |
| 28 | + | source, err := os.ReadFile(filepath.Join(root, "protocol/core/core_test.go")) | |
| 29 | + | if err != nil { | |
| 30 | + | t.Fatal(err) | |
| 31 | + | } | |
| 32 | + | // The external module has no access to the package's test helpers except | |
| 33 | + | // the ones copied into this temporary module. Compile and exercise only | |
| 34 | + | // the public integration scenarios, including typed key, frame and release. | |
| 35 | + | if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil { | |
| 36 | + | t.Fatal(err) | |
| 37 | + | } | |
| 38 | + | cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild", "./...") | |
| 39 | + | cmd.Dir = temp | |
| 40 | + | cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod") | |
| 41 | + | if output, err := cmd.CombinedOutput(); err != nil { | |
| 42 | + | t.Fatalf("external consumer: %v\n%s", err, output) | |
| 43 | + | } | |
| 44 | + | } | |
| @@ -0,0 +1,44 @@ | |||||
| 1 | + | //go:build linux | |||
| 2 | + | ||||
| 3 | + | package wlturbo | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "os" | |||
| 7 | + | "os/exec" | |||
| 8 | + | "path/filepath" | |||
| 9 | + | "testing" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // Run the public socketpair integration against a separate module, with no | |||
| 13 | + | // imports from internal/ and no worktree replacement outside its temp go.mod. | |||
| 14 | + | func TestExternalConsumer(t *testing.T) { | |||
| 15 | + | goBin, err := exec.LookPath("go") | |||
| 16 | + | if err != nil { | |||
| 17 | + | t.Skip("go executable unavailable: external module integration requires go") | |||
| 18 | + | } | |||
| 19 | + | root, err := os.Getwd() | |||
| 20 | + | if err != nil { | |||
| 21 | + | t.Fatal(err) | |||
| 22 | + | } | |||
| 23 | + | temp := t.TempDir() | |||
| 24 | + | mod := "module example.com/wlturbo-consumer\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => " + root + "\n" | |||
| 25 | + | if err := os.WriteFile(filepath.Join(temp, "go.mod"), []byte(mod), 0600); err != nil { | |||
| 26 | + | t.Fatal(err) | |||
| 27 | + | } | |||
| 28 | + | source, err := os.ReadFile(filepath.Join(root, "protocol/core/core_test.go")) | |||
| 29 | + | if err != nil { | |||
| 30 | + | t.Fatal(err) | |||
| 31 | + | } | |||
| 32 | + | // The external module has no access to the package's test helpers except | |||
| 33 | + | // the ones copied into this temporary module. Compile and exercise only | |||
| 34 | + | // the public integration scenarios, including typed key, frame and release. | |||
| 35 | + | if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil { | |||
| 36 | + | t.Fatal(err) | |||
| 37 | + | } | |||
| 38 | + | cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild", "./...") | |||
| 39 | + | cmd.Dir = temp | |||
| 40 | + | cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod") | |||
| 41 | + | if output, err := cmd.CombinedOutput(); err != nil { | |||
| 42 | + | t.Fatalf("external consumer: %v\n%s", err, output) | |||
| 43 | + | } | |||
| 44 | + | } | |||
M core.go +127 -379
| @@ -3,10 +3,10 @@ package wlturbo | |||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | 5 | "errors" | |
| 6 | - | ||
| 7 | - | // "log" | |
| 8 | 6 | "sync" | |
| 9 | 7 | "sync/atomic" | |
| 8 | + | ||
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | 12 | // Context provides a compatibility layer for wl.Context | |
| @@ -37,6 +37,8 @@ type Event struct { | |||
| 37 | 37 | data []byte | |
| 38 | 38 | offset int | |
| 39 | 39 | display *Display | |
| 40 | + | fds []*OwnedFD | |
| 41 | + | fdIndex int | |
| 40 | 42 | } | |
| 41 | 43 | ||
| 42 | 44 | // Data returns the raw event data | |
| @@ -56,33 +58,75 @@ func NewContext(display *Display) *Context { | |||
| 56 | 58 | } | |
| 57 | 59 | } | |
| 58 | 60 | ||
| 59 | - | // SendRequest sends a request through the context | |
| 61 | + | // SendRequest sends a request through the context. The proxy is validated | |
| 62 | + | // under the connection send lock, so no request reaches the wire after a | |
| 63 | + | // destructor for the same proxy. | |
| 60 | 64 | func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error { | |
| 61 | - | if c.closed.Load() { | |
| 62 | - | return errors.New("context is closed") | |
| 63 | - | } | |
| 64 | - | return c.display.SendRequest(proxy.ID(), uint16(opcode), args...) | |
| 65 | + | return c.SendRequestWithFDs(proxy, opcode, nil, args...) | |
| 65 | 66 | } | |
| 66 | 67 | ||
| 67 | - | // SendRequestWithFDs sends a request with file descriptors through the context | |
| 68 | + | // SendRequestWithFDs sends a request with file descriptors through the context. | |
| 69 | + | // On error the FDs remain owned by the caller. | |
| 68 | 70 | func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error { | |
| 71 | + | if err := c.CheckProxy(proxy); err != nil { | |
| 72 | + | return err | |
| 73 | + | } | |
| 74 | + | return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.CheckProxy(proxy) }, args) | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // CheckProxy rejects stale and foreign proxies before any bytes are written. | |
| 78 | + | func (c *Context) CheckProxy(proxy Proxy) error { | |
| 69 | 79 | if c.closed.Load() { | |
| 70 | 80 | return errors.New("context is closed") | |
| 71 | 81 | } | |
| 72 | - | return c.display.SendRequestWithFDs(proxy.ID(), uint16(opcode), fds, args...) | |
| 82 | + | if proxy == nil || proxy.ID() == 0 || proxy.Context() != c { | |
| 83 | + | return errors.New("invalid proxy") | |
| 84 | + | } | |
| 85 | + | registered, ok := c.proxies.Load(proxy.ID()) | |
| 86 | + | if !ok || registered != proxy { | |
| 87 | + | return errors.New("proxy is not registered") | |
| 88 | + | } | |
| 89 | + | return nil | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | // SendDestructor sends a destructor request exactly once. The proxy is | |
| 93 | + | // claimed (unregistered) under the connection send lock, after marshaling | |
| 94 | + | // and immediately before the write: concurrent destructors and later requests | |
| 95 | + | // on the proxy fail without writing. A marshaling error leaves the proxy | |
| 96 | + | // registered. A write error after the claim leaves the proxy unregistered; | |
| 97 | + | // such an error means the connection is broken and must be closed. | |
| 98 | + | func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error { | |
| 99 | + | return c.SendDestructorWithFDs(proxy, opcode, nil, args...) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // SendDestructorWithFDs is SendDestructor for requests carrying FDs. On error | |
| 103 | + | // the FDs remain owned by the caller. | |
| 104 | + | func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error { | |
| 105 | + | if err := c.CheckProxy(proxy); err != nil { | |
| 106 | + | return err | |
| 107 | + | } | |
| 108 | + | return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.claimDestroy(proxy) }, args) | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func (c *Context) claimDestroy(proxy Proxy) error { | |
| 112 | + | if err := c.CheckProxy(proxy); err != nil { | |
| 113 | + | return err | |
| 114 | + | } | |
| 115 | + | if !c.proxies.CompareAndDelete(proxy.ID(), proxy) { | |
| 116 | + | return errors.New("proxy is not registered") | |
| 117 | + | } | |
| 118 | + | c.display.objects.CompareAndDelete(proxy.ID(), proxy) | |
| 119 | + | return nil | |
| 73 | 120 | } | |
| 74 | 121 | ||
| 75 | 122 | // Register registers a proxy object | |
| 76 | 123 | func (c *Context) Register(proxy Proxy) { | |
| 77 | - | if proxy != nil && proxy.ID() != 0 { | |
| 124 | + | if proxy != nil && proxy.ID() != 0 && !c.closed.Load() { | |
| 125 | + | if proxy.Context() != c { | |
| 126 | + | return | |
| 127 | + | } | |
| 78 | 128 | c.proxies.Store(proxy.ID(), proxy) | |
| 79 | 129 | c.display.objects.Store(proxy.ID(), proxy) | |
| 80 | - | // Debug large IDs | |
| 81 | - | // if proxy.ID() > 1000000 { | |
| 82 | - | // log.Printf("wlclient: WARNING: Registering proxy with suspicious ID %d (0x%x)", proxy.ID(), proxy.ID()) | |
| 83 | - | // } else if proxy.ID() == 5 { | |
| 84 | - | // log.Printf("wlclient: Registered output manager proxy with ID 5") | |
| 85 | - | // } | |
| 86 | 130 | } | |
| 87 | 131 | } | |
| 88 | 132 | ||
| @@ -221,19 +265,61 @@ func (e *Event) Array() []byte { | |||
| 221 | 265 | return arr | |
| 222 | 266 | } | |
| 223 | 267 | ||
| 224 | - | // Fd reads a file descriptor that was delivered out-of-band for this event. | |
| 225 | - | // Descriptors are queued on the connection in arrival order, as the Wayland | |
| 226 | - | // wire protocol requires. The returned descriptor is owned by the caller, | |
| 227 | - | // which must close it. It returns 0 when the event carried no descriptor. | |
| 268 | + | // OwnedFD owns one received descriptor. Close is idempotent; Take transfers | |
| 269 | + | // ownership to the caller (who must close it). Descriptor zero is valid. | |
| 270 | + | type OwnedFD struct { | |
| 271 | + | mu sync.Mutex | |
| 272 | + | fd int | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | func (f *OwnedFD) Take() (int, error) { | |
| 276 | + | if f == nil { | |
| 277 | + | return -1, errors.New("missing descriptor") | |
| 278 | + | } | |
| 279 | + | f.mu.Lock() | |
| 280 | + | defer f.mu.Unlock() | |
| 281 | + | if f.fd < 0 { | |
| 282 | + | return -1, errors.New("descriptor already taken or closed") | |
| 283 | + | } | |
| 284 | + | n := f.fd | |
| 285 | + | f.fd = -1 | |
| 286 | + | return n, nil | |
| 287 | + | } | |
| 288 | + | func (f *OwnedFD) Close() error { | |
| 289 | + | if f == nil { | |
| 290 | + | return nil | |
| 291 | + | } | |
| 292 | + | f.mu.Lock() | |
| 293 | + | defer f.mu.Unlock() | |
| 294 | + | if f.fd < 0 { | |
| 295 | + | return nil | |
| 296 | + | } | |
| 297 | + | n := f.fd | |
| 298 | + | f.fd = -1 | |
| 299 | + | return unix.Close(n) | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | // FD transfers ownership of the next descriptor assigned to this event. | |
| 303 | + | // An unclaimed descriptor is closed when dispatch completes. | |
| 304 | + | func (e *Event) FD() *OwnedFD { | |
| 305 | + | if e.fdIndex >= len(e.fds) { | |
| 306 | + | return nil | |
| 307 | + | } | |
| 308 | + | fd := e.fds[e.fdIndex] | |
| 309 | + | e.fds[e.fdIndex] = nil | |
| 310 | + | e.fdIndex++ | |
| 311 | + | return fd | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | // Fd is the legacy raw descriptor API; prefer FD for explicit ownership. | |
| 228 | 315 | func (e *Event) Fd() uintptr { | |
| 229 | - | if e.display != nil { | |
| 230 | - | if fd, ok := e.display.nextFD(); ok { | |
| 231 | - | return uintptr(fd) | |
| 232 | - | } | |
| 316 | + | fd := e.FD() | |
| 317 | + | if fd == nil { | |
| 318 | + | _ = e.Uint32() | |
| 319 | + | return 0 | |
| 233 | 320 | } | |
| 234 | - | // No descriptor was delivered: consume the placeholder argument. | |
| 235 | - | _ = e.Uint32() | |
| 236 | - | return 0 | |
| 321 | + | n, _ := fd.Take() | |
| 322 | + | return uintptr(n) | |
| 237 | 323 | } | |
| 238 | 324 | ||
| 239 | 325 | // NewId reads a new object ID from the event | |
| @@ -278,32 +364,24 @@ type RegistryGlobalRemoveEvent struct { | |||
| 278 | 364 | ||
| 279 | 365 | // AddGlobalHandler adds a global handler to the registry | |
| 280 | 366 | func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) { | |
| 281 | - | // Store the handler to be called for ALL globals | |
| 282 | - | r.mu.Lock() | |
| 283 | - | defer r.mu.Unlock() | |
| 284 | - | ||
| 285 | - | // Initialize handlers map if needed | |
| 286 | - | if r.handlers == nil { | |
| 287 | - | r.handlers = make(map[string]GlobalHandler) | |
| 367 | + | if handler == nil { | |
| 368 | + | return | |
| 288 | 369 | } | |
| 289 | - | ||
| 290 | 370 | r.AddHandler("*", func(registry *Registry, name uint32, version uint32) { | |
| 291 | - | global, ok := r.FindGlobalByName(name) | |
| 292 | - | if ok { | |
| 293 | - | event := RegistryGlobalEvent{ | |
| 294 | - | Registry: r, | |
| 295 | - | Name: name, | |
| 296 | - | Interface: global.Interface, | |
| 297 | - | Version: version, | |
| 298 | - | } | |
| 299 | - | handler.HandleRegistryGlobal(event) | |
| 371 | + | if global, ok := r.FindGlobalByName(name); ok { | |
| 372 | + | handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version}) | |
| 300 | 373 | } | |
| 301 | 374 | }) | |
| 302 | 375 | } | |
| 303 | 376 | ||
| 304 | - | // AddGlobalRemoveHandler adds a global remove handler (placeholder) | |
| 377 | + | // AddGlobalRemoveHandler registers a handler for registry removals. | |
| 305 | 378 | func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) { | |
| 306 | - | // Would need to implement removal handling | |
| 379 | + | if handler == nil { | |
| 380 | + | return | |
| 381 | + | } | |
| 382 | + | r.mu.Lock() | |
| 383 | + | r.removeHandlers = append(r.removeHandlers, handler) | |
| 384 | + | r.mu.Unlock() | |
| 307 | 385 | } | |
| 308 | 386 | ||
| 309 | 387 | // FindGlobalByName finds a global by its name ID | |
| @@ -340,342 +418,12 @@ func (d *Display) Sync() (Object, error) { | |||
| 340 | 418 | display: d, | |
| 341 | 419 | } | |
| 342 | 420 | ||
| 343 | - | // Send sync request (opcode 0) | |
| 421 | + | // Store before sending: the compositor may reply immediately. | |
| 422 | + | d.context.Register(callback) | |
| 344 | 423 | if err := d.SendRequest(1, 0, callbackID); err != nil { | |
| 345 | - | return nil, err | |
| 346 | - | } | |
| 347 | - | ||
| 348 | - | // Store the callback object | |
| 349 | - | d.objects.Store(callbackID, callback) | |
| 350 | - | ||
| 351 | - | return callback, nil | |
| 352 | - | } | |
| 353 | - | ||
| 354 | - | // Additional protocol object types needed for compatibility | |
| 355 | - | ||
| 356 | - | // Seat capability constants | |
| 357 | - | const ( | |
| 358 | - | SeatCapabilityPointer = 1 | |
| 359 | - | SeatCapabilityKeyboard = 2 | |
| 360 | - | SeatCapabilityTouch = 4 | |
| 361 | - | ) | |
| 362 | - | ||
| 363 | - | // Seat represents a wl_seat | |
| 364 | - | type Seat struct { | |
| 365 | - | BaseProxy | |
| 366 | - | capabilities uint32 | |
| 367 | - | name string | |
| 368 | - | } | |
| 369 | - | ||
| 370 | - | // SeatCapabilitiesHandler handles seat capabilities events | |
| 371 | - | type SeatCapabilitiesHandler interface { | |
| 372 | - | HandleSeatCapabilities(seat *Seat, capabilities uint32) | |
| 373 | - | } | |
| 374 | - | ||
| 375 | - | // SeatNameHandler handles seat name events | |
| 376 | - | type SeatNameHandler interface { | |
| 377 | - | HandleSeatName(seat *Seat, name string) | |
| 378 | - | } | |
| 379 | - | ||
| 380 | - | // NewSeat creates a new seat proxy | |
| 381 | - | func NewSeat(ctx *Context) *Seat { | |
| 382 | - | return &Seat{ | |
| 383 | - | BaseProxy: BaseProxy{ | |
| 384 | - | context: ctx, | |
| 385 | - | }, | |
| 386 | - | } | |
| 387 | - | } | |
| 388 | - | ||
| 389 | - | // GetPointer gets the pointer device | |
| 390 | - | func (s *Seat) GetPointer() (*Pointer, error) { | |
| 391 | - | pointer := &Pointer{ | |
| 392 | - | BaseProxy: BaseProxy{ | |
| 393 | - | context: s.context, | |
| 394 | - | id: s.context.display.allocateID(), | |
| 395 | - | }, | |
| 396 | - | } | |
| 397 | - | ||
| 398 | - | // Register pointer before sending request | |
| 399 | - | s.context.Register(pointer) | |
| 400 | - | ||
| 401 | - | // Send get_pointer request (opcode 0) | |
| 402 | - | err := s.context.SendRequest(s, 0, pointer.id) | |
| 403 | - | if err != nil { | |
| 404 | - | s.context.Unregister(pointer) | |
| 405 | - | return nil, err | |
| 406 | - | } | |
| 407 | - | ||
| 408 | - | return pointer, nil | |
| 409 | - | } | |
| 410 | - | ||
| 411 | - | // GetKeyboard gets the keyboard device | |
| 412 | - | func (s *Seat) GetKeyboard() (*Keyboard, error) { | |
| 413 | - | keyboard := &Keyboard{ | |
| 414 | - | BaseProxy: BaseProxy{ | |
| 415 | - | context: s.context, | |
| 416 | - | id: s.context.display.allocateID(), | |
| 417 | - | }, | |
| 418 | - | } | |
| 419 | - | ||
| 420 | - | // Register keyboard before sending request | |
| 421 | - | s.context.Register(keyboard) | |
| 422 | - | ||
| 423 | - | // Send get_keyboard request (opcode 1) | |
| 424 | - | err := s.context.SendRequest(s, 1, keyboard.id) | |
| 425 | - | if err != nil { | |
| 426 | - | s.context.Unregister(keyboard) | |
| 427 | - | return nil, err | |
| 428 | - | } | |
| 429 | - | ||
| 430 | - | return keyboard, nil | |
| 431 | - | } | |
| 432 | - | ||
| 433 | - | // GetTouch gets the touch device | |
| 434 | - | func (s *Seat) GetTouch() (*Touch, error) { | |
| 435 | - | touch := &Touch{ | |
| 436 | - | BaseProxy: BaseProxy{ | |
| 437 | - | context: s.context, | |
| 438 | - | id: s.context.display.allocateID(), | |
| 439 | - | }, | |
| 440 | - | } | |
| 441 | - | ||
| 442 | - | // Register touch before sending request | |
| 443 | - | s.context.Register(touch) | |
| 444 | - | ||
| 445 | - | // Send get_touch request (opcode 2) | |
| 446 | - | err := s.context.SendRequest(s, 2, touch.id) | |
| 447 | - | if err != nil { | |
| 448 | - | s.context.Unregister(touch) | |
| 449 | - | return nil, err | |
| 450 | - | } | |
| 451 | - | ||
| 452 | - | return touch, nil | |
| 453 | - | } | |
| 454 | - | ||
| 455 | - | // Release releases the seat | |
| 456 | - | func (s *Seat) Release() error { | |
| 457 | - | err := s.context.SendRequest(s, 3) // opcode 3 | |
| 458 | - | if err == nil { | |
| 459 | - | s.context.Unregister(s) | |
| 460 | - | } | |
| 461 | - | return err | |
| 462 | - | } | |
| 463 | - | ||
| 464 | - | // Capabilities returns the seat capabilities | |
| 465 | - | func (s *Seat) Capabilities() uint32 { | |
| 466 | - | return s.capabilities | |
| 467 | - | } | |
| 468 | - | ||
| 469 | - | // Name returns the seat name | |
| 470 | - | func (s *Seat) Name() string { | |
| 471 | - | return s.name | |
| 472 | - | } | |
| 473 | - | ||
| 474 | - | // Dispatch handles events for the seat | |
| 475 | - | func (s *Seat) Dispatch(event *Event) { | |
| 476 | - | // Handle seat events | |
| 477 | - | switch event.Opcode { | |
| 478 | - | case 0: // capabilities | |
| 479 | - | s.capabilities = event.Uint32() | |
| 480 | - | // TODO: Call registered handlers | |
| 481 | - | case 1: // name | |
| 482 | - | s.name = event.String() | |
| 483 | - | // TODO: Call registered handlers | |
| 484 | - | } | |
| 485 | - | } | |
| 486 | - | ||
| 487 | - | // Surface represents a wl_surface | |
| 488 | - | type Surface struct { | |
| 489 | - | BaseProxy | |
| 490 | - | } | |
| 491 | - | ||
| 492 | - | // NewSurface creates a new surface proxy | |
| 493 | - | func NewSurface(ctx *Context) *Surface { | |
| 494 | - | return &Surface{ | |
| 495 | - | BaseProxy: BaseProxy{ | |
| 496 | - | context: ctx, | |
| 497 | - | }, | |
| 498 | - | } | |
| 499 | - | } | |
| 500 | - | ||
| 501 | - | // Destroy destroys the surface | |
| 502 | - | func (s *Surface) Destroy() error { | |
| 503 | - | err := s.context.SendRequest(s, 0) // opcode 0 | |
| 504 | - | if err == nil { | |
| 505 | - | s.context.Unregister(s) | |
| 506 | - | } | |
| 507 | - | return err | |
| 508 | - | } | |
| 509 | - | ||
| 510 | - | // Attach attaches a buffer to the surface | |
| 511 | - | func (s *Surface) Attach(buffer Object, x, y int32) error { | |
| 512 | - | return s.context.SendRequest(s, 1, buffer, x, y) // opcode 1 | |
| 513 | - | } | |
| 514 | - | ||
| 515 | - | // Damage marks a region of the surface as damaged | |
| 516 | - | func (s *Surface) Damage(x, y, width, height int32) error { | |
| 517 | - | return s.context.SendRequest(s, 2, x, y, width, height) // opcode 2 | |
| 518 | - | } | |
| 519 | - | ||
| 520 | - | // Frame requests a frame callback | |
| 521 | - | func (s *Surface) Frame() (Object, error) { | |
| 522 | - | callback := &callbackObject{ | |
| 523 | - | BaseProxy: BaseProxy{ | |
| 524 | - | context: s.context, | |
| 525 | - | id: s.context.display.allocateID(), | |
| 526 | - | }, | |
| 527 | - | display: s.context.display, | |
| 528 | - | } | |
| 529 | - | s.context.Register(callback) | |
| 530 | - | ||
| 531 | - | err := s.context.SendRequest(s, 3, callback.id) // opcode 3 | |
| 532 | - | if err != nil { | |
| 533 | - | s.context.Unregister(callback) | |
| 424 | + | d.context.Unregister(callback) | |
| 534 | 425 | return nil, err | |
| 535 | 426 | } | |
| 536 | 427 | ||
| 537 | 428 | return callback, nil | |
| 538 | 429 | } | |
| 539 | - | ||
| 540 | - | // SetOpaqueRegion sets the opaque region | |
| 541 | - | func (s *Surface) SetOpaqueRegion(region *Region) error { | |
| 542 | - | return s.context.SendRequest(s, 4, region) // opcode 4 | |
| 543 | - | } | |
| 544 | - | ||
| 545 | - | // SetInputRegion sets the input region | |
| 546 | - | func (s *Surface) SetInputRegion(region *Region) error { | |
| 547 | - | return s.context.SendRequest(s, 5, region) // opcode 5 | |
| 548 | - | } | |
| 549 | - | ||
| 550 | - | // Commit commits pending surface state | |
| 551 | - | func (s *Surface) Commit() error { | |
| 552 | - | return s.context.SendRequest(s, 6) // opcode 6 | |
| 553 | - | } | |
| 554 | - | ||
| 555 | - | // SetBufferTransform sets the buffer transform | |
| 556 | - | func (s *Surface) SetBufferTransform(transform int32) error { | |
| 557 | - | return s.context.SendRequest(s, 7, transform) // opcode 7 | |
| 558 | - | } | |
| 559 | - | ||
| 560 | - | // SetBufferScale sets the buffer scale | |
| 561 | - | func (s *Surface) SetBufferScale(scale int32) error { | |
| 562 | - | return s.context.SendRequest(s, 8, scale) // opcode 8 | |
| 563 | - | } | |
| 564 | - | ||
| 565 | - | // DamageBuffer marks a region of the buffer as damaged | |
| 566 | - | func (s *Surface) DamageBuffer(x, y, width, height int32) error { | |
| 567 | - | return s.context.SendRequest(s, 9, x, y, width, height) // opcode 9 | |
| 568 | - | } | |
| 569 | - | ||
| 570 | - | // Offset sets the buffer offset | |
| 571 | - | func (s *Surface) Offset(x, y int32) error { | |
| 572 | - | return s.context.SendRequest(s, 10, x, y) // opcode 10 | |
| 573 | - | } | |
| 574 | - | ||
| 575 | - | // Dispatch handles surface events | |
| 576 | - | func (s *Surface) Dispatch(event *Event) { | |
| 577 | - | // Surface can receive enter/leave/preferred_buffer_scale/preferred_buffer_transform events | |
| 578 | - | // But for now we don't handle them | |
| 579 | - | } | |
| 580 | - | ||
| 581 | - | // Pointer represents a wl_pointer | |
| 582 | - | type Pointer struct { | |
| 583 | - | BaseProxy | |
| 584 | - | } | |
| 585 | - | ||
| 586 | - | // Keyboard represents a wl_keyboard | |
| 587 | - | type Keyboard struct { | |
| 588 | - | BaseProxy | |
| 589 | - | } | |
| 590 | - | ||
| 591 | - | // Touch represents a wl_touch | |
| 592 | - | type Touch struct { | |
| 593 | - | BaseProxy | |
| 594 | - | } | |
| 595 | - | ||
| 596 | - | // Output represents a wl_output | |
| 597 | - | type Output struct { | |
| 598 | - | BaseProxy | |
| 599 | - | } | |
| 600 | - | ||
| 601 | - | // Region represents a wl_region | |
| 602 | - | type Region struct { | |
| 603 | - | BaseProxy | |
| 604 | - | } | |
| 605 | - | ||
| 606 | - | // Add adds a rectangle to the region | |
| 607 | - | func (r *Region) Add(x, y, width, height int32) error { | |
| 608 | - | return r.context.SendRequest(r, 0, x, y, width, height) // opcode 0 | |
| 609 | - | } | |
| 610 | - | ||
| 611 | - | // Subtract subtracts a rectangle from the region | |
| 612 | - | func (r *Region) Subtract(x, y, width, height int32) error { | |
| 613 | - | return r.context.SendRequest(r, 1, x, y, width, height) // opcode 1 | |
| 614 | - | } | |
| 615 | - | ||
| 616 | - | // Destroy destroys the region | |
| 617 | - | func (r *Region) Destroy() error { | |
| 618 | - | err := r.context.SendRequest(r, 2) // opcode 2 | |
| 619 | - | if err == nil { | |
| 620 | - | r.context.Unregister(r) | |
| 621 | - | } | |
| 622 | - | return err | |
| 623 | - | } | |
| 624 | - | ||
| 625 | - | // Compositor represents a wl_compositor | |
| 626 | - | type Compositor struct { | |
| 627 | - | BaseProxy | |
| 628 | - | } | |
| 629 | - | ||
| 630 | - | // NewCompositor creates a new compositor proxy | |
| 631 | - | func NewCompositor(ctx *Context) *Compositor { | |
| 632 | - | return &Compositor{ | |
| 633 | - | BaseProxy: BaseProxy{ | |
| 634 | - | context: ctx, | |
| 635 | - | }, | |
| 636 | - | } | |
| 637 | - | } | |
| 638 | - | ||
| 639 | - | // CreateSurface creates a new surface | |
| 640 | - | func (c *Compositor) CreateSurface() (*Surface, error) { | |
| 641 | - | surface := &Surface{ | |
| 642 | - | BaseProxy: BaseProxy{ | |
| 643 | - | context: c.context, | |
| 644 | - | id: c.context.display.allocateID(), | |
| 645 | - | }, | |
| 646 | - | } | |
| 647 | - | ||
| 648 | - | // Register surface before sending request | |
| 649 | - | c.context.Register(surface) | |
| 650 | - | ||
| 651 | - | // Send create_surface request (opcode 0) | |
| 652 | - | err := c.context.SendRequest(c, 0, surface.id) | |
| 653 | - | if err != nil { | |
| 654 | - | c.context.Unregister(surface) | |
| 655 | - | return nil, err | |
| 656 | - | } | |
| 657 | - | ||
| 658 | - | return surface, nil | |
| 659 | - | } | |
| 660 | - | ||
| 661 | - | // CreateRegion creates a new region | |
| 662 | - | func (c *Compositor) CreateRegion() (*Region, error) { | |
| 663 | - | region := &Region{ | |
| 664 | - | BaseProxy: BaseProxy{ | |
| 665 | - | context: c.context, | |
| 666 | - | id: c.context.display.allocateID(), | |
| 667 | - | }, | |
| 668 | - | } | |
| 669 | - | ||
| 670 | - | // Register region before sending request | |
| 671 | - | c.context.Register(region) | |
| 672 | - | ||
| 673 | - | // Send create_region request (opcode 1) | |
| 674 | - | err := c.context.SendRequest(c, 1, region.id) | |
| 675 | - | if err != nil { | |
| 676 | - | c.context.Unregister(region) | |
| 677 | - | return nil, err | |
| 678 | - | } | |
| 679 | - | ||
| 680 | - | return region, nil | |
| 681 | - | } | |
| @@ -3,10 +3,10 @@ package wlturbo | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "encoding/binary" | 4 | "encoding/binary" | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | - | ||||
| 7 | - | // "log" | |||
| 8 | "sync" | 6 | "sync" | ||
| 9 | "sync/atomic" | 7 | "sync/atomic" | ||
| 8 | + | ||||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | // Context provides a compatibility layer for wl.Context | 12 | // Context provides a compatibility layer for wl.Context | ||
| @@ -37,6 +37,8 @@ type Event struct { | |||||
| 37 | data []byte | 37 | data []byte | ||
| 38 | offset int | 38 | offset int | ||
| 39 | display *Display | 39 | display *Display | ||
| 40 | + | fds []*OwnedFD | |||
| 41 | + | fdIndex int | |||
| 40 | } | 42 | } | ||
| 41 | 43 | ||||
| 42 | // Data returns the raw event data | 44 | // Data returns the raw event data | ||
| @@ -56,33 +58,75 @@ func NewContext(display *Display) *Context { | |||||
| 56 | } | 58 | } | ||
| 57 | } | 59 | } | ||
| 58 | 60 | ||||
| 59 | - | // SendRequest sends a request through the context | 61 | + | // SendRequest sends a request through the context. The proxy is validated |
| 62 | + | // under the connection send lock, so no request reaches the wire after a | |||
| 63 | + | // destructor for the same proxy. | |||
| 60 | func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error { | 64 | func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error { | ||
| 61 | - | if c.closed.Load() { | 65 | + | return c.SendRequestWithFDs(proxy, opcode, nil, args...) |
| 62 | - | return errors.New("context is closed") | |||
| 63 | - | } | |||
| 64 | - | return c.display.SendRequest(proxy.ID(), uint16(opcode), args...) | |||
| 65 | } | 66 | } | ||
| 66 | 67 | ||||
| 67 | - | // SendRequestWithFDs sends a request with file descriptors through the context | 68 | + | // SendRequestWithFDs sends a request with file descriptors through the context. |
| 69 | + | // On error the FDs remain owned by the caller. | |||
| 68 | func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error { | 70 | func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error { | ||
| 71 | + | if err := c.CheckProxy(proxy); err != nil { | |||
| 72 | + | return err | |||
| 73 | + | } | |||
| 74 | + | return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.CheckProxy(proxy) }, args) | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // CheckProxy rejects stale and foreign proxies before any bytes are written. | |||
| 78 | + | func (c *Context) CheckProxy(proxy Proxy) error { | |||
| 69 | if c.closed.Load() { | 79 | if c.closed.Load() { | ||
| 70 | return errors.New("context is closed") | 80 | return errors.New("context is closed") | ||
| 71 | } | 81 | } | ||
| 72 | - | return c.display.SendRequestWithFDs(proxy.ID(), uint16(opcode), fds, args...) | 82 | + | if proxy == nil || proxy.ID() == 0 || proxy.Context() != c { |
| 83 | + | return errors.New("invalid proxy") | |||
| 84 | + | } | |||
| 85 | + | registered, ok := c.proxies.Load(proxy.ID()) | |||
| 86 | + | if !ok || registered != proxy { | |||
| 87 | + | return errors.New("proxy is not registered") | |||
| 88 | + | } | |||
| 89 | + | return nil | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | // SendDestructor sends a destructor request exactly once. The proxy is | |||
| 93 | + | // claimed (unregistered) under the connection send lock, after marshaling | |||
| 94 | + | // and immediately before the write: concurrent destructors and later requests | |||
| 95 | + | // on the proxy fail without writing. A marshaling error leaves the proxy | |||
| 96 | + | // registered. A write error after the claim leaves the proxy unregistered; | |||
| 97 | + | // such an error means the connection is broken and must be closed. | |||
| 98 | + | func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error { | |||
| 99 | + | return c.SendDestructorWithFDs(proxy, opcode, nil, args...) | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // SendDestructorWithFDs is SendDestructor for requests carrying FDs. On error | |||
| 103 | + | // the FDs remain owned by the caller. | |||
| 104 | + | func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error { | |||
| 105 | + | if err := c.CheckProxy(proxy); err != nil { | |||
| 106 | + | return err | |||
| 107 | + | } | |||
| 108 | + | return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.claimDestroy(proxy) }, args) | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func (c *Context) claimDestroy(proxy Proxy) error { | |||
| 112 | + | if err := c.CheckProxy(proxy); err != nil { | |||
| 113 | + | return err | |||
| 114 | + | } | |||
| 115 | + | if !c.proxies.CompareAndDelete(proxy.ID(), proxy) { | |||
| 116 | + | return errors.New("proxy is not registered") | |||
| 117 | + | } | |||
| 118 | + | c.display.objects.CompareAndDelete(proxy.ID(), proxy) | |||
| 119 | + | return nil | |||
| 73 | } | 120 | } | ||
| 74 | 121 | ||||
| 75 | // Register registers a proxy object | 122 | // Register registers a proxy object | ||
| 76 | func (c *Context) Register(proxy Proxy) { | 123 | func (c *Context) Register(proxy Proxy) { | ||
| 77 | - | if proxy != nil && proxy.ID() != 0 { | 124 | + | if proxy != nil && proxy.ID() != 0 && !c.closed.Load() { |
| 125 | + | if proxy.Context() != c { | |||
| 126 | + | return | |||
| 127 | + | } | |||
| 78 | c.proxies.Store(proxy.ID(), proxy) | 128 | c.proxies.Store(proxy.ID(), proxy) | ||
| 79 | c.display.objects.Store(proxy.ID(), proxy) | 129 | c.display.objects.Store(proxy.ID(), proxy) | ||
| 80 | - | // Debug large IDs | |||
| 81 | - | // if proxy.ID() > 1000000 { | |||
| 82 | - | // log.Printf("wlclient: WARNING: Registering proxy with suspicious ID %d (0x%x)", proxy.ID(), proxy.ID()) | |||
| 83 | - | // } else if proxy.ID() == 5 { | |||
| 84 | - | // log.Printf("wlclient: Registered output manager proxy with ID 5") | |||
| 85 | - | // } | |||
| 86 | } | 130 | } | ||
| 87 | } | 131 | } | ||
| 88 | 132 | ||||
| @@ -221,19 +265,61 @@ func (e *Event) Array() []byte { | |||||
| 221 | return arr | 265 | return arr | ||
| 222 | } | 266 | } | ||
| 223 | 267 | ||||
| 224 | - | // Fd reads a file descriptor that was delivered out-of-band for this event. | 268 | + | // OwnedFD owns one received descriptor. Close is idempotent; Take transfers |
| 225 | - | // Descriptors are queued on the connection in arrival order, as the Wayland | 269 | + | // ownership to the caller (who must close it). Descriptor zero is valid. |
| 226 | - | // wire protocol requires. The returned descriptor is owned by the caller, | 270 | + | type OwnedFD struct { |
| 227 | - | // which must close it. It returns 0 when the event carried no descriptor. | 271 | + | mu sync.Mutex |
| 272 | + | fd int | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | func (f *OwnedFD) Take() (int, error) { | |||
| 276 | + | if f == nil { | |||
| 277 | + | return -1, errors.New("missing descriptor") | |||
| 278 | + | } | |||
| 279 | + | f.mu.Lock() | |||
| 280 | + | defer f.mu.Unlock() | |||
| 281 | + | if f.fd < 0 { | |||
| 282 | + | return -1, errors.New("descriptor already taken or closed") | |||
| 283 | + | } | |||
| 284 | + | n := f.fd | |||
| 285 | + | f.fd = -1 | |||
| 286 | + | return n, nil | |||
| 287 | + | } | |||
| 288 | + | func (f *OwnedFD) Close() error { | |||
| 289 | + | if f == nil { | |||
| 290 | + | return nil | |||
| 291 | + | } | |||
| 292 | + | f.mu.Lock() | |||
| 293 | + | defer f.mu.Unlock() | |||
| 294 | + | if f.fd < 0 { | |||
| 295 | + | return nil | |||
| 296 | + | } | |||
| 297 | + | n := f.fd | |||
| 298 | + | f.fd = -1 | |||
| 299 | + | return unix.Close(n) | |||
| 300 | + | } | |||
| 301 | + | ||||
| 302 | + | // FD transfers ownership of the next descriptor assigned to this event. | |||
| 303 | + | // An unclaimed descriptor is closed when dispatch completes. | |||
| 304 | + | func (e *Event) FD() *OwnedFD { | |||
| 305 | + | if e.fdIndex >= len(e.fds) { | |||
| 306 | + | return nil | |||
| 307 | + | } | |||
| 308 | + | fd := e.fds[e.fdIndex] | |||
| 309 | + | e.fds[e.fdIndex] = nil | |||
| 310 | + | e.fdIndex++ | |||
| 311 | + | return fd | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | // Fd is the legacy raw descriptor API; prefer FD for explicit ownership. | |||
| 228 | func (e *Event) Fd() uintptr { | 315 | func (e *Event) Fd() uintptr { | ||
| 229 | - | if e.display != nil { | 316 | + | fd := e.FD() |
| 230 | - | if fd, ok := e.display.nextFD(); ok { | 317 | + | if fd == nil { |
| 231 | - | return uintptr(fd) | 318 | + | _ = e.Uint32() |
| 232 | - | } | 319 | + | return 0 |
| 233 | } | 320 | } | ||
| 234 | - | // No descriptor was delivered: consume the placeholder argument. | 321 | + | n, _ := fd.Take() |
| 235 | - | _ = e.Uint32() | 322 | + | return uintptr(n) |
| 236 | - | return 0 | |||
| 237 | } | 323 | } | ||
| 238 | 324 | ||||
| 239 | // NewId reads a new object ID from the event | 325 | // NewId reads a new object ID from the event | ||
| @@ -278,32 +364,24 @@ type RegistryGlobalRemoveEvent struct { | |||||
| 278 | 364 | ||||
| 279 | // AddGlobalHandler adds a global handler to the registry | 365 | // AddGlobalHandler adds a global handler to the registry | ||
| 280 | func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) { | 366 | func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) { | ||
| 281 | - | // Store the handler to be called for ALL globals | 367 | + | if handler == nil { |
| 282 | - | r.mu.Lock() | 368 | + | return |
| 283 | - | defer r.mu.Unlock() | |||
| 284 | - | ||||
| 285 | - | // Initialize handlers map if needed | |||
| 286 | - | if r.handlers == nil { | |||
| 287 | - | r.handlers = make(map[string]GlobalHandler) | |||
| 288 | } | 369 | } | ||
| 289 | - | ||||
| 290 | r.AddHandler("*", func(registry *Registry, name uint32, version uint32) { | 370 | r.AddHandler("*", func(registry *Registry, name uint32, version uint32) { | ||
| 291 | - | global, ok := r.FindGlobalByName(name) | 371 | + | if global, ok := r.FindGlobalByName(name); ok { |
| 292 | - | if ok { | 372 | + | handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version}) |
| 293 | - | event := RegistryGlobalEvent{ | |||
| 294 | - | Registry: r, | |||
| 295 | - | Name: name, | |||
| 296 | - | Interface: global.Interface, | |||
| 297 | - | Version: version, | |||
| 298 | - | } | |||
| 299 | - | handler.HandleRegistryGlobal(event) | |||
| 300 | } | 373 | } | ||
| 301 | }) | 374 | }) | ||
| 302 | } | 375 | } | ||
| 303 | 376 | ||||
| 304 | - | // AddGlobalRemoveHandler adds a global remove handler (placeholder) | 377 | + | // AddGlobalRemoveHandler registers a handler for registry removals. |
| 305 | func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) { | 378 | func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) { | ||
| 306 | - | // Would need to implement removal handling | 379 | + | if handler == nil { |
| 380 | + | return | |||
| 381 | + | } | |||
| 382 | + | r.mu.Lock() | |||
| 383 | + | r.removeHandlers = append(r.removeHandlers, handler) | |||
| 384 | + | r.mu.Unlock() | |||
| 307 | } | 385 | } | ||
| 308 | 386 | ||||
| 309 | // FindGlobalByName finds a global by its name ID | 387 | // FindGlobalByName finds a global by its name ID | ||
| @@ -340,342 +418,12 @@ func (d *Display) Sync() (Object, error) { | |||||
| 340 | display: d, | 418 | display: d, | ||
| 341 | } | 419 | } | ||
| 342 | 420 | ||||
| 343 | - | // Send sync request (opcode 0) | 421 | + | // Store before sending: the compositor may reply immediately. |
| 422 | + | d.context.Register(callback) | |||
| 344 | if err := d.SendRequest(1, 0, callbackID); err != nil { | 423 | if err := d.SendRequest(1, 0, callbackID); err != nil { | ||
| 345 | - | return nil, err | 424 | + | d.context.Unregister(callback) |
| 346 | - | } | |||
| 347 | - | ||||
| 348 | - | // Store the callback object | |||
| 349 | - | d.objects.Store(callbackID, callback) | |||
| 350 | - | ||||
| 351 | - | return callback, nil | |||
| 352 | - | } | |||
| 353 | - | ||||
| 354 | - | // Additional protocol object types needed for compatibility | |||
| 355 | - | ||||
| 356 | - | // Seat capability constants | |||
| 357 | - | const ( | |||
| 358 | - | SeatCapabilityPointer = 1 | |||
| 359 | - | SeatCapabilityKeyboard = 2 | |||
| 360 | - | SeatCapabilityTouch = 4 | |||
| 361 | - | ) | |||
| 362 | - | ||||
| 363 | - | // Seat represents a wl_seat | |||
| 364 | - | type Seat struct { | |||
| 365 | - | BaseProxy | |||
| 366 | - | capabilities uint32 | |||
| 367 | - | name string | |||
| 368 | - | } | |||
| 369 | - | ||||
| 370 | - | // SeatCapabilitiesHandler handles seat capabilities events | |||
| 371 | - | type SeatCapabilitiesHandler interface { | |||
| 372 | - | HandleSeatCapabilities(seat *Seat, capabilities uint32) | |||
| 373 | - | } | |||
| 374 | - | ||||
| 375 | - | // SeatNameHandler handles seat name events | |||
| 376 | - | type SeatNameHandler interface { | |||
| 377 | - | HandleSeatName(seat *Seat, name string) | |||
| 378 | - | } | |||
| 379 | - | ||||
| 380 | - | // NewSeat creates a new seat proxy | |||
| 381 | - | func NewSeat(ctx *Context) *Seat { | |||
| 382 | - | return &Seat{ | |||
| 383 | - | BaseProxy: BaseProxy{ | |||
| 384 | - | context: ctx, | |||
| 385 | - | }, | |||
| 386 | - | } | |||
| 387 | - | } | |||
| 388 | - | ||||
| 389 | - | // GetPointer gets the pointer device | |||
| 390 | - | func (s *Seat) GetPointer() (*Pointer, error) { | |||
| 391 | - | pointer := &Pointer{ | |||
| 392 | - | BaseProxy: BaseProxy{ | |||
| 393 | - | context: s.context, | |||
| 394 | - | id: s.context.display.allocateID(), | |||
| 395 | - | }, | |||
| 396 | - | } | |||
| 397 | - | ||||
| 398 | - | // Register pointer before sending request | |||
| 399 | - | s.context.Register(pointer) | |||
| 400 | - | ||||
| 401 | - | // Send get_pointer request (opcode 0) | |||
| 402 | - | err := s.context.SendRequest(s, 0, pointer.id) | |||
| 403 | - | if err != nil { | |||
| 404 | - | s.context.Unregister(pointer) | |||
| 405 | - | return nil, err | |||
| 406 | - | } | |||
| 407 | - | ||||
| 408 | - | return pointer, nil | |||
| 409 | - | } | |||
| 410 | - | ||||
| 411 | - | // GetKeyboard gets the keyboard device | |||
| 412 | - | func (s *Seat) GetKeyboard() (*Keyboard, error) { | |||
| 413 | - | keyboard := &Keyboard{ | |||
| 414 | - | BaseProxy: BaseProxy{ | |||
| 415 | - | context: s.context, | |||
| 416 | - | id: s.context.display.allocateID(), | |||
| 417 | - | }, | |||
| 418 | - | } | |||
| 419 | - | ||||
| 420 | - | // Register keyboard before sending request | |||
| 421 | - | s.context.Register(keyboard) | |||
| 422 | - | ||||
| 423 | - | // Send get_keyboard request (opcode 1) | |||
| 424 | - | err := s.context.SendRequest(s, 1, keyboard.id) | |||
| 425 | - | if err != nil { | |||
| 426 | - | s.context.Unregister(keyboard) | |||
| 427 | - | return nil, err | |||
| 428 | - | } | |||
| 429 | - | ||||
| 430 | - | return keyboard, nil | |||
| 431 | - | } | |||
| 432 | - | ||||
| 433 | - | // GetTouch gets the touch device | |||
| 434 | - | func (s *Seat) GetTouch() (*Touch, error) { | |||
| 435 | - | touch := &Touch{ | |||
| 436 | - | BaseProxy: BaseProxy{ | |||
| 437 | - | context: s.context, | |||
| 438 | - | id: s.context.display.allocateID(), | |||
| 439 | - | }, | |||
| 440 | - | } | |||
| 441 | - | ||||
| 442 | - | // Register touch before sending request | |||
| 443 | - | s.context.Register(touch) | |||
| 444 | - | ||||
| 445 | - | // Send get_touch request (opcode 2) | |||
| 446 | - | err := s.context.SendRequest(s, 2, touch.id) | |||
| 447 | - | if err != nil { | |||
| 448 | - | s.context.Unregister(touch) | |||
| 449 | - | return nil, err | |||
| 450 | - | } | |||
| 451 | - | ||||
| 452 | - | return touch, nil | |||
| 453 | - | } | |||
| 454 | - | ||||
| 455 | - | // Release releases the seat | |||
| 456 | - | func (s *Seat) Release() error { | |||
| 457 | - | err := s.context.SendRequest(s, 3) // opcode 3 | |||
| 458 | - | if err == nil { | |||
| 459 | - | s.context.Unregister(s) | |||
| 460 | - | } | |||
| 461 | - | return err | |||
| 462 | - | } | |||
| 463 | - | ||||
| 464 | - | // Capabilities returns the seat capabilities | |||
| 465 | - | func (s *Seat) Capabilities() uint32 { | |||
| 466 | - | return s.capabilities | |||
| 467 | - | } | |||
| 468 | - | ||||
| 469 | - | // Name returns the seat name | |||
| 470 | - | func (s *Seat) Name() string { | |||
| 471 | - | return s.name | |||
| 472 | - | } | |||
| 473 | - | ||||
| 474 | - | // Dispatch handles events for the seat | |||
| 475 | - | func (s *Seat) Dispatch(event *Event) { | |||
| 476 | - | // Handle seat events | |||
| 477 | - | switch event.Opcode { | |||
| 478 | - | case 0: // capabilities | |||
| 479 | - | s.capabilities = event.Uint32() | |||
| 480 | - | // TODO: Call registered handlers | |||
| 481 | - | case 1: // name | |||
| 482 | - | s.name = event.String() | |||
| 483 | - | // TODO: Call registered handlers | |||
| 484 | - | } | |||
| 485 | - | } | |||
| 486 | - | ||||
| 487 | - | // Surface represents a wl_surface | |||
| 488 | - | type Surface struct { | |||
| 489 | - | BaseProxy | |||
| 490 | - | } | |||
| 491 | - | ||||
| 492 | - | // NewSurface creates a new surface proxy | |||
| 493 | - | func NewSurface(ctx *Context) *Surface { | |||
| 494 | - | return &Surface{ | |||
| 495 | - | BaseProxy: BaseProxy{ | |||
| 496 | - | context: ctx, | |||
| 497 | - | }, | |||
| 498 | - | } | |||
| 499 | - | } | |||
| 500 | - | ||||
| 501 | - | // Destroy destroys the surface | |||
| 502 | - | func (s *Surface) Destroy() error { | |||
| 503 | - | err := s.context.SendRequest(s, 0) // opcode 0 | |||
| 504 | - | if err == nil { | |||
| 505 | - | s.context.Unregister(s) | |||
| 506 | - | } | |||
| 507 | - | return err | |||
| 508 | - | } | |||
| 509 | - | ||||
| 510 | - | // Attach attaches a buffer to the surface | |||
| 511 | - | func (s *Surface) Attach(buffer Object, x, y int32) error { | |||
| 512 | - | return s.context.SendRequest(s, 1, buffer, x, y) // opcode 1 | |||
| 513 | - | } | |||
| 514 | - | ||||
| 515 | - | // Damage marks a region of the surface as damaged | |||
| 516 | - | func (s *Surface) Damage(x, y, width, height int32) error { | |||
| 517 | - | return s.context.SendRequest(s, 2, x, y, width, height) // opcode 2 | |||
| 518 | - | } | |||
| 519 | - | ||||
| 520 | - | // Frame requests a frame callback | |||
| 521 | - | func (s *Surface) Frame() (Object, error) { | |||
| 522 | - | callback := &callbackObject{ | |||
| 523 | - | BaseProxy: BaseProxy{ | |||
| 524 | - | context: s.context, | |||
| 525 | - | id: s.context.display.allocateID(), | |||
| 526 | - | }, | |||
| 527 | - | display: s.context.display, | |||
| 528 | - | } | |||
| 529 | - | s.context.Register(callback) | |||
| 530 | - | ||||
| 531 | - | err := s.context.SendRequest(s, 3, callback.id) // opcode 3 | |||
| 532 | - | if err != nil { | |||
| 533 | - | s.context.Unregister(callback) | |||
| 534 | return nil, err | 425 | return nil, err | ||
| 535 | } | 426 | } | ||
| 536 | 427 | ||||
| 537 | return callback, nil | 428 | return callback, nil | ||
| 538 | } | 429 | } | ||
| 539 | - | ||||
| 540 | - | // SetOpaqueRegion sets the opaque region | |||
| 541 | - | func (s *Surface) SetOpaqueRegion(region *Region) error { | |||
| 542 | - | return s.context.SendRequest(s, 4, region) // opcode 4 | |||
| 543 | - | } | |||
| 544 | - | ||||
| 545 | - | // SetInputRegion sets the input region | |||
| 546 | - | func (s *Surface) SetInputRegion(region *Region) error { | |||
| 547 | - | return s.context.SendRequest(s, 5, region) // opcode 5 | |||
| 548 | - | } | |||
| 549 | - | ||||
| 550 | - | // Commit commits pending surface state | |||
| 551 | - | func (s *Surface) Commit() error { | |||
| 552 | - | return s.context.SendRequest(s, 6) // opcode 6 | |||
| 553 | - | } | |||
| 554 | - | ||||
| 555 | - | // SetBufferTransform sets the buffer transform | |||
| 556 | - | func (s *Surface) SetBufferTransform(transform int32) error { | |||
| 557 | - | return s.context.SendRequest(s, 7, transform) // opcode 7 | |||
| 558 | - | } | |||
| 559 | - | ||||
| 560 | - | // SetBufferScale sets the buffer scale | |||
| 561 | - | func (s *Surface) SetBufferScale(scale int32) error { | |||
| 562 | - | return s.context.SendRequest(s, 8, scale) // opcode 8 | |||
| 563 | - | } | |||
| 564 | - | ||||
| 565 | - | // DamageBuffer marks a region of the buffer as damaged | |||
| 566 | - | func (s *Surface) DamageBuffer(x, y, width, height int32) error { | |||
| 567 | - | return s.context.SendRequest(s, 9, x, y, width, height) // opcode 9 | |||
| 568 | - | } | |||
| 569 | - | ||||
| 570 | - | // Offset sets the buffer offset | |||
| 571 | - | func (s *Surface) Offset(x, y int32) error { | |||
| 572 | - | return s.context.SendRequest(s, 10, x, y) // opcode 10 | |||
| 573 | - | } | |||
| 574 | - | ||||
| 575 | - | // Dispatch handles surface events | |||
| 576 | - | func (s *Surface) Dispatch(event *Event) { | |||
| 577 | - | // Surface can receive enter/leave/preferred_buffer_scale/preferred_buffer_transform events | |||
| 578 | - | // But for now we don't handle them | |||
| 579 | - | } | |||
| 580 | - | ||||
| 581 | - | // Pointer represents a wl_pointer | |||
| 582 | - | type Pointer struct { | |||
| 583 | - | BaseProxy | |||
| 584 | - | } | |||
| 585 | - | ||||
| 586 | - | // Keyboard represents a wl_keyboard | |||
| 587 | - | type Keyboard struct { | |||
| 588 | - | BaseProxy | |||
| 589 | - | } | |||
| 590 | - | ||||
| 591 | - | // Touch represents a wl_touch | |||
| 592 | - | type Touch struct { | |||
| 593 | - | BaseProxy | |||
| 594 | - | } | |||
| 595 | - | ||||
| 596 | - | // Output represents a wl_output | |||
| 597 | - | type Output struct { | |||
| 598 | - | BaseProxy | |||
| 599 | - | } | |||
| 600 | - | ||||
| 601 | - | // Region represents a wl_region | |||
| 602 | - | type Region struct { | |||
| 603 | - | BaseProxy | |||
| 604 | - | } | |||
| 605 | - | ||||
| 606 | - | // Add adds a rectangle to the region | |||
| 607 | - | func (r *Region) Add(x, y, width, height int32) error { | |||
| 608 | - | return r.context.SendRequest(r, 0, x, y, width, height) // opcode 0 | |||
| 609 | - | } | |||
| 610 | - | ||||
| 611 | - | // Subtract subtracts a rectangle from the region | |||
| 612 | - | func (r *Region) Subtract(x, y, width, height int32) error { | |||
| 613 | - | return r.context.SendRequest(r, 1, x, y, width, height) // opcode 1 | |||
| 614 | - | } | |||
| 615 | - | ||||
| 616 | - | // Destroy destroys the region | |||
| 617 | - | func (r *Region) Destroy() error { | |||
| 618 | - | err := r.context.SendRequest(r, 2) // opcode 2 | |||
| 619 | - | if err == nil { | |||
| 620 | - | r.context.Unregister(r) | |||
| 621 | - | } | |||
| 622 | - | return err | |||
| 623 | - | } | |||
| 624 | - | ||||
| 625 | - | // Compositor represents a wl_compositor | |||
| 626 | - | type Compositor struct { | |||
| 627 | - | BaseProxy | |||
| 628 | - | } | |||
| 629 | - | ||||
| 630 | - | // NewCompositor creates a new compositor proxy | |||
| 631 | - | func NewCompositor(ctx *Context) *Compositor { | |||
| 632 | - | return &Compositor{ | |||
| 633 | - | BaseProxy: BaseProxy{ | |||
| 634 | - | context: ctx, | |||
| 635 | - | }, | |||
| 636 | - | } | |||
| 637 | - | } | |||
| 638 | - | ||||
| 639 | - | // CreateSurface creates a new surface | |||
| 640 | - | func (c *Compositor) CreateSurface() (*Surface, error) { | |||
| 641 | - | surface := &Surface{ | |||
| 642 | - | BaseProxy: BaseProxy{ | |||
| 643 | - | context: c.context, | |||
| 644 | - | id: c.context.display.allocateID(), | |||
| 645 | - | }, | |||
| 646 | - | } | |||
| 647 | - | ||||
| 648 | - | // Register surface before sending request | |||
| 649 | - | c.context.Register(surface) | |||
| 650 | - | ||||
| 651 | - | // Send create_surface request (opcode 0) | |||
| 652 | - | err := c.context.SendRequest(c, 0, surface.id) | |||
| 653 | - | if err != nil { | |||
| 654 | - | c.context.Unregister(surface) | |||
| 655 | - | return nil, err | |||
| 656 | - | } | |||
| 657 | - | ||||
| 658 | - | return surface, nil | |||
| 659 | - | } | |||
| 660 | - | ||||
| 661 | - | // CreateRegion creates a new region | |||
| 662 | - | func (c *Compositor) CreateRegion() (*Region, error) { | |||
| 663 | - | region := &Region{ | |||
| 664 | - | BaseProxy: BaseProxy{ | |||
| 665 | - | context: c.context, | |||
| 666 | - | id: c.context.display.allocateID(), | |||
| 667 | - | }, | |||
| 668 | - | } | |||
| 669 | - | ||||
| 670 | - | // Register region before sending request | |||
| 671 | - | c.context.Register(region) | |||
| 672 | - | ||||
| 673 | - | // Send create_region request (opcode 1) | |||
| 674 | - | err := c.context.SendRequest(c, 1, region.id) | |||
| 675 | - | if err != nil { | |||
| 676 | - | c.context.Unregister(region) | |||
| 677 | - | return nil, err | |||
| 678 | - | } | |||
| 679 | - | ||||
| 680 | - | return region, nil | |||
| 681 | - | } | |||
A docs/protocol-migration.md +29 -0
| @@ -0,0 +1,29 @@ | |||
| 1 | + | # Canonical core / migration (v0.x) | |
| 2 | + | ||
| 3 | + | `protocol/core` replaces root-package and `wl` shim core object types. Bootstrap | |
| 4 | + | `Display`, `Registry`, `Context`, and `BaseProxy` remain in the root/`wl` | |
| 5 | + | transport. Bind globals using `core.NewCompositor(display.Context())`, | |
| 6 | + | `core.NewSeat(display.Context())`, etc. and `display.Registry().Bind(name, | |
| 7 | + | core.CompositorInterface, version, compositor)`. Requests create typed registered | |
| 8 | + | children (`seat.GetKeyboard()`, `surface.Frame()`, `shm.CreatePool()`) and | |
| 9 | + | server-side `new_id` events register concrete objects before invoking handlers. | |
| 10 | + | Do not overwrite registered objects with a second wrapper. | |
| 11 | + | ||
| 12 | + | Breaking changes: `wlturbo`/`wl` `Seat`, `Surface`, `Pointer`, `Keyboard`, | |
| 13 | + | `Touch`, `Output`, `Region`, `Compositor`, their constructors, and seat | |
| 14 | + | capability aliases have moved to `protocol/core`. Generated methods accept | |
| 15 | + | concrete core objects and return concrete children, not `wl.Object`. Legacy | |
| 16 | + | raw `Event.Fd()` is not for generated events; generated FD handlers receive | |
| 17 | + | `*wlturbo.OwnedFD` with `Take() (int,error)` or `Close()`; second and later | |
| 18 | + | handlers receive nil FD. On a successful full send a generated FD request | |
| 19 | + | closes the local descriptor; on error the caller keeps it. `Registry.BindID` | |
| 20 | + | remains for legacy callers but cannot register typed event dispatch: prefer | |
| 21 | + | `Bind` and generated constructors. Unrecognized custom-proxy event opcodes | |
| 22 | + | now fail unless declared via `Display.RegisterEventSignature`. | |
| 23 | + | ||
| 24 | + | The scanner accepts `-import wl_surface=github.com/bnema/wlturbo/protocol/core` | |
| 25 | + | (repeat for each external type); local XML definitions take precedence, then | |
| 26 | + | explicit external mapping, then only bootstrap display/registry. Generate with | |
| 27 | + | `GOWORK=off go generate ./...`; metadata and XML license are in | |
| 28 | + | `protocol/SOURCE.md`. P3 extensions will use the same scanner and public core | |
| 29 | + | mappings; no generator-owned compositor policy is introduced here. | |
| @@ -0,0 +1,29 @@ | |||||
| 1 | + | # Canonical core / migration (v0.x) | |||
| 2 | + | ||||
| 3 | + | `protocol/core` replaces root-package and `wl` shim core object types. Bootstrap | |||
| 4 | + | `Display`, `Registry`, `Context`, and `BaseProxy` remain in the root/`wl` | |||
| 5 | + | transport. Bind globals using `core.NewCompositor(display.Context())`, | |||
| 6 | + | `core.NewSeat(display.Context())`, etc. and `display.Registry().Bind(name, | |||
| 7 | + | core.CompositorInterface, version, compositor)`. Requests create typed registered | |||
| 8 | + | children (`seat.GetKeyboard()`, `surface.Frame()`, `shm.CreatePool()`) and | |||
| 9 | + | server-side `new_id` events register concrete objects before invoking handlers. | |||
| 10 | + | Do not overwrite registered objects with a second wrapper. | |||
| 11 | + | ||||
| 12 | + | Breaking changes: `wlturbo`/`wl` `Seat`, `Surface`, `Pointer`, `Keyboard`, | |||
| 13 | + | `Touch`, `Output`, `Region`, `Compositor`, their constructors, and seat | |||
| 14 | + | capability aliases have moved to `protocol/core`. Generated methods accept | |||
| 15 | + | concrete core objects and return concrete children, not `wl.Object`. Legacy | |||
| 16 | + | raw `Event.Fd()` is not for generated events; generated FD handlers receive | |||
| 17 | + | `*wlturbo.OwnedFD` with `Take() (int,error)` or `Close()`; second and later | |||
| 18 | + | handlers receive nil FD. On a successful full send a generated FD request | |||
| 19 | + | closes the local descriptor; on error the caller keeps it. `Registry.BindID` | |||
| 20 | + | remains for legacy callers but cannot register typed event dispatch: prefer | |||
| 21 | + | `Bind` and generated constructors. Unrecognized custom-proxy event opcodes | |||
| 22 | + | now fail unless declared via `Display.RegisterEventSignature`. | |||
| 23 | + | ||||
| 24 | + | The scanner accepts `-import wl_surface=github.com/bnema/wlturbo/protocol/core` | |||
| 25 | + | (repeat for each external type); local XML definitions take precedence, then | |||
| 26 | + | explicit external mapping, then only bootstrap display/registry. Generate with | |||
| 27 | + | `GOWORK=off go generate ./...`; metadata and XML license are in | |||
| 28 | + | `protocol/SOURCE.md`. P3 extensions will use the same scanner and public core | |||
| 29 | + | mappings; no generator-owned compositor policy is introduced here. | |||
M event_pool.go +0 -34
| @@ -27,10 +27,6 @@ type EventDispatcher struct { | |||
| 27 | 27 | ||
| 28 | 28 | // Extended handlers for object IDs >= 1024 (rare case) | |
| 29 | 29 | extHandlers sync.Map | |
| 30 | - | ||
| 31 | - | // Pre-allocated event objects to avoid allocations | |
| 32 | - | eventCache [64]Event | |
| 33 | - | eventIndex atomic.Uint32 | |
| 34 | 30 | } | |
| 35 | 31 | ||
| 36 | 32 | // handlerEntry stores handlers for a specific object | |
| @@ -166,36 +162,6 @@ type RawEvent struct { | |||
| 166 | 162 | Data []byte | |
| 167 | 163 | } | |
| 168 | 164 | ||
| 169 | - | // DirectDispatcher provides the absolute fastest dispatch path for hot events | |
| 170 | - | type DirectDispatcher struct { | |
| 171 | - | // Direct function pointers for the hottest paths (e.g., pointer motion) | |
| 172 | - | pointerMotion func(surfaceX, surfaceY Fixed) | |
| 173 | - | pointerButton func(button, state uint32) | |
| 174 | - | keyboardKey func(key, state uint32) | |
| 175 | - | frameCallback func(callbackData uint32) | |
| 176 | - | ||
| 177 | - | // Fallback to regular dispatcher | |
| 178 | - | fallback *EventDispatcher | |
| 179 | - | } | |
| 180 | - | ||
| 181 | - | // DispatchPointerMotion dispatches pointer motion events with zero overhead | |
| 182 | - | // | |
| 183 | - | //go:inline | |
| 184 | - | func (d *DirectDispatcher) DispatchPointerMotion(surfaceX, surfaceY Fixed) { | |
| 185 | - | if d.pointerMotion != nil { | |
| 186 | - | d.pointerMotion(surfaceX, surfaceY) | |
| 187 | - | } | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | // DispatchPointerButton dispatches pointer button events with zero overhead | |
| 191 | - | // | |
| 192 | - | //go:inline | |
| 193 | - | func (d *DirectDispatcher) DispatchPointerButton(button, state uint32) { | |
| 194 | - | if d.pointerButton != nil { | |
| 195 | - | d.pointerButton(button, state) | |
| 196 | - | } | |
| 197 | - | } | |
| 198 | - | ||
| 199 | 165 | // Ensure cache line alignment for hot data structures | |
| 200 | 166 | type cacheLinePad [64]byte | |
| 201 | 167 | ||
| @@ -27,10 +27,6 @@ type EventDispatcher struct { | |||||
| 27 | 27 | ||||
| 28 | // Extended handlers for object IDs >= 1024 (rare case) | 28 | // Extended handlers for object IDs >= 1024 (rare case) | ||
| 29 | extHandlers sync.Map | 29 | extHandlers sync.Map | ||
| 30 | - | ||||
| 31 | - | // Pre-allocated event objects to avoid allocations | |||
| 32 | - | eventCache [64]Event | |||
| 33 | - | eventIndex atomic.Uint32 | |||
| 34 | } | 30 | } | ||
| 35 | 31 | ||||
| 36 | // handlerEntry stores handlers for a specific object | 32 | // handlerEntry stores handlers for a specific object | ||
| @@ -166,36 +162,6 @@ type RawEvent struct { | |||||
| 166 | Data []byte | 162 | Data []byte | ||
| 167 | } | 163 | } | ||
| 168 | 164 | ||||
| 169 | - | // DirectDispatcher provides the absolute fastest dispatch path for hot events | |||
| 170 | - | type DirectDispatcher struct { | |||
| 171 | - | // Direct function pointers for the hottest paths (e.g., pointer motion) | |||
| 172 | - | pointerMotion func(surfaceX, surfaceY Fixed) | |||
| 173 | - | pointerButton func(button, state uint32) | |||
| 174 | - | keyboardKey func(key, state uint32) | |||
| 175 | - | frameCallback func(callbackData uint32) | |||
| 176 | - | ||||
| 177 | - | // Fallback to regular dispatcher | |||
| 178 | - | fallback *EventDispatcher | |||
| 179 | - | } | |||
| 180 | - | ||||
| 181 | - | // DispatchPointerMotion dispatches pointer motion events with zero overhead | |||
| 182 | - | // | |||
| 183 | - | //go:inline | |||
| 184 | - | func (d *DirectDispatcher) DispatchPointerMotion(surfaceX, surfaceY Fixed) { | |||
| 185 | - | if d.pointerMotion != nil { | |||
| 186 | - | d.pointerMotion(surfaceX, surfaceY) | |||
| 187 | - | } | |||
| 188 | - | } | |||
| 189 | - | ||||
| 190 | - | // DispatchPointerButton dispatches pointer button events with zero overhead | |||
| 191 | - | // | |||
| 192 | - | //go:inline | |||
| 193 | - | func (d *DirectDispatcher) DispatchPointerButton(button, state uint32) { | |||
| 194 | - | if d.pointerButton != nil { | |||
| 195 | - | d.pointerButton(button, state) | |||
| 196 | - | } | |||
| 197 | - | } | |||
| 198 | - | ||||
| 199 | // Ensure cache line alignment for hot data structures | 165 | // Ensure cache line alignment for hot data structures | ||
| 200 | type cacheLinePad [64]byte | 166 | type cacheLinePad [64]byte | ||
| 201 | 167 | ||||
M fd_test.go +92 -48
| @@ -90,6 +90,8 @@ func newFDDisplay(t *testing.T, client *net.UnixConn) (*Display, *[]int) { | |||
| 90 | 90 | ||
| 91 | 91 | d := newDisplay(client) | |
| 92 | 92 | fds := []int{} | |
| 93 | + | d.RegisterEventSignature(7, 3, "uint,fd,") | |
| 94 | + | d.RegisterEventSignature(7, 4, "uint,") | |
| 93 | 95 | d.objects.Store(uint32(7), &fdRecordingProxy{ | |
| 94 | 96 | BaseProxy: BaseProxy{id: 7, context: d.context}, | |
| 95 | 97 | fds: &fds, | |
| @@ -146,49 +148,19 @@ func TestDisplayFD_Single(t *testing.T) { | |||
| 146 | 148 | readSentinel(t, r, "single-fd-sentinel") | |
| 147 | 149 | } | |
| 148 | 150 | ||
| 149 | - | func TestDisplayFD_MultipleInOneControlMessage(t *testing.T) { | |
| 151 | + | func TestDisplayFD_ExtraDescriptorRejected(t *testing.T) { | |
| 150 | 152 | client, peer := unixSocketPair(t) | |
| 151 | - | d, received := newFDDisplay(t, client) | |
| 152 | - | ||
| 153 | - | firstR, firstW, err := os.Pipe() | |
| 154 | - | if err != nil { | |
| 155 | - | t.Fatalf("pipe: %v", err) | |
| 156 | - | } | |
| 157 | - | defer firstR.Close() | |
| 158 | - | secondR, secondW, err := os.Pipe() | |
| 153 | + | d, _ := newFDDisplay(t, client) | |
| 154 | + | r, w, err := os.Pipe() | |
| 159 | 155 | if err != nil { | |
| 160 | - | t.Fatalf("pipe: %v", err) | |
| 156 | + | t.Fatal(err) | |
| 161 | 157 | } | |
| 162 | - | defer secondR.Close() | |
| 163 | - | ||
| 164 | - | sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), | |
| 165 | - | []int{int(firstW.Fd()), int(secondW.Fd())}) | |
| 166 | - | firstW.Close() | |
| 167 | - | secondW.Close() | |
| 168 | - | ||
| 169 | - | if err := d.Dispatch(); err != nil { | |
| 170 | - | t.Fatalf("Dispatch: %v", err) | |
| 171 | - | } | |
| 172 | - | ||
| 173 | - | // A handler consumes one descriptor per fd argument, so the test drains | |
| 174 | - | // whatever the connection still holds, in arrival order. | |
| 175 | - | for { | |
| 176 | - | fd, ok := d.nextFD() | |
| 177 | - | if !ok { | |
| 178 | - | break | |
| 179 | - | } | |
| 180 | - | *received = append(*received, fd) | |
| 181 | - | } | |
| 182 | - | ||
| 183 | - | if len(*received) != 2 { | |
| 184 | - | t.Fatalf("received %d descriptors, want 2", len(*received)) | |
| 158 | + | defer r.Close() | |
| 159 | + | defer w.Close() | |
| 160 | + | sendMessageWithFDs(t, peer, message(7, 3, nil), []int{int(w.Fd()), int(w.Fd())}) | |
| 161 | + | if err := d.Dispatch(); !errors.Is(err, ErrMalformedFrame) { | |
| 162 | + | t.Fatalf("extra FD: %v", err) | |
| 185 | 163 | } | |
| 186 | - | ||
| 187 | - | writeSentinel(t, uintptr((*received)[0]), "first-pipe") | |
| 188 | - | writeSentinel(t, uintptr((*received)[1]), "second-pipe") | |
| 189 | - | ||
| 190 | - | readSentinel(t, firstR, "first-pipe") | |
| 191 | - | readSentinel(t, secondR, "second-pipe") | |
| 192 | 164 | } | |
| 193 | 165 | ||
| 194 | 166 | func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) { | |
| @@ -304,9 +276,9 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) { | |||
| 304 | 276 | } | |
| 305 | 277 | ||
| 306 | 278 | // A truncated control message must not leave descriptors queued. | |
| 307 | - | d.fdMu.Lock() | |
| 279 | + | d.recvMu.Lock() | |
| 308 | 280 | pending := len(d.pendingFDs) | |
| 309 | - | d.fdMu.Unlock() | |
| 281 | + | d.recvMu.Unlock() | |
| 310 | 282 | if pending != 0 { | |
| 311 | 283 | t.Fatalf("%d descriptors still queued after a truncated control message", pending) | |
| 312 | 284 | } | |
| @@ -315,6 +287,7 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) { | |||
| 315 | 287 | func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) { | |
| 316 | 288 | client, peer := unixSocketPair(t) | |
| 317 | 289 | d := newDisplay(client) | |
| 290 | + | d.RegisterEventSignature(7, 3, "uint,fd,") | |
| 318 | 291 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | |
| 319 | 292 | BaseProxy: BaseProxy{id: 7, context: d.context}, | |
| 320 | 293 | }) | |
| @@ -333,12 +306,12 @@ func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) { | |||
| 333 | 306 | t.Fatalf("Dispatch: %v", err) | |
| 334 | 307 | } | |
| 335 | 308 | ||
| 336 | - | // While the display still holds the descriptor the pipe stays open. | |
| 337 | - | if err := r.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil { | |
| 338 | - | t.Fatalf("SetReadDeadline: %v", err) | |
| 309 | + | // An ignored FD is closed at the end of the event, not at shutdown. | |
| 310 | + | if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { | |
| 311 | + | t.Fatal(err) | |
| 339 | 312 | } | |
| 340 | - | if _, err := r.Read(make([]byte, 1)); !errors.Is(err, os.ErrDeadlineExceeded) { | |
| 341 | - | t.Fatalf("read with the display holding the descriptor = %v, want %v", err, os.ErrDeadlineExceeded) | |
| 313 | + | if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) { | |
| 314 | + | t.Fatalf("ignored FD: %v", err) | |
| 342 | 315 | } | |
| 343 | 316 | ||
| 344 | 317 | // Closing the display must release it, so the read end sees EOF. | |
| @@ -421,6 +394,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) { | |||
| 421 | 394 | ||
| 422 | 395 | client, peer := rawSocketPair(t) | |
| 423 | 396 | d := newDisplay(client) | |
| 397 | + | d.RegisterEventSignature(7, 3, "uint,fd,") | |
| 424 | 398 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | |
| 425 | 399 | BaseProxy: BaseProxy{id: 7, context: d.context}, | |
| 426 | 400 | }) | |
| @@ -448,6 +422,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) { | |||
| 448 | 422 | func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) { | |
| 449 | 423 | client, peer := unixSocketPair(t) | |
| 450 | 424 | d := newDisplay(client) | |
| 425 | + | d.RegisterEventSignature(7, 3, "uint,fd,") | |
| 451 | 426 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | |
| 452 | 427 | BaseProxy: BaseProxy{id: 7, context: d.context}, | |
| 453 | 428 | }) | |
| @@ -467,9 +442,9 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) { | |||
| 467 | 442 | t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame) | |
| 468 | 443 | } | |
| 469 | 444 | ||
| 470 | - | d.fdMu.Lock() | |
| 445 | + | d.recvMu.Lock() | |
| 471 | 446 | pending := len(d.pendingFDs) | |
| 472 | - | d.fdMu.Unlock() | |
| 447 | + | d.recvMu.Unlock() | |
| 473 | 448 | if pending != 0 { | |
| 474 | 449 | t.Fatalf("%d descriptors still queued after a malformed frame", pending) | |
| 475 | 450 | } | |
| @@ -481,3 +456,72 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) { | |||
| 481 | 456 | t.Fatalf("read after malformed frame = %v, want %v", err, io.EOF) | |
| 482 | 457 | } | |
| 483 | 458 | } | |
| 459 | + | ||
| 460 | + | // Closing in a callback must not wait for the dispatch reader's state lock. | |
| 461 | + | type closingProxy struct { | |
| 462 | + | BaseProxy | |
| 463 | + | d *Display | |
| 464 | + | returned bool | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | func (p *closingProxy) EventSignature(op uint16) (string, bool) { return "fd,", op == 0 } | |
| 468 | + | func (p *closingProxy) Dispatch(*Event) { p.returned = p.d.Close() == nil } | |
| 469 | + | ||
| 470 | + | func TestCloseInsideHandlerClosesQueuedFDs(t *testing.T) { | |
| 471 | + | client, peer := unixSocketPair(t) | |
| 472 | + | d := newDisplay(client) | |
| 473 | + | proxy := &closingProxy{BaseProxy: BaseProxy{id: 7, context: d.context}, d: d} | |
| 474 | + | d.context.Register(proxy) | |
| 475 | + | r, w, err := os.Pipe() | |
| 476 | + | if err != nil { | |
| 477 | + | t.Fatal(err) | |
| 478 | + | } | |
| 479 | + | defer r.Close() | |
| 480 | + | extra, xw, err := os.Pipe() | |
| 481 | + | if err != nil { | |
| 482 | + | t.Fatal(err) | |
| 483 | + | } | |
| 484 | + | defer extra.Close() | |
| 485 | + | // Two frames in one read; second FD remains queued while first handler runs. | |
| 486 | + | payload := append(message(7, 0, nil), message(7, 0, nil)...) | |
| 487 | + | sendMessageWithFDs(t, peer, payload, []int{int(w.Fd()), int(xw.Fd())}) | |
| 488 | + | w.Close() | |
| 489 | + | xw.Close() | |
| 490 | + | if err := d.Dispatch(); err != nil { | |
| 491 | + | t.Fatal(err) | |
| 492 | + | } | |
| 493 | + | if !proxy.returned { | |
| 494 | + | t.Fatal("Close did not return from handler") | |
| 495 | + | } | |
| 496 | + | for _, reader := range []*os.File{r, extra} { | |
| 497 | + | if err := reader.SetReadDeadline(time.Now().Add(time.Second)); err != nil { | |
| 498 | + | t.Fatal(err) | |
| 499 | + | } | |
| 500 | + | if _, err := reader.Read(make([]byte, 1)); !errors.Is(err, io.EOF) { | |
| 501 | + | t.Fatalf("fd not closed: %v", err) | |
| 502 | + | } | |
| 503 | + | } | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | func TestParseFDsKeepsRightsBeforeMalformedHeader(t *testing.T) { | |
| 507 | + | r, w, err := os.Pipe() | |
| 508 | + | if err != nil { | |
| 509 | + | t.Fatal(err) | |
| 510 | + | } | |
| 511 | + | defer r.Close() | |
| 512 | + | defer w.Close() | |
| 513 | + | rights := unix.UnixRights(int(w.Fd())) | |
| 514 | + | oob := append(rights, make([]byte, unix.SizeofCmsghdr)...) | |
| 515 | + | fds, err := parseFDs(oob) | |
| 516 | + | if err == nil || len(fds) != 1 { | |
| 517 | + | t.Fatalf("fds=%v err=%v", fds, err) | |
| 518 | + | } | |
| 519 | + | if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != nil { | |
| 520 | + | t.Fatal(err) | |
| 521 | + | } | |
| 522 | + | for _, fd := range fds { | |
| 523 | + | if err := unix.Close(fd); err != nil { | |
| 524 | + | t.Fatal(err) | |
| 525 | + | } | |
| 526 | + | } | |
| 527 | + | } | |
| @@ -90,6 +90,8 @@ func newFDDisplay(t *testing.T, client *net.UnixConn) (*Display, *[]int) { | |||||
| 90 | 90 | ||||
| 91 | d := newDisplay(client) | 91 | d := newDisplay(client) | ||
| 92 | fds := []int{} | 92 | fds := []int{} | ||
| 93 | + | d.RegisterEventSignature(7, 3, "uint,fd,") | |||
| 94 | + | d.RegisterEventSignature(7, 4, "uint,") | |||
| 93 | d.objects.Store(uint32(7), &fdRecordingProxy{ | 95 | d.objects.Store(uint32(7), &fdRecordingProxy{ | ||
| 94 | BaseProxy: BaseProxy{id: 7, context: d.context}, | 96 | BaseProxy: BaseProxy{id: 7, context: d.context}, | ||
| 95 | fds: &fds, | 97 | fds: &fds, | ||
| @@ -146,49 +148,19 @@ func TestDisplayFD_Single(t *testing.T) { | |||||
| 146 | readSentinel(t, r, "single-fd-sentinel") | 148 | readSentinel(t, r, "single-fd-sentinel") | ||
| 147 | } | 149 | } | ||
| 148 | 150 | ||||
| 149 | - | func TestDisplayFD_MultipleInOneControlMessage(t *testing.T) { | 151 | + | func TestDisplayFD_ExtraDescriptorRejected(t *testing.T) { |
| 150 | client, peer := unixSocketPair(t) | 152 | client, peer := unixSocketPair(t) | ||
| 151 | - | d, received := newFDDisplay(t, client) | 153 | + | d, _ := newFDDisplay(t, client) |
| 152 | - | 154 | + | r, w, err := os.Pipe() | |
| 153 | - | firstR, firstW, err := os.Pipe() | |||
| 154 | - | if err != nil { | |||
| 155 | - | t.Fatalf("pipe: %v", err) | |||
| 156 | - | } | |||
| 157 | - | defer firstR.Close() | |||
| 158 | - | secondR, secondW, err := os.Pipe() | |||
| 159 | if err != nil { | 155 | if err != nil { | ||
| 160 | - | t.Fatalf("pipe: %v", err) | 156 | + | t.Fatal(err) |
| 161 | } | 157 | } | ||
| 162 | - | defer secondR.Close() | 158 | + | defer r.Close() |
| 163 | - | 159 | + | defer w.Close() | |
| 164 | - | sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), | 160 | + | sendMessageWithFDs(t, peer, message(7, 3, nil), []int{int(w.Fd()), int(w.Fd())}) |
| 165 | - | []int{int(firstW.Fd()), int(secondW.Fd())}) | 161 | + | if err := d.Dispatch(); !errors.Is(err, ErrMalformedFrame) { |
| 166 | - | firstW.Close() | 162 | + | t.Fatalf("extra FD: %v", err) |
| 167 | - | secondW.Close() | |||
| 168 | - | ||||
| 169 | - | if err := d.Dispatch(); err != nil { | |||
| 170 | - | t.Fatalf("Dispatch: %v", err) | |||
| 171 | - | } | |||
| 172 | - | ||||
| 173 | - | // A handler consumes one descriptor per fd argument, so the test drains | |||
| 174 | - | // whatever the connection still holds, in arrival order. | |||
| 175 | - | for { | |||
| 176 | - | fd, ok := d.nextFD() | |||
| 177 | - | if !ok { | |||
| 178 | - | break | |||
| 179 | - | } | |||
| 180 | - | *received = append(*received, fd) | |||
| 181 | - | } | |||
| 182 | - | ||||
| 183 | - | if len(*received) != 2 { | |||
| 184 | - | t.Fatalf("received %d descriptors, want 2", len(*received)) | |||
| 185 | } | 163 | } | ||
| 186 | - | ||||
| 187 | - | writeSentinel(t, uintptr((*received)[0]), "first-pipe") | |||
| 188 | - | writeSentinel(t, uintptr((*received)[1]), "second-pipe") | |||
| 189 | - | ||||
| 190 | - | readSentinel(t, firstR, "first-pipe") | |||
| 191 | - | readSentinel(t, secondR, "second-pipe") | |||
| 192 | } | 164 | } | ||
| 193 | 165 | ||||
| 194 | func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) { | 166 | func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) { | ||
| @@ -304,9 +276,9 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) { | |||||
| 304 | } | 276 | } | ||
| 305 | 277 | ||||
| 306 | // A truncated control message must not leave descriptors queued. | 278 | // A truncated control message must not leave descriptors queued. | ||
| 307 | - | d.fdMu.Lock() | 279 | + | d.recvMu.Lock() |
| 308 | pending := len(d.pendingFDs) | 280 | pending := len(d.pendingFDs) | ||
| 309 | - | d.fdMu.Unlock() | 281 | + | d.recvMu.Unlock() |
| 310 | if pending != 0 { | 282 | if pending != 0 { | ||
| 311 | t.Fatalf("%d descriptors still queued after a truncated control message", pending) | 283 | t.Fatalf("%d descriptors still queued after a truncated control message", pending) | ||
| 312 | } | 284 | } | ||
| @@ -315,6 +287,7 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) { | |||||
| 315 | func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) { | 287 | func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) { | ||
| 316 | client, peer := unixSocketPair(t) | 288 | client, peer := unixSocketPair(t) | ||
| 317 | d := newDisplay(client) | 289 | d := newDisplay(client) | ||
| 290 | + | d.RegisterEventSignature(7, 3, "uint,fd,") | |||
| 318 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | 291 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | ||
| 319 | BaseProxy: BaseProxy{id: 7, context: d.context}, | 292 | BaseProxy: BaseProxy{id: 7, context: d.context}, | ||
| 320 | }) | 293 | }) | ||
| @@ -333,12 +306,12 @@ func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) { | |||||
| 333 | t.Fatalf("Dispatch: %v", err) | 306 | t.Fatalf("Dispatch: %v", err) | ||
| 334 | } | 307 | } | ||
| 335 | 308 | ||||
| 336 | - | // While the display still holds the descriptor the pipe stays open. | 309 | + | // An ignored FD is closed at the end of the event, not at shutdown. |
| 337 | - | if err := r.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil { | 310 | + | if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { |
| 338 | - | t.Fatalf("SetReadDeadline: %v", err) | 311 | + | t.Fatal(err) |
| 339 | } | 312 | } | ||
| 340 | - | if _, err := r.Read(make([]byte, 1)); !errors.Is(err, os.ErrDeadlineExceeded) { | 313 | + | if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) { |
| 341 | - | t.Fatalf("read with the display holding the descriptor = %v, want %v", err, os.ErrDeadlineExceeded) | 314 | + | t.Fatalf("ignored FD: %v", err) |
| 342 | } | 315 | } | ||
| 343 | 316 | ||||
| 344 | // Closing the display must release it, so the read end sees EOF. | 317 | // Closing the display must release it, so the read end sees EOF. | ||
| @@ -421,6 +394,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) { | |||||
| 421 | 394 | ||||
| 422 | client, peer := rawSocketPair(t) | 395 | client, peer := rawSocketPair(t) | ||
| 423 | d := newDisplay(client) | 396 | d := newDisplay(client) | ||
| 397 | + | d.RegisterEventSignature(7, 3, "uint,fd,") | |||
| 424 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | 398 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | ||
| 425 | BaseProxy: BaseProxy{id: 7, context: d.context}, | 399 | BaseProxy: BaseProxy{id: 7, context: d.context}, | ||
| 426 | }) | 400 | }) | ||
| @@ -448,6 +422,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) { | |||||
| 448 | func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) { | 422 | func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) { | ||
| 449 | client, peer := unixSocketPair(t) | 423 | client, peer := unixSocketPair(t) | ||
| 450 | d := newDisplay(client) | 424 | d := newDisplay(client) | ||
| 425 | + | d.RegisterEventSignature(7, 3, "uint,fd,") | |||
| 451 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | 426 | d.objects.Store(uint32(7), &fdIgnoringProxy{ | ||
| 452 | BaseProxy: BaseProxy{id: 7, context: d.context}, | 427 | BaseProxy: BaseProxy{id: 7, context: d.context}, | ||
| 453 | }) | 428 | }) | ||
| @@ -467,9 +442,9 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) { | |||||
| 467 | t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame) | 442 | t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame) | ||
| 468 | } | 443 | } | ||
| 469 | 444 | ||||
| 470 | - | d.fdMu.Lock() | 445 | + | d.recvMu.Lock() |
| 471 | pending := len(d.pendingFDs) | 446 | pending := len(d.pendingFDs) | ||
| 472 | - | d.fdMu.Unlock() | 447 | + | d.recvMu.Unlock() |
| 473 | if pending != 0 { | 448 | if pending != 0 { | ||
| 474 | t.Fatalf("%d descriptors still queued after a malformed frame", pending) | 449 | t.Fatalf("%d descriptors still queued after a malformed frame", pending) | ||
| 475 | } | 450 | } | ||
| @@ -481,3 +456,72 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) { | |||||
| 481 | t.Fatalf("read after malformed frame = %v, want %v", err, io.EOF) | 456 | t.Fatalf("read after malformed frame = %v, want %v", err, io.EOF) | ||
| 482 | } | 457 | } | ||
| 483 | } | 458 | } | ||
| 459 | + | ||||
| 460 | + | // Closing in a callback must not wait for the dispatch reader's state lock. | |||
| 461 | + | type closingProxy struct { | |||
| 462 | + | BaseProxy | |||
| 463 | + | d *Display | |||
| 464 | + | returned bool | |||
| 465 | + | } | |||
| 466 | + | ||||
| 467 | + | func (p *closingProxy) EventSignature(op uint16) (string, bool) { return "fd,", op == 0 } | |||
| 468 | + | func (p *closingProxy) Dispatch(*Event) { p.returned = p.d.Close() == nil } | |||
| 469 | + | ||||
| 470 | + | func TestCloseInsideHandlerClosesQueuedFDs(t *testing.T) { | |||
| 471 | + | client, peer := unixSocketPair(t) | |||
| 472 | + | d := newDisplay(client) | |||
| 473 | + | proxy := &closingProxy{BaseProxy: BaseProxy{id: 7, context: d.context}, d: d} | |||
| 474 | + | d.context.Register(proxy) | |||
| 475 | + | r, w, err := os.Pipe() | |||
| 476 | + | if err != nil { | |||
| 477 | + | t.Fatal(err) | |||
| 478 | + | } | |||
| 479 | + | defer r.Close() | |||
| 480 | + | extra, xw, err := os.Pipe() | |||
| 481 | + | if err != nil { | |||
| 482 | + | t.Fatal(err) | |||
| 483 | + | } | |||
| 484 | + | defer extra.Close() | |||
| 485 | + | // Two frames in one read; second FD remains queued while first handler runs. | |||
| 486 | + | payload := append(message(7, 0, nil), message(7, 0, nil)...) | |||
| 487 | + | sendMessageWithFDs(t, peer, payload, []int{int(w.Fd()), int(xw.Fd())}) | |||
| 488 | + | w.Close() | |||
| 489 | + | xw.Close() | |||
| 490 | + | if err := d.Dispatch(); err != nil { | |||
| 491 | + | t.Fatal(err) | |||
| 492 | + | } | |||
| 493 | + | if !proxy.returned { | |||
| 494 | + | t.Fatal("Close did not return from handler") | |||
| 495 | + | } | |||
| 496 | + | for _, reader := range []*os.File{r, extra} { | |||
| 497 | + | if err := reader.SetReadDeadline(time.Now().Add(time.Second)); err != nil { | |||
| 498 | + | t.Fatal(err) | |||
| 499 | + | } | |||
| 500 | + | if _, err := reader.Read(make([]byte, 1)); !errors.Is(err, io.EOF) { | |||
| 501 | + | t.Fatalf("fd not closed: %v", err) | |||
| 502 | + | } | |||
| 503 | + | } | |||
| 504 | + | } | |||
| 505 | + | ||||
| 506 | + | func TestParseFDsKeepsRightsBeforeMalformedHeader(t *testing.T) { | |||
| 507 | + | r, w, err := os.Pipe() | |||
| 508 | + | if err != nil { | |||
| 509 | + | t.Fatal(err) | |||
| 510 | + | } | |||
| 511 | + | defer r.Close() | |||
| 512 | + | defer w.Close() | |||
| 513 | + | rights := unix.UnixRights(int(w.Fd())) | |||
| 514 | + | oob := append(rights, make([]byte, unix.SizeofCmsghdr)...) | |||
| 515 | + | fds, err := parseFDs(oob) | |||
| 516 | + | if err == nil || len(fds) != 1 { | |||
| 517 | + | t.Fatalf("fds=%v err=%v", fds, err) | |||
| 518 | + | } | |||
| 519 | + | if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != nil { | |||
| 520 | + | t.Fatal(err) | |||
| 521 | + | } | |||
| 522 | + | for _, fd := range fds { | |||
| 523 | + | if err := unix.Close(fd); err != nil { | |||
| 524 | + | t.Fatal(err) | |||
| 525 | + | } | |||
| 526 | + | } | |||
| 527 | + | } | |||
A fd_zero_test.go +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | package wlturbo | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | func TestOwnedFDZero(t *testing.T) { | |
| 6 | + | fd := &OwnedFD{fd: 0} | |
| 7 | + | n, e := fd.Take() | |
| 8 | + | if e != nil || n != 0 { | |
| 9 | + | t.Fatalf("Take fd zero = %d %v", n, e) | |
| 10 | + | } | |
| 11 | + | if e := fd.Close(); e != nil { | |
| 12 | + | t.Fatal(e) | |
| 13 | + | } | |
| 14 | + | } | |
| @@ -0,0 +1,14 @@ | |||||
| 1 | + | package wlturbo | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | func TestOwnedFDZero(t *testing.T) { | |||
| 6 | + | fd := &OwnedFD{fd: 0} | |||
| 7 | + | n, e := fd.Take() | |||
| 8 | + | if e != nil || n != 0 { | |||
| 9 | + | t.Fatalf("Take fd zero = %d %v", n, e) | |||
| 10 | + | } | |||
| 11 | + | if e := fd.Close(); e != nil { | |||
| 12 | + | t.Fatal(e) | |||
| 13 | + | } | |||
| 14 | + | } | |||
A internal/scanner/scanner.go +731 -0
| @@ -0,0 +1,731 @@ | |||
| 1 | + | // Package scanner generates WLTurbo-backed Go bindings from Wayland protocol | |
| 2 | + | // XML files. | |
| 3 | + | // | |
| 4 | + | // The generated code owns protocol semantics only: it embeds wl.BaseProxy, | |
| 5 | + | // allocates and registers child objects through the wl.Context, sends requests | |
| 6 | + | // through that context, and dispatches typed events to registered handlers. | |
| 7 | + | // Message framing, descriptor passing and connection lifetime stay in the | |
| 8 | + | // transport, so a generated binding never opens a socket or writes a header. | |
| 9 | + | package scanner | |
| 10 | + | ||
| 11 | + | import ( | |
| 12 | + | "bytes" | |
| 13 | + | "encoding/xml" | |
| 14 | + | "fmt" | |
| 15 | + | "go/format" | |
| 16 | + | "io" | |
| 17 | + | "os" | |
| 18 | + | "path/filepath" | |
| 19 | + | "sort" | |
| 20 | + | "strings" | |
| 21 | + | "text/template" | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | // TransportImport is the import path generated bindings use for the Wayland | |
| 25 | + | // transport. | |
| 26 | + | const TransportImport = "github.com/bnema/wlturbo/wl" | |
| 27 | + | ||
| 28 | + | // Protocol represents a Wayland protocol specification | |
| 29 | + | type Protocol struct { | |
| 30 | + | XMLName xml.Name `xml:"protocol"` | |
| 31 | + | Name string `xml:"name,attr"` | |
| 32 | + | Copyright string `xml:"copyright"` | |
| 33 | + | Description *Description `xml:"description"` | |
| 34 | + | Interfaces []Interface `xml:"interface"` | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // Interface represents a Wayland interface in the protocol | |
| 38 | + | type Interface struct { | |
| 39 | + | Name string `xml:"name,attr"` | |
| 40 | + | Version int `xml:"version,attr"` | |
| 41 | + | Description *Description `xml:"description"` | |
| 42 | + | Requests []Request `xml:"request"` | |
| 43 | + | Events []Event `xml:"event"` | |
| 44 | + | Enums []Enum `xml:"enum"` | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // Request represents a client-to-server message | |
| 48 | + | type Request struct { | |
| 49 | + | Name string `xml:"name,attr"` | |
| 50 | + | Type string `xml:"type,attr"` // "destructor" or empty | |
| 51 | + | Since int `xml:"since,attr"` | |
| 52 | + | Description *Description `xml:"description"` | |
| 53 | + | Args []Arg `xml:"arg"` | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // Event represents a server-to-client message | |
| 57 | + | type Event struct { | |
| 58 | + | Type string `xml:"type,attr"` | |
| 59 | + | Name string `xml:"name,attr"` | |
| 60 | + | Since int `xml:"since,attr"` | |
| 61 | + | Description *Description `xml:"description"` | |
| 62 | + | Args []Arg `xml:"arg"` | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // Arg represents a message argument | |
| 66 | + | type Arg struct { | |
| 67 | + | Name string `xml:"name,attr"` | |
| 68 | + | Type string `xml:"type,attr"` | |
| 69 | + | Summary string `xml:"summary,attr"` | |
| 70 | + | Interface string `xml:"interface,attr"` | |
| 71 | + | AllowNull bool `xml:"allow-null,attr"` | |
| 72 | + | Enum string `xml:"enum,attr"` | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // Enum represents an enumeration type | |
| 76 | + | type Enum struct { | |
| 77 | + | Name string `xml:"name,attr"` | |
| 78 | + | Since int `xml:"since,attr"` | |
| 79 | + | Bitfield bool `xml:"bitfield,attr"` | |
| 80 | + | Description *Description `xml:"description"` | |
| 81 | + | Entries []Entry `xml:"entry"` | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Entry represents an enum value | |
| 85 | + | type Entry struct { | |
| 86 | + | Name string `xml:"name,attr"` | |
| 87 | + | Value string `xml:"value,attr"` | |
| 88 | + | Summary string `xml:"summary,attr"` | |
| 89 | + | Since int `xml:"since,attr"` | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | // Description represents documentation | |
| 93 | + | type Description struct { | |
| 94 | + | Summary string `xml:"summary,attr"` | |
| 95 | + | Text string `xml:",chardata"` | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // Scanner generates Go bindings from Wayland protocol XML | |
| 99 | + | type Scanner struct { | |
| 100 | + | protocol *Protocol | |
| 101 | + | source string | |
| 102 | + | // CrossPackage maps interface names to Go package import paths. Local wins. | |
| 103 | + | CrossPackage map[string]string | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // NewScanner creates a new protocol scanner | |
| 107 | + | func NewScanner() *Scanner { | |
| 108 | + | return &Scanner{} | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | // ParseXML parses a Wayland protocol XML file | |
| 112 | + | func (s *Scanner) ParseXML(path string) error { | |
| 113 | + | file, err := os.Open(path) | |
| 114 | + | if err != nil { | |
| 115 | + | return fmt.Errorf("failed to open XML file: %w", err) | |
| 116 | + | } | |
| 117 | + | defer func() { _ = file.Close() }() | |
| 118 | + | ||
| 119 | + | data, err := io.ReadAll(file) | |
| 120 | + | if err != nil { | |
| 121 | + | return fmt.Errorf("failed to read XML file: %w", err) | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | var protocol Protocol | |
| 125 | + | if err := xml.Unmarshal(data, &protocol); err != nil { | |
| 126 | + | return fmt.Errorf("failed to parse XML: %w", err) | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | s.protocol = &protocol | |
| 130 | + | // Only the base name is recorded: generated output must not depend on the | |
| 131 | + | // path the XML happened to be read from. | |
| 132 | + | s.source = filepath.Base(path) | |
| 133 | + | return nil | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // Generate creates Go source code for the parsed protocol | |
| 137 | + | func (s *Scanner) Generate(packageName string) ([]byte, error) { | |
| 138 | + | if s.protocol == nil { | |
| 139 | + | return nil, fmt.Errorf("no protocol parsed") | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | data, err := s.prepareTemplateData(packageName) | |
| 143 | + | if err != nil { | |
| 144 | + | return nil, err | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | var buf bytes.Buffer | |
| 148 | + | tmpl := template.Must(template.New("protocol").Parse(protocolTemplate)) | |
| 149 | + | if err := tmpl.Execute(&buf, data); err != nil { | |
| 150 | + | return nil, fmt.Errorf("failed to execute template: %w", err) | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | formatted, err := format.Source(buf.Bytes()) | |
| 154 | + | if err != nil { | |
| 155 | + | return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err) | |
| 156 | + | } | |
| 157 | + | return formatted, nil | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | // templateData holds data for template generation | |
| 161 | + | type templateData struct { | |
| 162 | + | Package string | |
| 163 | + | Protocol string | |
| 164 | + | Source string | |
| 165 | + | ImportBlock string | |
| 166 | + | Constants []constantData | |
| 167 | + | Interfaces []interfaceData | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | type interfaceData struct { | |
| 171 | + | Name string | |
| 172 | + | GoName string | |
| 173 | + | Version int | |
| 174 | + | HasEvents bool | |
| 175 | + | SignatureCases []signatureCase | |
| 176 | + | Requests []requestData | |
| 177 | + | Events []eventData | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | type signatureCase struct { | |
| 181 | + | Opcode int | |
| 182 | + | Signature string | |
| 183 | + | } | |
| 184 | + | type requestData struct { | |
| 185 | + | Name string | |
| 186 | + | GoName string | |
| 187 | + | Doc string | |
| 188 | + | Opcode int | |
| 189 | + | Destructor bool | |
| 190 | + | Params string | |
| 191 | + | Results string | |
| 192 | + | ArgPreparations []string | |
| 193 | + | ArgExprs []string | |
| 194 | + | CreatesChild bool | |
| 195 | + | ChildVar string | |
| 196 | + | ChildType string | |
| 197 | + | ChildFactory string | |
| 198 | + | ErrorReturn string | |
| 199 | + | FDArgs []string | |
| 200 | + | HasFDs bool | |
| 201 | + | SendCall string | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | // argSlot is one positional argument of a request, in declaration order. A | |
| 205 | + | // new_id slot is filled in once the child object has been created. | |
| 206 | + | type argSlot struct { | |
| 207 | + | newID bool | |
| 208 | + | expr string | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | type eventData struct { | |
| 212 | + | HasFD bool | |
| 213 | + | ServerDestroy bool | |
| 214 | + | FDNames []string | |
| 215 | + | Name string | |
| 216 | + | GoName string | |
| 217 | + | Opcode int | |
| 218 | + | HandlerField string | |
| 219 | + | HandlerAccessor string | |
| 220 | + | HandlerParams string | |
| 221 | + | HandlerArgs string | |
| 222 | + | DecodeLines []string | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | type constantData struct { | |
| 226 | + | Name string | |
| 227 | + | Value string | |
| 228 | + | Type string | |
| 229 | + | Enum string | |
| 230 | + | Entry string | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | // Only display and registry have handwritten bootstrap bindings. | |
| 234 | + | var bootstrapTypes = map[string]bool{ | |
| 235 | + | "Display": true, | |
| 236 | + | "Registry": true, | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | // goKeywords are avoided as parameter names because generated identifiers | |
| 240 | + | // must not collide with the language. | |
| 241 | + | var goKeywords = map[string]bool{ | |
| 242 | + | "break": true, "case": true, "chan": true, "const": true, "continue": true, | |
| 243 | + | "default": true, "defer": true, "else": true, "fallthrough": true, "for": true, | |
| 244 | + | "func": true, "go": true, "goto": true, "if": true, "import": true, | |
| 245 | + | "interface": true, "map": true, "package": true, "range": true, "return": true, | |
| 246 | + | "select": true, "struct": true, "switch": true, "type": true, "var": true, | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) { | |
| 250 | + | data := templateData{ | |
| 251 | + | Package: packageName, | |
| 252 | + | Protocol: s.protocol.Name, | |
| 253 | + | Source: s.source, | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | imports := []string{TransportImport} | |
| 257 | + | usedCross := make(map[string]bool) | |
| 258 | + | for _, candidate := range s.protocol.Interfaces { | |
| 259 | + | for _, req := range candidate.Requests { | |
| 260 | + | for _, arg := range req.Args { | |
| 261 | + | if arg.Type == "object" || arg.Type == "new_id" { | |
| 262 | + | usedCross[arg.Interface] = true | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | } | |
| 266 | + | for _, ev := range candidate.Events { | |
| 267 | + | for _, arg := range ev.Args { | |
| 268 | + | if arg.Type == "object" || arg.Type == "new_id" { | |
| 269 | + | usedCross[arg.Interface] = true | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | for iface, path := range s.CrossPackage { | |
| 275 | + | local := false | |
| 276 | + | for _, candidate := range s.protocol.Interfaces { | |
| 277 | + | if candidate.Name == iface { | |
| 278 | + | local = true | |
| 279 | + | break | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] { | |
| 283 | + | continue | |
| 284 | + | } | |
| 285 | + | alias := "cross_" + strings.ReplaceAll(iface, "-", "_") | |
| 286 | + | imports = append(imports, alias+" "+path) | |
| 287 | + | } | |
| 288 | + | sort.Strings(imports) | |
| 289 | + | stdImports := []string{} | |
| 290 | + | needsSync := false | |
| 291 | + | ||
| 292 | + | // Enum constants are package-level declarations, so two interfaces that | |
| 293 | + | // declare the same enum entry (for example xdg_surface.error.invalid_size | |
| 294 | + | // and xdg_toplevel.error.invalid_size) would collide. Entries whose name | |
| 295 | + | // occurs in more than one interface are qualified with their interface | |
| 296 | + | // name below; unique names keep their historical, unprefixed form. | |
| 297 | + | enumEntryCounts := map[string]int{} | |
| 298 | + | for _, iface := range s.protocol.Interfaces { | |
| 299 | + | for _, enum := range iface.Enums { | |
| 300 | + | for _, entry := range enum.Entries { | |
| 301 | + | enumEntryCounts[constantName(enum.Name, entry.Name)]++ | |
| 302 | + | } | |
| 303 | + | } | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | for _, iface := range s.protocol.Interfaces { | |
| 307 | + | if packageName == "core" && (iface.Name == "wl_display" || iface.Name == "wl_registry") { | |
| 308 | + | continue | |
| 309 | + | } | |
| 310 | + | ifaceData, err := s.processInterface(iface) | |
| 311 | + | if err != nil { | |
| 312 | + | return data, err | |
| 313 | + | } | |
| 314 | + | data.Interfaces = append(data.Interfaces, ifaceData) | |
| 315 | + | if len(iface.Events) > 0 { | |
| 316 | + | needsSync = true | |
| 317 | + | } | |
| 318 | + | ||
| 319 | + | for _, enum := range iface.Enums { | |
| 320 | + | for _, entry := range enum.Entries { | |
| 321 | + | name := constantName(enum.Name, entry.Name) | |
| 322 | + | if enumEntryCounts[name] > 1 { | |
| 323 | + | name = qualifiedConstantName(iface.Name, enum.Name, entry.Name) | |
| 324 | + | } | |
| 325 | + | // Enum values are emitted untyped: the same enum travels as a uint | |
| 326 | + | // in one protocol and as an int in another, and an untyped constant | |
| 327 | + | // assigns to either without casts at the call site. | |
| 328 | + | data.Constants = append(data.Constants, constantData{ | |
| 329 | + | Name: name, | |
| 330 | + | Value: entry.Value, | |
| 331 | + | Enum: enum.Name, | |
| 332 | + | Entry: entry.Name, | |
| 333 | + | }) | |
| 334 | + | } | |
| 335 | + | } | |
| 336 | + | } | |
| 337 | + | ||
| 338 | + | // Handlers are registered and dispatched from different goroutines, so an | |
| 339 | + | // interface with events needs a mutex. | |
| 340 | + | if needsSync { | |
| 341 | + | stdImports = append(stdImports, "sync") | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | var block strings.Builder | |
| 345 | + | block.WriteString("import (\n") | |
| 346 | + | for _, imp := range stdImports { | |
| 347 | + | fmt.Fprintf(&block, "\t%q\n", imp) | |
| 348 | + | } | |
| 349 | + | if len(stdImports) > 0 && len(imports) > 0 { | |
| 350 | + | block.WriteString("\n") | |
| 351 | + | } | |
| 352 | + | for _, imp := range imports { | |
| 353 | + | if strings.Contains(imp, " ") { | |
| 354 | + | parts := strings.SplitN(imp, " ", 2) | |
| 355 | + | fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1]) | |
| 356 | + | } else { | |
| 357 | + | fmt.Fprintf(&block, "\t%q\n", imp) | |
| 358 | + | } | |
| 359 | + | } | |
| 360 | + | block.WriteString(")") | |
| 361 | + | data.ImportBlock = block.String() | |
| 362 | + | ||
| 363 | + | return data, nil | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | func (s *Scanner) processInterface(iface Interface) (interfaceData, error) { | |
| 367 | + | data := interfaceData{ | |
| 368 | + | Name: iface.Name, | |
| 369 | + | GoName: s.toGoName(iface.Name), | |
| 370 | + | Version: iface.Version, | |
| 371 | + | HasEvents: len(iface.Events) > 0, | |
| 372 | + | } | |
| 373 | + | ||
| 374 | + | for i, req := range iface.Requests { | |
| 375 | + | request, err := s.processRequest(iface, req, i) | |
| 376 | + | if err != nil { | |
| 377 | + | return data, err | |
| 378 | + | } | |
| 379 | + | data.Requests = append(data.Requests, request) | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | for i, event := range iface.Events { | |
| 383 | + | var sig strings.Builder | |
| 384 | + | for _, arg := range event.Args { | |
| 385 | + | sig.WriteString(arg.Type) | |
| 386 | + | sig.WriteByte(',') | |
| 387 | + | } | |
| 388 | + | data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()}) | |
| 389 | + | eventData, err := s.processEvent(event, i) | |
| 390 | + | if iface.Name == "wl_callback" && event.Name == "done" { | |
| 391 | + | eventData.ServerDestroy = true | |
| 392 | + | } | |
| 393 | + | if err != nil { | |
| 394 | + | return data, err | |
| 395 | + | } | |
| 396 | + | data.Events = append(data.Events, eventData) | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | return data, nil | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | func (s *Scanner) processRequest(iface Interface, req Request, opcode int) (requestData, error) { | |
| 403 | + | data := requestData{ | |
| 404 | + | Name: req.Name, | |
| 405 | + | GoName: s.toGoName(req.Name), | |
| 406 | + | Doc: s.formatDescription(req.Description), | |
| 407 | + | Opcode: opcode, | |
| 408 | + | Destructor: req.Type == "destructor", | |
| 409 | + | ChildVar: "child", | |
| 410 | + | } | |
| 411 | + | ||
| 412 | + | var ( | |
| 413 | + | params []string | |
| 414 | + | argSlots []argSlot | |
| 415 | + | argExprs []string | |
| 416 | + | fdExprs []string | |
| 417 | + | newIDArg *Arg | |
| 418 | + | ) | |
| 419 | + | ||
| 420 | + | for i, arg := range req.Args { | |
| 421 | + | if arg.Type == "new_id" { | |
| 422 | + | if newIDArg != nil { | |
| 423 | + | return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name) | |
| 424 | + | } | |
| 425 | + | newIDArg = &arg | |
| 426 | + | // The child object is created later, but it keeps its declared | |
| 427 | + | // position: the wire order is the argument order. | |
| 428 | + | argSlots = append(argSlots, argSlot{newID: true}) | |
| 429 | + | continue | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | name := s.paramName(arg.Name, i) | |
| 433 | + | goType, err := s.goTypeForArg(arg) | |
| 434 | + | if err != nil { | |
| 435 | + | return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err) | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | expr := name | |
| 439 | + | switch arg.Type { | |
| 440 | + | case "object": | |
| 441 | + | // Objects are pointer types, and travel as wl.Object so a nil | |
| 442 | + | // pointer is never sent as a non-nil interface holding a typed nil. | |
| 443 | + | params = append(params, name+" *"+goType) | |
| 444 | + | holder := fmt.Sprintf("arg%d", i) | |
| 445 | + | data.ArgPreparations = append(data.ArgPreparations, | |
| 446 | + | fmt.Sprintf("var %s wl.Object", holder), | |
| 447 | + | fmt.Sprintf("if %s != nil {", name), | |
| 448 | + | fmt.Sprintf("\t%s = %s", holder, name), | |
| 449 | + | "}") | |
| 450 | + | expr = holder | |
| 451 | + | ||
| 452 | + | case "fd": | |
| 453 | + | // Descriptors travel out of band: the body carries no value for | |
| 454 | + | // them, and the descriptor list is attached to the request. | |
| 455 | + | params = append(params, name+" int") | |
| 456 | + | fdExprs = append(fdExprs, name) | |
| 457 | + | data.FDArgs = append(data.FDArgs, name) | |
| 458 | + | expr = "uintptr(" + name + ")" | |
| 459 | + | ||
| 460 | + | default: | |
| 461 | + | params = append(params, name+" "+goType) | |
| 462 | + | } | |
| 463 | + | ||
| 464 | + | argSlots = append(argSlots, argSlot{expr: expr}) | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | if newIDArg != nil { | |
| 468 | + | childType, err := s.goTypeForArg(*newIDArg) | |
| 469 | + | if err != nil { | |
| 470 | + | return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err) | |
| 471 | + | } | |
| 472 | + | data.CreatesChild = true | |
| 473 | + | data.ChildType = childType | |
| 474 | + | if strings.Contains(childType, ".") { | |
| 475 | + | data.ChildFactory = childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(newIDArg.Interface) | |
| 476 | + | } | |
| 477 | + | } | |
| 478 | + | ||
| 479 | + | for _, slot := range argSlots { | |
| 480 | + | if slot.newID { | |
| 481 | + | argExprs = append(argExprs, data.ChildVar) | |
| 482 | + | continue | |
| 483 | + | } | |
| 484 | + | argExprs = append(argExprs, slot.expr) | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | data.Params = strings.Join(params, ", ") | |
| 488 | + | data.ArgExprs = argExprs | |
| 489 | + | args := "" | |
| 490 | + | if len(argExprs) > 0 { | |
| 491 | + | args = ", " + strings.Join(argExprs, ", ") | |
| 492 | + | } | |
| 493 | + | // Destructors claim the proxy atomically before sending so a concurrent | |
| 494 | + | // second destroy cannot reach the wire. | |
| 495 | + | send := "SendRequest" | |
| 496 | + | if data.Destructor { | |
| 497 | + | send = "SendDestructor" | |
| 498 | + | } | |
| 499 | + | if len(fdExprs) > 0 { | |
| 500 | + | data.HasFDs = true | |
| 501 | + | data.SendCall = fmt.Sprintf("%sWithFDs(o, %d, []int{%s}%s)", send, opcode, strings.Join(fdExprs, ", "), args) | |
| 502 | + | } else { | |
| 503 | + | data.SendCall = fmt.Sprintf("%s(o, %d%s)", send, opcode, args) | |
| 504 | + | } | |
| 505 | + | if data.CreatesChild { | |
| 506 | + | data.Results = "(*" + data.ChildType + ", error)" | |
| 507 | + | data.ErrorReturn = "nil, err" | |
| 508 | + | } else { | |
| 509 | + | data.Results = "error" | |
| 510 | + | data.ErrorReturn = "err" | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | return data, nil | |
| 514 | + | } | |
| 515 | + | ||
| 516 | + | func (s *Scanner) processEvent(event Event, opcode int) (eventData, error) { | |
| 517 | + | goName := s.toGoName(event.Name) | |
| 518 | + | data := eventData{ | |
| 519 | + | ServerDestroy: event.Type == "destructor", | |
| 520 | + | Name: event.Name, | |
| 521 | + | GoName: goName, | |
| 522 | + | Opcode: opcode, | |
| 523 | + | HandlerField: "on" + goName, | |
| 524 | + | HandlerAccessor: "handlersFor" + goName, | |
| 525 | + | } | |
| 526 | + | ||
| 527 | + | var params, args []string | |
| 528 | + | ||
| 529 | + | for i, arg := range event.Args { | |
| 530 | + | name := s.paramName(arg.Name, i) | |
| 531 | + | ||
| 532 | + | switch arg.Type { | |
| 533 | + | case "new_id": | |
| 534 | + | childType, err := s.goTypeForArg(arg) | |
| 535 | + | if err != nil { | |
| 536 | + | return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err) | |
| 537 | + | } | |
| 538 | + | child := name + "Object" | |
| 539 | + | data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name)) | |
| 540 | + | if strings.Contains(childType, ".") { | |
| 541 | + | factory := childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(arg.Interface) | |
| 542 | + | data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := %s(o.Context())", child, factory)) | |
| 543 | + | } else { | |
| 544 | + | data.DecodeLines = append(data.DecodeLines, | |
| 545 | + | fmt.Sprintf("%s := &%s{}", child, childType), | |
| 546 | + | fmt.Sprintf("%s.SetContext(o.Context())", child), | |
| 547 | + | ) | |
| 548 | + | } | |
| 549 | + | data.DecodeLines = append(data.DecodeLines, | |
| 550 | + | fmt.Sprintf("%s.SetID(%sID)", child, name), | |
| 551 | + | fmt.Sprintf("o.Context().Register(%s)", child), | |
| 552 | + | ) | |
| 553 | + | params = append(params, child+" *"+childType) | |
| 554 | + | args = append(args, child) | |
| 555 | + | ||
| 556 | + | case "object": | |
| 557 | + | // Object references are reported as raw object IDs: the generated | |
| 558 | + | // layer does not own a registry of foreign proxies. | |
| 559 | + | data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name)) | |
| 560 | + | params = append(params, name+"ID uint32") | |
| 561 | + | args = append(args, name+"ID") | |
| 562 | + | ||
| 563 | + | default: | |
| 564 | + | if arg.Type == "fd" { | |
| 565 | + | data.HasFD = true | |
| 566 | + | data.FDNames = append(data.FDNames, name) | |
| 567 | + | } | |
| 568 | + | goType, decoder, err := s.scalarDecoder(arg) | |
| 569 | + | if err != nil { | |
| 570 | + | return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err) | |
| 571 | + | } | |
| 572 | + | data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder)) | |
| 573 | + | params = append(params, name+" "+goType) | |
| 574 | + | args = append(args, name) | |
| 575 | + | } | |
| 576 | + | } | |
| 577 | + | ||
| 578 | + | data.HandlerParams = strings.Join(params, ", ") | |
| 579 | + | data.HandlerArgs = strings.Join(args, ", ") | |
| 580 | + | return data, nil | |
| 581 | + | } | |
| 582 | + | ||
| 583 | + | // scalarDecoder maps a simple argument to its Go type and event decoder. | |
| 584 | + | func (s *Scanner) scalarDecoder(arg Arg) (goType, decoder string, err error) { | |
| 585 | + | switch arg.Type { | |
| 586 | + | case "int": | |
| 587 | + | return "int32", "Int32", nil | |
| 588 | + | case "uint": | |
| 589 | + | return "uint32", "Uint32", nil | |
| 590 | + | case "fixed": | |
| 591 | + | return "wl.Fixed", "Fixed", nil | |
| 592 | + | case "string": | |
| 593 | + | return "string", "String", nil | |
| 594 | + | case "array": | |
| 595 | + | return "[]byte", "Array", nil | |
| 596 | + | case "fd": | |
| 597 | + | return "*wl.OwnedFD", "FD", nil | |
| 598 | + | } | |
| 599 | + | return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type) | |
| 600 | + | } | |
| 601 | + | ||
| 602 | + | // goTypeForArg returns the Go type of an object-like argument. Protocol-local | |
| 603 | + | // interfaces map to generated types, explicit external references to their | |
| 604 | + | // mapped package, and bootstrap display/registry to transport wrappers. | |
| 605 | + | func (s *Scanner) goTypeForArg(arg Arg) (string, error) { | |
| 606 | + | switch arg.Type { | |
| 607 | + | case "int": | |
| 608 | + | return "int32", nil | |
| 609 | + | case "uint": | |
| 610 | + | return "uint32", nil | |
| 611 | + | case "fixed": | |
| 612 | + | return "wl.Fixed", nil | |
| 613 | + | case "string": | |
| 614 | + | return "string", nil | |
| 615 | + | case "array": | |
| 616 | + | return "[]byte", nil | |
| 617 | + | case "fd": | |
| 618 | + | return "int", nil | |
| 619 | + | case "object", "new_id": | |
| 620 | + | return s.goTypeForInterface(arg.Interface) | |
| 621 | + | } | |
| 622 | + | return "", fmt.Errorf("unsupported argument type %q", arg.Type) | |
| 623 | + | } | |
| 624 | + | ||
| 625 | + | func (s *Scanner) goTypeForInterface(iface string) (string, error) { | |
| 626 | + | if iface == "" { | |
| 627 | + | return "wl.BaseProxy", nil | |
| 628 | + | } | |
| 629 | + | // The XML package owns its own definitions even when the name starts wl_. | |
| 630 | + | for _, local := range s.protocol.Interfaces { | |
| 631 | + | if local.Name == iface && iface != "wl_display" && iface != "wl_registry" { | |
| 632 | + | return s.toGoName(iface), nil | |
| 633 | + | } | |
| 634 | + | } | |
| 635 | + | if bootstrapTypes[s.toGoName(iface)] { | |
| 636 | + | return "wl." + s.toGoName(iface), nil | |
| 637 | + | } | |
| 638 | + | if _, ok := s.CrossPackage[iface]; ok { | |
| 639 | + | return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface), nil | |
| 640 | + | } | |
| 641 | + | return "", fmt.Errorf("unresolved interface %q: add CrossPackage mapping", iface) | |
| 642 | + | } | |
| 643 | + | ||
| 644 | + | // paramName turns a protocol argument name into a Go parameter name. | |
| 645 | + | func (s *Scanner) paramName(name string, index int) string { | |
| 646 | + | if name == "" { | |
| 647 | + | return fmt.Sprintf("arg%d", index) | |
| 648 | + | } | |
| 649 | + | ||
| 650 | + | parts := strings.Split(name, "_") | |
| 651 | + | for i, part := range parts { | |
| 652 | + | if part == "" { | |
| 653 | + | continue | |
| 654 | + | } | |
| 655 | + | if i == 0 { | |
| 656 | + | parts[i] = strings.ToLower(part[:1]) + part[1:] | |
| 657 | + | continue | |
| 658 | + | } | |
| 659 | + | parts[i] = strings.ToUpper(part[:1]) + part[1:] | |
| 660 | + | } | |
| 661 | + | result := strings.Join(parts, "") | |
| 662 | + | if result == "" { | |
| 663 | + | return fmt.Sprintf("arg%d", index) | |
| 664 | + | } | |
| 665 | + | if goKeywords[result] { | |
| 666 | + | return result + "Arg" | |
| 667 | + | } | |
| 668 | + | return result | |
| 669 | + | } | |
| 670 | + | ||
| 671 | + | func (s *Scanner) toGoName(name string) string { | |
| 672 | + | name = strings.TrimPrefix(name, "zwlr_") | |
| 673 | + | name = strings.TrimPrefix(name, "zwp_") | |
| 674 | + | name = strings.TrimPrefix(name, "wl_") | |
| 675 | + | name = strings.TrimSuffix(name, "_v1") | |
| 676 | + | name = strings.TrimSuffix(name, "_v2") | |
| 677 | + | ||
| 678 | + | parts := strings.Split(name, "_") | |
| 679 | + | for i, part := range parts { | |
| 680 | + | if part != "" { | |
| 681 | + | parts[i] = strings.ToUpper(part[:1]) + part[1:] | |
| 682 | + | } | |
| 683 | + | } | |
| 684 | + | return strings.Join(parts, "") | |
| 685 | + | } | |
| 686 | + | ||
| 687 | + | // constantName renders an enum entry as the unprefixed constant name. | |
| 688 | + | func constantName(enum, entry string) string { | |
| 689 | + | return strings.Join(append(joinName(enum), joinName(entry)...), "_") | |
| 690 | + | } | |
| 691 | + | ||
| 692 | + | // qualifiedConstantName prefixes an enum entry constant with its interface | |
| 693 | + | // name. It is used for entries that several interfaces declare, which would | |
| 694 | + | // otherwise collide in the generated package. | |
| 695 | + | func qualifiedConstantName(iface, enum, entry string) string { | |
| 696 | + | return strings.Join(append(joinName(iface), constantName(enum, entry)), "_") | |
| 697 | + | } | |
| 698 | + | ||
| 699 | + | // joinName upper-cases the underscore-separated parts of a protocol name. | |
| 700 | + | func joinName(value string) []string { | |
| 701 | + | parts := strings.Split(value, "_") | |
| 702 | + | out := make([]string, 0, len(parts)) | |
| 703 | + | for _, part := range parts { | |
| 704 | + | if part != "" { | |
| 705 | + | out = append(out, strings.ToUpper(part)) | |
| 706 | + | } | |
| 707 | + | } | |
| 708 | + | return out | |
| 709 | + | } | |
| 710 | + | ||
| 711 | + | // formatDescription renders an argument or request description as a single | |
| 712 | + | // documentation line. | |
| 713 | + | func (s *Scanner) formatDescription(desc *Description) string { | |
| 714 | + | if desc == nil { | |
| 715 | + | return "" | |
| 716 | + | } | |
| 717 | + | ||
| 718 | + | text := strings.TrimSpace(desc.Text) | |
| 719 | + | if text == "" { | |
| 720 | + | return desc.Summary | |
| 721 | + | } | |
| 722 | + | ||
| 723 | + | lines := strings.Split(text, "\n") | |
| 724 | + | formatted := make([]string, 0, len(lines)) | |
| 725 | + | for _, line := range lines { | |
| 726 | + | if line = strings.TrimSpace(line); line != "" { | |
| 727 | + | formatted = append(formatted, line) | |
| 728 | + | } | |
| 729 | + | } | |
| 730 | + | return strings.Join(formatted, " ") | |
| 731 | + | } | |
| @@ -0,0 +1,731 @@ | |||||
| 1 | + | // Package scanner generates WLTurbo-backed Go bindings from Wayland protocol | |||
| 2 | + | // XML files. | |||
| 3 | + | // | |||
| 4 | + | // The generated code owns protocol semantics only: it embeds wl.BaseProxy, | |||
| 5 | + | // allocates and registers child objects through the wl.Context, sends requests | |||
| 6 | + | // through that context, and dispatches typed events to registered handlers. | |||
| 7 | + | // Message framing, descriptor passing and connection lifetime stay in the | |||
| 8 | + | // transport, so a generated binding never opens a socket or writes a header. | |||
| 9 | + | package scanner | |||
| 10 | + | ||||
| 11 | + | import ( | |||
| 12 | + | "bytes" | |||
| 13 | + | "encoding/xml" | |||
| 14 | + | "fmt" | |||
| 15 | + | "go/format" | |||
| 16 | + | "io" | |||
| 17 | + | "os" | |||
| 18 | + | "path/filepath" | |||
| 19 | + | "sort" | |||
| 20 | + | "strings" | |||
| 21 | + | "text/template" | |||
| 22 | + | ) | |||
| 23 | + | ||||
| 24 | + | // TransportImport is the import path generated bindings use for the Wayland | |||
| 25 | + | // transport. | |||
| 26 | + | const TransportImport = "github.com/bnema/wlturbo/wl" | |||
| 27 | + | ||||
| 28 | + | // Protocol represents a Wayland protocol specification | |||
| 29 | + | type Protocol struct { | |||
| 30 | + | XMLName xml.Name `xml:"protocol"` | |||
| 31 | + | Name string `xml:"name,attr"` | |||
| 32 | + | Copyright string `xml:"copyright"` | |||
| 33 | + | Description *Description `xml:"description"` | |||
| 34 | + | Interfaces []Interface `xml:"interface"` | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // Interface represents a Wayland interface in the protocol | |||
| 38 | + | type Interface struct { | |||
| 39 | + | Name string `xml:"name,attr"` | |||
| 40 | + | Version int `xml:"version,attr"` | |||
| 41 | + | Description *Description `xml:"description"` | |||
| 42 | + | Requests []Request `xml:"request"` | |||
| 43 | + | Events []Event `xml:"event"` | |||
| 44 | + | Enums []Enum `xml:"enum"` | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // Request represents a client-to-server message | |||
| 48 | + | type Request struct { | |||
| 49 | + | Name string `xml:"name,attr"` | |||
| 50 | + | Type string `xml:"type,attr"` // "destructor" or empty | |||
| 51 | + | Since int `xml:"since,attr"` | |||
| 52 | + | Description *Description `xml:"description"` | |||
| 53 | + | Args []Arg `xml:"arg"` | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // Event represents a server-to-client message | |||
| 57 | + | type Event struct { | |||
| 58 | + | Type string `xml:"type,attr"` | |||
| 59 | + | Name string `xml:"name,attr"` | |||
| 60 | + | Since int `xml:"since,attr"` | |||
| 61 | + | Description *Description `xml:"description"` | |||
| 62 | + | Args []Arg `xml:"arg"` | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // Arg represents a message argument | |||
| 66 | + | type Arg struct { | |||
| 67 | + | Name string `xml:"name,attr"` | |||
| 68 | + | Type string `xml:"type,attr"` | |||
| 69 | + | Summary string `xml:"summary,attr"` | |||
| 70 | + | Interface string `xml:"interface,attr"` | |||
| 71 | + | AllowNull bool `xml:"allow-null,attr"` | |||
| 72 | + | Enum string `xml:"enum,attr"` | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // Enum represents an enumeration type | |||
| 76 | + | type Enum struct { | |||
| 77 | + | Name string `xml:"name,attr"` | |||
| 78 | + | Since int `xml:"since,attr"` | |||
| 79 | + | Bitfield bool `xml:"bitfield,attr"` | |||
| 80 | + | Description *Description `xml:"description"` | |||
| 81 | + | Entries []Entry `xml:"entry"` | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // Entry represents an enum value | |||
| 85 | + | type Entry struct { | |||
| 86 | + | Name string `xml:"name,attr"` | |||
| 87 | + | Value string `xml:"value,attr"` | |||
| 88 | + | Summary string `xml:"summary,attr"` | |||
| 89 | + | Since int `xml:"since,attr"` | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | // Description represents documentation | |||
| 93 | + | type Description struct { | |||
| 94 | + | Summary string `xml:"summary,attr"` | |||
| 95 | + | Text string `xml:",chardata"` | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // Scanner generates Go bindings from Wayland protocol XML | |||
| 99 | + | type Scanner struct { | |||
| 100 | + | protocol *Protocol | |||
| 101 | + | source string | |||
| 102 | + | // CrossPackage maps interface names to Go package import paths. Local wins. | |||
| 103 | + | CrossPackage map[string]string | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // NewScanner creates a new protocol scanner | |||
| 107 | + | func NewScanner() *Scanner { | |||
| 108 | + | return &Scanner{} | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | // ParseXML parses a Wayland protocol XML file | |||
| 112 | + | func (s *Scanner) ParseXML(path string) error { | |||
| 113 | + | file, err := os.Open(path) | |||
| 114 | + | if err != nil { | |||
| 115 | + | return fmt.Errorf("failed to open XML file: %w", err) | |||
| 116 | + | } | |||
| 117 | + | defer func() { _ = file.Close() }() | |||
| 118 | + | ||||
| 119 | + | data, err := io.ReadAll(file) | |||
| 120 | + | if err != nil { | |||
| 121 | + | return fmt.Errorf("failed to read XML file: %w", err) | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | var protocol Protocol | |||
| 125 | + | if err := xml.Unmarshal(data, &protocol); err != nil { | |||
| 126 | + | return fmt.Errorf("failed to parse XML: %w", err) | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | s.protocol = &protocol | |||
| 130 | + | // Only the base name is recorded: generated output must not depend on the | |||
| 131 | + | // path the XML happened to be read from. | |||
| 132 | + | s.source = filepath.Base(path) | |||
| 133 | + | return nil | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | // Generate creates Go source code for the parsed protocol | |||
| 137 | + | func (s *Scanner) Generate(packageName string) ([]byte, error) { | |||
| 138 | + | if s.protocol == nil { | |||
| 139 | + | return nil, fmt.Errorf("no protocol parsed") | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | data, err := s.prepareTemplateData(packageName) | |||
| 143 | + | if err != nil { | |||
| 144 | + | return nil, err | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | var buf bytes.Buffer | |||
| 148 | + | tmpl := template.Must(template.New("protocol").Parse(protocolTemplate)) | |||
| 149 | + | if err := tmpl.Execute(&buf, data); err != nil { | |||
| 150 | + | return nil, fmt.Errorf("failed to execute template: %w", err) | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | formatted, err := format.Source(buf.Bytes()) | |||
| 154 | + | if err != nil { | |||
| 155 | + | return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err) | |||
| 156 | + | } | |||
| 157 | + | return formatted, nil | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | // templateData holds data for template generation | |||
| 161 | + | type templateData struct { | |||
| 162 | + | Package string | |||
| 163 | + | Protocol string | |||
| 164 | + | Source string | |||
| 165 | + | ImportBlock string | |||
| 166 | + | Constants []constantData | |||
| 167 | + | Interfaces []interfaceData | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | type interfaceData struct { | |||
| 171 | + | Name string | |||
| 172 | + | GoName string | |||
| 173 | + | Version int | |||
| 174 | + | HasEvents bool | |||
| 175 | + | SignatureCases []signatureCase | |||
| 176 | + | Requests []requestData | |||
| 177 | + | Events []eventData | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | type signatureCase struct { | |||
| 181 | + | Opcode int | |||
| 182 | + | Signature string | |||
| 183 | + | } | |||
| 184 | + | type requestData struct { | |||
| 185 | + | Name string | |||
| 186 | + | GoName string | |||
| 187 | + | Doc string | |||
| 188 | + | Opcode int | |||
| 189 | + | Destructor bool | |||
| 190 | + | Params string | |||
| 191 | + | Results string | |||
| 192 | + | ArgPreparations []string | |||
| 193 | + | ArgExprs []string | |||
| 194 | + | CreatesChild bool | |||
| 195 | + | ChildVar string | |||
| 196 | + | ChildType string | |||
| 197 | + | ChildFactory string | |||
| 198 | + | ErrorReturn string | |||
| 199 | + | FDArgs []string | |||
| 200 | + | HasFDs bool | |||
| 201 | + | SendCall string | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // argSlot is one positional argument of a request, in declaration order. A | |||
| 205 | + | // new_id slot is filled in once the child object has been created. | |||
| 206 | + | type argSlot struct { | |||
| 207 | + | newID bool | |||
| 208 | + | expr string | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | type eventData struct { | |||
| 212 | + | HasFD bool | |||
| 213 | + | ServerDestroy bool | |||
| 214 | + | FDNames []string | |||
| 215 | + | Name string | |||
| 216 | + | GoName string | |||
| 217 | + | Opcode int | |||
| 218 | + | HandlerField string | |||
| 219 | + | HandlerAccessor string | |||
| 220 | + | HandlerParams string | |||
| 221 | + | HandlerArgs string | |||
| 222 | + | DecodeLines []string | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | type constantData struct { | |||
| 226 | + | Name string | |||
| 227 | + | Value string | |||
| 228 | + | Type string | |||
| 229 | + | Enum string | |||
| 230 | + | Entry string | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | // Only display and registry have handwritten bootstrap bindings. | |||
| 234 | + | var bootstrapTypes = map[string]bool{ | |||
| 235 | + | "Display": true, | |||
| 236 | + | "Registry": true, | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | // goKeywords are avoided as parameter names because generated identifiers | |||
| 240 | + | // must not collide with the language. | |||
| 241 | + | var goKeywords = map[string]bool{ | |||
| 242 | + | "break": true, "case": true, "chan": true, "const": true, "continue": true, | |||
| 243 | + | "default": true, "defer": true, "else": true, "fallthrough": true, "for": true, | |||
| 244 | + | "func": true, "go": true, "goto": true, "if": true, "import": true, | |||
| 245 | + | "interface": true, "map": true, "package": true, "range": true, "return": true, | |||
| 246 | + | "select": true, "struct": true, "switch": true, "type": true, "var": true, | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) { | |||
| 250 | + | data := templateData{ | |||
| 251 | + | Package: packageName, | |||
| 252 | + | Protocol: s.protocol.Name, | |||
| 253 | + | Source: s.source, | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | imports := []string{TransportImport} | |||
| 257 | + | usedCross := make(map[string]bool) | |||
| 258 | + | for _, candidate := range s.protocol.Interfaces { | |||
| 259 | + | for _, req := range candidate.Requests { | |||
| 260 | + | for _, arg := range req.Args { | |||
| 261 | + | if arg.Type == "object" || arg.Type == "new_id" { | |||
| 262 | + | usedCross[arg.Interface] = true | |||
| 263 | + | } | |||
| 264 | + | } | |||
| 265 | + | } | |||
| 266 | + | for _, ev := range candidate.Events { | |||
| 267 | + | for _, arg := range ev.Args { | |||
| 268 | + | if arg.Type == "object" || arg.Type == "new_id" { | |||
| 269 | + | usedCross[arg.Interface] = true | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | } | |||
| 273 | + | } | |||
| 274 | + | for iface, path := range s.CrossPackage { | |||
| 275 | + | local := false | |||
| 276 | + | for _, candidate := range s.protocol.Interfaces { | |||
| 277 | + | if candidate.Name == iface { | |||
| 278 | + | local = true | |||
| 279 | + | break | |||
| 280 | + | } | |||
| 281 | + | } | |||
| 282 | + | if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] { | |||
| 283 | + | continue | |||
| 284 | + | } | |||
| 285 | + | alias := "cross_" + strings.ReplaceAll(iface, "-", "_") | |||
| 286 | + | imports = append(imports, alias+" "+path) | |||
| 287 | + | } | |||
| 288 | + | sort.Strings(imports) | |||
| 289 | + | stdImports := []string{} | |||
| 290 | + | needsSync := false | |||
| 291 | + | ||||
| 292 | + | // Enum constants are package-level declarations, so two interfaces that | |||
| 293 | + | // declare the same enum entry (for example xdg_surface.error.invalid_size | |||
| 294 | + | // and xdg_toplevel.error.invalid_size) would collide. Entries whose name | |||
| 295 | + | // occurs in more than one interface are qualified with their interface | |||
| 296 | + | // name below; unique names keep their historical, unprefixed form. | |||
| 297 | + | enumEntryCounts := map[string]int{} | |||
| 298 | + | for _, iface := range s.protocol.Interfaces { | |||
| 299 | + | for _, enum := range iface.Enums { | |||
| 300 | + | for _, entry := range enum.Entries { | |||
| 301 | + | enumEntryCounts[constantName(enum.Name, entry.Name)]++ | |||
| 302 | + | } | |||
| 303 | + | } | |||
| 304 | + | } | |||
| 305 | + | ||||
| 306 | + | for _, iface := range s.protocol.Interfaces { | |||
| 307 | + | if packageName == "core" && (iface.Name == "wl_display" || iface.Name == "wl_registry") { | |||
| 308 | + | continue | |||
| 309 | + | } | |||
| 310 | + | ifaceData, err := s.processInterface(iface) | |||
| 311 | + | if err != nil { | |||
| 312 | + | return data, err | |||
| 313 | + | } | |||
| 314 | + | data.Interfaces = append(data.Interfaces, ifaceData) | |||
| 315 | + | if len(iface.Events) > 0 { | |||
| 316 | + | needsSync = true | |||
| 317 | + | } | |||
| 318 | + | ||||
| 319 | + | for _, enum := range iface.Enums { | |||
| 320 | + | for _, entry := range enum.Entries { | |||
| 321 | + | name := constantName(enum.Name, entry.Name) | |||
| 322 | + | if enumEntryCounts[name] > 1 { | |||
| 323 | + | name = qualifiedConstantName(iface.Name, enum.Name, entry.Name) | |||
| 324 | + | } | |||
| 325 | + | // Enum values are emitted untyped: the same enum travels as a uint | |||
| 326 | + | // in one protocol and as an int in another, and an untyped constant | |||
| 327 | + | // assigns to either without casts at the call site. | |||
| 328 | + | data.Constants = append(data.Constants, constantData{ | |||
| 329 | + | Name: name, | |||
| 330 | + | Value: entry.Value, | |||
| 331 | + | Enum: enum.Name, | |||
| 332 | + | Entry: entry.Name, | |||
| 333 | + | }) | |||
| 334 | + | } | |||
| 335 | + | } | |||
| 336 | + | } | |||
| 337 | + | ||||
| 338 | + | // Handlers are registered and dispatched from different goroutines, so an | |||
| 339 | + | // interface with events needs a mutex. | |||
| 340 | + | if needsSync { | |||
| 341 | + | stdImports = append(stdImports, "sync") | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | var block strings.Builder | |||
| 345 | + | block.WriteString("import (\n") | |||
| 346 | + | for _, imp := range stdImports { | |||
| 347 | + | fmt.Fprintf(&block, "\t%q\n", imp) | |||
| 348 | + | } | |||
| 349 | + | if len(stdImports) > 0 && len(imports) > 0 { | |||
| 350 | + | block.WriteString("\n") | |||
| 351 | + | } | |||
| 352 | + | for _, imp := range imports { | |||
| 353 | + | if strings.Contains(imp, " ") { | |||
| 354 | + | parts := strings.SplitN(imp, " ", 2) | |||
| 355 | + | fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1]) | |||
| 356 | + | } else { | |||
| 357 | + | fmt.Fprintf(&block, "\t%q\n", imp) | |||
| 358 | + | } | |||
| 359 | + | } | |||
| 360 | + | block.WriteString(")") | |||
| 361 | + | data.ImportBlock = block.String() | |||
| 362 | + | ||||
| 363 | + | return data, nil | |||
| 364 | + | } | |||
| 365 | + | ||||
| 366 | + | func (s *Scanner) processInterface(iface Interface) (interfaceData, error) { | |||
| 367 | + | data := interfaceData{ | |||
| 368 | + | Name: iface.Name, | |||
| 369 | + | GoName: s.toGoName(iface.Name), | |||
| 370 | + | Version: iface.Version, | |||
| 371 | + | HasEvents: len(iface.Events) > 0, | |||
| 372 | + | } | |||
| 373 | + | ||||
| 374 | + | for i, req := range iface.Requests { | |||
| 375 | + | request, err := s.processRequest(iface, req, i) | |||
| 376 | + | if err != nil { | |||
| 377 | + | return data, err | |||
| 378 | + | } | |||
| 379 | + | data.Requests = append(data.Requests, request) | |||
| 380 | + | } | |||
| 381 | + | ||||
| 382 | + | for i, event := range iface.Events { | |||
| 383 | + | var sig strings.Builder | |||
| 384 | + | for _, arg := range event.Args { | |||
| 385 | + | sig.WriteString(arg.Type) | |||
| 386 | + | sig.WriteByte(',') | |||
| 387 | + | } | |||
| 388 | + | data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()}) | |||
| 389 | + | eventData, err := s.processEvent(event, i) | |||
| 390 | + | if iface.Name == "wl_callback" && event.Name == "done" { | |||
| 391 | + | eventData.ServerDestroy = true | |||
| 392 | + | } | |||
| 393 | + | if err != nil { | |||
| 394 | + | return data, err | |||
| 395 | + | } | |||
| 396 | + | data.Events = append(data.Events, eventData) | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | return data, nil | |||
| 400 | + | } | |||
| 401 | + | ||||
| 402 | + | func (s *Scanner) processRequest(iface Interface, req Request, opcode int) (requestData, error) { | |||
| 403 | + | data := requestData{ | |||
| 404 | + | Name: req.Name, | |||
| 405 | + | GoName: s.toGoName(req.Name), | |||
| 406 | + | Doc: s.formatDescription(req.Description), | |||
| 407 | + | Opcode: opcode, | |||
| 408 | + | Destructor: req.Type == "destructor", | |||
| 409 | + | ChildVar: "child", | |||
| 410 | + | } | |||
| 411 | + | ||||
| 412 | + | var ( | |||
| 413 | + | params []string | |||
| 414 | + | argSlots []argSlot | |||
| 415 | + | argExprs []string | |||
| 416 | + | fdExprs []string | |||
| 417 | + | newIDArg *Arg | |||
| 418 | + | ) | |||
| 419 | + | ||||
| 420 | + | for i, arg := range req.Args { | |||
| 421 | + | if arg.Type == "new_id" { | |||
| 422 | + | if newIDArg != nil { | |||
| 423 | + | return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name) | |||
| 424 | + | } | |||
| 425 | + | newIDArg = &arg | |||
| 426 | + | // The child object is created later, but it keeps its declared | |||
| 427 | + | // position: the wire order is the argument order. | |||
| 428 | + | argSlots = append(argSlots, argSlot{newID: true}) | |||
| 429 | + | continue | |||
| 430 | + | } | |||
| 431 | + | ||||
| 432 | + | name := s.paramName(arg.Name, i) | |||
| 433 | + | goType, err := s.goTypeForArg(arg) | |||
| 434 | + | if err != nil { | |||
| 435 | + | return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err) | |||
| 436 | + | } | |||
| 437 | + | ||||
| 438 | + | expr := name | |||
| 439 | + | switch arg.Type { | |||
| 440 | + | case "object": | |||
| 441 | + | // Objects are pointer types, and travel as wl.Object so a nil | |||
| 442 | + | // pointer is never sent as a non-nil interface holding a typed nil. | |||
| 443 | + | params = append(params, name+" *"+goType) | |||
| 444 | + | holder := fmt.Sprintf("arg%d", i) | |||
| 445 | + | data.ArgPreparations = append(data.ArgPreparations, | |||
| 446 | + | fmt.Sprintf("var %s wl.Object", holder), | |||
| 447 | + | fmt.Sprintf("if %s != nil {", name), | |||
| 448 | + | fmt.Sprintf("\t%s = %s", holder, name), | |||
| 449 | + | "}") | |||
| 450 | + | expr = holder | |||
| 451 | + | ||||
| 452 | + | case "fd": | |||
| 453 | + | // Descriptors travel out of band: the body carries no value for | |||
| 454 | + | // them, and the descriptor list is attached to the request. | |||
| 455 | + | params = append(params, name+" int") | |||
| 456 | + | fdExprs = append(fdExprs, name) | |||
| 457 | + | data.FDArgs = append(data.FDArgs, name) | |||
| 458 | + | expr = "uintptr(" + name + ")" | |||
| 459 | + | ||||
| 460 | + | default: | |||
| 461 | + | params = append(params, name+" "+goType) | |||
| 462 | + | } | |||
| 463 | + | ||||
| 464 | + | argSlots = append(argSlots, argSlot{expr: expr}) | |||
| 465 | + | } | |||
| 466 | + | ||||
| 467 | + | if newIDArg != nil { | |||
| 468 | + | childType, err := s.goTypeForArg(*newIDArg) | |||
| 469 | + | if err != nil { | |||
| 470 | + | return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err) | |||
| 471 | + | } | |||
| 472 | + | data.CreatesChild = true | |||
| 473 | + | data.ChildType = childType | |||
| 474 | + | if strings.Contains(childType, ".") { | |||
| 475 | + | data.ChildFactory = childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(newIDArg.Interface) | |||
| 476 | + | } | |||
| 477 | + | } | |||
| 478 | + | ||||
| 479 | + | for _, slot := range argSlots { | |||
| 480 | + | if slot.newID { | |||
| 481 | + | argExprs = append(argExprs, data.ChildVar) | |||
| 482 | + | continue | |||
| 483 | + | } | |||
| 484 | + | argExprs = append(argExprs, slot.expr) | |||
| 485 | + | } | |||
| 486 | + | ||||
| 487 | + | data.Params = strings.Join(params, ", ") | |||
| 488 | + | data.ArgExprs = argExprs | |||
| 489 | + | args := "" | |||
| 490 | + | if len(argExprs) > 0 { | |||
| 491 | + | args = ", " + strings.Join(argExprs, ", ") | |||
| 492 | + | } | |||
| 493 | + | // Destructors claim the proxy atomically before sending so a concurrent | |||
| 494 | + | // second destroy cannot reach the wire. | |||
| 495 | + | send := "SendRequest" | |||
| 496 | + | if data.Destructor { | |||
| 497 | + | send = "SendDestructor" | |||
| 498 | + | } | |||
| 499 | + | if len(fdExprs) > 0 { | |||
| 500 | + | data.HasFDs = true | |||
| 501 | + | data.SendCall = fmt.Sprintf("%sWithFDs(o, %d, []int{%s}%s)", send, opcode, strings.Join(fdExprs, ", "), args) | |||
| 502 | + | } else { | |||
| 503 | + | data.SendCall = fmt.Sprintf("%s(o, %d%s)", send, opcode, args) | |||
| 504 | + | } | |||
| 505 | + | if data.CreatesChild { | |||
| 506 | + | data.Results = "(*" + data.ChildType + ", error)" | |||
| 507 | + | data.ErrorReturn = "nil, err" | |||
| 508 | + | } else { | |||
| 509 | + | data.Results = "error" | |||
| 510 | + | data.ErrorReturn = "err" | |||
| 511 | + | } | |||
| 512 | + | ||||
| 513 | + | return data, nil | |||
| 514 | + | } | |||
| 515 | + | ||||
| 516 | + | func (s *Scanner) processEvent(event Event, opcode int) (eventData, error) { | |||
| 517 | + | goName := s.toGoName(event.Name) | |||
| 518 | + | data := eventData{ | |||
| 519 | + | ServerDestroy: event.Type == "destructor", | |||
| 520 | + | Name: event.Name, | |||
| 521 | + | GoName: goName, | |||
| 522 | + | Opcode: opcode, | |||
| 523 | + | HandlerField: "on" + goName, | |||
| 524 | + | HandlerAccessor: "handlersFor" + goName, | |||
| 525 | + | } | |||
| 526 | + | ||||
| 527 | + | var params, args []string | |||
| 528 | + | ||||
| 529 | + | for i, arg := range event.Args { | |||
| 530 | + | name := s.paramName(arg.Name, i) | |||
| 531 | + | ||||
| 532 | + | switch arg.Type { | |||
| 533 | + | case "new_id": | |||
| 534 | + | childType, err := s.goTypeForArg(arg) | |||
| 535 | + | if err != nil { | |||
| 536 | + | return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err) | |||
| 537 | + | } | |||
| 538 | + | child := name + "Object" | |||
| 539 | + | data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name)) | |||
| 540 | + | if strings.Contains(childType, ".") { | |||
| 541 | + | factory := childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(arg.Interface) | |||
| 542 | + | data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := %s(o.Context())", child, factory)) | |||
| 543 | + | } else { | |||
| 544 | + | data.DecodeLines = append(data.DecodeLines, | |||
| 545 | + | fmt.Sprintf("%s := &%s{}", child, childType), | |||
| 546 | + | fmt.Sprintf("%s.SetContext(o.Context())", child), | |||
| 547 | + | ) | |||
| 548 | + | } | |||
| 549 | + | data.DecodeLines = append(data.DecodeLines, | |||
| 550 | + | fmt.Sprintf("%s.SetID(%sID)", child, name), | |||
| 551 | + | fmt.Sprintf("o.Context().Register(%s)", child), | |||
| 552 | + | ) | |||
| 553 | + | params = append(params, child+" *"+childType) | |||
| 554 | + | args = append(args, child) | |||
| 555 | + | ||||
| 556 | + | case "object": | |||
| 557 | + | // Object references are reported as raw object IDs: the generated | |||
| 558 | + | // layer does not own a registry of foreign proxies. | |||
| 559 | + | data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name)) | |||
| 560 | + | params = append(params, name+"ID uint32") | |||
| 561 | + | args = append(args, name+"ID") | |||
| 562 | + | ||||
| 563 | + | default: | |||
| 564 | + | if arg.Type == "fd" { | |||
| 565 | + | data.HasFD = true | |||
| 566 | + | data.FDNames = append(data.FDNames, name) | |||
| 567 | + | } | |||
| 568 | + | goType, decoder, err := s.scalarDecoder(arg) | |||
| 569 | + | if err != nil { | |||
| 570 | + | return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err) | |||
| 571 | + | } | |||
| 572 | + | data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder)) | |||
| 573 | + | params = append(params, name+" "+goType) | |||
| 574 | + | args = append(args, name) | |||
| 575 | + | } | |||
| 576 | + | } | |||
| 577 | + | ||||
| 578 | + | data.HandlerParams = strings.Join(params, ", ") | |||
| 579 | + | data.HandlerArgs = strings.Join(args, ", ") | |||
| 580 | + | return data, nil | |||
| 581 | + | } | |||
| 582 | + | ||||
| 583 | + | // scalarDecoder maps a simple argument to its Go type and event decoder. | |||
| 584 | + | func (s *Scanner) scalarDecoder(arg Arg) (goType, decoder string, err error) { | |||
| 585 | + | switch arg.Type { | |||
| 586 | + | case "int": | |||
| 587 | + | return "int32", "Int32", nil | |||
| 588 | + | case "uint": | |||
| 589 | + | return "uint32", "Uint32", nil | |||
| 590 | + | case "fixed": | |||
| 591 | + | return "wl.Fixed", "Fixed", nil | |||
| 592 | + | case "string": | |||
| 593 | + | return "string", "String", nil | |||
| 594 | + | case "array": | |||
| 595 | + | return "[]byte", "Array", nil | |||
| 596 | + | case "fd": | |||
| 597 | + | return "*wl.OwnedFD", "FD", nil | |||
| 598 | + | } | |||
| 599 | + | return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type) | |||
| 600 | + | } | |||
| 601 | + | ||||
| 602 | + | // goTypeForArg returns the Go type of an object-like argument. Protocol-local | |||
| 603 | + | // interfaces map to generated types, explicit external references to their | |||
| 604 | + | // mapped package, and bootstrap display/registry to transport wrappers. | |||
| 605 | + | func (s *Scanner) goTypeForArg(arg Arg) (string, error) { | |||
| 606 | + | switch arg.Type { | |||
| 607 | + | case "int": | |||
| 608 | + | return "int32", nil | |||
| 609 | + | case "uint": | |||
| 610 | + | return "uint32", nil | |||
| 611 | + | case "fixed": | |||
| 612 | + | return "wl.Fixed", nil | |||
| 613 | + | case "string": | |||
| 614 | + | return "string", nil | |||
| 615 | + | case "array": | |||
| 616 | + | return "[]byte", nil | |||
| 617 | + | case "fd": | |||
| 618 | + | return "int", nil | |||
| 619 | + | case "object", "new_id": | |||
| 620 | + | return s.goTypeForInterface(arg.Interface) | |||
| 621 | + | } | |||
| 622 | + | return "", fmt.Errorf("unsupported argument type %q", arg.Type) | |||
| 623 | + | } | |||
| 624 | + | ||||
| 625 | + | func (s *Scanner) goTypeForInterface(iface string) (string, error) { | |||
| 626 | + | if iface == "" { | |||
| 627 | + | return "wl.BaseProxy", nil | |||
| 628 | + | } | |||
| 629 | + | // The XML package owns its own definitions even when the name starts wl_. | |||
| 630 | + | for _, local := range s.protocol.Interfaces { | |||
| 631 | + | if local.Name == iface && iface != "wl_display" && iface != "wl_registry" { | |||
| 632 | + | return s.toGoName(iface), nil | |||
| 633 | + | } | |||
| 634 | + | } | |||
| 635 | + | if bootstrapTypes[s.toGoName(iface)] { | |||
| 636 | + | return "wl." + s.toGoName(iface), nil | |||
| 637 | + | } | |||
| 638 | + | if _, ok := s.CrossPackage[iface]; ok { | |||
| 639 | + | return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface), nil | |||
| 640 | + | } | |||
| 641 | + | return "", fmt.Errorf("unresolved interface %q: add CrossPackage mapping", iface) | |||
| 642 | + | } | |||
| 643 | + | ||||
| 644 | + | // paramName turns a protocol argument name into a Go parameter name. | |||
| 645 | + | func (s *Scanner) paramName(name string, index int) string { | |||
| 646 | + | if name == "" { | |||
| 647 | + | return fmt.Sprintf("arg%d", index) | |||
| 648 | + | } | |||
| 649 | + | ||||
| 650 | + | parts := strings.Split(name, "_") | |||
| 651 | + | for i, part := range parts { | |||
| 652 | + | if part == "" { | |||
| 653 | + | continue | |||
| 654 | + | } | |||
| 655 | + | if i == 0 { | |||
| 656 | + | parts[i] = strings.ToLower(part[:1]) + part[1:] | |||
| 657 | + | continue | |||
| 658 | + | } | |||
| 659 | + | parts[i] = strings.ToUpper(part[:1]) + part[1:] | |||
| 660 | + | } | |||
| 661 | + | result := strings.Join(parts, "") | |||
| 662 | + | if result == "" { | |||
| 663 | + | return fmt.Sprintf("arg%d", index) | |||
| 664 | + | } | |||
| 665 | + | if goKeywords[result] { | |||
| 666 | + | return result + "Arg" | |||
| 667 | + | } | |||
| 668 | + | return result | |||
| 669 | + | } | |||
| 670 | + | ||||
| 671 | + | func (s *Scanner) toGoName(name string) string { | |||
| 672 | + | name = strings.TrimPrefix(name, "zwlr_") | |||
| 673 | + | name = strings.TrimPrefix(name, "zwp_") | |||
| 674 | + | name = strings.TrimPrefix(name, "wl_") | |||
| 675 | + | name = strings.TrimSuffix(name, "_v1") | |||
| 676 | + | name = strings.TrimSuffix(name, "_v2") | |||
| 677 | + | ||||
| 678 | + | parts := strings.Split(name, "_") | |||
| 679 | + | for i, part := range parts { | |||
| 680 | + | if part != "" { | |||
| 681 | + | parts[i] = strings.ToUpper(part[:1]) + part[1:] | |||
| 682 | + | } | |||
| 683 | + | } | |||
| 684 | + | return strings.Join(parts, "") | |||
| 685 | + | } | |||
| 686 | + | ||||
| 687 | + | // constantName renders an enum entry as the unprefixed constant name. | |||
| 688 | + | func constantName(enum, entry string) string { | |||
| 689 | + | return strings.Join(append(joinName(enum), joinName(entry)...), "_") | |||
| 690 | + | } | |||
| 691 | + | ||||
| 692 | + | // qualifiedConstantName prefixes an enum entry constant with its interface | |||
| 693 | + | // name. It is used for entries that several interfaces declare, which would | |||
| 694 | + | // otherwise collide in the generated package. | |||
| 695 | + | func qualifiedConstantName(iface, enum, entry string) string { | |||
| 696 | + | return strings.Join(append(joinName(iface), constantName(enum, entry)), "_") | |||
| 697 | + | } | |||
| 698 | + | ||||
| 699 | + | // joinName upper-cases the underscore-separated parts of a protocol name. | |||
| 700 | + | func joinName(value string) []string { | |||
| 701 | + | parts := strings.Split(value, "_") | |||
| 702 | + | out := make([]string, 0, len(parts)) | |||
| 703 | + | for _, part := range parts { | |||
| 704 | + | if part != "" { | |||
| 705 | + | out = append(out, strings.ToUpper(part)) | |||
| 706 | + | } | |||
| 707 | + | } | |||
| 708 | + | return out | |||
| 709 | + | } | |||
| 710 | + | ||||
| 711 | + | // formatDescription renders an argument or request description as a single | |||
| 712 | + | // documentation line. | |||
| 713 | + | func (s *Scanner) formatDescription(desc *Description) string { | |||
| 714 | + | if desc == nil { | |||
| 715 | + | return "" | |||
| 716 | + | } | |||
| 717 | + | ||||
| 718 | + | text := strings.TrimSpace(desc.Text) | |||
| 719 | + | if text == "" { | |||
| 720 | + | return desc.Summary | |||
| 721 | + | } | |||
| 722 | + | ||||
| 723 | + | lines := strings.Split(text, "\n") | |||
| 724 | + | formatted := make([]string, 0, len(lines)) | |||
| 725 | + | for _, line := range lines { | |||
| 726 | + | if line = strings.TrimSpace(line); line != "" { | |||
| 727 | + | formatted = append(formatted, line) | |||
| 728 | + | } | |||
| 729 | + | } | |||
| 730 | + | return strings.Join(formatted, " ") | |||
| 731 | + | } | |||
A internal/scanner/scanner_test.go +163 -0
| @@ -0,0 +1,163 @@ | |||
| 1 | + | package scanner | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "os/exec" | |
| 6 | + | "path/filepath" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestResolution(t *testing.T) { | |
| 12 | + | p := filepath.Join(t.TempDir(), "fixture.xml") | |
| 13 | + | xml := `<protocol name="sample"><interface name="wl_surface" version="1"><event name="done"><arg name="value" type="uint"/></event></interface><interface name="custom_thing" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/><arg name="output" type="object" interface="wl_output"/></request></interface></protocol>` | |
| 14 | + | if err := os.WriteFile(p, []byte(xml), 0600); err != nil { | |
| 15 | + | t.Fatal(err) | |
| 16 | + | } | |
| 17 | + | s := NewScanner() | |
| 18 | + | if err := s.ParseXML(p); err != nil { | |
| 19 | + | t.Fatal(err) | |
| 20 | + | } | |
| 21 | + | s.CrossPackage = map[string]string{"wl_surface": "github.com/bnema/wlturbo/protocol/core", "wl_output": "github.com/bnema/wlturbo/protocol/core"} | |
| 22 | + | b, err := s.Generate("fixture") | |
| 23 | + | if err != nil { | |
| 24 | + | t.Fatal(err) | |
| 25 | + | } | |
| 26 | + | text := string(b) | |
| 27 | + | if !strings.Contains(text, "(*Surface, error)") || !strings.Contains(text, "output *cross_wl_output.Output") || strings.Contains(text, "cross_wl_surface") { | |
| 28 | + | t.Fatalf("wrong resolution: %s", text) | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | func TestCoreGeneration(t *testing.T) { | |
| 32 | + | s := NewScanner() | |
| 33 | + | if err := s.ParseXML("../../protocol/wayland.xml"); err != nil { | |
| 34 | + | t.Fatal(err) | |
| 35 | + | } | |
| 36 | + | b, err := s.Generate("core") | |
| 37 | + | if err != nil { | |
| 38 | + | t.Fatal(err) | |
| 39 | + | } | |
| 40 | + | for _, want := range []string{"type Keyboard struct", "type Subsurface struct", "type Buffer struct", "type Callback struct", "return \"uint,fd,uint,\", true", "child := &Keyboard{}"} { | |
| 41 | + | if !strings.Contains(string(b), want) { | |
| 42 | + | t.Errorf("missing %s", want) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") { | |
| 46 | + | t.Fatal("bootstrap generated twice") | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // P3 uses wl_buffer as both an event-created child and a request-created child. | |
| 51 | + | // Verify the generated code uses the public constructor instead of embedding | |
| 52 | + | // an inaccessible BaseProxy context field across package boundaries. | |
| 53 | + | func TestExternalChildFactory(t *testing.T) { | |
| 54 | + | path := filepath.Join(t.TempDir(), "external.xml") | |
| 55 | + | xml := `<protocol name="fixture"><interface name="ext_params" version="1"><request name="create"><arg name="id" type="new_id" interface="wl_buffer"/></request><event name="created"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>` | |
| 56 | + | if err := os.WriteFile(path, []byte(xml), 0600); err != nil { | |
| 57 | + | t.Fatal(err) | |
| 58 | + | } | |
| 59 | + | s := NewScanner() | |
| 60 | + | if err := s.ParseXML(path); err != nil { | |
| 61 | + | t.Fatal(err) | |
| 62 | + | } | |
| 63 | + | s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"} | |
| 64 | + | generated, err := s.Generate("ext") | |
| 65 | + | if err != nil { | |
| 66 | + | t.Fatal(err) | |
| 67 | + | } | |
| 68 | + | for _, part := range []string{"cross_wl_buffer.NewBuffer(o.Context())", "(*cross_wl_buffer.Buffer, error)", "idObject.SetID(idID)"} { | |
| 69 | + | if !strings.Contains(string(generated), part) { | |
| 70 | + | t.Errorf("generated code missing %q", part) | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | if strings.Contains(string(generated), "&cross_wl_buffer.Buffer{}") { | |
| 74 | + | t.Fatal("foreign child was constructed without its package factory") | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func TestUnresolvedExternalInterface(t *testing.T) { | |
| 79 | + | path := filepath.Join(t.TempDir(), "extension.xml") | |
| 80 | + | xml := `<protocol name="ext"><interface name="ext_parent" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/></request></interface></protocol>` | |
| 81 | + | if err := os.WriteFile(path, []byte(xml), 0600); err != nil { | |
| 82 | + | t.Fatal(err) | |
| 83 | + | } | |
| 84 | + | s := NewScanner() | |
| 85 | + | if err := s.ParseXML(path); err != nil { | |
| 86 | + | t.Fatal(err) | |
| 87 | + | } | |
| 88 | + | if _, err := s.Generate("ext"); err == nil || !strings.Contains(err.Error(), "wl_surface") { | |
| 89 | + | t.Fatalf("unmapped external reference: %v", err) | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func TestMappedExtensionCompilesAndDispatchesChild(t *testing.T) { | |
| 94 | + | goBin, err := exec.LookPath("go") | |
| 95 | + | if err != nil { | |
| 96 | + | t.Skip("go unavailable: generated extension compilation requires go") | |
| 97 | + | } | |
| 98 | + | root, err := filepath.Abs("../..") | |
| 99 | + | if err != nil { | |
| 100 | + | t.Fatal(err) | |
| 101 | + | } | |
| 102 | + | dir := t.TempDir() | |
| 103 | + | xml := `<protocol name="ext"><interface name="ext_parent" version="1"><event name="child"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>` | |
| 104 | + | path := filepath.Join(dir, "ext.xml") | |
| 105 | + | if err := os.WriteFile(path, []byte(xml), 0600); err != nil { | |
| 106 | + | t.Fatal(err) | |
| 107 | + | } | |
| 108 | + | s := NewScanner() | |
| 109 | + | if err := s.ParseXML(path); err != nil { | |
| 110 | + | t.Fatal(err) | |
| 111 | + | } | |
| 112 | + | s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"} | |
| 113 | + | generated, err := s.Generate("ext") | |
| 114 | + | if err != nil { | |
| 115 | + | t.Fatal(err) | |
| 116 | + | } | |
| 117 | + | if err := os.WriteFile(filepath.Join(dir, "ext.go"), generated, 0600); err != nil { | |
| 118 | + | t.Fatal(err) | |
| 119 | + | } | |
| 120 | + | mod := "module example.com/ext\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => " + root + "\n" | |
| 121 | + | if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0600); err != nil { | |
| 122 | + | t.Fatal(err) | |
| 123 | + | } | |
| 124 | + | test := `package ext | |
| 125 | + | import ( | |
| 126 | + | "encoding/binary" | |
| 127 | + | "net" | |
| 128 | + | "os" | |
| 129 | + | "testing" | |
| 130 | + | "github.com/bnema/wlturbo" | |
| 131 | + | "github.com/bnema/wlturbo/protocol/core" | |
| 132 | + | "golang.org/x/sys/unix" | |
| 133 | + | ) | |
| 134 | + | func TestTypedChild(t *testing.T) { | |
| 135 | + | f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0); if err != nil { t.Fatal(err) } | |
| 136 | + | a, b := os.NewFile(uintptr(f[0]), "a"), os.NewFile(uintptr(f[1]), "b") | |
| 137 | + | c, err := net.FileConn(a); a.Close(); if err != nil { t.Fatal(err) }; defer c.Close() | |
| 138 | + | p, err := net.FileConn(b); b.Close(); if err != nil { t.Fatal(err) }; defer p.Close() | |
| 139 | + | d, err := wlturbo.ConnectFromConn(c); if err != nil { t.Fatal(err) }; defer d.Close() | |
| 140 | + | parent := NewExtParent(d.Context()); parent.SetID(d.AllocateID()); d.Context().Register(parent) | |
| 141 | + | var child *core.Buffer | |
| 142 | + | parent.OnChild(func(b *core.Buffer) { child = b }) | |
| 143 | + | frame := make([]byte, 12); binary.LittleEndian.PutUint32(frame, parent.ID()); binary.LittleEndian.PutUint32(frame[4:], 12<<16); binary.LittleEndian.PutUint32(frame[8:], 0xff000001) | |
| 144 | + | if _, err := p.Write(frame); err != nil { t.Fatal(err) } | |
| 145 | + | if err := d.Dispatch(); err != nil { t.Fatal(err) } | |
| 146 | + | if child == nil || child.ID() != 0xff000001 { t.Fatalf("child: %v", child) } | |
| 147 | + | called := false; child.OnRelease(func(){ called = true }) | |
| 148 | + | frame = frame[:8]; binary.LittleEndian.PutUint32(frame, child.ID()); binary.LittleEndian.PutUint32(frame[4:], 8<<16) | |
| 149 | + | if _, err := p.Write(frame); err != nil { t.Fatal(err) } | |
| 150 | + | if err := d.Dispatch(); err != nil { t.Fatal(err) } | |
| 151 | + | if !called { t.Fatal("typed child did not dispatch release") } | |
| 152 | + | } | |
| 153 | + | ` | |
| 154 | + | if err := os.WriteFile(filepath.Join(dir, "ext_test.go"), []byte(test), 0600); err != nil { | |
| 155 | + | t.Fatal(err) | |
| 156 | + | } | |
| 157 | + | cmd := exec.Command(goBin, "test", "./...") | |
| 158 | + | cmd.Dir = dir | |
| 159 | + | cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod") | |
| 160 | + | if out, err := cmd.CombinedOutput(); err != nil { | |
| 161 | + | t.Fatalf("generated extension test: %v\n%s", err, out) | |
| 162 | + | } | |
| 163 | + | } | |
| @@ -0,0 +1,163 @@ | |||||
| 1 | + | package scanner | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "os/exec" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "strings" | |||
| 8 | + | "testing" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestResolution(t *testing.T) { | |||
| 12 | + | p := filepath.Join(t.TempDir(), "fixture.xml") | |||
| 13 | + | xml := `<protocol name="sample"><interface name="wl_surface" version="1"><event name="done"><arg name="value" type="uint"/></event></interface><interface name="custom_thing" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/><arg name="output" type="object" interface="wl_output"/></request></interface></protocol>` | |||
| 14 | + | if err := os.WriteFile(p, []byte(xml), 0600); err != nil { | |||
| 15 | + | t.Fatal(err) | |||
| 16 | + | } | |||
| 17 | + | s := NewScanner() | |||
| 18 | + | if err := s.ParseXML(p); err != nil { | |||
| 19 | + | t.Fatal(err) | |||
| 20 | + | } | |||
| 21 | + | s.CrossPackage = map[string]string{"wl_surface": "github.com/bnema/wlturbo/protocol/core", "wl_output": "github.com/bnema/wlturbo/protocol/core"} | |||
| 22 | + | b, err := s.Generate("fixture") | |||
| 23 | + | if err != nil { | |||
| 24 | + | t.Fatal(err) | |||
| 25 | + | } | |||
| 26 | + | text := string(b) | |||
| 27 | + | if !strings.Contains(text, "(*Surface, error)") || !strings.Contains(text, "output *cross_wl_output.Output") || strings.Contains(text, "cross_wl_surface") { | |||
| 28 | + | t.Fatalf("wrong resolution: %s", text) | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | func TestCoreGeneration(t *testing.T) { | |||
| 32 | + | s := NewScanner() | |||
| 33 | + | if err := s.ParseXML("../../protocol/wayland.xml"); err != nil { | |||
| 34 | + | t.Fatal(err) | |||
| 35 | + | } | |||
| 36 | + | b, err := s.Generate("core") | |||
| 37 | + | if err != nil { | |||
| 38 | + | t.Fatal(err) | |||
| 39 | + | } | |||
| 40 | + | for _, want := range []string{"type Keyboard struct", "type Subsurface struct", "type Buffer struct", "type Callback struct", "return \"uint,fd,uint,\", true", "child := &Keyboard{}"} { | |||
| 41 | + | if !strings.Contains(string(b), want) { | |||
| 42 | + | t.Errorf("missing %s", want) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") { | |||
| 46 | + | t.Fatal("bootstrap generated twice") | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // P3 uses wl_buffer as both an event-created child and a request-created child. | |||
| 51 | + | // Verify the generated code uses the public constructor instead of embedding | |||
| 52 | + | // an inaccessible BaseProxy context field across package boundaries. | |||
| 53 | + | func TestExternalChildFactory(t *testing.T) { | |||
| 54 | + | path := filepath.Join(t.TempDir(), "external.xml") | |||
| 55 | + | xml := `<protocol name="fixture"><interface name="ext_params" version="1"><request name="create"><arg name="id" type="new_id" interface="wl_buffer"/></request><event name="created"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>` | |||
| 56 | + | if err := os.WriteFile(path, []byte(xml), 0600); err != nil { | |||
| 57 | + | t.Fatal(err) | |||
| 58 | + | } | |||
| 59 | + | s := NewScanner() | |||
| 60 | + | if err := s.ParseXML(path); err != nil { | |||
| 61 | + | t.Fatal(err) | |||
| 62 | + | } | |||
| 63 | + | s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"} | |||
| 64 | + | generated, err := s.Generate("ext") | |||
| 65 | + | if err != nil { | |||
| 66 | + | t.Fatal(err) | |||
| 67 | + | } | |||
| 68 | + | for _, part := range []string{"cross_wl_buffer.NewBuffer(o.Context())", "(*cross_wl_buffer.Buffer, error)", "idObject.SetID(idID)"} { | |||
| 69 | + | if !strings.Contains(string(generated), part) { | |||
| 70 | + | t.Errorf("generated code missing %q", part) | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | if strings.Contains(string(generated), "&cross_wl_buffer.Buffer{}") { | |||
| 74 | + | t.Fatal("foreign child was constructed without its package factory") | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func TestUnresolvedExternalInterface(t *testing.T) { | |||
| 79 | + | path := filepath.Join(t.TempDir(), "extension.xml") | |||
| 80 | + | xml := `<protocol name="ext"><interface name="ext_parent" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/></request></interface></protocol>` | |||
| 81 | + | if err := os.WriteFile(path, []byte(xml), 0600); err != nil { | |||
| 82 | + | t.Fatal(err) | |||
| 83 | + | } | |||
| 84 | + | s := NewScanner() | |||
| 85 | + | if err := s.ParseXML(path); err != nil { | |||
| 86 | + | t.Fatal(err) | |||
| 87 | + | } | |||
| 88 | + | if _, err := s.Generate("ext"); err == nil || !strings.Contains(err.Error(), "wl_surface") { | |||
| 89 | + | t.Fatalf("unmapped external reference: %v", err) | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | func TestMappedExtensionCompilesAndDispatchesChild(t *testing.T) { | |||
| 94 | + | goBin, err := exec.LookPath("go") | |||
| 95 | + | if err != nil { | |||
| 96 | + | t.Skip("go unavailable: generated extension compilation requires go") | |||
| 97 | + | } | |||
| 98 | + | root, err := filepath.Abs("../..") | |||
| 99 | + | if err != nil { | |||
| 100 | + | t.Fatal(err) | |||
| 101 | + | } | |||
| 102 | + | dir := t.TempDir() | |||
| 103 | + | xml := `<protocol name="ext"><interface name="ext_parent" version="1"><event name="child"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>` | |||
| 104 | + | path := filepath.Join(dir, "ext.xml") | |||
| 105 | + | if err := os.WriteFile(path, []byte(xml), 0600); err != nil { | |||
| 106 | + | t.Fatal(err) | |||
| 107 | + | } | |||
| 108 | + | s := NewScanner() | |||
| 109 | + | if err := s.ParseXML(path); err != nil { | |||
| 110 | + | t.Fatal(err) | |||
| 111 | + | } | |||
| 112 | + | s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"} | |||
| 113 | + | generated, err := s.Generate("ext") | |||
| 114 | + | if err != nil { | |||
| 115 | + | t.Fatal(err) | |||
| 116 | + | } | |||
| 117 | + | if err := os.WriteFile(filepath.Join(dir, "ext.go"), generated, 0600); err != nil { | |||
| 118 | + | t.Fatal(err) | |||
| 119 | + | } | |||
| 120 | + | mod := "module example.com/ext\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => " + root + "\n" | |||
| 121 | + | if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0600); err != nil { | |||
| 122 | + | t.Fatal(err) | |||
| 123 | + | } | |||
| 124 | + | test := `package ext | |||
| 125 | + | import ( | |||
| 126 | + | "encoding/binary" | |||
| 127 | + | "net" | |||
| 128 | + | "os" | |||
| 129 | + | "testing" | |||
| 130 | + | "github.com/bnema/wlturbo" | |||
| 131 | + | "github.com/bnema/wlturbo/protocol/core" | |||
| 132 | + | "golang.org/x/sys/unix" | |||
| 133 | + | ) | |||
| 134 | + | func TestTypedChild(t *testing.T) { | |||
| 135 | + | f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0); if err != nil { t.Fatal(err) } | |||
| 136 | + | a, b := os.NewFile(uintptr(f[0]), "a"), os.NewFile(uintptr(f[1]), "b") | |||
| 137 | + | c, err := net.FileConn(a); a.Close(); if err != nil { t.Fatal(err) }; defer c.Close() | |||
| 138 | + | p, err := net.FileConn(b); b.Close(); if err != nil { t.Fatal(err) }; defer p.Close() | |||
| 139 | + | d, err := wlturbo.ConnectFromConn(c); if err != nil { t.Fatal(err) }; defer d.Close() | |||
| 140 | + | parent := NewExtParent(d.Context()); parent.SetID(d.AllocateID()); d.Context().Register(parent) | |||
| 141 | + | var child *core.Buffer | |||
| 142 | + | parent.OnChild(func(b *core.Buffer) { child = b }) | |||
| 143 | + | frame := make([]byte, 12); binary.LittleEndian.PutUint32(frame, parent.ID()); binary.LittleEndian.PutUint32(frame[4:], 12<<16); binary.LittleEndian.PutUint32(frame[8:], 0xff000001) | |||
| 144 | + | if _, err := p.Write(frame); err != nil { t.Fatal(err) } | |||
| 145 | + | if err := d.Dispatch(); err != nil { t.Fatal(err) } | |||
| 146 | + | if child == nil || child.ID() != 0xff000001 { t.Fatalf("child: %v", child) } | |||
| 147 | + | called := false; child.OnRelease(func(){ called = true }) | |||
| 148 | + | frame = frame[:8]; binary.LittleEndian.PutUint32(frame, child.ID()); binary.LittleEndian.PutUint32(frame[4:], 8<<16) | |||
| 149 | + | if _, err := p.Write(frame); err != nil { t.Fatal(err) } | |||
| 150 | + | if err := d.Dispatch(); err != nil { t.Fatal(err) } | |||
| 151 | + | if !called { t.Fatal("typed child did not dispatch release") } | |||
| 152 | + | } | |||
| 153 | + | ` | |||
| 154 | + | if err := os.WriteFile(filepath.Join(dir, "ext_test.go"), []byte(test), 0600); err != nil { | |||
| 155 | + | t.Fatal(err) | |||
| 156 | + | } | |||
| 157 | + | cmd := exec.Command(goBin, "test", "./...") | |||
| 158 | + | cmd.Dir = dir | |||
| 159 | + | cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod") | |||
| 160 | + | if out, err := cmd.CombinedOutput(); err != nil { | |||
| 161 | + | t.Fatalf("generated extension test: %v\n%s", err, out) | |||
| 162 | + | } | |||
| 163 | + | } | |||
A internal/scanner/template.go +142 -0
| @@ -0,0 +1,142 @@ | |||
| 1 | + | package scanner | |
| 2 | + | ||
| 3 | + | // protocolTemplate is the template for generating WLTurbo-backed Go bindings. | |
| 4 | + | // | |
| 5 | + | // Generated objects embed wl.BaseProxy, allocate and register child objects | |
| 6 | + | // through the wl.Context, send requests through that context, and dispatch | |
| 7 | + | // typed events to registered handlers. Nothing in the generated code opens a | |
| 8 | + | // socket or frames messages itself: that is the transport's job. | |
| 9 | + | const protocolTemplate = `// Code generated by wlturbo-scanner. DO NOT EDIT. | |
| 10 | + | // Protocol: {{.Protocol}} | |
| 11 | + | // Source: {{.Source}} | |
| 12 | + | ||
| 13 | + | package {{.Package}} | |
| 14 | + | ||
| 15 | + | {{.ImportBlock}} | |
| 16 | + | ||
| 17 | + | {{range .Constants}} | |
| 18 | + | // {{.Name}} is the {{.Enum}} {{.Entry}} value. | |
| 19 | + | const {{.Name}}{{if .Type}} {{.Type}}{{end}} = {{.Value}} | |
| 20 | + | {{- end}} | |
| 21 | + | {{range .Interfaces}} | |
| 22 | + | {{- $iface := .}} | |
| 23 | + | ||
| 24 | + | // {{$iface.GoName}}Interface is the protocol interface name. | |
| 25 | + | const {{$iface.GoName}}Interface = "{{$iface.Name}}" | |
| 26 | + | ||
| 27 | + | // {{$iface.GoName}} is the generated binding for {{$iface.Name}} version {{$iface.Version}}. | |
| 28 | + | type {{$iface.GoName}} struct { | |
| 29 | + | wl.BaseProxy | |
| 30 | + | {{- if $iface.HasEvents}} | |
| 31 | + | // handlersMu guards the handler slices: a client may register or replace | |
| 32 | + | // handlers while another goroutine dispatches events. | |
| 33 | + | handlersMu sync.Mutex | |
| 34 | + | {{- end}} | |
| 35 | + | {{- range $iface.Events}} | |
| 36 | + | {{.HandlerField}} []func({{.HandlerParams}}) | |
| 37 | + | {{- end}} | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // New{{$iface.GoName}} returns an unbound {{$iface.GoName}}. | |
| 41 | + | func New{{$iface.GoName}}(ctx *wl.Context) *{{$iface.GoName}} { | |
| 42 | + | obj := &{{$iface.GoName}}{} | |
| 43 | + | obj.SetContext(ctx) | |
| 44 | + | ctx.Register(obj) | |
| 45 | + | return obj | |
| 46 | + | } | |
| 47 | + | {{- range $iface.Requests}} | |
| 48 | + | ||
| 49 | + | {{- if .Doc}} | |
| 50 | + | // {{.Doc}} | |
| 51 | + | {{- end}} | |
| 52 | + | func (o *{{$iface.GoName}}) {{.GoName}}({{.Params}}) {{.Results}} { | |
| 53 | + | if err := o.Context().CheckProxy(o); err != nil { return {{.ErrorReturn}} } | |
| 54 | + | {{- range .ArgPreparations}} | |
| 55 | + | {{.}} | |
| 56 | + | {{- end}} | |
| 57 | + | {{- if .CreatesChild}} | |
| 58 | + | {{- if .ChildFactory}} | |
| 59 | + | {{.ChildVar}} := {{.ChildFactory}}(o.Context()) | |
| 60 | + | {{- else}} | |
| 61 | + | {{.ChildVar}} := &{{.ChildType}}{} | |
| 62 | + | {{.ChildVar}}.SetContext(o.Context()) | |
| 63 | + | {{- end}} | |
| 64 | + | {{.ChildVar}}.SetID(o.Context().AllocateID()) | |
| 65 | + | o.Context().Register({{.ChildVar}}) | |
| 66 | + | {{- end}} | |
| 67 | + | if err := o.Context().{{.SendCall}}; err != nil { | |
| 68 | + | {{- if .CreatesChild}} | |
| 69 | + | o.Context().Unregister({{.ChildVar}}) | |
| 70 | + | {{- end}} | |
| 71 | + | return {{.ErrorReturn}} | |
| 72 | + | } | |
| 73 | + | {{- range .FDArgs}} | |
| 74 | + | _ = wl.CloseSentFD({{.}}) | |
| 75 | + | {{- end}} | |
| 76 | + | {{- if .CreatesChild}} | |
| 77 | + | return {{.ChildVar}}, nil | |
| 78 | + | {{- else}} | |
| 79 | + | return nil | |
| 80 | + | {{- end}} | |
| 81 | + | } | |
| 82 | + | {{- end}} | |
| 83 | + | {{- range $iface.Events}} | |
| 84 | + | ||
| 85 | + | // On{{.GoName}} registers a handler for the {{.Name}} event. | |
| 86 | + | // | |
| 87 | + | // Handlers are appended, so a second call adds another handler rather than | |
| 88 | + | // replacing the first. Registration is safe while another goroutine dispatches. | |
| 89 | + | func (o *{{$iface.GoName}}) On{{.GoName}}(handler func({{.HandlerParams}})) { | |
| 90 | + | if handler == nil { return } | |
| 91 | + | o.handlersMu.Lock() | |
| 92 | + | defer o.handlersMu.Unlock() | |
| 93 | + | o.{{.HandlerField}} = append(o.{{.HandlerField}}, handler) | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // {{.HandlerAccessor}} returns the handlers registered for {{.Name}}, taken | |
| 97 | + | // under the lock so a handler may register another handler while events are | |
| 98 | + | // being dispatched. | |
| 99 | + | func (o *{{$iface.GoName}}) {{.HandlerAccessor}}() []func({{.HandlerParams}}) { | |
| 100 | + | o.handlersMu.Lock() | |
| 101 | + | defer o.handlersMu.Unlock() | |
| 102 | + | return o.{{.HandlerField}} | |
| 103 | + | } | |
| 104 | + | {{- end}} | |
| 105 | + | ||
| 106 | + | // EventSignature returns the declared event argument types in wire order. | |
| 107 | + | func (o *{{$iface.GoName}}) EventSignature(opcode uint16) (string, bool) { | |
| 108 | + | switch opcode { | |
| 109 | + | {{- range $iface.SignatureCases}} | |
| 110 | + | case {{.Opcode}}: return "{{.Signature}}", true | |
| 111 | + | {{- end}} | |
| 112 | + | default: return "", false | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // Dispatch decodes one event on {{$iface.Name}} and calls the registered handlers. | |
| 117 | + | func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) { | |
| 118 | + | switch event.Opcode { | |
| 119 | + | {{- range $iface.Events}} | |
| 120 | + | case {{.Opcode}}: // {{.Name}} | |
| 121 | + | {{- range .DecodeLines}} | |
| 122 | + | {{.}} | |
| 123 | + | {{- end}} | |
| 124 | + | {{if .HasFD}}{{range .FDNames}}owned{{.}} := {{.}}; defer owned{{.}}.Close() | |
| 125 | + | {{end}}{{end}} | |
| 126 | + | for i, handler := range o.{{.HandlerAccessor}}() { | |
| 127 | + | {{- if .HasFD}} | |
| 128 | + | if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} } | |
| 129 | + | {{- else}} | |
| 130 | + | _ = i | |
| 131 | + | {{- end}} | |
| 132 | + | handler({{.HandlerArgs}}) | |
| 133 | + | } | |
| 134 | + | {{- if .ServerDestroy}} | |
| 135 | + | o.Context().Unregister(o) | |
| 136 | + | {{- end}} | |
| 137 | + | {{- end}} | |
| 138 | + | default: | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | {{end}} | |
| 142 | + | ` | |
| @@ -0,0 +1,142 @@ | |||||
| 1 | + | package scanner | |||
| 2 | + | ||||
| 3 | + | // protocolTemplate is the template for generating WLTurbo-backed Go bindings. | |||
| 4 | + | // | |||
| 5 | + | // Generated objects embed wl.BaseProxy, allocate and register child objects | |||
| 6 | + | // through the wl.Context, send requests through that context, and dispatch | |||
| 7 | + | // typed events to registered handlers. Nothing in the generated code opens a | |||
| 8 | + | // socket or frames messages itself: that is the transport's job. | |||
| 9 | + | const protocolTemplate = `// Code generated by wlturbo-scanner. DO NOT EDIT. | |||
| 10 | + | // Protocol: {{.Protocol}} | |||
| 11 | + | // Source: {{.Source}} | |||
| 12 | + | ||||
| 13 | + | package {{.Package}} | |||
| 14 | + | ||||
| 15 | + | {{.ImportBlock}} | |||
| 16 | + | ||||
| 17 | + | {{range .Constants}} | |||
| 18 | + | // {{.Name}} is the {{.Enum}} {{.Entry}} value. | |||
| 19 | + | const {{.Name}}{{if .Type}} {{.Type}}{{end}} = {{.Value}} | |||
| 20 | + | {{- end}} | |||
| 21 | + | {{range .Interfaces}} | |||
| 22 | + | {{- $iface := .}} | |||
| 23 | + | ||||
| 24 | + | // {{$iface.GoName}}Interface is the protocol interface name. | |||
| 25 | + | const {{$iface.GoName}}Interface = "{{$iface.Name}}" | |||
| 26 | + | ||||
| 27 | + | // {{$iface.GoName}} is the generated binding for {{$iface.Name}} version {{$iface.Version}}. | |||
| 28 | + | type {{$iface.GoName}} struct { | |||
| 29 | + | wl.BaseProxy | |||
| 30 | + | {{- if $iface.HasEvents}} | |||
| 31 | + | // handlersMu guards the handler slices: a client may register or replace | |||
| 32 | + | // handlers while another goroutine dispatches events. | |||
| 33 | + | handlersMu sync.Mutex | |||
| 34 | + | {{- end}} | |||
| 35 | + | {{- range $iface.Events}} | |||
| 36 | + | {{.HandlerField}} []func({{.HandlerParams}}) | |||
| 37 | + | {{- end}} | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // New{{$iface.GoName}} returns an unbound {{$iface.GoName}}. | |||
| 41 | + | func New{{$iface.GoName}}(ctx *wl.Context) *{{$iface.GoName}} { | |||
| 42 | + | obj := &{{$iface.GoName}}{} | |||
| 43 | + | obj.SetContext(ctx) | |||
| 44 | + | ctx.Register(obj) | |||
| 45 | + | return obj | |||
| 46 | + | } | |||
| 47 | + | {{- range $iface.Requests}} | |||
| 48 | + | ||||
| 49 | + | {{- if .Doc}} | |||
| 50 | + | // {{.Doc}} | |||
| 51 | + | {{- end}} | |||
| 52 | + | func (o *{{$iface.GoName}}) {{.GoName}}({{.Params}}) {{.Results}} { | |||
| 53 | + | if err := o.Context().CheckProxy(o); err != nil { return {{.ErrorReturn}} } | |||
| 54 | + | {{- range .ArgPreparations}} | |||
| 55 | + | {{.}} | |||
| 56 | + | {{- end}} | |||
| 57 | + | {{- if .CreatesChild}} | |||
| 58 | + | {{- if .ChildFactory}} | |||
| 59 | + | {{.ChildVar}} := {{.ChildFactory}}(o.Context()) | |||
| 60 | + | {{- else}} | |||
| 61 | + | {{.ChildVar}} := &{{.ChildType}}{} | |||
| 62 | + | {{.ChildVar}}.SetContext(o.Context()) | |||
| 63 | + | {{- end}} | |||
| 64 | + | {{.ChildVar}}.SetID(o.Context().AllocateID()) | |||
| 65 | + | o.Context().Register({{.ChildVar}}) | |||
| 66 | + | {{- end}} | |||
| 67 | + | if err := o.Context().{{.SendCall}}; err != nil { | |||
| 68 | + | {{- if .CreatesChild}} | |||
| 69 | + | o.Context().Unregister({{.ChildVar}}) | |||
| 70 | + | {{- end}} | |||
| 71 | + | return {{.ErrorReturn}} | |||
| 72 | + | } | |||
| 73 | + | {{- range .FDArgs}} | |||
| 74 | + | _ = wl.CloseSentFD({{.}}) | |||
| 75 | + | {{- end}} | |||
| 76 | + | {{- if .CreatesChild}} | |||
| 77 | + | return {{.ChildVar}}, nil | |||
| 78 | + | {{- else}} | |||
| 79 | + | return nil | |||
| 80 | + | {{- end}} | |||
| 81 | + | } | |||
| 82 | + | {{- end}} | |||
| 83 | + | {{- range $iface.Events}} | |||
| 84 | + | ||||
| 85 | + | // On{{.GoName}} registers a handler for the {{.Name}} event. | |||
| 86 | + | // | |||
| 87 | + | // Handlers are appended, so a second call adds another handler rather than | |||
| 88 | + | // replacing the first. Registration is safe while another goroutine dispatches. | |||
| 89 | + | func (o *{{$iface.GoName}}) On{{.GoName}}(handler func({{.HandlerParams}})) { | |||
| 90 | + | if handler == nil { return } | |||
| 91 | + | o.handlersMu.Lock() | |||
| 92 | + | defer o.handlersMu.Unlock() | |||
| 93 | + | o.{{.HandlerField}} = append(o.{{.HandlerField}}, handler) | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // {{.HandlerAccessor}} returns the handlers registered for {{.Name}}, taken | |||
| 97 | + | // under the lock so a handler may register another handler while events are | |||
| 98 | + | // being dispatched. | |||
| 99 | + | func (o *{{$iface.GoName}}) {{.HandlerAccessor}}() []func({{.HandlerParams}}) { | |||
| 100 | + | o.handlersMu.Lock() | |||
| 101 | + | defer o.handlersMu.Unlock() | |||
| 102 | + | return o.{{.HandlerField}} | |||
| 103 | + | } | |||
| 104 | + | {{- end}} | |||
| 105 | + | ||||
| 106 | + | // EventSignature returns the declared event argument types in wire order. | |||
| 107 | + | func (o *{{$iface.GoName}}) EventSignature(opcode uint16) (string, bool) { | |||
| 108 | + | switch opcode { | |||
| 109 | + | {{- range $iface.SignatureCases}} | |||
| 110 | + | case {{.Opcode}}: return "{{.Signature}}", true | |||
| 111 | + | {{- end}} | |||
| 112 | + | default: return "", false | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | // Dispatch decodes one event on {{$iface.Name}} and calls the registered handlers. | |||
| 117 | + | func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) { | |||
| 118 | + | switch event.Opcode { | |||
| 119 | + | {{- range $iface.Events}} | |||
| 120 | + | case {{.Opcode}}: // {{.Name}} | |||
| 121 | + | {{- range .DecodeLines}} | |||
| 122 | + | {{.}} | |||
| 123 | + | {{- end}} | |||
| 124 | + | {{if .HasFD}}{{range .FDNames}}owned{{.}} := {{.}}; defer owned{{.}}.Close() | |||
| 125 | + | {{end}}{{end}} | |||
| 126 | + | for i, handler := range o.{{.HandlerAccessor}}() { | |||
| 127 | + | {{- if .HasFD}} | |||
| 128 | + | if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} } | |||
| 129 | + | {{- else}} | |||
| 130 | + | _ = i | |||
| 131 | + | {{- end}} | |||
| 132 | + | handler({{.HandlerArgs}}) | |||
| 133 | + | } | |||
| 134 | + | {{- if .ServerDestroy}} | |||
| 135 | + | o.Context().Unregister(o) | |||
| 136 | + | {{- end}} | |||
| 137 | + | {{- end}} | |||
| 138 | + | default: | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | {{end}} | |||
| 142 | + | ` | |||
A internal/scanner/testdata/protocol_fixture.xml +112 -0
| @@ -0,0 +1,112 @@ | |||
| 1 | + | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | + | <protocol name="wlvision_fixture"> | |
| 3 | + | <copyright> | |
| 4 | + | Synthetic protocol used to test the wlvision protocol generator. It exists | |
| 5 | + | only in this repository's tests. | |
| 6 | + | </copyright> | |
| 7 | + | ||
| 8 | + | <interface name="zwlr_fixture_manager_v1" version="2"> | |
| 9 | + | <description summary="fixture manager"> | |
| 10 | + | Exercises new_id requests, object references, fixed point numbers, | |
| 11 | + | strings, arrays, file descriptors and destructors. | |
| 12 | + | </description> | |
| 13 | + | ||
| 14 | + | <request name="create_thing"> | |
| 15 | + | <description summary="create a fixture object"/> | |
| 16 | + | <arg name="seat" type="object" interface="wl_seat"/> | |
| 17 | + | <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/> | |
| 18 | + | </request> | |
| 19 | + | ||
| 20 | + | <request name="set_ratio"> | |
| 21 | + | <arg name="ratio" type="fixed"/> | |
| 22 | + | </request> | |
| 23 | + | ||
| 24 | + | <request name="set_label"> | |
| 25 | + | <arg name="label" type="string"/> | |
| 26 | + | </request> | |
| 27 | + | ||
| 28 | + | <request name="set_region"> | |
| 29 | + | <arg name="region" type="object" interface="wl_region"/> | |
| 30 | + | </request> | |
| 31 | + | ||
| 32 | + | <request name="set_buffer"> | |
| 33 | + | <description summary="reference an interface without a typed wrapper"/> | |
| 34 | + | <arg name="buffer" type="object" interface="wl_buffer"/> | |
| 35 | + | </request> | |
| 36 | + | ||
| 37 | + | <request name="set_optional_output"> | |
| 38 | + | <arg name="output" type="object" interface="wl_output" allow-null="true"/> | |
| 39 | + | </request> | |
| 40 | + | ||
| 41 | + | <request name="attach_raw"> | |
| 42 | + | <arg name="data" type="array"/> | |
| 43 | + | </request> | |
| 44 | + | ||
| 45 | + | <request name="pass_fd"> | |
| 46 | + | <arg name="fd" type="fd"/> | |
| 47 | + | </request> | |
| 48 | + | ||
| 49 | + | <request name="destroy" type="destructor"> | |
| 50 | + | <description summary="destroy the manager"/> | |
| 51 | + | </request> | |
| 52 | + | ||
| 53 | + | <request name="create_labeled_thing"> | |
| 54 | + | <description summary="new_id argument that is not the last one"/> | |
| 55 | + | <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/> | |
| 56 | + | <arg name="label" type="string"/> | |
| 57 | + | <arg name="ratio" type="fixed"/> | |
| 58 | + | </request> | |
| 59 | + | </interface> | |
| 60 | + | ||
| 61 | + | <interface name="zwlr_fixture_thing_v1" version="1"> | |
| 62 | + | <description summary="fixture object"/> | |
| 63 | + | ||
| 64 | + | <enum name="fixture_state"> | |
| 65 | + | <entry name="idle" value="0"/> | |
| 66 | + | <entry name="busy" value="1"/> | |
| 67 | + | </enum> | |
| 68 | + | ||
| 69 | + | <request name="set_scale"> | |
| 70 | + | <arg name="scale" type="fixed"/> | |
| 71 | + | </request> | |
| 72 | + | ||
| 73 | + | <request name="destroy" type="destructor"/> | |
| 74 | + | ||
| 75 | + | <event name="ready"> | |
| 76 | + | <arg name="serial" type="uint"/> | |
| 77 | + | </event> | |
| 78 | + | ||
| 79 | + | <event name="label_changed"> | |
| 80 | + | <arg name="label" type="string"/> | |
| 81 | + | </event> | |
| 82 | + | ||
| 83 | + | <event name="geometry"> | |
| 84 | + | <arg name="x" type="int"/> | |
| 85 | + | <arg name="y" type="int"/> | |
| 86 | + | <arg name="width" type="uint"/> | |
| 87 | + | <arg name="height" type="uint"/> | |
| 88 | + | </event> | |
| 89 | + | ||
| 90 | + | <event name="mode"> | |
| 91 | + | <arg name="mode" type="new_id" interface="zwlr_fixture_mode_v1"/> | |
| 92 | + | </event> | |
| 93 | + | ||
| 94 | + | <event name="peer"> | |
| 95 | + | <arg name="surface" type="object" interface="wl_surface"/> | |
| 96 | + | </event> | |
| 97 | + | ||
| 98 | + | <event name="scaled"> | |
| 99 | + | <arg name="scale" type="fixed"/> | |
| 100 | + | </event> | |
| 101 | + | ||
| 102 | + | <event name="blob"> | |
| 103 | + | <arg name="data" type="array"/> | |
| 104 | + | </event> | |
| 105 | + | </interface> | |
| 106 | + | ||
| 107 | + | <interface name="zwlr_fixture_mode_v1" version="1"> | |
| 108 | + | <description summary="fixture mode object"/> | |
| 109 | + | ||
| 110 | + | <request name="destroy" type="destructor"/> | |
| 111 | + | </interface> | |
| 112 | + | </protocol> | |
| @@ -0,0 +1,112 @@ | |||||
| 1 | + | <?xml version="1.0" encoding="UTF-8"?> | |||
| 2 | + | <protocol name="wlvision_fixture"> | |||
| 3 | + | <copyright> | |||
| 4 | + | Synthetic protocol used to test the wlvision protocol generator. It exists | |||
| 5 | + | only in this repository's tests. | |||
| 6 | + | </copyright> | |||
| 7 | + | ||||
| 8 | + | <interface name="zwlr_fixture_manager_v1" version="2"> | |||
| 9 | + | <description summary="fixture manager"> | |||
| 10 | + | Exercises new_id requests, object references, fixed point numbers, | |||
| 11 | + | strings, arrays, file descriptors and destructors. | |||
| 12 | + | </description> | |||
| 13 | + | ||||
| 14 | + | <request name="create_thing"> | |||
| 15 | + | <description summary="create a fixture object"/> | |||
| 16 | + | <arg name="seat" type="object" interface="wl_seat"/> | |||
| 17 | + | <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/> | |||
| 18 | + | </request> | |||
| 19 | + | ||||
| 20 | + | <request name="set_ratio"> | |||
| 21 | + | <arg name="ratio" type="fixed"/> | |||
| 22 | + | </request> | |||
| 23 | + | ||||
| 24 | + | <request name="set_label"> | |||
| 25 | + | <arg name="label" type="string"/> | |||
| 26 | + | </request> | |||
| 27 | + | ||||
| 28 | + | <request name="set_region"> | |||
| 29 | + | <arg name="region" type="object" interface="wl_region"/> | |||
| 30 | + | </request> | |||
| 31 | + | ||||
| 32 | + | <request name="set_buffer"> | |||
| 33 | + | <description summary="reference an interface without a typed wrapper"/> | |||
| 34 | + | <arg name="buffer" type="object" interface="wl_buffer"/> | |||
| 35 | + | </request> | |||
| 36 | + | ||||
| 37 | + | <request name="set_optional_output"> | |||
| 38 | + | <arg name="output" type="object" interface="wl_output" allow-null="true"/> | |||
| 39 | + | </request> | |||
| 40 | + | ||||
| 41 | + | <request name="attach_raw"> | |||
| 42 | + | <arg name="data" type="array"/> | |||
| 43 | + | </request> | |||
| 44 | + | ||||
| 45 | + | <request name="pass_fd"> | |||
| 46 | + | <arg name="fd" type="fd"/> | |||
| 47 | + | </request> | |||
| 48 | + | ||||
| 49 | + | <request name="destroy" type="destructor"> | |||
| 50 | + | <description summary="destroy the manager"/> | |||
| 51 | + | </request> | |||
| 52 | + | ||||
| 53 | + | <request name="create_labeled_thing"> | |||
| 54 | + | <description summary="new_id argument that is not the last one"/> | |||
| 55 | + | <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/> | |||
| 56 | + | <arg name="label" type="string"/> | |||
| 57 | + | <arg name="ratio" type="fixed"/> | |||
| 58 | + | </request> | |||
| 59 | + | </interface> | |||
| 60 | + | ||||
| 61 | + | <interface name="zwlr_fixture_thing_v1" version="1"> | |||
| 62 | + | <description summary="fixture object"/> | |||
| 63 | + | ||||
| 64 | + | <enum name="fixture_state"> | |||
| 65 | + | <entry name="idle" value="0"/> | |||
| 66 | + | <entry name="busy" value="1"/> | |||
| 67 | + | </enum> | |||
| 68 | + | ||||
| 69 | + | <request name="set_scale"> | |||
| 70 | + | <arg name="scale" type="fixed"/> | |||
| 71 | + | </request> | |||
| 72 | + | ||||
| 73 | + | <request name="destroy" type="destructor"/> | |||
| 74 | + | ||||
| 75 | + | <event name="ready"> | |||
| 76 | + | <arg name="serial" type="uint"/> | |||
| 77 | + | </event> | |||
| 78 | + | ||||
| 79 | + | <event name="label_changed"> | |||
| 80 | + | <arg name="label" type="string"/> | |||
| 81 | + | </event> | |||
| 82 | + | ||||
| 83 | + | <event name="geometry"> | |||
| 84 | + | <arg name="x" type="int"/> | |||
| 85 | + | <arg name="y" type="int"/> | |||
| 86 | + | <arg name="width" type="uint"/> | |||
| 87 | + | <arg name="height" type="uint"/> | |||
| 88 | + | </event> | |||
| 89 | + | ||||
| 90 | + | <event name="mode"> | |||
| 91 | + | <arg name="mode" type="new_id" interface="zwlr_fixture_mode_v1"/> | |||
| 92 | + | </event> | |||
| 93 | + | ||||
| 94 | + | <event name="peer"> | |||
| 95 | + | <arg name="surface" type="object" interface="wl_surface"/> | |||
| 96 | + | </event> | |||
| 97 | + | ||||
| 98 | + | <event name="scaled"> | |||
| 99 | + | <arg name="scale" type="fixed"/> | |||
| 100 | + | </event> | |||
| 101 | + | ||||
| 102 | + | <event name="blob"> | |||
| 103 | + | <arg name="data" type="array"/> | |||
| 104 | + | </event> | |||
| 105 | + | </interface> | |||
| 106 | + | ||||
| 107 | + | <interface name="zwlr_fixture_mode_v1" version="1"> | |||
| 108 | + | <description summary="fixture mode object"/> | |||
| 109 | + | ||||
| 110 | + | <request name="destroy" type="destructor"/> | |||
| 111 | + | </interface> | |||
| 112 | + | </protocol> | |||
A protocol/SOURCE.md +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | # Core protocol source | |
| 2 | + | ||
| 3 | + | `wayland.xml` is from Wayland 1.26.0, upstream GitLab | |
| 4 | + | `https://gitlab.freedesktop.org/wayland/wayland`, tag `1.26.0`, peeled commit | |
| 5 | + | `87cc8a8728a923fc57938faa81ba0e74f34ecdc7`. | |
| 6 | + | SHA-256: `cc860987e54f8d85c940e97fa1270c69b6e4ad31fbcf5a7f00107ce1157f5e07`. | |
| 7 | + | License: MIT (full copyright and grant in `wayland.xml` `<copyright>`). | |
| @@ -0,0 +1,7 @@ | |||||
| 1 | + | # Core protocol source | |||
| 2 | + | ||||
| 3 | + | `wayland.xml` is from Wayland 1.26.0, upstream GitLab | |||
| 4 | + | `https://gitlab.freedesktop.org/wayland/wayland`, tag `1.26.0`, peeled commit | |||
| 5 | + | `87cc8a8728a923fc57938faa81ba0e74f34ecdc7`. | |||
| 6 | + | SHA-256: `cc860987e54f8d85c940e97fa1270c69b6e4ad31fbcf5a7f00107ce1157f5e07`. | |||
| 7 | + | License: MIT (full copyright and grant in `wayland.xml` `<copyright>`). | |||
A protocol/core/core_test.go +532 -0
| @@ -0,0 +1,532 @@ | |||
| 1 | + | //go:build linux | |
| 2 | + | ||
| 3 | + | package core_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "encoding/binary" | |
| 7 | + | "errors" | |
| 8 | + | "io" | |
| 9 | + | "net" | |
| 10 | + | "os" | |
| 11 | + | "testing" | |
| 12 | + | "time" | |
| 13 | + | ||
| 14 | + | "github.com/bnema/wlturbo" | |
| 15 | + | "github.com/bnema/wlturbo/protocol/core" | |
| 16 | + | "golang.org/x/sys/unix" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) { | |
| 20 | + | t.Helper() | |
| 21 | + | f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 22 | + | if err != nil { | |
| 23 | + | t.Fatal(err) | |
| 24 | + | } | |
| 25 | + | a := os.NewFile(uintptr(f[0]), "a") | |
| 26 | + | b := os.NewFile(uintptr(f[1]), "b") | |
| 27 | + | ca, e := net.FileConn(a) | |
| 28 | + | a.Close() | |
| 29 | + | if e != nil { | |
| 30 | + | t.Fatal(e) | |
| 31 | + | } | |
| 32 | + | cb, e := net.FileConn(b) | |
| 33 | + | b.Close() | |
| 34 | + | if e != nil { | |
| 35 | + | t.Fatal(e) | |
| 36 | + | } | |
| 37 | + | t.Cleanup(func() { ca.Close(); cb.Close() }) | |
| 38 | + | return ca.(*net.UnixConn), cb.(*net.UnixConn) | |
| 39 | + | } | |
| 40 | + | func frame(id uint32, op uint16, body ...uint32) []byte { | |
| 41 | + | p := make([]byte, 8+4*len(body)) | |
| 42 | + | binary.LittleEndian.PutUint32(p, id) | |
| 43 | + | binary.LittleEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op)) | |
| 44 | + | for i, v := range body { | |
| 45 | + | binary.LittleEndian.PutUint32(p[8+i*4:], v) | |
| 46 | + | } | |
| 47 | + | return p | |
| 48 | + | } | |
| 49 | + | func receiveRequest(t *testing.T, p *net.UnixConn) { | |
| 50 | + | t.Helper() | |
| 51 | + | p.SetReadDeadline(testDeadline()) | |
| 52 | + | b := make([]byte, 256) | |
| 53 | + | if _, e := p.Read(b); e != nil { | |
| 54 | + | t.Fatal(e) | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | func testDeadline() (d time.Time) { return time.Now().Add(2 * time.Second) } | |
| 58 | + | func send(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) { | |
| 59 | + | t.Helper() | |
| 60 | + | var oob []byte | |
| 61 | + | if len(fd) > 0 { | |
| 62 | + | oob = unix.UnixRights(fd...) | |
| 63 | + | } | |
| 64 | + | if _, _, e := p.WriteMsgUnix(msg, oob, nil); e != nil { | |
| 65 | + | t.Fatal(e) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | func TestGeneratedCoreOverSocketpair(t *testing.T) { | |
| 69 | + | c, p := pair(t) | |
| 70 | + | d, err := wlturbo.ConnectFromConn(c) | |
| 71 | + | if err != nil { | |
| 72 | + | t.Fatal(err) | |
| 73 | + | } | |
| 74 | + | defer d.Close() | |
| 75 | + | receiveRequest(t, p) // get_registry | |
| 76 | + | seat := core.NewSeat(d.Context()) | |
| 77 | + | if err := d.Registry().Bind(1, core.SeatInterface, 1, seat); err != nil { | |
| 78 | + | t.Fatal(err) | |
| 79 | + | } | |
| 80 | + | receiveRequest(t, p) | |
| 81 | + | kb, err := seat.GetKeyboard() | |
| 82 | + | if err != nil { | |
| 83 | + | t.Fatal(err) | |
| 84 | + | } | |
| 85 | + | receiveRequest(t, p) | |
| 86 | + | var key uint32 | |
| 87 | + | kb.OnKey(func(_, _, k, _ uint32) { key = k }) | |
| 88 | + | send(t, p, frame(kb.ID(), 3, 1, 2, 42, 1)) | |
| 89 | + | if err := d.Dispatch(); err != nil { | |
| 90 | + | t.Fatal(err) | |
| 91 | + | } | |
| 92 | + | if key != 42 { | |
| 93 | + | t.Fatalf("key=%d", key) | |
| 94 | + | } | |
| 95 | + | compositor := core.NewCompositor(d.Context()) | |
| 96 | + | if err := d.Registry().Bind(2, core.CompositorInterface, 1, compositor); err != nil { | |
| 97 | + | t.Fatal(err) | |
| 98 | + | } | |
| 99 | + | receiveRequest(t, p) | |
| 100 | + | surface, err := compositor.CreateSurface() | |
| 101 | + | if err != nil { | |
| 102 | + | t.Fatal(err) | |
| 103 | + | } | |
| 104 | + | receiveRequest(t, p) | |
| 105 | + | callback, err := surface.Frame() | |
| 106 | + | if err != nil { | |
| 107 | + | t.Fatal(err) | |
| 108 | + | } | |
| 109 | + | receiveRequest(t, p) | |
| 110 | + | var done uint32 | |
| 111 | + | callback.OnDone(func(ms uint32) { done = ms }) | |
| 112 | + | send(t, p, frame(callback.ID(), 0, 123)) | |
| 113 | + | if err := d.Dispatch(); err != nil { | |
| 114 | + | t.Fatal(err) | |
| 115 | + | } | |
| 116 | + | if done != 123 { | |
| 117 | + | t.Fatalf("done=%d", done) | |
| 118 | + | } | |
| 119 | + | send(t, p, frame(callback.ID(), 0, 124)) | |
| 120 | + | if err := d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) { | |
| 121 | + | t.Fatalf("destroyed callback: %v", err) | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | func TestKeymapFDNoHandlerAndMultipleHandlers(t *testing.T) { | |
| 125 | + | c, p := pair(t) | |
| 126 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 127 | + | if e != nil { | |
| 128 | + | t.Fatal(e) | |
| 129 | + | } | |
| 130 | + | defer d.Close() | |
| 131 | + | receiveRequest(t, p) | |
| 132 | + | kb := core.NewKeyboard(d.Context()) | |
| 133 | + | kb.SetID(d.AllocateID()) | |
| 134 | + | d.Context().Register(kb) | |
| 135 | + | r, w, e := os.Pipe() | |
| 136 | + | if e != nil { | |
| 137 | + | t.Fatal(e) | |
| 138 | + | } | |
| 139 | + | defer r.Close() | |
| 140 | + | send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd())) | |
| 141 | + | w.Close() | |
| 142 | + | if e := d.Dispatch(); e != nil { | |
| 143 | + | t.Fatal(e) | |
| 144 | + | } | |
| 145 | + | buf := make([]byte, 1) | |
| 146 | + | if n, e := r.Read(buf); n != 0 || e != io.EOF { | |
| 147 | + | t.Fatalf("unclaimed FD n=%d err=%v", n, e) | |
| 148 | + | } | |
| 149 | + | r2, w2, e := os.Pipe() | |
| 150 | + | if e != nil { | |
| 151 | + | t.Fatal(e) | |
| 152 | + | } | |
| 153 | + | defer r2.Close() | |
| 154 | + | received := 0 | |
| 155 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |
| 156 | + | if fd == nil { | |
| 157 | + | t.Fatal("first FD missing") | |
| 158 | + | } | |
| 159 | + | n, e := fd.Take() | |
| 160 | + | if e != nil { | |
| 161 | + | t.Fatal(e) | |
| 162 | + | } | |
| 163 | + | received++ | |
| 164 | + | unix.Close(n) | |
| 165 | + | }) | |
| 166 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |
| 167 | + | if fd != nil { | |
| 168 | + | t.Fatal("duplicated owner") | |
| 169 | + | } | |
| 170 | + | received++ | |
| 171 | + | }) | |
| 172 | + | send(t, p, frame(kb.ID(), 0, 1, 8), int(w2.Fd())) | |
| 173 | + | w2.Close() | |
| 174 | + | if e := d.Dispatch(); e != nil { | |
| 175 | + | t.Fatal(e) | |
| 176 | + | } | |
| 177 | + | if received != 2 { | |
| 178 | + | t.Fatalf("handlers=%d", received) | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | func TestBufferReleaseAndOfferChild(t *testing.T) { | |
| 182 | + | c, p := pair(t) | |
| 183 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 184 | + | if e != nil { | |
| 185 | + | t.Fatal(e) | |
| 186 | + | } | |
| 187 | + | defer d.Close() | |
| 188 | + | receiveRequest(t, p) | |
| 189 | + | b := core.NewBuffer(d.Context()) | |
| 190 | + | b.SetID(d.AllocateID()) | |
| 191 | + | d.Context().Register(b) | |
| 192 | + | releases := 0 | |
| 193 | + | b.OnRelease(func() { releases++ }) | |
| 194 | + | send(t, p, frame(b.ID(), 0)) | |
| 195 | + | if e := d.Dispatch(); e != nil { | |
| 196 | + | t.Fatal(e) | |
| 197 | + | } | |
| 198 | + | if releases != 1 { | |
| 199 | + | t.Fatal("release not dispatched") | |
| 200 | + | } | |
| 201 | + | device := core.NewDataDevice(d.Context()) | |
| 202 | + | device.SetID(d.AllocateID()) | |
| 203 | + | d.Context().Register(device) | |
| 204 | + | var offer *core.DataOffer | |
| 205 | + | device.OnDataOffer(func(o *core.DataOffer) { offer = o }) | |
| 206 | + | send(t, p, frame(device.ID(), 0, 0xff000001)) | |
| 207 | + | if e := d.Dispatch(); e != nil { | |
| 208 | + | t.Fatal(e) | |
| 209 | + | } | |
| 210 | + | if offer == nil || offer.ID() != 0xff000001 { | |
| 211 | + | t.Fatalf("offer=%v", offer) | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | func TestMissingKeymapDescriptor(t *testing.T) { | |
| 216 | + | c, p := pair(t) | |
| 217 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 218 | + | if e != nil { | |
| 219 | + | t.Fatal(e) | |
| 220 | + | } | |
| 221 | + | defer d.Close() | |
| 222 | + | receiveRequest(t, p) | |
| 223 | + | kb := core.NewKeyboard(d.Context()) | |
| 224 | + | kb.SetID(d.AllocateID()) | |
| 225 | + | d.Context().Register(kb) | |
| 226 | + | send(t, p, frame(kb.ID(), 0, 1, 8)) | |
| 227 | + | if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) { | |
| 228 | + | t.Fatalf("missing FD: %v", e) | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | func TestInterleavedFDAndPlainMessages(t *testing.T) { | |
| 233 | + | c, p := pair(t) | |
| 234 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 235 | + | if e != nil { | |
| 236 | + | t.Fatal(e) | |
| 237 | + | } | |
| 238 | + | defer d.Close() | |
| 239 | + | receiveRequest(t, p) | |
| 240 | + | kb := core.NewKeyboard(d.Context()) | |
| 241 | + | kb.SetID(d.AllocateID()) | |
| 242 | + | d.Context().Register(kb) | |
| 243 | + | calls := 0 | |
| 244 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |
| 245 | + | if fd == nil { | |
| 246 | + | t.Fatal("FD not assigned") | |
| 247 | + | } | |
| 248 | + | _ = fd.Close() | |
| 249 | + | calls++ | |
| 250 | + | }) | |
| 251 | + | kb.OnKey(func(_, _, key, _ uint32) { | |
| 252 | + | if key != 33 { | |
| 253 | + | t.Errorf("key=%d", key) | |
| 254 | + | } | |
| 255 | + | calls++ | |
| 256 | + | }) | |
| 257 | + | r, w, e := os.Pipe() | |
| 258 | + | if e != nil { | |
| 259 | + | t.Fatal(e) | |
| 260 | + | } | |
| 261 | + | defer r.Close() | |
| 262 | + | send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd())) | |
| 263 | + | w.Close() | |
| 264 | + | send(t, p, frame(kb.ID(), 3, 1, 2, 33, 1)) | |
| 265 | + | if e := d.Dispatch(); e != nil { | |
| 266 | + | t.Fatal(e) | |
| 267 | + | } | |
| 268 | + | if e := d.Dispatch(); e != nil { | |
| 269 | + | t.Fatal(e) | |
| 270 | + | } | |
| 271 | + | if calls != 2 { | |
| 272 | + | t.Fatalf("calls=%d", calls) | |
| 273 | + | } | |
| 274 | + | } | |
| 275 | + | func TestMalformedGeneratedPayload(t *testing.T) { | |
| 276 | + | c, p := pair(t) | |
| 277 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 278 | + | if e != nil { | |
| 279 | + | t.Fatal(e) | |
| 280 | + | } | |
| 281 | + | defer d.Close() | |
| 282 | + | receiveRequest(t, p) | |
| 283 | + | kb := core.NewKeyboard(d.Context()) | |
| 284 | + | kb.SetID(d.AllocateID()) | |
| 285 | + | d.Context().Register(kb) | |
| 286 | + | send(t, p, frame(kb.ID(), 3, 1, 2)) | |
| 287 | + | if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) { | |
| 288 | + | t.Fatalf("truncated key: %v", e) | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | func TestServerChildEventsAfterOffer(t *testing.T) { | |
| 293 | + | c, p := pair(t) | |
| 294 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 295 | + | if e != nil { | |
| 296 | + | t.Fatal(e) | |
| 297 | + | } | |
| 298 | + | defer d.Close() | |
| 299 | + | receiveRequest(t, p) | |
| 300 | + | device := core.NewDataDevice(d.Context()) | |
| 301 | + | device.SetID(d.AllocateID()) | |
| 302 | + | d.Context().Register(device) | |
| 303 | + | const offerID = 0xff000011 | |
| 304 | + | var offer *core.DataOffer | |
| 305 | + | device.OnDataOffer(func(o *core.DataOffer) { offer = o }) | |
| 306 | + | send(t, p, frame(device.ID(), 0, offerID)) | |
| 307 | + | if e := d.Dispatch(); e != nil { | |
| 308 | + | t.Fatal(e) | |
| 309 | + | } | |
| 310 | + | if offer == nil { | |
| 311 | + | t.Fatal("missing concrete child") | |
| 312 | + | } | |
| 313 | + | got := "" | |
| 314 | + | offer.OnOffer(func(mime string) { got = mime }) | |
| 315 | + | payload := append(frame(offerID, 0), 4, 0, 0, 0, 'a', 'b', 'c', 0) | |
| 316 | + | binary.LittleEndian.PutUint32(payload[4:], uint32(len(payload))<<16) | |
| 317 | + | send(t, p, payload) | |
| 318 | + | if e := d.Dispatch(); e != nil { | |
| 319 | + | t.Fatal(e) | |
| 320 | + | } | |
| 321 | + | if got != "abc" { | |
| 322 | + | t.Fatalf("typed child event: %q", got) | |
| 323 | + | } | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | func TestRegistryBootstrapAnnouncements(t *testing.T) { | |
| 327 | + | c, p := pair(t) | |
| 328 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 329 | + | if e != nil { | |
| 330 | + | t.Fatal(e) | |
| 331 | + | } | |
| 332 | + | defer d.Close() | |
| 333 | + | receiveRequest(t, p) | |
| 334 | + | var got wlturbo.Global | |
| 335 | + | d.Registry().AddHandler("wl_compositor", func(r *wlturbo.Registry, n, v uint32) { got, _ = r.FindGlobalByName(n) }) | |
| 336 | + | name := "wl_compositor" | |
| 337 | + | body := make([]byte, 4+4+((len(name)+1+3)&^3)+4) | |
| 338 | + | binary.LittleEndian.PutUint32(body, 17) | |
| 339 | + | binary.LittleEndian.PutUint32(body[4:], uint32(len(name)+1)) | |
| 340 | + | copy(body[8:], name) | |
| 341 | + | binary.LittleEndian.PutUint32(body[len(body)-4:], 7) | |
| 342 | + | msg := append(frame(d.Registry().ID(), 0), body...) | |
| 343 | + | binary.LittleEndian.PutUint32(msg[4:], uint32(len(msg))<<16) | |
| 344 | + | send(t, p, msg) | |
| 345 | + | if e := d.Dispatch(); e != nil { | |
| 346 | + | t.Fatal(e) | |
| 347 | + | } | |
| 348 | + | if got.Name != 17 || got.Interface != name || got.Version != 7 { | |
| 349 | + | t.Fatalf("global: %+v", got) | |
| 350 | + | } | |
| 351 | + | send(t, p, frame(d.Registry().ID(), 1, 17)) | |
| 352 | + | if e := d.Dispatch(); e != nil { | |
| 353 | + | t.Fatal(e) | |
| 354 | + | } | |
| 355 | + | if _, ok := d.Registry().FindGlobalByName(17); ok { | |
| 356 | + | t.Fatal("global not removed") | |
| 357 | + | } | |
| 358 | + | send(t, p, frame(d.Registry().ID(), 2)) | |
| 359 | + | if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrUnknownOpcode) { | |
| 360 | + | t.Fatalf("unknown registry opcode: %v", e) | |
| 361 | + | } | |
| 362 | + | } | |
| 363 | + | func TestUnclaimedFirstHandlerFDClosed(t *testing.T) { | |
| 364 | + | c, p := pair(t) | |
| 365 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 366 | + | if e != nil { | |
| 367 | + | t.Fatal(e) | |
| 368 | + | } | |
| 369 | + | defer d.Close() | |
| 370 | + | receiveRequest(t, p) | |
| 371 | + | kb := core.NewKeyboard(d.Context()) | |
| 372 | + | kb.SetID(d.AllocateID()) | |
| 373 | + | d.Context().Register(kb) | |
| 374 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |
| 375 | + | if fd == nil { | |
| 376 | + | t.Fatal("missing first owner") | |
| 377 | + | } | |
| 378 | + | }) | |
| 379 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |
| 380 | + | if fd != nil { | |
| 381 | + | t.Fatal("duplicate owner") | |
| 382 | + | } | |
| 383 | + | }) | |
| 384 | + | r, w, e := os.Pipe() | |
| 385 | + | if e != nil { | |
| 386 | + | t.Fatal(e) | |
| 387 | + | } | |
| 388 | + | defer r.Close() | |
| 389 | + | send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd())) | |
| 390 | + | w.Close() | |
| 391 | + | if e := d.Dispatch(); e != nil { | |
| 392 | + | t.Fatal(e) | |
| 393 | + | } | |
| 394 | + | if e := r.SetReadDeadline(testDeadline()); e != nil { | |
| 395 | + | t.Fatal(e) | |
| 396 | + | } | |
| 397 | + | var b [1]byte | |
| 398 | + | if n, e := r.Read(b[:]); n != 0 || e != io.EOF { | |
| 399 | + | t.Fatalf("unclaimed FD not closed: %d %v", n, e) | |
| 400 | + | } | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | func TestDestroyedProxyRejectsRequests(t *testing.T) { | |
| 404 | + | c, p := pair(t) | |
| 405 | + | d, err := wlturbo.ConnectFromConn(c) | |
| 406 | + | if err != nil { | |
| 407 | + | t.Fatal(err) | |
| 408 | + | } | |
| 409 | + | defer d.Close() | |
| 410 | + | receiveRequest(t, p) | |
| 411 | + | surface := core.NewSurface(d.Context()) | |
| 412 | + | surface.SetID(d.AllocateID()) | |
| 413 | + | d.Context().Register(surface) | |
| 414 | + | if err := surface.Destroy(); err != nil { | |
| 415 | + | t.Fatal(err) | |
| 416 | + | } | |
| 417 | + | receiveRequest(t, p) | |
| 418 | + | if err := surface.Destroy(); err == nil { | |
| 419 | + | t.Fatal("second destroy succeeded") | |
| 420 | + | } | |
| 421 | + | if err := surface.Commit(); err == nil { | |
| 422 | + | t.Fatal("request on destroyed surface succeeded") | |
| 423 | + | } | |
| 424 | + | if _, err := surface.Frame(); err == nil { | |
| 425 | + | t.Fatal("child request on destroyed surface succeeded") | |
| 426 | + | } | |
| 427 | + | p.SetReadDeadline(time.Now().Add(20 * time.Millisecond)) | |
| 428 | + | _, err = p.Read(make([]byte, 32)) | |
| 429 | + | ne, ok := err.(net.Error) | |
| 430 | + | if !ok || !ne.Timeout() { | |
| 431 | + | t.Fatalf("unexpected bytes on wire: %v", err) | |
| 432 | + | } | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | func TestConcurrentDestroySendsOnce(t *testing.T) { | |
| 436 | + | c, p := pair(t) | |
| 437 | + | d, err := wlturbo.ConnectFromConn(c) | |
| 438 | + | if err != nil { | |
| 439 | + | t.Fatal(err) | |
| 440 | + | } | |
| 441 | + | defer d.Close() | |
| 442 | + | receiveRequest(t, p) | |
| 443 | + | const rounds, destroyers, committers = 100, 4, 4 | |
| 444 | + | totalCommits := 0 | |
| 445 | + | for round := 0; round < rounds; round++ { | |
| 446 | + | surface := core.NewSurface(d.Context()) | |
| 447 | + | surface.SetID(d.AllocateID()) | |
| 448 | + | d.Context().Register(surface) | |
| 449 | + | start := make(chan struct{}) | |
| 450 | + | destroyed := make(chan error, destroyers) | |
| 451 | + | committed := make(chan error, committers) | |
| 452 | + | for i := 0; i < destroyers; i++ { | |
| 453 | + | go func() { <-start; destroyed <- surface.Destroy() }() | |
| 454 | + | } | |
| 455 | + | for i := 0; i < committers; i++ { | |
| 456 | + | go func() { <-start; committed <- surface.Commit() }() | |
| 457 | + | } | |
| 458 | + | close(start) | |
| 459 | + | okDestroy, okCommit := 0, 0 | |
| 460 | + | for i := 0; i < destroyers; i++ { | |
| 461 | + | if <-destroyed == nil { | |
| 462 | + | okDestroy++ | |
| 463 | + | } | |
| 464 | + | } | |
| 465 | + | for i := 0; i < committers; i++ { | |
| 466 | + | if <-committed == nil { | |
| 467 | + | okCommit++ | |
| 468 | + | } | |
| 469 | + | } | |
| 470 | + | if okDestroy != 1 { | |
| 471 | + | t.Fatalf("round %d: %d destroys succeeded, want 1", round, okDestroy) | |
| 472 | + | } | |
| 473 | + | totalCommits += okCommit | |
| 474 | + | // Decode every frame: exactly okCommit commits (opcode 6), then one | |
| 475 | + | // destroy (opcode 0) for this surface, and nothing after it. | |
| 476 | + | frames := readFrames(t, p, okCommit+1) | |
| 477 | + | for i, f := range frames { | |
| 478 | + | if f.id != surface.ID() { | |
| 479 | + | t.Fatalf("round %d: frame %d targets object %d", round, i, f.id) | |
| 480 | + | } | |
| 481 | + | last := i == len(frames)-1 | |
| 482 | + | if last && f.opcode != 0 || !last && f.opcode != 6 { | |
| 483 | + | t.Fatalf("round %d: frame %d opcode %d (last=%v)", round, i, f.opcode, last) | |
| 484 | + | } | |
| 485 | + | } | |
| 486 | + | p.SetReadDeadline(time.Now().Add(2 * time.Millisecond)) | |
| 487 | + | if n, err := p.Read(make([]byte, 8)); n != 0 { | |
| 488 | + | t.Fatalf("round %d: request after destroy (%d bytes, %v)", round, n, err) | |
| 489 | + | } | |
| 490 | + | p.SetReadDeadline(time.Time{}) | |
| 491 | + | } | |
| 492 | + | // Ordering is only exercised when some commits won the race. | |
| 493 | + | if totalCommits == 0 { | |
| 494 | + | t.Fatal("no commit succeeded in any round; ordering was not exercised") | |
| 495 | + | } | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | type wireFrame struct { | |
| 499 | + | id uint32 | |
| 500 | + | opcode uint16 | |
| 501 | + | } | |
| 502 | + | ||
| 503 | + | func readFrames(t *testing.T, p *net.UnixConn, n int) []wireFrame { | |
| 504 | + | t.Helper() | |
| 505 | + | p.SetReadDeadline(testDeadline()) | |
| 506 | + | defer p.SetReadDeadline(time.Time{}) | |
| 507 | + | var out []wireFrame | |
| 508 | + | var buf []byte | |
| 509 | + | tmp := make([]byte, 4096) | |
| 510 | + | for len(out) < n { | |
| 511 | + | for len(buf) >= 8 { | |
| 512 | + | size := int(binary.LittleEndian.Uint32(buf[4:8]) >> 16) | |
| 513 | + | if size < 8 || len(buf) < size { | |
| 514 | + | break | |
| 515 | + | } | |
| 516 | + | out = append(out, wireFrame{binary.LittleEndian.Uint32(buf[0:4]), uint16(binary.LittleEndian.Uint32(buf[4:8]))}) | |
| 517 | + | buf = buf[size:] | |
| 518 | + | } | |
| 519 | + | if len(out) >= n { | |
| 520 | + | break | |
| 521 | + | } | |
| 522 | + | m, err := p.Read(tmp) | |
| 523 | + | if err != nil { | |
| 524 | + | t.Fatalf("read frames: got %d of %d: %v", len(out), n, err) | |
| 525 | + | } | |
| 526 | + | buf = append(buf, tmp[:m]...) | |
| 527 | + | } | |
| 528 | + | if len(out) != n || len(buf) != 0 { | |
| 529 | + | t.Fatalf("got %d frames (+%d bytes), want %d", len(out), len(buf), n) | |
| 530 | + | } | |
| 531 | + | return out | |
| 532 | + | } | |
| @@ -0,0 +1,532 @@ | |||||
| 1 | + | //go:build linux | |||
| 2 | + | ||||
| 3 | + | package core_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "encoding/binary" | |||
| 7 | + | "errors" | |||
| 8 | + | "io" | |||
| 9 | + | "net" | |||
| 10 | + | "os" | |||
| 11 | + | "testing" | |||
| 12 | + | "time" | |||
| 13 | + | ||||
| 14 | + | "github.com/bnema/wlturbo" | |||
| 15 | + | "github.com/bnema/wlturbo/protocol/core" | |||
| 16 | + | "golang.org/x/sys/unix" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) { | |||
| 20 | + | t.Helper() | |||
| 21 | + | f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 22 | + | if err != nil { | |||
| 23 | + | t.Fatal(err) | |||
| 24 | + | } | |||
| 25 | + | a := os.NewFile(uintptr(f[0]), "a") | |||
| 26 | + | b := os.NewFile(uintptr(f[1]), "b") | |||
| 27 | + | ca, e := net.FileConn(a) | |||
| 28 | + | a.Close() | |||
| 29 | + | if e != nil { | |||
| 30 | + | t.Fatal(e) | |||
| 31 | + | } | |||
| 32 | + | cb, e := net.FileConn(b) | |||
| 33 | + | b.Close() | |||
| 34 | + | if e != nil { | |||
| 35 | + | t.Fatal(e) | |||
| 36 | + | } | |||
| 37 | + | t.Cleanup(func() { ca.Close(); cb.Close() }) | |||
| 38 | + | return ca.(*net.UnixConn), cb.(*net.UnixConn) | |||
| 39 | + | } | |||
| 40 | + | func frame(id uint32, op uint16, body ...uint32) []byte { | |||
| 41 | + | p := make([]byte, 8+4*len(body)) | |||
| 42 | + | binary.LittleEndian.PutUint32(p, id) | |||
| 43 | + | binary.LittleEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op)) | |||
| 44 | + | for i, v := range body { | |||
| 45 | + | binary.LittleEndian.PutUint32(p[8+i*4:], v) | |||
| 46 | + | } | |||
| 47 | + | return p | |||
| 48 | + | } | |||
| 49 | + | func receiveRequest(t *testing.T, p *net.UnixConn) { | |||
| 50 | + | t.Helper() | |||
| 51 | + | p.SetReadDeadline(testDeadline()) | |||
| 52 | + | b := make([]byte, 256) | |||
| 53 | + | if _, e := p.Read(b); e != nil { | |||
| 54 | + | t.Fatal(e) | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | func testDeadline() (d time.Time) { return time.Now().Add(2 * time.Second) } | |||
| 58 | + | func send(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) { | |||
| 59 | + | t.Helper() | |||
| 60 | + | var oob []byte | |||
| 61 | + | if len(fd) > 0 { | |||
| 62 | + | oob = unix.UnixRights(fd...) | |||
| 63 | + | } | |||
| 64 | + | if _, _, e := p.WriteMsgUnix(msg, oob, nil); e != nil { | |||
| 65 | + | t.Fatal(e) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | func TestGeneratedCoreOverSocketpair(t *testing.T) { | |||
| 69 | + | c, p := pair(t) | |||
| 70 | + | d, err := wlturbo.ConnectFromConn(c) | |||
| 71 | + | if err != nil { | |||
| 72 | + | t.Fatal(err) | |||
| 73 | + | } | |||
| 74 | + | defer d.Close() | |||
| 75 | + | receiveRequest(t, p) // get_registry | |||
| 76 | + | seat := core.NewSeat(d.Context()) | |||
| 77 | + | if err := d.Registry().Bind(1, core.SeatInterface, 1, seat); err != nil { | |||
| 78 | + | t.Fatal(err) | |||
| 79 | + | } | |||
| 80 | + | receiveRequest(t, p) | |||
| 81 | + | kb, err := seat.GetKeyboard() | |||
| 82 | + | if err != nil { | |||
| 83 | + | t.Fatal(err) | |||
| 84 | + | } | |||
| 85 | + | receiveRequest(t, p) | |||
| 86 | + | var key uint32 | |||
| 87 | + | kb.OnKey(func(_, _, k, _ uint32) { key = k }) | |||
| 88 | + | send(t, p, frame(kb.ID(), 3, 1, 2, 42, 1)) | |||
| 89 | + | if err := d.Dispatch(); err != nil { | |||
| 90 | + | t.Fatal(err) | |||
| 91 | + | } | |||
| 92 | + | if key != 42 { | |||
| 93 | + | t.Fatalf("key=%d", key) | |||
| 94 | + | } | |||
| 95 | + | compositor := core.NewCompositor(d.Context()) | |||
| 96 | + | if err := d.Registry().Bind(2, core.CompositorInterface, 1, compositor); err != nil { | |||
| 97 | + | t.Fatal(err) | |||
| 98 | + | } | |||
| 99 | + | receiveRequest(t, p) | |||
| 100 | + | surface, err := compositor.CreateSurface() | |||
| 101 | + | if err != nil { | |||
| 102 | + | t.Fatal(err) | |||
| 103 | + | } | |||
| 104 | + | receiveRequest(t, p) | |||
| 105 | + | callback, err := surface.Frame() | |||
| 106 | + | if err != nil { | |||
| 107 | + | t.Fatal(err) | |||
| 108 | + | } | |||
| 109 | + | receiveRequest(t, p) | |||
| 110 | + | var done uint32 | |||
| 111 | + | callback.OnDone(func(ms uint32) { done = ms }) | |||
| 112 | + | send(t, p, frame(callback.ID(), 0, 123)) | |||
| 113 | + | if err := d.Dispatch(); err != nil { | |||
| 114 | + | t.Fatal(err) | |||
| 115 | + | } | |||
| 116 | + | if done != 123 { | |||
| 117 | + | t.Fatalf("done=%d", done) | |||
| 118 | + | } | |||
| 119 | + | send(t, p, frame(callback.ID(), 0, 124)) | |||
| 120 | + | if err := d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) { | |||
| 121 | + | t.Fatalf("destroyed callback: %v", err) | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | func TestKeymapFDNoHandlerAndMultipleHandlers(t *testing.T) { | |||
| 125 | + | c, p := pair(t) | |||
| 126 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 127 | + | if e != nil { | |||
| 128 | + | t.Fatal(e) | |||
| 129 | + | } | |||
| 130 | + | defer d.Close() | |||
| 131 | + | receiveRequest(t, p) | |||
| 132 | + | kb := core.NewKeyboard(d.Context()) | |||
| 133 | + | kb.SetID(d.AllocateID()) | |||
| 134 | + | d.Context().Register(kb) | |||
| 135 | + | r, w, e := os.Pipe() | |||
| 136 | + | if e != nil { | |||
| 137 | + | t.Fatal(e) | |||
| 138 | + | } | |||
| 139 | + | defer r.Close() | |||
| 140 | + | send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd())) | |||
| 141 | + | w.Close() | |||
| 142 | + | if e := d.Dispatch(); e != nil { | |||
| 143 | + | t.Fatal(e) | |||
| 144 | + | } | |||
| 145 | + | buf := make([]byte, 1) | |||
| 146 | + | if n, e := r.Read(buf); n != 0 || e != io.EOF { | |||
| 147 | + | t.Fatalf("unclaimed FD n=%d err=%v", n, e) | |||
| 148 | + | } | |||
| 149 | + | r2, w2, e := os.Pipe() | |||
| 150 | + | if e != nil { | |||
| 151 | + | t.Fatal(e) | |||
| 152 | + | } | |||
| 153 | + | defer r2.Close() | |||
| 154 | + | received := 0 | |||
| 155 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |||
| 156 | + | if fd == nil { | |||
| 157 | + | t.Fatal("first FD missing") | |||
| 158 | + | } | |||
| 159 | + | n, e := fd.Take() | |||
| 160 | + | if e != nil { | |||
| 161 | + | t.Fatal(e) | |||
| 162 | + | } | |||
| 163 | + | received++ | |||
| 164 | + | unix.Close(n) | |||
| 165 | + | }) | |||
| 166 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |||
| 167 | + | if fd != nil { | |||
| 168 | + | t.Fatal("duplicated owner") | |||
| 169 | + | } | |||
| 170 | + | received++ | |||
| 171 | + | }) | |||
| 172 | + | send(t, p, frame(kb.ID(), 0, 1, 8), int(w2.Fd())) | |||
| 173 | + | w2.Close() | |||
| 174 | + | if e := d.Dispatch(); e != nil { | |||
| 175 | + | t.Fatal(e) | |||
| 176 | + | } | |||
| 177 | + | if received != 2 { | |||
| 178 | + | t.Fatalf("handlers=%d", received) | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 181 | + | func TestBufferReleaseAndOfferChild(t *testing.T) { | |||
| 182 | + | c, p := pair(t) | |||
| 183 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 184 | + | if e != nil { | |||
| 185 | + | t.Fatal(e) | |||
| 186 | + | } | |||
| 187 | + | defer d.Close() | |||
| 188 | + | receiveRequest(t, p) | |||
| 189 | + | b := core.NewBuffer(d.Context()) | |||
| 190 | + | b.SetID(d.AllocateID()) | |||
| 191 | + | d.Context().Register(b) | |||
| 192 | + | releases := 0 | |||
| 193 | + | b.OnRelease(func() { releases++ }) | |||
| 194 | + | send(t, p, frame(b.ID(), 0)) | |||
| 195 | + | if e := d.Dispatch(); e != nil { | |||
| 196 | + | t.Fatal(e) | |||
| 197 | + | } | |||
| 198 | + | if releases != 1 { | |||
| 199 | + | t.Fatal("release not dispatched") | |||
| 200 | + | } | |||
| 201 | + | device := core.NewDataDevice(d.Context()) | |||
| 202 | + | device.SetID(d.AllocateID()) | |||
| 203 | + | d.Context().Register(device) | |||
| 204 | + | var offer *core.DataOffer | |||
| 205 | + | device.OnDataOffer(func(o *core.DataOffer) { offer = o }) | |||
| 206 | + | send(t, p, frame(device.ID(), 0, 0xff000001)) | |||
| 207 | + | if e := d.Dispatch(); e != nil { | |||
| 208 | + | t.Fatal(e) | |||
| 209 | + | } | |||
| 210 | + | if offer == nil || offer.ID() != 0xff000001 { | |||
| 211 | + | t.Fatalf("offer=%v", offer) | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | func TestMissingKeymapDescriptor(t *testing.T) { | |||
| 216 | + | c, p := pair(t) | |||
| 217 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 218 | + | if e != nil { | |||
| 219 | + | t.Fatal(e) | |||
| 220 | + | } | |||
| 221 | + | defer d.Close() | |||
| 222 | + | receiveRequest(t, p) | |||
| 223 | + | kb := core.NewKeyboard(d.Context()) | |||
| 224 | + | kb.SetID(d.AllocateID()) | |||
| 225 | + | d.Context().Register(kb) | |||
| 226 | + | send(t, p, frame(kb.ID(), 0, 1, 8)) | |||
| 227 | + | if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) { | |||
| 228 | + | t.Fatalf("missing FD: %v", e) | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | func TestInterleavedFDAndPlainMessages(t *testing.T) { | |||
| 233 | + | c, p := pair(t) | |||
| 234 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 235 | + | if e != nil { | |||
| 236 | + | t.Fatal(e) | |||
| 237 | + | } | |||
| 238 | + | defer d.Close() | |||
| 239 | + | receiveRequest(t, p) | |||
| 240 | + | kb := core.NewKeyboard(d.Context()) | |||
| 241 | + | kb.SetID(d.AllocateID()) | |||
| 242 | + | d.Context().Register(kb) | |||
| 243 | + | calls := 0 | |||
| 244 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |||
| 245 | + | if fd == nil { | |||
| 246 | + | t.Fatal("FD not assigned") | |||
| 247 | + | } | |||
| 248 | + | _ = fd.Close() | |||
| 249 | + | calls++ | |||
| 250 | + | }) | |||
| 251 | + | kb.OnKey(func(_, _, key, _ uint32) { | |||
| 252 | + | if key != 33 { | |||
| 253 | + | t.Errorf("key=%d", key) | |||
| 254 | + | } | |||
| 255 | + | calls++ | |||
| 256 | + | }) | |||
| 257 | + | r, w, e := os.Pipe() | |||
| 258 | + | if e != nil { | |||
| 259 | + | t.Fatal(e) | |||
| 260 | + | } | |||
| 261 | + | defer r.Close() | |||
| 262 | + | send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd())) | |||
| 263 | + | w.Close() | |||
| 264 | + | send(t, p, frame(kb.ID(), 3, 1, 2, 33, 1)) | |||
| 265 | + | if e := d.Dispatch(); e != nil { | |||
| 266 | + | t.Fatal(e) | |||
| 267 | + | } | |||
| 268 | + | if e := d.Dispatch(); e != nil { | |||
| 269 | + | t.Fatal(e) | |||
| 270 | + | } | |||
| 271 | + | if calls != 2 { | |||
| 272 | + | t.Fatalf("calls=%d", calls) | |||
| 273 | + | } | |||
| 274 | + | } | |||
| 275 | + | func TestMalformedGeneratedPayload(t *testing.T) { | |||
| 276 | + | c, p := pair(t) | |||
| 277 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 278 | + | if e != nil { | |||
| 279 | + | t.Fatal(e) | |||
| 280 | + | } | |||
| 281 | + | defer d.Close() | |||
| 282 | + | receiveRequest(t, p) | |||
| 283 | + | kb := core.NewKeyboard(d.Context()) | |||
| 284 | + | kb.SetID(d.AllocateID()) | |||
| 285 | + | d.Context().Register(kb) | |||
| 286 | + | send(t, p, frame(kb.ID(), 3, 1, 2)) | |||
| 287 | + | if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) { | |||
| 288 | + | t.Fatalf("truncated key: %v", e) | |||
| 289 | + | } | |||
| 290 | + | } | |||
| 291 | + | ||||
| 292 | + | func TestServerChildEventsAfterOffer(t *testing.T) { | |||
| 293 | + | c, p := pair(t) | |||
| 294 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 295 | + | if e != nil { | |||
| 296 | + | t.Fatal(e) | |||
| 297 | + | } | |||
| 298 | + | defer d.Close() | |||
| 299 | + | receiveRequest(t, p) | |||
| 300 | + | device := core.NewDataDevice(d.Context()) | |||
| 301 | + | device.SetID(d.AllocateID()) | |||
| 302 | + | d.Context().Register(device) | |||
| 303 | + | const offerID = 0xff000011 | |||
| 304 | + | var offer *core.DataOffer | |||
| 305 | + | device.OnDataOffer(func(o *core.DataOffer) { offer = o }) | |||
| 306 | + | send(t, p, frame(device.ID(), 0, offerID)) | |||
| 307 | + | if e := d.Dispatch(); e != nil { | |||
| 308 | + | t.Fatal(e) | |||
| 309 | + | } | |||
| 310 | + | if offer == nil { | |||
| 311 | + | t.Fatal("missing concrete child") | |||
| 312 | + | } | |||
| 313 | + | got := "" | |||
| 314 | + | offer.OnOffer(func(mime string) { got = mime }) | |||
| 315 | + | payload := append(frame(offerID, 0), 4, 0, 0, 0, 'a', 'b', 'c', 0) | |||
| 316 | + | binary.LittleEndian.PutUint32(payload[4:], uint32(len(payload))<<16) | |||
| 317 | + | send(t, p, payload) | |||
| 318 | + | if e := d.Dispatch(); e != nil { | |||
| 319 | + | t.Fatal(e) | |||
| 320 | + | } | |||
| 321 | + | if got != "abc" { | |||
| 322 | + | t.Fatalf("typed child event: %q", got) | |||
| 323 | + | } | |||
| 324 | + | } | |||
| 325 | + | ||||
| 326 | + | func TestRegistryBootstrapAnnouncements(t *testing.T) { | |||
| 327 | + | c, p := pair(t) | |||
| 328 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 329 | + | if e != nil { | |||
| 330 | + | t.Fatal(e) | |||
| 331 | + | } | |||
| 332 | + | defer d.Close() | |||
| 333 | + | receiveRequest(t, p) | |||
| 334 | + | var got wlturbo.Global | |||
| 335 | + | d.Registry().AddHandler("wl_compositor", func(r *wlturbo.Registry, n, v uint32) { got, _ = r.FindGlobalByName(n) }) | |||
| 336 | + | name := "wl_compositor" | |||
| 337 | + | body := make([]byte, 4+4+((len(name)+1+3)&^3)+4) | |||
| 338 | + | binary.LittleEndian.PutUint32(body, 17) | |||
| 339 | + | binary.LittleEndian.PutUint32(body[4:], uint32(len(name)+1)) | |||
| 340 | + | copy(body[8:], name) | |||
| 341 | + | binary.LittleEndian.PutUint32(body[len(body)-4:], 7) | |||
| 342 | + | msg := append(frame(d.Registry().ID(), 0), body...) | |||
| 343 | + | binary.LittleEndian.PutUint32(msg[4:], uint32(len(msg))<<16) | |||
| 344 | + | send(t, p, msg) | |||
| 345 | + | if e := d.Dispatch(); e != nil { | |||
| 346 | + | t.Fatal(e) | |||
| 347 | + | } | |||
| 348 | + | if got.Name != 17 || got.Interface != name || got.Version != 7 { | |||
| 349 | + | t.Fatalf("global: %+v", got) | |||
| 350 | + | } | |||
| 351 | + | send(t, p, frame(d.Registry().ID(), 1, 17)) | |||
| 352 | + | if e := d.Dispatch(); e != nil { | |||
| 353 | + | t.Fatal(e) | |||
| 354 | + | } | |||
| 355 | + | if _, ok := d.Registry().FindGlobalByName(17); ok { | |||
| 356 | + | t.Fatal("global not removed") | |||
| 357 | + | } | |||
| 358 | + | send(t, p, frame(d.Registry().ID(), 2)) | |||
| 359 | + | if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrUnknownOpcode) { | |||
| 360 | + | t.Fatalf("unknown registry opcode: %v", e) | |||
| 361 | + | } | |||
| 362 | + | } | |||
| 363 | + | func TestUnclaimedFirstHandlerFDClosed(t *testing.T) { | |||
| 364 | + | c, p := pair(t) | |||
| 365 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 366 | + | if e != nil { | |||
| 367 | + | t.Fatal(e) | |||
| 368 | + | } | |||
| 369 | + | defer d.Close() | |||
| 370 | + | receiveRequest(t, p) | |||
| 371 | + | kb := core.NewKeyboard(d.Context()) | |||
| 372 | + | kb.SetID(d.AllocateID()) | |||
| 373 | + | d.Context().Register(kb) | |||
| 374 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |||
| 375 | + | if fd == nil { | |||
| 376 | + | t.Fatal("missing first owner") | |||
| 377 | + | } | |||
| 378 | + | }) | |||
| 379 | + | kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) { | |||
| 380 | + | if fd != nil { | |||
| 381 | + | t.Fatal("duplicate owner") | |||
| 382 | + | } | |||
| 383 | + | }) | |||
| 384 | + | r, w, e := os.Pipe() | |||
| 385 | + | if e != nil { | |||
| 386 | + | t.Fatal(e) | |||
| 387 | + | } | |||
| 388 | + | defer r.Close() | |||
| 389 | + | send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd())) | |||
| 390 | + | w.Close() | |||
| 391 | + | if e := d.Dispatch(); e != nil { | |||
| 392 | + | t.Fatal(e) | |||
| 393 | + | } | |||
| 394 | + | if e := r.SetReadDeadline(testDeadline()); e != nil { | |||
| 395 | + | t.Fatal(e) | |||
| 396 | + | } | |||
| 397 | + | var b [1]byte | |||
| 398 | + | if n, e := r.Read(b[:]); n != 0 || e != io.EOF { | |||
| 399 | + | t.Fatalf("unclaimed FD not closed: %d %v", n, e) | |||
| 400 | + | } | |||
| 401 | + | } | |||
| 402 | + | ||||
| 403 | + | func TestDestroyedProxyRejectsRequests(t *testing.T) { | |||
| 404 | + | c, p := pair(t) | |||
| 405 | + | d, err := wlturbo.ConnectFromConn(c) | |||
| 406 | + | if err != nil { | |||
| 407 | + | t.Fatal(err) | |||
| 408 | + | } | |||
| 409 | + | defer d.Close() | |||
| 410 | + | receiveRequest(t, p) | |||
| 411 | + | surface := core.NewSurface(d.Context()) | |||
| 412 | + | surface.SetID(d.AllocateID()) | |||
| 413 | + | d.Context().Register(surface) | |||
| 414 | + | if err := surface.Destroy(); err != nil { | |||
| 415 | + | t.Fatal(err) | |||
| 416 | + | } | |||
| 417 | + | receiveRequest(t, p) | |||
| 418 | + | if err := surface.Destroy(); err == nil { | |||
| 419 | + | t.Fatal("second destroy succeeded") | |||
| 420 | + | } | |||
| 421 | + | if err := surface.Commit(); err == nil { | |||
| 422 | + | t.Fatal("request on destroyed surface succeeded") | |||
| 423 | + | } | |||
| 424 | + | if _, err := surface.Frame(); err == nil { | |||
| 425 | + | t.Fatal("child request on destroyed surface succeeded") | |||
| 426 | + | } | |||
| 427 | + | p.SetReadDeadline(time.Now().Add(20 * time.Millisecond)) | |||
| 428 | + | _, err = p.Read(make([]byte, 32)) | |||
| 429 | + | ne, ok := err.(net.Error) | |||
| 430 | + | if !ok || !ne.Timeout() { | |||
| 431 | + | t.Fatalf("unexpected bytes on wire: %v", err) | |||
| 432 | + | } | |||
| 433 | + | } | |||
| 434 | + | ||||
| 435 | + | func TestConcurrentDestroySendsOnce(t *testing.T) { | |||
| 436 | + | c, p := pair(t) | |||
| 437 | + | d, err := wlturbo.ConnectFromConn(c) | |||
| 438 | + | if err != nil { | |||
| 439 | + | t.Fatal(err) | |||
| 440 | + | } | |||
| 441 | + | defer d.Close() | |||
| 442 | + | receiveRequest(t, p) | |||
| 443 | + | const rounds, destroyers, committers = 100, 4, 4 | |||
| 444 | + | totalCommits := 0 | |||
| 445 | + | for round := 0; round < rounds; round++ { | |||
| 446 | + | surface := core.NewSurface(d.Context()) | |||
| 447 | + | surface.SetID(d.AllocateID()) | |||
| 448 | + | d.Context().Register(surface) | |||
| 449 | + | start := make(chan struct{}) | |||
| 450 | + | destroyed := make(chan error, destroyers) | |||
| 451 | + | committed := make(chan error, committers) | |||
| 452 | + | for i := 0; i < destroyers; i++ { | |||
| 453 | + | go func() { <-start; destroyed <- surface.Destroy() }() | |||
| 454 | + | } | |||
| 455 | + | for i := 0; i < committers; i++ { | |||
| 456 | + | go func() { <-start; committed <- surface.Commit() }() | |||
| 457 | + | } | |||
| 458 | + | close(start) | |||
| 459 | + | okDestroy, okCommit := 0, 0 | |||
| 460 | + | for i := 0; i < destroyers; i++ { | |||
| 461 | + | if <-destroyed == nil { | |||
| 462 | + | okDestroy++ | |||
| 463 | + | } | |||
| 464 | + | } | |||
| 465 | + | for i := 0; i < committers; i++ { | |||
| 466 | + | if <-committed == nil { | |||
| 467 | + | okCommit++ | |||
| 468 | + | } | |||
| 469 | + | } | |||
| 470 | + | if okDestroy != 1 { | |||
| 471 | + | t.Fatalf("round %d: %d destroys succeeded, want 1", round, okDestroy) | |||
| 472 | + | } | |||
| 473 | + | totalCommits += okCommit | |||
| 474 | + | // Decode every frame: exactly okCommit commits (opcode 6), then one | |||
| 475 | + | // destroy (opcode 0) for this surface, and nothing after it. | |||
| 476 | + | frames := readFrames(t, p, okCommit+1) | |||
| 477 | + | for i, f := range frames { | |||
| 478 | + | if f.id != surface.ID() { | |||
| 479 | + | t.Fatalf("round %d: frame %d targets object %d", round, i, f.id) | |||
| 480 | + | } | |||
| 481 | + | last := i == len(frames)-1 | |||
| 482 | + | if last && f.opcode != 0 || !last && f.opcode != 6 { | |||
| 483 | + | t.Fatalf("round %d: frame %d opcode %d (last=%v)", round, i, f.opcode, last) | |||
| 484 | + | } | |||
| 485 | + | } | |||
| 486 | + | p.SetReadDeadline(time.Now().Add(2 * time.Millisecond)) | |||
| 487 | + | if n, err := p.Read(make([]byte, 8)); n != 0 { | |||
| 488 | + | t.Fatalf("round %d: request after destroy (%d bytes, %v)", round, n, err) | |||
| 489 | + | } | |||
| 490 | + | p.SetReadDeadline(time.Time{}) | |||
| 491 | + | } | |||
| 492 | + | // Ordering is only exercised when some commits won the race. | |||
| 493 | + | if totalCommits == 0 { | |||
| 494 | + | t.Fatal("no commit succeeded in any round; ordering was not exercised") | |||
| 495 | + | } | |||
| 496 | + | } | |||
| 497 | + | ||||
| 498 | + | type wireFrame struct { | |||
| 499 | + | id uint32 | |||
| 500 | + | opcode uint16 | |||
| 501 | + | } | |||
| 502 | + | ||||
| 503 | + | func readFrames(t *testing.T, p *net.UnixConn, n int) []wireFrame { | |||
| 504 | + | t.Helper() | |||
| 505 | + | p.SetReadDeadline(testDeadline()) | |||
| 506 | + | defer p.SetReadDeadline(time.Time{}) | |||
| 507 | + | var out []wireFrame | |||
| 508 | + | var buf []byte | |||
| 509 | + | tmp := make([]byte, 4096) | |||
| 510 | + | for len(out) < n { | |||
| 511 | + | for len(buf) >= 8 { | |||
| 512 | + | size := int(binary.LittleEndian.Uint32(buf[4:8]) >> 16) | |||
| 513 | + | if size < 8 || len(buf) < size { | |||
| 514 | + | break | |||
| 515 | + | } | |||
| 516 | + | out = append(out, wireFrame{binary.LittleEndian.Uint32(buf[0:4]), uint16(binary.LittleEndian.Uint32(buf[4:8]))}) | |||
| 517 | + | buf = buf[size:] | |||
| 518 | + | } | |||
| 519 | + | if len(out) >= n { | |||
| 520 | + | break | |||
| 521 | + | } | |||
| 522 | + | m, err := p.Read(tmp) | |||
| 523 | + | if err != nil { | |||
| 524 | + | t.Fatalf("read frames: got %d of %d: %v", len(out), n, err) | |||
| 525 | + | } | |||
| 526 | + | buf = append(buf, tmp[:m]...) | |||
| 527 | + | } | |||
| 528 | + | if len(out) != n || len(buf) != 0 { | |||
| 529 | + | t.Fatalf("got %d frames (+%d bytes), want %d", len(out), len(buf), n) | |||
| 530 | + | } | |||
| 531 | + | return out | |||
| 532 | + | } | |||
A protocol/core/doc.go +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | // Package core contains canonical Wayland core protocol bindings generated from | |
| 2 | + | // pinned wayland.xml. Bind globals with display.Registry().Bind and the | |
| 3 | + | // corresponding New* constructor. Client-created and server-created children | |
| 4 | + | // are registered with their concrete generated types; never replace them with | |
| 5 | + | // transport wrappers. On* handlers receive typed values. Received OwnedFDs | |
| 6 | + | // are closed after the event unless a handler calls Take; request FDs are | |
| 7 | + | // transferred (and closed locally) only after a complete successful send. | |
| 8 | + | package core | |
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | // Package core contains canonical Wayland core protocol bindings generated from | |||
| 2 | + | // pinned wayland.xml. Bind globals with display.Registry().Bind and the | |||
| 3 | + | // corresponding New* constructor. Client-created and server-created children | |||
| 4 | + | // are registered with their concrete generated types; never replace them with | |||
| 5 | + | // transport wrappers. On* handlers receive typed values. Received OwnedFDs | |||
| 6 | + | // are closed after the event unless a handler calls Take; request FDs are | |||
| 7 | + | // transferred (and closed locally) only after a complete successful send. | |||
| 8 | + | package core | |||
A protocol/core/wayland_generated.go +4216 -0 large diff (4216 lines changed): collapsed
A protocol/generate.go +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | package protocol | |
| 2 | + | ||
| 3 | + | //go:generate go run ../cmd/wlturbo-scanner -p core -o core/wayland_generated.go wayland.xml | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | package protocol | |||
| 2 | + | ||||
| 3 | + | //go:generate go run ../cmd/wlturbo-scanner -p core -o core/wayland_generated.go wayland.xml | |||
A protocol/wayland.xml +3442 -0 large diff (3442 lines changed): collapsed
M scm_linux.go +77 -49
| @@ -6,9 +6,13 @@ package wlturbo | |||
| 6 | 6 | import ( | |
| 7 | 7 | "errors" | |
| 8 | 8 | "fmt" | |
| 9 | - | "golang.org/x/sys/unix" | |
| 9 | + | "io" | |
| 10 | + | "net" | |
| 10 | 11 | "sync" | |
| 11 | 12 | "syscall" | |
| 13 | + | "unsafe" | |
| 14 | + | ||
| 15 | + | "golang.org/x/sys/unix" | |
| 12 | 16 | ) | |
| 13 | 17 | ||
| 14 | 18 | // maxRecvFDs bounds how many ancillary file descriptors a single read accepts. | |
| @@ -16,10 +20,12 @@ import ( | |||
| 16 | 20 | // instead of silently dropping descriptors. | |
| 17 | 21 | const maxRecvFDs = 64 | |
| 18 | 22 | ||
| 23 | + | type controlBuffer struct{ data []byte } | |
| 24 | + | ||
| 19 | 25 | // controlBufferPool provides control-message buffers sized for maxRecvFDs. | |
| 20 | 26 | var controlBufferPool = sync.Pool{ | |
| 21 | 27 | New: func() interface{} { | |
| 22 | - | return make([]byte, unix.CmsgSpace(maxRecvFDs*4)) | |
| 28 | + | return &controlBuffer{data: make([]byte, unix.CmsgSpace(maxRecvFDs*4))} | |
| 23 | 29 | }, | |
| 24 | 30 | } | |
| 25 | 31 | ||
| @@ -40,8 +46,9 @@ func (d *Display) readChunk() (int, error) { | |||
| 40 | 46 | return n, err | |
| 41 | 47 | } | |
| 42 | 48 | ||
| 43 | - | oob := controlBufferPool.Get().([]byte) | |
| 44 | - | defer controlBufferPool.Put(oob) | |
| 49 | + | buffer := controlBufferPool.Get().(*controlBuffer) | |
| 50 | + | defer controlBufferPool.Put(buffer) | |
| 51 | + | oob := buffer.data | |
| 45 | 52 | ||
| 46 | 53 | n, oobn, flags, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob) | |
| 47 | 54 | if n < 0 { | |
| @@ -51,12 +58,12 @@ func (d *Display) readChunk() (int, error) { | |||
| 51 | 58 | if oobn > 0 { | |
| 52 | 59 | fds, perr := parseFDs(oob[:oobn]) | |
| 53 | 60 | d.queueFDs(fds) | |
| 54 | - | if perr != nil && err == nil { | |
| 55 | - | err = perr | |
| 61 | + | if perr != nil { | |
| 62 | + | err = &ProtocolError{Kind: "malformed_control", Err: perr} | |
| 56 | 63 | } | |
| 57 | 64 | } | |
| 58 | 65 | ||
| 59 | - | if flags&unix.MSG_CTRUNC != 0 && err == nil { | |
| 66 | + | if flags&unix.MSG_CTRUNC != 0 { | |
| 60 | 67 | // Descriptors were dropped by the kernel, so the connection can no | |
| 61 | 68 | // longer be demarshalled correctly: treat it as a protocol failure so | |
| 62 | 69 | // the caller closes the descriptors that did arrive. | |
| @@ -68,55 +75,54 @@ func (d *Display) readChunk() (int, error) { | |||
| 68 | 75 | // parseFDs extracts the file descriptors carried by socket control messages. | |
| 69 | 76 | // Descriptors parsed before an error are returned so the caller can close them. | |
| 70 | 77 | func parseFDs(oob []byte) ([]int, error) { | |
| 71 | - | scms, err := syscall.ParseSocketControlMessage(oob) | |
| 72 | - | if err != nil { | |
| 73 | - | return nil, fmt.Errorf("parse control message: %w", err) | |
| 74 | - | } | |
| 75 | - | ||
| 76 | 78 | var fds []int | |
| 77 | - | for i := range scms { | |
| 78 | - | scm := &scms[i] | |
| 79 | - | if scm.Header.Type != syscall.SCM_RIGHTS { | |
| 80 | - | continue | |
| 79 | + | for len(oob) > 0 { | |
| 80 | + | if len(oob) < unix.SizeofCmsghdr { | |
| 81 | + | return fds, fmt.Errorf("short control header") | |
| 81 | 82 | } | |
| 82 | - | parsed, err := syscall.ParseUnixRights(scm) | |
| 83 | - | if err != nil { | |
| 84 | - | return fds, fmt.Errorf("parse unix rights: %w", err) | |
| 83 | + | var hdr unix.Cmsghdr | |
| 84 | + | // Parse each header independently: a malformed later header must not | |
| 85 | + | // discard rights already decoded from earlier messages. | |
| 86 | + | if err := readControlHeader(oob, &hdr); err != nil { | |
| 87 | + | return fds, err | |
| 88 | + | } | |
| 89 | + | length := int(hdr.Len) | |
| 90 | + | if length < unix.SizeofCmsghdr || length > len(oob) { | |
| 91 | + | return fds, fmt.Errorf("invalid control length %d", length) | |
| 92 | + | } | |
| 93 | + | if hdr.Level == unix.SOL_SOCKET && hdr.Type == unix.SCM_RIGHTS { | |
| 94 | + | message := syscall.SocketControlMessage{Header: syscall.Cmsghdr{Len: uint64(length), Level: int32(hdr.Level), Type: int32(hdr.Type)}, Data: oob[unix.SizeofCmsghdr:length]} | |
| 95 | + | parsed, err := syscall.ParseUnixRights(&message) | |
| 96 | + | if err != nil { | |
| 97 | + | return fds, fmt.Errorf("parse unix rights: %w", err) | |
| 98 | + | } | |
| 99 | + | fds = append(fds, parsed...) | |
| 85 | 100 | } | |
| 86 | - | fds = append(fds, parsed...) | |
| 101 | + | step := unix.CmsgSpace(length - unix.SizeofCmsghdr) | |
| 102 | + | if step > len(oob) { | |
| 103 | + | break | |
| 104 | + | } // final control message may omit trailing padding | |
| 105 | + | oob = oob[step:] | |
| 87 | 106 | } | |
| 88 | 107 | return fds, nil | |
| 89 | 108 | } | |
| 90 | 109 | ||
| 91 | - | // queueFDs appends descriptors to this connection's pending list. | |
| 92 | - | func (d *Display) queueFDs(fds []int) { | |
| 93 | - | if len(fds) == 0 { | |
| 94 | - | return | |
| 110 | + | func readControlHeader(data []byte, hdr *unix.Cmsghdr) error { | |
| 111 | + | // Linux cmsghdr has a native-endian size_t followed by two int32s. | |
| 112 | + | if len(data) < unix.SizeofCmsghdr { | |
| 113 | + | return fmt.Errorf("short control header") | |
| 95 | 114 | } | |
| 96 | - | d.fdMu.Lock() | |
| 97 | - | d.pendingFDs = append(d.pendingFDs, fds...) | |
| 98 | - | d.fdMu.Unlock() | |
| 115 | + | *hdr = *(*unix.Cmsghdr)(unsafe.Pointer(&data[0])) | |
| 116 | + | return nil | |
| 99 | 117 | } | |
| 100 | 118 | ||
| 101 | - | // nextFD removes and returns the oldest pending descriptor. | |
| 102 | - | func (d *Display) nextFD() (int, bool) { | |
| 103 | - | d.fdMu.Lock() | |
| 104 | - | defer d.fdMu.Unlock() | |
| 105 | - | if len(d.pendingFDs) == 0 { | |
| 106 | - | return -1, false | |
| 107 | - | } | |
| 108 | - | fd := d.pendingFDs[0] | |
| 109 | - | d.pendingFDs = d.pendingFDs[1:] | |
| 110 | - | return fd, true | |
| 111 | - | } | |
| 119 | + | // queueFDs is called only by the active Dispatch reader. | |
| 120 | + | func (d *Display) queueFDs(fds []int) { d.pendingFDs = append(d.pendingFDs, fds...) } | |
| 112 | 121 | ||
| 113 | - | // closePendingFDs closes every descriptor this connection still holds. | |
| 122 | + | // closePendingFDs is called under recvMu (including during shutdown). | |
| 114 | 123 | func (d *Display) closePendingFDs() { | |
| 115 | - | d.fdMu.Lock() | |
| 116 | 124 | fds := d.pendingFDs | |
| 117 | 125 | d.pendingFDs = nil | |
| 118 | - | d.fdMu.Unlock() | |
| 119 | - | ||
| 120 | 126 | for _, fd := range fds { | |
| 121 | 127 | if fd >= 0 { | |
| 122 | 128 | _ = unix.Close(fd) | |
| @@ -124,20 +130,42 @@ func (d *Display) closePendingFDs() { | |||
| 124 | 130 | } | |
| 125 | 131 | } | |
| 126 | 132 | ||
| 133 | + | // CloseSentFD releases a descriptor after a complete successful Wayland send. | |
| 134 | + | func CloseSentFD(fd int) error { return unix.Close(fd) } | |
| 135 | + | ||
| 127 | 136 | // sendmsgWithFDs sends a message, attaching file descriptors when present. | |
| 128 | - | func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error { | |
| 137 | + | // A non-nil guard runs under the send lock, so its checks and the write are | |
| 138 | + | // ordered atomically with every other request on the connection. | |
| 139 | + | func (d *Display) sendmsgWithFDs(buf []byte, fds []int, guard func() error) error { | |
| 129 | 140 | d.sendMu.Lock() | |
| 130 | 141 | defer d.sendMu.Unlock() | |
| 131 | 142 | ||
| 132 | - | if len(fds) == 0 { | |
| 133 | - | _, err := d.conn.Write(buf) | |
| 134 | - | return err | |
| 143 | + | if d.closed.Load() { | |
| 144 | + | return net.ErrClosed | |
| 135 | 145 | } | |
| 136 | - | if d.unix == nil { | |
| 146 | + | if len(fds) > 0 && d.unix == nil { | |
| 137 | 147 | return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection") | |
| 138 | 148 | } | |
| 139 | - | ||
| 140 | - | _, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil) | |
| 149 | + | if guard != nil { | |
| 150 | + | if err := guard(); err != nil { | |
| 151 | + | return err | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | var n int | |
| 155 | + | var err error | |
| 156 | + | if len(fds) == 0 { | |
| 157 | + | n, err = d.conn.Write(buf) | |
| 158 | + | } else { | |
| 159 | + | n, _, err = d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil) | |
| 160 | + | } | |
| 161 | + | if err == nil && n != len(buf) { | |
| 162 | + | err = io.ErrShortWrite | |
| 163 | + | } | |
| 164 | + | if err != nil { | |
| 165 | + | // A failed or partial write desynchronizes the stream: shut the socket | |
| 166 | + | // down so every later send and receive fails instead of corrupting it. | |
| 167 | + | _ = d.conn.Close() | |
| 168 | + | } | |
| 141 | 169 | return err | |
| 142 | 170 | } | |
| 143 | 171 | ||
| @@ -6,9 +6,13 @@ package wlturbo | |||||
| 6 | import ( | 6 | import ( | ||
| 7 | "errors" | 7 | "errors" | ||
| 8 | "fmt" | 8 | "fmt" | ||
| 9 | - | "golang.org/x/sys/unix" | 9 | + | "io" |
| 10 | + | "net" | |||
| 10 | "sync" | 11 | "sync" | ||
| 11 | "syscall" | 12 | "syscall" | ||
| 13 | + | "unsafe" | |||
| 14 | + | ||||
| 15 | + | "golang.org/x/sys/unix" | |||
| 12 | ) | 16 | ) | ||
| 13 | 17 | ||||
| 14 | // maxRecvFDs bounds how many ancillary file descriptors a single read accepts. | 18 | // maxRecvFDs bounds how many ancillary file descriptors a single read accepts. | ||
| @@ -16,10 +20,12 @@ import ( | |||||
| 16 | // instead of silently dropping descriptors. | 20 | // instead of silently dropping descriptors. | ||
| 17 | const maxRecvFDs = 64 | 21 | const maxRecvFDs = 64 | ||
| 18 | 22 | ||||
| 23 | + | type controlBuffer struct{ data []byte } | |||
| 24 | + | ||||
| 19 | // controlBufferPool provides control-message buffers sized for maxRecvFDs. | 25 | // controlBufferPool provides control-message buffers sized for maxRecvFDs. | ||
| 20 | var controlBufferPool = sync.Pool{ | 26 | var controlBufferPool = sync.Pool{ | ||
| 21 | New: func() interface{} { | 27 | New: func() interface{} { | ||
| 22 | - | return make([]byte, unix.CmsgSpace(maxRecvFDs*4)) | 28 | + | return &controlBuffer{data: make([]byte, unix.CmsgSpace(maxRecvFDs*4))} |
| 23 | }, | 29 | }, | ||
| 24 | } | 30 | } | ||
| 25 | 31 | ||||
| @@ -40,8 +46,9 @@ func (d *Display) readChunk() (int, error) { | |||||
| 40 | return n, err | 46 | return n, err | ||
| 41 | } | 47 | } | ||
| 42 | 48 | ||||
| 43 | - | oob := controlBufferPool.Get().([]byte) | 49 | + | buffer := controlBufferPool.Get().(*controlBuffer) |
| 44 | - | defer controlBufferPool.Put(oob) | 50 | + | defer controlBufferPool.Put(buffer) |
| 51 | + | oob := buffer.data | |||
| 45 | 52 | ||||
| 46 | n, oobn, flags, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob) | 53 | n, oobn, flags, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob) | ||
| 47 | if n < 0 { | 54 | if n < 0 { | ||
| @@ -51,12 +58,12 @@ func (d *Display) readChunk() (int, error) { | |||||
| 51 | if oobn > 0 { | 58 | if oobn > 0 { | ||
| 52 | fds, perr := parseFDs(oob[:oobn]) | 59 | fds, perr := parseFDs(oob[:oobn]) | ||
| 53 | d.queueFDs(fds) | 60 | d.queueFDs(fds) | ||
| 54 | - | if perr != nil && err == nil { | 61 | + | if perr != nil { |
| 55 | - | err = perr | 62 | + | err = &ProtocolError{Kind: "malformed_control", Err: perr} |
| 56 | } | 63 | } | ||
| 57 | } | 64 | } | ||
| 58 | 65 | ||||
| 59 | - | if flags&unix.MSG_CTRUNC != 0 && err == nil { | 66 | + | if flags&unix.MSG_CTRUNC != 0 { |
| 60 | // Descriptors were dropped by the kernel, so the connection can no | 67 | // Descriptors were dropped by the kernel, so the connection can no | ||
| 61 | // longer be demarshalled correctly: treat it as a protocol failure so | 68 | // longer be demarshalled correctly: treat it as a protocol failure so | ||
| 62 | // the caller closes the descriptors that did arrive. | 69 | // the caller closes the descriptors that did arrive. | ||
| @@ -68,55 +75,54 @@ func (d *Display) readChunk() (int, error) { | |||||
| 68 | // parseFDs extracts the file descriptors carried by socket control messages. | 75 | // parseFDs extracts the file descriptors carried by socket control messages. | ||
| 69 | // Descriptors parsed before an error are returned so the caller can close them. | 76 | // Descriptors parsed before an error are returned so the caller can close them. | ||
| 70 | func parseFDs(oob []byte) ([]int, error) { | 77 | func parseFDs(oob []byte) ([]int, error) { | ||
| 71 | - | scms, err := syscall.ParseSocketControlMessage(oob) | |||
| 72 | - | if err != nil { | |||
| 73 | - | return nil, fmt.Errorf("parse control message: %w", err) | |||
| 74 | - | } | |||
| 75 | - | ||||
| 76 | var fds []int | 78 | var fds []int | ||
| 77 | - | for i := range scms { | 79 | + | for len(oob) > 0 { |
| 78 | - | scm := &scms[i] | 80 | + | if len(oob) < unix.SizeofCmsghdr { |
| 79 | - | if scm.Header.Type != syscall.SCM_RIGHTS { | 81 | + | return fds, fmt.Errorf("short control header") |
| 80 | - | continue | |||
| 81 | } | 82 | } | ||
| 82 | - | parsed, err := syscall.ParseUnixRights(scm) | 83 | + | var hdr unix.Cmsghdr |
| 83 | - | if err != nil { | 84 | + | // Parse each header independently: a malformed later header must not |
| 84 | - | return fds, fmt.Errorf("parse unix rights: %w", err) | 85 | + | // discard rights already decoded from earlier messages. |
| 86 | + | if err := readControlHeader(oob, &hdr); err != nil { | |||
| 87 | + | return fds, err | |||
| 88 | + | } | |||
| 89 | + | length := int(hdr.Len) | |||
| 90 | + | if length < unix.SizeofCmsghdr || length > len(oob) { | |||
| 91 | + | return fds, fmt.Errorf("invalid control length %d", length) | |||
| 92 | + | } | |||
| 93 | + | if hdr.Level == unix.SOL_SOCKET && hdr.Type == unix.SCM_RIGHTS { | |||
| 94 | + | message := syscall.SocketControlMessage{Header: syscall.Cmsghdr{Len: uint64(length), Level: int32(hdr.Level), Type: int32(hdr.Type)}, Data: oob[unix.SizeofCmsghdr:length]} | |||
| 95 | + | parsed, err := syscall.ParseUnixRights(&message) | |||
| 96 | + | if err != nil { | |||
| 97 | + | return fds, fmt.Errorf("parse unix rights: %w", err) | |||
| 98 | + | } | |||
| 99 | + | fds = append(fds, parsed...) | |||
| 85 | } | 100 | } | ||
| 86 | - | fds = append(fds, parsed...) | 101 | + | step := unix.CmsgSpace(length - unix.SizeofCmsghdr) |
| 102 | + | if step > len(oob) { | |||
| 103 | + | break | |||
| 104 | + | } // final control message may omit trailing padding | |||
| 105 | + | oob = oob[step:] | |||
| 87 | } | 106 | } | ||
| 88 | return fds, nil | 107 | return fds, nil | ||
| 89 | } | 108 | } | ||
| 90 | 109 | ||||
| 91 | - | // queueFDs appends descriptors to this connection's pending list. | 110 | + | func readControlHeader(data []byte, hdr *unix.Cmsghdr) error { |
| 92 | - | func (d *Display) queueFDs(fds []int) { | 111 | + | // Linux cmsghdr has a native-endian size_t followed by two int32s. |
| 93 | - | if len(fds) == 0 { | 112 | + | if len(data) < unix.SizeofCmsghdr { |
| 94 | - | return | 113 | + | return fmt.Errorf("short control header") |
| 95 | } | 114 | } | ||
| 96 | - | d.fdMu.Lock() | 115 | + | *hdr = *(*unix.Cmsghdr)(unsafe.Pointer(&data[0])) |
| 97 | - | d.pendingFDs = append(d.pendingFDs, fds...) | 116 | + | return nil |
| 98 | - | d.fdMu.Unlock() | |||
| 99 | } | 117 | } | ||
| 100 | 118 | ||||
| 101 | - | // nextFD removes and returns the oldest pending descriptor. | 119 | + | // queueFDs is called only by the active Dispatch reader. |
| 102 | - | func (d *Display) nextFD() (int, bool) { | 120 | + | func (d *Display) queueFDs(fds []int) { d.pendingFDs = append(d.pendingFDs, fds...) } |
| 103 | - | d.fdMu.Lock() | |||
| 104 | - | defer d.fdMu.Unlock() | |||
| 105 | - | if len(d.pendingFDs) == 0 { | |||
| 106 | - | return -1, false | |||
| 107 | - | } | |||
| 108 | - | fd := d.pendingFDs[0] | |||
| 109 | - | d.pendingFDs = d.pendingFDs[1:] | |||
| 110 | - | return fd, true | |||
| 111 | - | } | |||
| 112 | 121 | ||||
| 113 | - | // closePendingFDs closes every descriptor this connection still holds. | 122 | + | // closePendingFDs is called under recvMu (including during shutdown). |
| 114 | func (d *Display) closePendingFDs() { | 123 | func (d *Display) closePendingFDs() { | ||
| 115 | - | d.fdMu.Lock() | |||
| 116 | fds := d.pendingFDs | 124 | fds := d.pendingFDs | ||
| 117 | d.pendingFDs = nil | 125 | d.pendingFDs = nil | ||
| 118 | - | d.fdMu.Unlock() | |||
| 119 | - | ||||
| 120 | for _, fd := range fds { | 126 | for _, fd := range fds { | ||
| 121 | if fd >= 0 { | 127 | if fd >= 0 { | ||
| 122 | _ = unix.Close(fd) | 128 | _ = unix.Close(fd) | ||
| @@ -124,20 +130,42 @@ func (d *Display) closePendingFDs() { | |||||
| 124 | } | 130 | } | ||
| 125 | } | 131 | } | ||
| 126 | 132 | ||||
| 133 | + | // CloseSentFD releases a descriptor after a complete successful Wayland send. | |||
| 134 | + | func CloseSentFD(fd int) error { return unix.Close(fd) } | |||
| 135 | + | ||||
| 127 | // sendmsgWithFDs sends a message, attaching file descriptors when present. | 136 | // sendmsgWithFDs sends a message, attaching file descriptors when present. | ||
| 128 | - | func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error { | 137 | + | // A non-nil guard runs under the send lock, so its checks and the write are |
| 138 | + | // ordered atomically with every other request on the connection. | |||
| 139 | + | func (d *Display) sendmsgWithFDs(buf []byte, fds []int, guard func() error) error { | |||
| 129 | d.sendMu.Lock() | 140 | d.sendMu.Lock() | ||
| 130 | defer d.sendMu.Unlock() | 141 | defer d.sendMu.Unlock() | ||
| 131 | 142 | ||||
| 132 | - | if len(fds) == 0 { | 143 | + | if d.closed.Load() { |
| 133 | - | _, err := d.conn.Write(buf) | 144 | + | return net.ErrClosed |
| 134 | - | return err | |||
| 135 | } | 145 | } | ||
| 136 | - | if d.unix == nil { | 146 | + | if len(fds) > 0 && d.unix == nil { |
| 137 | return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection") | 147 | return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection") | ||
| 138 | } | 148 | } | ||
| 139 | - | 149 | + | if guard != nil { | |
| 140 | - | _, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil) | 150 | + | if err := guard(); err != nil { |
| 151 | + | return err | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | var n int | |||
| 155 | + | var err error | |||
| 156 | + | if len(fds) == 0 { | |||
| 157 | + | n, err = d.conn.Write(buf) | |||
| 158 | + | } else { | |||
| 159 | + | n, _, err = d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil) | |||
| 160 | + | } | |||
| 161 | + | if err == nil && n != len(buf) { | |||
| 162 | + | err = io.ErrShortWrite | |||
| 163 | + | } | |||
| 164 | + | if err != nil { | |||
| 165 | + | // A failed or partial write desynchronizes the stream: shut the socket | |||
| 166 | + | // down so every later send and receive fails instead of corrupting it. | |||
| 167 | + | _ = d.conn.Close() | |||
| 168 | + | } | |||
| 141 | return err | 169 | return err | ||
| 142 | } | 170 | } | ||
| 143 | 171 | ||||
M shm.go +0 -1
| @@ -7,7 +7,6 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | // ShmPool represents a shared memory pool | |
| 9 | 9 | type ShmPool struct { | |
| 10 | - | id uint32 | |
| 11 | 10 | fd int | |
| 12 | 11 | size int | |
| 13 | 12 | data []byte | |
| @@ -7,7 +7,6 @@ import ( | |||||
| 7 | 7 | ||||
| 8 | // ShmPool represents a shared memory pool | 8 | // ShmPool represents a shared memory pool | ||
| 9 | type ShmPool struct { | 9 | type ShmPool struct { | ||
| 10 | - | id uint32 | |||
| 11 | fd int | 10 | fd int | ||
| 12 | size int | 11 | size int | ||
| 13 | data []byte | 12 | data []byte | ||
A signature.go +50 -0
| @@ -0,0 +1,50 @@ | |||
| 1 | + | package wlturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "fmt" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // validateEventBody rejects truncated, unterminated and trailing wire values | |
| 9 | + | // before generated code decodes or registers any children. | |
| 10 | + | func validateEventBody(sig string, body []byte) error { | |
| 11 | + | pos := 0 | |
| 12 | + | for len(sig) > 0 { | |
| 13 | + | end := 0 | |
| 14 | + | for end < len(sig) && sig[end] != ',' { | |
| 15 | + | end++ | |
| 16 | + | } | |
| 17 | + | kind := sig[:end] | |
| 18 | + | if end == len(sig) { | |
| 19 | + | return fmt.Errorf("invalid signature %q", sig) | |
| 20 | + | } | |
| 21 | + | sig = sig[end+1:] | |
| 22 | + | if kind == "fd" { | |
| 23 | + | continue | |
| 24 | + | } | |
| 25 | + | if len(body)-pos < 4 { | |
| 26 | + | return ErrMalformedFrame | |
| 27 | + | } | |
| 28 | + | switch kind { | |
| 29 | + | case "int", "uint", "fixed", "object", "new_id": | |
| 30 | + | pos += 4 | |
| 31 | + | case "string", "array": | |
| 32 | + | n := uint64(binary.LittleEndian.Uint32(body[pos:])) | |
| 33 | + | pos += 4 | |
| 34 | + | padded := (n + 3) &^ uint64(3) | |
| 35 | + | if padded > uint64(len(body)-pos) { | |
| 36 | + | return ErrMalformedFrame | |
| 37 | + | } | |
| 38 | + | if kind == "string" && n > 0 && body[pos+int(n)-1] != 0 { | |
| 39 | + | return ErrMalformedFrame | |
| 40 | + | } | |
| 41 | + | pos += int(padded) | |
| 42 | + | default: | |
| 43 | + | return fmt.Errorf("invalid argument kind %q: %w", kind, ErrMalformedFrame) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | if pos != len(body) { | |
| 47 | + | return ErrMalformedFrame | |
| 48 | + | } | |
| 49 | + | return nil | |
| 50 | + | } | |
| @@ -0,0 +1,50 @@ | |||||
| 1 | + | package wlturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "fmt" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // validateEventBody rejects truncated, unterminated and trailing wire values | |||
| 9 | + | // before generated code decodes or registers any children. | |||
| 10 | + | func validateEventBody(sig string, body []byte) error { | |||
| 11 | + | pos := 0 | |||
| 12 | + | for len(sig) > 0 { | |||
| 13 | + | end := 0 | |||
| 14 | + | for end < len(sig) && sig[end] != ',' { | |||
| 15 | + | end++ | |||
| 16 | + | } | |||
| 17 | + | kind := sig[:end] | |||
| 18 | + | if end == len(sig) { | |||
| 19 | + | return fmt.Errorf("invalid signature %q", sig) | |||
| 20 | + | } | |||
| 21 | + | sig = sig[end+1:] | |||
| 22 | + | if kind == "fd" { | |||
| 23 | + | continue | |||
| 24 | + | } | |||
| 25 | + | if len(body)-pos < 4 { | |||
| 26 | + | return ErrMalformedFrame | |||
| 27 | + | } | |||
| 28 | + | switch kind { | |||
| 29 | + | case "int", "uint", "fixed", "object", "new_id": | |||
| 30 | + | pos += 4 | |||
| 31 | + | case "string", "array": | |||
| 32 | + | n := uint64(binary.LittleEndian.Uint32(body[pos:])) | |||
| 33 | + | pos += 4 | |||
| 34 | + | padded := (n + 3) &^ uint64(3) | |||
| 35 | + | if padded > uint64(len(body)-pos) { | |||
| 36 | + | return ErrMalformedFrame | |||
| 37 | + | } | |||
| 38 | + | if kind == "string" && n > 0 && body[pos+int(n)-1] != 0 { | |||
| 39 | + | return ErrMalformedFrame | |||
| 40 | + | } | |||
| 41 | + | pos += int(padded) | |||
| 42 | + | default: | |||
| 43 | + | return fmt.Errorf("invalid argument kind %q: %w", kind, ErrMalformedFrame) | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | if pos != len(body) { | |||
| 47 | + | return ErrMalformedFrame | |||
| 48 | + | } | |||
| 49 | + | return nil | |||
| 50 | + | } | |||
M transport_test.go +46 -5
| @@ -92,6 +92,13 @@ func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) { | |||
| 92 | 92 | BaseProxy: BaseProxy{id: 7, context: d.context}, | |
| 93 | 93 | records: &records, | |
| 94 | 94 | }) | |
| 95 | + | for _, opcode := range []uint16{3, 4, 5} { | |
| 96 | + | signature := "uint," | |
| 97 | + | if opcode == 3 { | |
| 98 | + | signature = "uint,uint," | |
| 99 | + | } | |
| 100 | + | d.RegisterEventSignature(7, opcode, signature) | |
| 101 | + | } | |
| 95 | 102 | return d, &records | |
| 96 | 103 | } | |
| 97 | 104 | ||
| @@ -132,7 +139,7 @@ func TestDisplayDispatch_Fragmented(t *testing.T) { | |||
| 132 | 139 | } | |
| 133 | 140 | ||
| 134 | 141 | func TestDisplayDispatch_Coalesced(t *testing.T) { | |
| 135 | - | first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14}) | |
| 142 | + | first := message(7, 4, []byte{0x11, 0x12, 0x13, 0x14}) | |
| 136 | 143 | second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24}) | |
| 137 | 144 | ||
| 138 | 145 | d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)}) | |
| @@ -146,7 +153,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) { | |||
| 146 | 153 | } | |
| 147 | 154 | ||
| 148 | 155 | want := []EventRecord{ | |
| 149 | - | {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}}, | |
| 156 | + | {Object: 7, Opcode: 4, Body: []byte{0x11, 0x12, 0x13, 0x14}}, | |
| 150 | 157 | {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}}, | |
| 151 | 158 | } | |
| 152 | 159 | if !reflect.DeepEqual(*records, want) { | |
| @@ -157,7 +164,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) { | |||
| 157 | 164 | func TestDisplayDispatch_HalfCoalesced(t *testing.T) { | |
| 158 | 165 | // A read boundary that ends between the header and the body of the second | |
| 159 | 166 | // message must not be mistaken for a frame boundary. | |
| 160 | - | whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}), | |
| 167 | + | whole := append(message(7, 4, []byte{0x01, 0x02, 0x03, 0x04}), | |
| 161 | 168 | message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...) | |
| 162 | 169 | ||
| 163 | 170 | split := HeaderSize + 4 + 4 // mid-body of the second message | |
| @@ -171,7 +178,7 @@ func TestDisplayDispatch_HalfCoalesced(t *testing.T) { | |||
| 171 | 178 | } | |
| 172 | 179 | ||
| 173 | 180 | want := []EventRecord{ | |
| 174 | - | {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}}, | |
| 181 | + | {Object: 7, Opcode: 4, Body: []byte{0x01, 0x02, 0x03, 0x04}}, | |
| 175 | 182 | {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}}, | |
| 176 | 183 | } | |
| 177 | 184 | if !reflect.DeepEqual(*records, want) { | |
| @@ -274,6 +281,10 @@ func TestDisplayDispatch_DisplayError(t *testing.T) { | |||
| 274 | 281 | ||
| 275 | 282 | func TestDisplayDispatch_DeleteID(t *testing.T) { | |
| 276 | 283 | d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})}) | |
| 284 | + | for d.nextID <= 7 { | |
| 285 | + | d.allocateID() | |
| 286 | + | } | |
| 287 | + | d.objects.Delete(uint32(7)) | |
| 277 | 288 | ||
| 278 | 289 | if err := d.Dispatch(); err != nil { | |
| 279 | 290 | t.Fatalf("Dispatch: %v", err) | |
| @@ -299,7 +310,7 @@ func TestDisplayDispatch_PeerClose(t *testing.T) { | |||
| 299 | 310 | }, | |
| 300 | 311 | { | |
| 301 | 312 | name: "closed after a complete message", | |
| 302 | - | chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})}, | |
| 313 | + | chunks: [][]byte{message(7, 4, []byte{0x01, 0x02, 0x03, 0x04})}, | |
| 303 | 314 | }, | |
| 304 | 315 | } | |
| 305 | 316 | ||
| @@ -350,3 +361,33 @@ func TestReadFrame(t *testing.T) { | |||
| 350 | 361 | t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF) | |
| 351 | 362 | } | |
| 352 | 363 | } | |
| 364 | + | ||
| 365 | + | func TestDeleteIDReuseAndInvalid(t *testing.T) { | |
| 366 | + | d := newDisplay(&chunkConn{}) | |
| 367 | + | id := d.AllocateID() | |
| 368 | + | body := make([]byte, 4) | |
| 369 | + | binary.LittleEndian.PutUint32(body, id) | |
| 370 | + | if err := d.handleDisplayEvent(1, body); err != nil { | |
| 371 | + | t.Fatal(err) | |
| 372 | + | } | |
| 373 | + | if err := d.handleDisplayEvent(1, body); err == nil { | |
| 374 | + | t.Fatal("duplicate delete_id accepted") | |
| 375 | + | } | |
| 376 | + | d.nextID = 0xff000000 // simulate exhaustion | |
| 377 | + | if got := d.AllocateID(); got != id { | |
| 378 | + | t.Fatalf("reused %d, want %d", got, id) | |
| 379 | + | } | |
| 380 | + | if got := d.AllocateID(); got != 0 { | |
| 381 | + | t.Fatalf("allocated server-range ID %x", got) | |
| 382 | + | } | |
| 383 | + | d.objects.Store(id, d) | |
| 384 | + | if err := d.handleDisplayEvent(1, body); err == nil { | |
| 385 | + | t.Fatal("live ID deleted") | |
| 386 | + | } | |
| 387 | + | for _, bad := range []uint32{0, 1, 0xff000001} { | |
| 388 | + | binary.LittleEndian.PutUint32(body, bad) | |
| 389 | + | if err := d.handleDisplayEvent(1, body); err == nil { | |
| 390 | + | t.Fatalf("accepted reserved ID %x", bad) | |
| 391 | + | } | |
| 392 | + | } | |
| 393 | + | } | |
| @@ -92,6 +92,13 @@ func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) { | |||||
| 92 | BaseProxy: BaseProxy{id: 7, context: d.context}, | 92 | BaseProxy: BaseProxy{id: 7, context: d.context}, | ||
| 93 | records: &records, | 93 | records: &records, | ||
| 94 | }) | 94 | }) | ||
| 95 | + | for _, opcode := range []uint16{3, 4, 5} { | |||
| 96 | + | signature := "uint," | |||
| 97 | + | if opcode == 3 { | |||
| 98 | + | signature = "uint,uint," | |||
| 99 | + | } | |||
| 100 | + | d.RegisterEventSignature(7, opcode, signature) | |||
| 101 | + | } | |||
| 95 | return d, &records | 102 | return d, &records | ||
| 96 | } | 103 | } | ||
| 97 | 104 | ||||
| @@ -132,7 +139,7 @@ func TestDisplayDispatch_Fragmented(t *testing.T) { | |||||
| 132 | } | 139 | } | ||
| 133 | 140 | ||||
| 134 | func TestDisplayDispatch_Coalesced(t *testing.T) { | 141 | func TestDisplayDispatch_Coalesced(t *testing.T) { | ||
| 135 | - | first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14}) | 142 | + | first := message(7, 4, []byte{0x11, 0x12, 0x13, 0x14}) |
| 136 | second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24}) | 143 | second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24}) | ||
| 137 | 144 | ||||
| 138 | d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)}) | 145 | d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)}) | ||
| @@ -146,7 +153,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) { | |||||
| 146 | } | 153 | } | ||
| 147 | 154 | ||||
| 148 | want := []EventRecord{ | 155 | want := []EventRecord{ | ||
| 149 | - | {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}}, | 156 | + | {Object: 7, Opcode: 4, Body: []byte{0x11, 0x12, 0x13, 0x14}}, |
| 150 | {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}}, | 157 | {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}}, | ||
| 151 | } | 158 | } | ||
| 152 | if !reflect.DeepEqual(*records, want) { | 159 | if !reflect.DeepEqual(*records, want) { | ||
| @@ -157,7 +164,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) { | |||||
| 157 | func TestDisplayDispatch_HalfCoalesced(t *testing.T) { | 164 | func TestDisplayDispatch_HalfCoalesced(t *testing.T) { | ||
| 158 | // A read boundary that ends between the header and the body of the second | 165 | // A read boundary that ends between the header and the body of the second | ||
| 159 | // message must not be mistaken for a frame boundary. | 166 | // message must not be mistaken for a frame boundary. | ||
| 160 | - | whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}), | 167 | + | whole := append(message(7, 4, []byte{0x01, 0x02, 0x03, 0x04}), |
| 161 | message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...) | 168 | message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...) | ||
| 162 | 169 | ||||
| 163 | split := HeaderSize + 4 + 4 // mid-body of the second message | 170 | split := HeaderSize + 4 + 4 // mid-body of the second message | ||
| @@ -171,7 +178,7 @@ func TestDisplayDispatch_HalfCoalesced(t *testing.T) { | |||||
| 171 | } | 178 | } | ||
| 172 | 179 | ||||
| 173 | want := []EventRecord{ | 180 | want := []EventRecord{ | ||
| 174 | - | {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}}, | 181 | + | {Object: 7, Opcode: 4, Body: []byte{0x01, 0x02, 0x03, 0x04}}, |
| 175 | {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}}, | 182 | {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}}, | ||
| 176 | } | 183 | } | ||
| 177 | if !reflect.DeepEqual(*records, want) { | 184 | if !reflect.DeepEqual(*records, want) { | ||
| @@ -274,6 +281,10 @@ func TestDisplayDispatch_DisplayError(t *testing.T) { | |||||
| 274 | 281 | ||||
| 275 | func TestDisplayDispatch_DeleteID(t *testing.T) { | 282 | func TestDisplayDispatch_DeleteID(t *testing.T) { | ||
| 276 | d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})}) | 283 | d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})}) | ||
| 284 | + | for d.nextID <= 7 { | |||
| 285 | + | d.allocateID() | |||
| 286 | + | } | |||
| 287 | + | d.objects.Delete(uint32(7)) | |||
| 277 | 288 | ||||
| 278 | if err := d.Dispatch(); err != nil { | 289 | if err := d.Dispatch(); err != nil { | ||
| 279 | t.Fatalf("Dispatch: %v", err) | 290 | t.Fatalf("Dispatch: %v", err) | ||
| @@ -299,7 +310,7 @@ func TestDisplayDispatch_PeerClose(t *testing.T) { | |||||
| 299 | }, | 310 | }, | ||
| 300 | { | 311 | { | ||
| 301 | name: "closed after a complete message", | 312 | name: "closed after a complete message", | ||
| 302 | - | chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})}, | 313 | + | chunks: [][]byte{message(7, 4, []byte{0x01, 0x02, 0x03, 0x04})}, |
| 303 | }, | 314 | }, | ||
| 304 | } | 315 | } | ||
| 305 | 316 | ||||
| @@ -350,3 +361,33 @@ func TestReadFrame(t *testing.T) { | |||||
| 350 | t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF) | 361 | t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF) | ||
| 351 | } | 362 | } | ||
| 352 | } | 363 | } | ||
| 364 | + | ||||
| 365 | + | func TestDeleteIDReuseAndInvalid(t *testing.T) { | |||
| 366 | + | d := newDisplay(&chunkConn{}) | |||
| 367 | + | id := d.AllocateID() | |||
| 368 | + | body := make([]byte, 4) | |||
| 369 | + | binary.LittleEndian.PutUint32(body, id) | |||
| 370 | + | if err := d.handleDisplayEvent(1, body); err != nil { | |||
| 371 | + | t.Fatal(err) | |||
| 372 | + | } | |||
| 373 | + | if err := d.handleDisplayEvent(1, body); err == nil { | |||
| 374 | + | t.Fatal("duplicate delete_id accepted") | |||
| 375 | + | } | |||
| 376 | + | d.nextID = 0xff000000 // simulate exhaustion | |||
| 377 | + | if got := d.AllocateID(); got != id { | |||
| 378 | + | t.Fatalf("reused %d, want %d", got, id) | |||
| 379 | + | } | |||
| 380 | + | if got := d.AllocateID(); got != 0 { | |||
| 381 | + | t.Fatalf("allocated server-range ID %x", got) | |||
| 382 | + | } | |||
| 383 | + | d.objects.Store(id, d) | |||
| 384 | + | if err := d.handleDisplayEvent(1, body); err == nil { | |||
| 385 | + | t.Fatal("live ID deleted") | |||
| 386 | + | } | |||
| 387 | + | for _, bad := range []uint32{0, 1, 0xff000001} { | |||
| 388 | + | binary.LittleEndian.PutUint32(body, bad) | |||
| 389 | + | if err := d.handleDisplayEvent(1, body); err == nil { | |||
| 390 | + | t.Fatalf("accepted reserved ID %x", bad) | |||
| 391 | + | } | |||
| 392 | + | } | |||
| 393 | + | } | |||
M wl/wl.go +2 -18
| @@ -21,14 +21,7 @@ type ( | |||
| 21 | 21 | RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler | |
| 22 | 22 | RegistryGlobalEvent = wlturbo.RegistryGlobalEvent | |
| 23 | 23 | RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent | |
| 24 | - | Seat = wlturbo.Seat | |
| 25 | - | Surface = wlturbo.Surface | |
| 26 | - | Pointer = wlturbo.Pointer | |
| 27 | - | Keyboard = wlturbo.Keyboard | |
| 28 | - | Touch = wlturbo.Touch | |
| 29 | - | Output = wlturbo.Output | |
| 30 | - | Region = wlturbo.Region | |
| 31 | - | Compositor = wlturbo.Compositor | |
| 24 | + | OwnedFD = wlturbo.OwnedFD | |
| 32 | 25 | ||
| 33 | 26 | // Transport failures. Without these, a caller that imports this shim has to | |
| 34 | 27 | // import the root package as well just to classify an error. | |
| @@ -40,10 +33,8 @@ type ( | |||
| 40 | 33 | var ( | |
| 41 | 34 | Connect = wlturbo.Connect | |
| 42 | 35 | NewFixed = wlturbo.NewFixed | |
| 43 | - | NewSeat = wlturbo.NewSeat | |
| 44 | 36 | NewContext = wlturbo.NewContext | |
| 45 | - | NewSurface = wlturbo.NewSurface | |
| 46 | - | NewCompositor = wlturbo.NewCompositor | |
| 37 | + | CloseSentFD = wlturbo.CloseSentFD | |
| 47 | 38 | CreateAnonymousFile = wlturbo.CreateAnonymousFile | |
| 48 | 39 | MapMemory = wlturbo.MapMemory | |
| 49 | 40 | UnmapMemory = wlturbo.UnmapMemory | |
| @@ -57,10 +48,3 @@ var ( | |||
| 57 | 48 | ErrUnknownOpcode = wlturbo.ErrUnknownOpcode | |
| 58 | 49 | ErrDisplayError = wlturbo.ErrDisplayError | |
| 59 | 50 | ) | |
| 60 | - | ||
| 61 | - | // Seat capability constants | |
| 62 | - | const ( | |
| 63 | - | SeatCapabilityPointer = wlturbo.SeatCapabilityPointer | |
| 64 | - | SeatCapabilityKeyboard = wlturbo.SeatCapabilityKeyboard | |
| 65 | - | SeatCapabilityTouch = wlturbo.SeatCapabilityTouch | |
| 66 | - | ) | |
| @@ -21,14 +21,7 @@ type ( | |||||
| 21 | RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler | 21 | RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler | ||
| 22 | RegistryGlobalEvent = wlturbo.RegistryGlobalEvent | 22 | RegistryGlobalEvent = wlturbo.RegistryGlobalEvent | ||
| 23 | RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent | 23 | RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent | ||
| 24 | - | Seat = wlturbo.Seat | 24 | + | OwnedFD = wlturbo.OwnedFD |
| 25 | - | Surface = wlturbo.Surface | |||
| 26 | - | Pointer = wlturbo.Pointer | |||
| 27 | - | Keyboard = wlturbo.Keyboard | |||
| 28 | - | Touch = wlturbo.Touch | |||
| 29 | - | Output = wlturbo.Output | |||
| 30 | - | Region = wlturbo.Region | |||
| 31 | - | Compositor = wlturbo.Compositor | |||
| 32 | 25 | ||||
| 33 | // Transport failures. Without these, a caller that imports this shim has to | 26 | // Transport failures. Without these, a caller that imports this shim has to | ||
| 34 | // import the root package as well just to classify an error. | 27 | // import the root package as well just to classify an error. | ||
| @@ -40,10 +33,8 @@ type ( | |||||
| 40 | var ( | 33 | var ( | ||
| 41 | Connect = wlturbo.Connect | 34 | Connect = wlturbo.Connect | ||
| 42 | NewFixed = wlturbo.NewFixed | 35 | NewFixed = wlturbo.NewFixed | ||
| 43 | - | NewSeat = wlturbo.NewSeat | |||
| 44 | NewContext = wlturbo.NewContext | 36 | NewContext = wlturbo.NewContext | ||
| 45 | - | NewSurface = wlturbo.NewSurface | 37 | + | CloseSentFD = wlturbo.CloseSentFD |
| 46 | - | NewCompositor = wlturbo.NewCompositor | |||
| 47 | CreateAnonymousFile = wlturbo.CreateAnonymousFile | 38 | CreateAnonymousFile = wlturbo.CreateAnonymousFile | ||
| 48 | MapMemory = wlturbo.MapMemory | 39 | MapMemory = wlturbo.MapMemory | ||
| 49 | UnmapMemory = wlturbo.UnmapMemory | 40 | UnmapMemory = wlturbo.UnmapMemory | ||
| @@ -57,10 +48,3 @@ var ( | |||||
| 57 | ErrUnknownOpcode = wlturbo.ErrUnknownOpcode | 48 | ErrUnknownOpcode = wlturbo.ErrUnknownOpcode | ||
| 58 | ErrDisplayError = wlturbo.ErrDisplayError | 49 | ErrDisplayError = wlturbo.ErrDisplayError | ||
| 59 | ) | 50 | ) | ||
| 60 | - | ||||
| 61 | - | // Seat capability constants | |||
| 62 | - | const ( | |||
| 63 | - | SeatCapabilityPointer = wlturbo.SeatCapabilityPointer | |||
| 64 | - | SeatCapabilityKeyboard = wlturbo.SeatCapabilityKeyboard | |||
| 65 | - | SeatCapabilityTouch = wlturbo.SeatCapabilityTouch | |||
| 66 | - | ) | |||