detached c47d0a5

fix(core): dispatch bootstrap registry and retain FD owner across handlers

commit
c47d0a5a9bd5a9ab455587c7461f2c9de35cc81e
author
brice · · 2026-09-28 07:23 UTC
parent
7576621
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +104 -3
M client.go +19 -0
@@ -454,6 +454,25 @@ func (d *Display) dispatchFrame(f receivedFrame) error {
454454 }
455455 return d.handleDisplayEvent(f.opcode, f.body)
456456 }
457+ if f.object == d.registry.id {
458+ var sig string
459+ switch f.opcode {
460+ case 0:
461+ sig = "uint,string,uint,"
462+ case 1:
463+ sig = "uint,"
464+ default:
465+ return &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
466+ }
467+ if err := validateEventBody(sig, f.body); err != nil {
468+ return &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
469+ }
470+ if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
471+ return &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
472+ }
473+ d.notifyListeners(f.object, f.opcode, f.body)
474+ return nil
475+ }
457476 obj, ok := d.objects.Load(f.object)
458477 if !ok {
459478 return &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
@@ -454,6 +454,25 @@ func (d *Display) dispatchFrame(f receivedFrame) error {
454 }454 }
455 return d.handleDisplayEvent(f.opcode, f.body)455 return d.handleDisplayEvent(f.opcode, f.body)
456 }456 }
457+ if f.object == d.registry.id {
458+ var sig string
459+ switch f.opcode {
460+ case 0:
461+ sig = "uint,string,uint,"
462+ case 1:
463+ sig = "uint,"
464+ default:
465+ return &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
466+ }
467+ if err := validateEventBody(sig, f.body); err != nil {
468+ return &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
469+ }
470+ if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
471+ return &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
472+ }
473+ d.notifyListeners(f.object, f.opcode, f.body)
474+ return nil
475+ }
457 obj, ok := d.objects.Load(f.object)476 obj, ok := d.objects.Load(f.object)
458 if !ok {477 if !ok {
459 return &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}478 return &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
M internal/scanner/template.go +2 -1
@@ -119,7 +119,8 @@ func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) {
119119 {{- range .DecodeLines}}
120120 {{.}}
121121 {{- end}}
122- {{if .HasFD}}defer func(){ {{range .FDNames}}_ = {{.}}.Close(); {{end}} }(){{end}}
122+ {{if .HasFD}}{{range .FDNames}}owned{{.}} := {{.}}; defer owned{{.}}.Close()
123+ {{end}}{{end}}
123124 for i, handler := range o.{{.HandlerAccessor}}() {
124125 {{- if .HasFD}}
125126 if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }
@@ -119,7 +119,8 @@ func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) {
119 {{- range .DecodeLines}}119 {{- range .DecodeLines}}
120 {{.}}120 {{.}}
121 {{- end}}121 {{- end}}
122- {{if .HasFD}}defer func(){ {{range .FDNames}}_ = {{.}}.Close(); {{end}} }(){{end}}122+ {{if .HasFD}}{{range .FDNames}}owned{{.}} := {{.}}; defer owned{{.}}.Close()
123+ {{end}}{{end}}
123 for i, handler := range o.{{.HandlerAccessor}}() {124 for i, handler := range o.{{.HandlerAccessor}}() {
124 {{- if .HasFD}}125 {{- if .HasFD}}
125 if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }126 if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }
M protocol/core/core_test.go +77 -0
@@ -322,3 +322,80 @@ func TestServerChildEventsAfterOffer(t *testing.T) {
322322 t.Fatalf("typed child event: %q", got)
323323 }
324324 }
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+}
@@ -322,3 +322,80 @@ func TestServerChildEventsAfterOffer(t *testing.T) {
322 t.Fatalf("typed child event: %q", got)322 t.Fatalf("typed child event: %q", got)
323 }323 }
324 }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+}
M protocol/core/wayland_generated.go +6 -2
@@ -1432,7 +1432,9 @@ func (o *DataSource) Dispatch(event *wl.Event) {
14321432 case 1: // send
14331433 mimeType := event.String()
14341434 fd := event.FD()
1435- defer func() { _ = fd.Close() }()
1435+ ownedfd := fd
1436+ defer ownedfd.Close()
1437+
14361438 for i, handler := range o.handlersForSend() {
14371439 if i > 0 {
14381440 fd = nil
@@ -3174,7 +3176,9 @@ func (o *Keyboard) Dispatch(event *wl.Event) {
31743176 format := event.Uint32()
31753177 fd := event.FD()
31763178 size := event.Uint32()
3177- defer func() { _ = fd.Close() }()
3179+ ownedfd := fd
3180+ defer ownedfd.Close()
3181+
31783182 for i, handler := range o.handlersForKeymap() {
31793183 if i > 0 {
31803184 fd = nil
@@ -1432,7 +1432,9 @@ func (o *DataSource) Dispatch(event *wl.Event) {
1432 case 1: // send1432 case 1: // send
1433 mimeType := event.String()1433 mimeType := event.String()
1434 fd := event.FD()1434 fd := event.FD()
1435- defer func() { _ = fd.Close() }()1435+ ownedfd := fd
1436+ defer ownedfd.Close()
1437+
1436 for i, handler := range o.handlersForSend() {1438 for i, handler := range o.handlersForSend() {
1437 if i > 0 {1439 if i > 0 {
1438 fd = nil1440 fd = nil
@@ -3174,7 +3176,9 @@ func (o *Keyboard) Dispatch(event *wl.Event) {
3174 format := event.Uint32()3176 format := event.Uint32()
3175 fd := event.FD()3177 fd := event.FD()
3176 size := event.Uint32()3178 size := event.Uint32()
3177- defer func() { _ = fd.Close() }()3179+ ownedfd := fd
3180+ defer ownedfd.Close()
3181+
3178 for i, handler := range o.handlersForKeymap() {3182 for i, handler := range o.handlersForKeymap() {
3179 if i > 0 {3183 if i > 0 {
3180 fd = nil3184 fd = nil

Keyboard shortcuts

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

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

Branches 1

Tags 8

↑↓ moveenter openesc close