fix(xwm): drop stale TARGETS replies after Wayland takes ownership
- commit
- a50e31e5c9b5f4f2d66a0b2237cb33a3c1c84096
- author
- brice · · 2026-10-22 16:50 UTC
- committer
- brice · · 2026-10-22 17:55 UTC
- parent
- 4afca7a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +33 -4
M internal/adapters/wayland/xwm/selection.go +8 -4
| @@ -32,6 +32,7 @@ var ( | |||
| 32 | 32 | type selState struct { | |
| 33 | 33 | atom uint32 | |
| 34 | 34 | gen uint32 // bumped on every ownership change; stales TARGETS probes | |
| 35 | + | probe uint32 // gen when the TARGETS ConvertSelection was issued | |
| 35 | 36 | owner uint32 // X owner window other than selWin, 0 none | |
| 36 | 37 | ours bool // selWin owns the selection (Wayland -> X) | |
| 37 | 38 | // wl is what the Wayland side offers while ours: raw text availability. | |
| @@ -206,6 +207,7 @@ func (x *XWM) onXFixesSelection(ev *xturbo.Event) { | |||
| 206 | 207 | x.host.SelectionOwned(k, nil) | |
| 207 | 208 | return | |
| 208 | 209 | } | |
| 210 | + | st.probe = st.gen | |
| 209 | 211 | x.log.Debug().Int("kind", k).Uint32("owner", owner).Msg("selection: X owner") | |
| 210 | 212 | xproto.ConvertSelection(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(st.atom), | |
| 211 | 213 | xproto.Atom(x.atoms[atomTargets]), xproto.Atom(x.atoms[atomNeferwlTargets]), 0) | |
| @@ -227,10 +229,12 @@ func (x *XWM) onSelectionNotify(ev *xturbo.Event) { | |||
| 227 | 229 | return | |
| 228 | 230 | } | |
| 229 | 231 | { | |
| 232 | + | st := &x.sel.st[k] | |
| 233 | + | if st.ours || st.owner == 0 || st.probe != st.gen { // stale probe | |
| 234 | + | return | |
| 235 | + | } | |
| 230 | 236 | if e.Property() == 0 { | |
| 231 | - | if x.sel.st[k].owner != 0 { | |
| 232 | - | x.host.SelectionOwned(k, nil) | |
| 233 | - | } | |
| 237 | + | x.host.SelectionOwned(k, nil) | |
| 234 | 238 | return | |
| 235 | 239 | } | |
| 236 | 240 | seq := xproto.GetProperty(x.conn.Buf(), true, xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlTargets]), 0, 0, 256) | |
| @@ -244,7 +248,7 @@ func (x *XWM) onSelectionNotify(ev *xturbo.Event) { | |||
| 244 | 248 | // to Wayland: the text subset, empty for images, files and the like. | |
| 245 | 249 | func (x *XWM) onTargetsReply(r *xturbo.Reply) { | |
| 246 | 250 | k := int(r.Args[0]) | |
| 247 | - | if k < 0 || k >= selKinds || r.Args[1] != x.sel.st[k].gen { | |
| 251 | + | if k < 0 || k >= selKinds || r.Args[1] != x.sel.st[k].gen || x.sel.st[k].ours || x.sel.st[k].owner == 0 || x.sel.st[k].probe != r.Args[1] { | |
| 248 | 252 | return | |
| 249 | 253 | } | |
| 250 | 254 | g, ok := xproto.ParseGetPropertyReply(r.Raw) | |
| @@ -32,6 +32,7 @@ var ( | |||||
| 32 | type selState struct { | 32 | type selState struct { | ||
| 33 | atom uint32 | 33 | atom uint32 | ||
| 34 | gen uint32 // bumped on every ownership change; stales TARGETS probes | 34 | gen uint32 // bumped on every ownership change; stales TARGETS probes | ||
| 35 | + | probe uint32 // gen when the TARGETS ConvertSelection was issued | |||
| 35 | owner uint32 // X owner window other than selWin, 0 none | 36 | owner uint32 // X owner window other than selWin, 0 none | ||
| 36 | ours bool // selWin owns the selection (Wayland -> X) | 37 | ours bool // selWin owns the selection (Wayland -> X) | ||
| 37 | // wl is what the Wayland side offers while ours: raw text availability. | 38 | // wl is what the Wayland side offers while ours: raw text availability. | ||
| @@ -206,6 +207,7 @@ func (x *XWM) onXFixesSelection(ev *xturbo.Event) { | |||||
| 206 | x.host.SelectionOwned(k, nil) | 207 | x.host.SelectionOwned(k, nil) | ||
| 207 | return | 208 | return | ||
| 208 | } | 209 | } | ||
| 210 | + | st.probe = st.gen | |||
| 209 | x.log.Debug().Int("kind", k).Uint32("owner", owner).Msg("selection: X owner") | 211 | x.log.Debug().Int("kind", k).Uint32("owner", owner).Msg("selection: X owner") | ||
| 210 | xproto.ConvertSelection(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(st.atom), | 212 | xproto.ConvertSelection(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(st.atom), | ||
| 211 | xproto.Atom(x.atoms[atomTargets]), xproto.Atom(x.atoms[atomNeferwlTargets]), 0) | 213 | xproto.Atom(x.atoms[atomTargets]), xproto.Atom(x.atoms[atomNeferwlTargets]), 0) | ||
| @@ -227,10 +229,12 @@ func (x *XWM) onSelectionNotify(ev *xturbo.Event) { | |||||
| 227 | return | 229 | return | ||
| 228 | } | 230 | } | ||
| 229 | { | 231 | { | ||
| 232 | + | st := &x.sel.st[k] | |||
| 233 | + | if st.ours || st.owner == 0 || st.probe != st.gen { // stale probe | |||
| 234 | + | return | |||
| 235 | + | } | |||
| 230 | if e.Property() == 0 { | 236 | if e.Property() == 0 { | ||
| 231 | - | if x.sel.st[k].owner != 0 { | 237 | + | x.host.SelectionOwned(k, nil) |
| 232 | - | x.host.SelectionOwned(k, nil) | |||
| 233 | - | } | |||
| 234 | return | 238 | return | ||
| 235 | } | 239 | } | ||
| 236 | seq := xproto.GetProperty(x.conn.Buf(), true, xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlTargets]), 0, 0, 256) | 240 | seq := xproto.GetProperty(x.conn.Buf(), true, xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlTargets]), 0, 0, 256) | ||
| @@ -244,7 +248,7 @@ func (x *XWM) onSelectionNotify(ev *xturbo.Event) { | |||||
| 244 | // to Wayland: the text subset, empty for images, files and the like. | 248 | // to Wayland: the text subset, empty for images, files and the like. | ||
| 245 | func (x *XWM) onTargetsReply(r *xturbo.Reply) { | 249 | func (x *XWM) onTargetsReply(r *xturbo.Reply) { | ||
| 246 | k := int(r.Args[0]) | 250 | k := int(r.Args[0]) | ||
| 247 | - | if k < 0 || k >= selKinds || r.Args[1] != x.sel.st[k].gen { | 251 | + | if k < 0 || k >= selKinds || r.Args[1] != x.sel.st[k].gen || x.sel.st[k].ours || x.sel.st[k].owner == 0 || x.sel.st[k].probe != r.Args[1] { |
| 248 | return | 252 | return | ||
| 249 | } | 253 | } | ||
| 250 | g, ok := xproto.ParseGetPropertyReply(r.Raw) | 254 | g, ok := xproto.ParseGetPropertyReply(r.Raw) | ||
M internal/adapters/wayland/xwm/selection_test.go +25 -0
| @@ -683,3 +683,28 @@ func TestSelectionCopyAllocationsWaylandToX(t *testing.T) { | |||
| 683 | 683 | require.Zero(t, allocs) | |
| 684 | 684 | require.Equal(t, outFree, x.sel.out[0].state) | |
| 685 | 685 | } | |
| 686 | + | ||
| 687 | + | // A TARGETS reply that arrives after Wayland took the selection is dropped. | |
| 688 | + | func TestSelectionStaleTargetsAfterWaylandOwner(t *testing.T) { | |
| 689 | + | x, p, h, _, _ := attached(t) | |
| 690 | + | const owner = 0x500010 | |
| 691 | + | clip := p.atom("CLIPBOARD") | |
| 692 | + | tgt := p.atom("TARGETS") | |
| 693 | + | p.xfixesSelection(owner, clip) | |
| 694 | + | req, _ := pumpReq(t, x, p, 0, func(r xpeerReq) bool { return r.major == 24 }) | |
| 695 | + | prop := req.u32(16) | |
| 696 | + | x.WaylandSelection(SelClipboard, []string{"text/plain"}) | |
| 697 | + | p.setProp(x.selWin, prop, 4, 32, le32(tgt, p.atom("UTF8_STRING"))) | |
| 698 | + | p.selectionNotify(x.selWin, clip, tgt, prop) // the mock fails on SelectionOwned | |
| 699 | + | ev := x.Stats().Events | |
| 700 | + | readUntil(t, x, func() bool { return x.Stats().Events > ev }) | |
| 701 | + | x.flush() | |
| 702 | + | require.True(t, x.sel.st[SelClipboard].ours) | |
| 703 | + | for i := 0; i < 20; i++ { // let any (wrong) GetProperty round trip finish | |
| 704 | + | x.onReadable() | |
| 705 | + | x.flush() | |
| 706 | + | time.Sleep(time.Millisecond) | |
| 707 | + | } | |
| 708 | + | require.Zero(t, getProps(p, 0, x.selWin), "the stale TARGETS reply is not fetched") | |
| 709 | + | _ = h | |
| 710 | + | } | |
| @@ -683,3 +683,28 @@ func TestSelectionCopyAllocationsWaylandToX(t *testing.T) { | |||||
| 683 | require.Zero(t, allocs) | 683 | require.Zero(t, allocs) | ||
| 684 | require.Equal(t, outFree, x.sel.out[0].state) | 684 | require.Equal(t, outFree, x.sel.out[0].state) | ||
| 685 | } | 685 | } | ||
| 686 | + | ||||
| 687 | + | // A TARGETS reply that arrives after Wayland took the selection is dropped. | |||
| 688 | + | func TestSelectionStaleTargetsAfterWaylandOwner(t *testing.T) { | |||
| 689 | + | x, p, h, _, _ := attached(t) | |||
| 690 | + | const owner = 0x500010 | |||
| 691 | + | clip := p.atom("CLIPBOARD") | |||
| 692 | + | tgt := p.atom("TARGETS") | |||
| 693 | + | p.xfixesSelection(owner, clip) | |||
| 694 | + | req, _ := pumpReq(t, x, p, 0, func(r xpeerReq) bool { return r.major == 24 }) | |||
| 695 | + | prop := req.u32(16) | |||
| 696 | + | x.WaylandSelection(SelClipboard, []string{"text/plain"}) | |||
| 697 | + | p.setProp(x.selWin, prop, 4, 32, le32(tgt, p.atom("UTF8_STRING"))) | |||
| 698 | + | p.selectionNotify(x.selWin, clip, tgt, prop) // the mock fails on SelectionOwned | |||
| 699 | + | ev := x.Stats().Events | |||
| 700 | + | readUntil(t, x, func() bool { return x.Stats().Events > ev }) | |||
| 701 | + | x.flush() | |||
| 702 | + | require.True(t, x.sel.st[SelClipboard].ours) | |||
| 703 | + | for i := 0; i < 20; i++ { // let any (wrong) GetProperty round trip finish | |||
| 704 | + | x.onReadable() | |||
| 705 | + | x.flush() | |||
| 706 | + | time.Sleep(time.Millisecond) | |||
| 707 | + | } | |||
| 708 | + | require.Zero(t, getProps(p, 0, x.selWin), "the stale TARGETS reply is not fetched") | |||
| 709 | + | _ = h | |||
| 710 | + | } | |||