detached 2717424

feat(server): return from Display.Do once the display loop stops

commit
2717424478690f2d3df92653983672c36d70ab56
author
brice · · 2026-09-23 12:16 UTC
parent
90ef96b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +129 -16
M internal/e2e/args_test.go +4 -2
@@ -139,7 +139,7 @@ func TestArgumentRoundtripAndPostError(t *testing.T) {
139139 socket, d, _ := startServer(t)
140140 requests := make(chan string, 1)
141141 stringsReceived := make(chan string, 1)
142- d.Do(func() {
142+ if !d.Do(func() {
143143 if err := wayland.NewDataDeviceManagerGlobal(d, 1, func(c server.Client, v, id uint32) {
144144 if _, e := wayland.NewDataDeviceManager(c, int32(v), id, managerHandler{stringsReceived, t}); e != nil {
145145 t.Error(e)
@@ -164,7 +164,9 @@ func TestArgumentRoundtripAndPostError(t *testing.T) {
164164 }); err != nil {
165165 t.Error(err)
166166 }
167- })
167+ }) {
168+ t.Fatal("display stopped")
169+ }
168170 c, err := wlturbo.Connect(socket)
169171 must(t, err)
170172 defer c.Close()
@@ -139,7 +139,7 @@ func TestArgumentRoundtripAndPostError(t *testing.T) {
139 socket, d, _ := startServer(t)139 socket, d, _ := startServer(t)
140 requests := make(chan string, 1)140 requests := make(chan string, 1)
141 stringsReceived := make(chan string, 1)141 stringsReceived := make(chan string, 1)
142- d.Do(func() {142+ if !d.Do(func() {
143 if err := wayland.NewDataDeviceManagerGlobal(d, 1, func(c server.Client, v, id uint32) {143 if err := wayland.NewDataDeviceManagerGlobal(d, 1, func(c server.Client, v, id uint32) {
144 if _, e := wayland.NewDataDeviceManager(c, int32(v), id, managerHandler{stringsReceived, t}); e != nil {144 if _, e := wayland.NewDataDeviceManager(c, int32(v), id, managerHandler{stringsReceived, t}); e != nil {
145 t.Error(e)145 t.Error(e)
@@ -164,7 +164,9 @@ func TestArgumentRoundtripAndPostError(t *testing.T) {
164 }); err != nil {164 }); err != nil {
165 t.Error(err)165 t.Error(err)
166 }166 }
167- })167+ }) {
168+ t.Fatal("display stopped")
169+ }
168 c, err := wlturbo.Connect(socket)170 c, err := wlturbo.Connect(socket)
169 must(t, err)171 must(t, err)
170 defer c.Close()172 defer c.Close()
M internal/e2e/feasibility_test.go +10 -4
@@ -144,7 +144,9 @@ func TestRequestEventRoundtripAndDisconnect(t *testing.T) {
144144 deadline := time.Now().Add(2 * time.Second)
145145 for {
146146 var n int
147- d.Do(func() { n = server.LiveResources() })
147+ if !d.Do(func() { n = server.LiveResources() }) {
148+ t.Fatal("display stopped")
149+ }
148150 if n == 0 {
149151 break
150152 }
@@ -186,7 +188,9 @@ func TestPostEventAfterDisconnect(t *testing.T) {
186188 deadline := time.Now().Add(2 * time.Second)
187189 for {
188190 var n int
189- d.Do(func() { n = server.LiveResources() })
191+ if !d.Do(func() { n = server.LiveResources() }) {
192+ t.Fatal("display stopped")
193+ }
190194 if n == 0 {
191195 break
192196 }
@@ -195,7 +199,7 @@ func TestPostEventAfterDisconnect(t *testing.T) {
195199 }
196200 time.Sleep(5 * time.Millisecond)
197201 }
198- d.Do(func() {
202+ if !d.Do(func() {
199203 if stale == nil {
200204 t.Error("resource was not created")
201205 return
@@ -208,7 +212,9 @@ func TestPostEventAfterDisconnect(t *testing.T) {
208212 if stale.ID() != 0 || stale.Version() != 0 || stale.Client() != (server.Client{}) {
209213 t.Error("destroyed resource returned nonzero metadata")
210214 }
211- })
215+ }) {
216+ t.Fatal("display stopped")
217+ }
212218 }
213219
214220 // doneProxy records wl_callback.done.
@@ -144,7 +144,9 @@ func TestRequestEventRoundtripAndDisconnect(t *testing.T) {
144 deadline := time.Now().Add(2 * time.Second)144 deadline := time.Now().Add(2 * time.Second)
145 for {145 for {
146 var n int146 var n int
147- d.Do(func() { n = server.LiveResources() })147+ if !d.Do(func() { n = server.LiveResources() }) {
148+ t.Fatal("display stopped")
149+ }
148 if n == 0 {150 if n == 0 {
149 break151 break
150 }152 }
@@ -186,7 +188,9 @@ func TestPostEventAfterDisconnect(t *testing.T) {
186 deadline := time.Now().Add(2 * time.Second)188 deadline := time.Now().Add(2 * time.Second)
187 for {189 for {
188 var n int190 var n int
189- d.Do(func() { n = server.LiveResources() })191+ if !d.Do(func() { n = server.LiveResources() }) {
192+ t.Fatal("display stopped")
193+ }
190 if n == 0 {194 if n == 0 {
191 break195 break
192 }196 }
@@ -195,7 +199,7 @@ func TestPostEventAfterDisconnect(t *testing.T) {
195 }199 }
196 time.Sleep(5 * time.Millisecond)200 time.Sleep(5 * time.Millisecond)
197 }201 }
198- d.Do(func() {202+ if !d.Do(func() {
199 if stale == nil {203 if stale == nil {
200 t.Error("resource was not created")204 t.Error("resource was not created")
201 return205 return
@@ -208,7 +212,9 @@ func TestPostEventAfterDisconnect(t *testing.T) {
208 if stale.ID() != 0 || stale.Version() != 0 || stale.Client() != (server.Client{}) {212 if stale.ID() != 0 || stale.Version() != 0 || stale.Client() != (server.Client{}) {
209 t.Error("destroyed resource returned nonzero metadata")213 t.Error("destroyed resource returned nonzero metadata")
210 }214 }
211- })215+ }) {
216+ t.Fatal("display stopped")
217+ }
212 }218 }
213 219
214 // doneProxy records wl_callback.done.220 // doneProxy records wl_callback.done.
M server/display.go +34 -10
@@ -43,9 +43,10 @@ var live = struct {
4343 // Display owns a wl_display. All methods except Do must run on the goroutine
4444 // that calls Run, or before Run starts.
4545 type Display struct {
46- c uintptr
47- loop uintptr
48- calls chan func()
46+ c uintptr
47+ loop uintptr
48+ calls chan func()
49+ stopped chan struct{}
4950 }
5051
5152 func NewDisplay() (*Display, error) {
@@ -56,7 +57,7 @@ func NewDisplay() (*Display, error) {
5657 if c == 0 {
5758 return nil, errors.New("purego-libwayland: wl_display_create failed")
5859 }
59- return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func())}, nil
60+ return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func()), stopped: make(chan struct{})}, nil
6061 }
6162
6263 // AddSocketFD serves clients on an already bound, listening unix socket.
@@ -82,11 +83,14 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc)
8283 func (d *Display) Run(ctx context.Context) error {
8384 runtime.LockOSThread()
8485 defer runtime.UnlockOSThread()
86+ defer close(d.stopped)
87+ defer func() {
88+ wlDisplayDestroyClients(d.c)
89+ wlDisplayDestroy(d.c)
90+ }()
8591 for {
8692 select {
8793 case <-ctx.Done():
88- wlDisplayDestroyClients(d.c)
89- wlDisplayDestroy(d.c)
9094 return nil
9195 case fn := <-d.calls:
9296 fn()
@@ -99,13 +103,33 @@ func (d *Display) Run(ctx context.Context) error {
99103 }
100104 }
101105
102-// Do runs fn on the display goroutine and waits for it.
103-func (d *Display) Do(fn func()) {
106+// Do runs fn on the display goroutine and waits for it. It returns false if
107+// the display loop has stopped (fn was not run, or its run was not confirmed).
108+// Do must not be called from the display goroutine: it would deadlock.
109+func (d *Display) Do(fn func()) bool {
104110 done := make(chan struct{})
105- d.calls <- func() { fn(); close(done) }
106- <-done
111+ select {
112+ case d.calls <- func() { fn(); close(done) }:
113+ case <-d.stopped:
114+ return false
115+ }
116+ select {
117+ case <-done:
118+ return true
119+ case <-d.stopped:
120+ // If both are ready, prefer confirmation of a completed call.
121+ select {
122+ case <-done:
123+ return true
124+ default:
125+ return false
126+ }
127+ }
107128 }
108129
130+// Stopped is closed after Run returns and the display is destroyed.
131+func (d *Display) Stopped() <-chan struct{} { return d.stopped }
132+
109133 // LiveResources reports resources not yet destroyed. Display goroutine only.
110134 func LiveResources() int { return len(live.resources) }
111135
@@ -43,9 +43,10 @@ var live = struct {
43 // Display owns a wl_display. All methods except Do must run on the goroutine43 // Display owns a wl_display. All methods except Do must run on the goroutine
44 // that calls Run, or before Run starts.44 // that calls Run, or before Run starts.
45 type Display struct {45 type Display struct {
46- c uintptr46+ c uintptr
47- loop uintptr47+ loop uintptr
48- calls chan func()48+ calls chan func()
49+ stopped chan struct{}
49 }50 }
50 51
51 func NewDisplay() (*Display, error) {52 func NewDisplay() (*Display, error) {
@@ -56,7 +57,7 @@ func NewDisplay() (*Display, error) {
56 if c == 0 {57 if c == 0 {
57 return nil, errors.New("purego-libwayland: wl_display_create failed")58 return nil, errors.New("purego-libwayland: wl_display_create failed")
58 }59 }
59- return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func())}, nil60+ return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func()), stopped: make(chan struct{})}, nil
60 }61 }
61 62
62 // AddSocketFD serves clients on an already bound, listening unix socket.63 // AddSocketFD serves clients on an already bound, listening unix socket.
@@ -82,11 +83,14 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc)
82 func (d *Display) Run(ctx context.Context) error {83 func (d *Display) Run(ctx context.Context) error {
83 runtime.LockOSThread()84 runtime.LockOSThread()
84 defer runtime.UnlockOSThread()85 defer runtime.UnlockOSThread()
86+ defer close(d.stopped)
87+ defer func() {
88+ wlDisplayDestroyClients(d.c)
89+ wlDisplayDestroy(d.c)
90+ }()
85 for {91 for {
86 select {92 select {
87 case <-ctx.Done():93 case <-ctx.Done():
88- wlDisplayDestroyClients(d.c)
89- wlDisplayDestroy(d.c)
90 return nil94 return nil
91 case fn := <-d.calls:95 case fn := <-d.calls:
92 fn()96 fn()
@@ -99,13 +103,33 @@ func (d *Display) Run(ctx context.Context) error {
99 }103 }
100 }104 }
101 105
102-// Do runs fn on the display goroutine and waits for it.106+// Do runs fn on the display goroutine and waits for it. It returns false if
103-func (d *Display) Do(fn func()) {107+// the display loop has stopped (fn was not run, or its run was not confirmed).
108+// Do must not be called from the display goroutine: it would deadlock.
109+func (d *Display) Do(fn func()) bool {
104 done := make(chan struct{})110 done := make(chan struct{})
105- d.calls <- func() { fn(); close(done) }111+ select {
106- <-done112+ case d.calls <- func() { fn(); close(done) }:
113+ case <-d.stopped:
114+ return false
115+ }
116+ select {
117+ case <-done:
118+ return true
119+ case <-d.stopped:
120+ // If both are ready, prefer confirmation of a completed call.
121+ select {
122+ case <-done:
123+ return true
124+ default:
125+ return false
126+ }
127+ }
107 }128 }
108 129
130+// Stopped is closed after Run returns and the display is destroyed.
131+func (d *Display) Stopped() <-chan struct{} { return d.stopped }
132+
109 // LiveResources reports resources not yet destroyed. Display goroutine only.133 // LiveResources reports resources not yet destroyed. Display goroutine only.
110 func LiveResources() int { return len(live.resources) }134 func LiveResources() int { return len(live.resources) }
111 135
M server/display_test.go +81 -0
@@ -1,8 +1,10 @@
11 package server
22
33 import (
4+ "context"
45 "golang.org/x/sys/unix"
56 "testing"
7+ "time"
68 "unsafe"
79 )
810
@@ -30,3 +32,82 @@ func TestNoHandlerClosesFDs(t *testing.T) {
3032 }
3133 }
3234 }
35+
36+func TestDoStopped(t *testing.T) {
37+ d, err := NewDisplay()
38+ if err != nil {
39+ t.Fatal(err)
40+ }
41+ ctx, cancel := context.WithCancel(context.Background())
42+ returned := make(chan error, 1)
43+ go func() { returned <- d.Run(ctx) }()
44+ ran := false
45+ if !d.Do(func() { ran = true }) || !ran {
46+ t.Fatal("call did not run")
47+ }
48+ cancel()
49+ select {
50+ case err := <-returned:
51+ if err != nil {
52+ t.Fatal(err)
53+ }
54+ case <-time.After(3 * time.Second):
55+ t.Fatal("Run did not return")
56+ }
57+ select {
58+ case <-d.Stopped():
59+ default:
60+ t.Fatal("Stopped not closed")
61+ }
62+ result := make(chan bool, 1)
63+ go func() { result <- d.Do(func() { t.Error("call ran after stop") }) }()
64+ select {
65+ case ok := <-result:
66+ if ok {
67+ t.Fatal("call confirmed after stop")
68+ }
69+ case <-time.After(100 * time.Millisecond):
70+ t.Fatal("Do blocked after stop")
71+ }
72+}
73+
74+func TestDoBlockedDuringCancellation(t *testing.T) {
75+ d, err := NewDisplay()
76+ if err != nil {
77+ t.Fatal(err)
78+ }
79+ ctx, cancel := context.WithCancel(context.Background())
80+ returned := make(chan error, 1)
81+ go func() { returned <- d.Run(ctx) }()
82+ entered := make(chan struct{})
83+ release := make(chan struct{})
84+ first := make(chan bool, 1)
85+ go func() { first <- d.Do(func() { close(entered); <-release }) }()
86+ select {
87+ case <-entered:
88+ case <-time.After(3 * time.Second):
89+ t.Fatal("first call not started")
90+ }
91+ result := make(chan bool, 1)
92+ go func() { result <- d.Do(func() {}) }()
93+ cancel()
94+ close(release)
95+ select {
96+ case <-first:
97+ case <-time.After(3 * time.Second):
98+ t.Fatal("first call blocked")
99+ }
100+ select {
101+ case err := <-returned:
102+ if err != nil {
103+ t.Fatal(err)
104+ }
105+ case <-time.After(3 * time.Second):
106+ t.Fatal("Run blocked")
107+ }
108+ select {
109+ case <-result:
110+ case <-time.After(100 * time.Millisecond):
111+ t.Fatal("blocked Do leaked")
112+ }
113+}
@@ -1,8 +1,10 @@
1 package server1 package server
2 2
3 import (3 import (
4+ "context"
4 "golang.org/x/sys/unix"5 "golang.org/x/sys/unix"
5 "testing"6 "testing"
7+ "time"
6 "unsafe"8 "unsafe"
7 )9 )
8 10
@@ -30,3 +32,82 @@ func TestNoHandlerClosesFDs(t *testing.T) {
30 }32 }
31 }33 }
32 }34 }
35+
36+func TestDoStopped(t *testing.T) {
37+ d, err := NewDisplay()
38+ if err != nil {
39+ t.Fatal(err)
40+ }
41+ ctx, cancel := context.WithCancel(context.Background())
42+ returned := make(chan error, 1)
43+ go func() { returned <- d.Run(ctx) }()
44+ ran := false
45+ if !d.Do(func() { ran = true }) || !ran {
46+ t.Fatal("call did not run")
47+ }
48+ cancel()
49+ select {
50+ case err := <-returned:
51+ if err != nil {
52+ t.Fatal(err)
53+ }
54+ case <-time.After(3 * time.Second):
55+ t.Fatal("Run did not return")
56+ }
57+ select {
58+ case <-d.Stopped():
59+ default:
60+ t.Fatal("Stopped not closed")
61+ }
62+ result := make(chan bool, 1)
63+ go func() { result <- d.Do(func() { t.Error("call ran after stop") }) }()
64+ select {
65+ case ok := <-result:
66+ if ok {
67+ t.Fatal("call confirmed after stop")
68+ }
69+ case <-time.After(100 * time.Millisecond):
70+ t.Fatal("Do blocked after stop")
71+ }
72+}
73+
74+func TestDoBlockedDuringCancellation(t *testing.T) {
75+ d, err := NewDisplay()
76+ if err != nil {
77+ t.Fatal(err)
78+ }
79+ ctx, cancel := context.WithCancel(context.Background())
80+ returned := make(chan error, 1)
81+ go func() { returned <- d.Run(ctx) }()
82+ entered := make(chan struct{})
83+ release := make(chan struct{})
84+ first := make(chan bool, 1)
85+ go func() { first <- d.Do(func() { close(entered); <-release }) }()
86+ select {
87+ case <-entered:
88+ case <-time.After(3 * time.Second):
89+ t.Fatal("first call not started")
90+ }
91+ result := make(chan bool, 1)
92+ go func() { result <- d.Do(func() {}) }()
93+ cancel()
94+ close(release)
95+ select {
96+ case <-first:
97+ case <-time.After(3 * time.Second):
98+ t.Fatal("first call blocked")
99+ }
100+ select {
101+ case err := <-returned:
102+ if err != nil {
103+ t.Fatal(err)
104+ }
105+ case <-time.After(3 * time.Second):
106+ t.Fatal("Run blocked")
107+ }
108+ select {
109+ case <-result:
110+ case <-time.After(100 * time.Millisecond):
111+ t.Fatal("blocked Do leaked")
112+ }
113+}

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 14

↑↓ moveenter openesc close