feat(server): add Display.Close for cleanup before Run
- commit
- c5f3abcfd4719b490ed2dba1c0f8281033e13c4f
- author
- brice · · 2026-09-23 12:26 UTC
- parent
- 2717424
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +39 -0
M server/display.go +16 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "runtime" | |
| 8 | 8 | "strings" | |
| 9 | + | "sync/atomic" | |
| 9 | 10 | "unsafe" | |
| 10 | 11 | ||
| 11 | 12 | "golang.org/x/sys/unix" | |
| @@ -47,6 +48,7 @@ type Display struct { | |||
| 47 | 48 | loop uintptr | |
| 48 | 49 | calls chan func() | |
| 49 | 50 | stopped chan struct{} | |
| 51 | + | state atomic.Uint32 | |
| 50 | 52 | } | |
| 51 | 53 | ||
| 52 | 54 | func NewDisplay() (*Display, error) { | |
| @@ -79,8 +81,22 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) | |||
| 79 | 81 | return nil | |
| 80 | 82 | } | |
| 81 | 83 | ||
| 84 | + | // Close destroys a display that has not started Run. It is a no-op once Run | |
| 85 | + | // has started; Run owns destruction in that case. Call before Run starts. | |
| 86 | + | func (d *Display) Close() { | |
| 87 | + | if !d.state.CompareAndSwap(0, 1) { | |
| 88 | + | return | |
| 89 | + | } | |
| 90 | + | wlDisplayDestroyClients(d.c) | |
| 91 | + | wlDisplayDestroy(d.c) | |
| 92 | + | close(d.stopped) | |
| 93 | + | } | |
| 94 | + | ||
| 82 | 95 | // Run dispatches libwayland on one OS-locked goroutine until ctx is done. | |
| 83 | 96 | func (d *Display) Run(ctx context.Context) error { | |
| 97 | + | if !d.state.CompareAndSwap(0, 2) { | |
| 98 | + | return errors.New("purego-libwayland: display already started or closed") | |
| 99 | + | } | |
| 84 | 100 | runtime.LockOSThread() | |
| 85 | 101 | defer runtime.UnlockOSThread() | |
| 86 | 102 | defer close(d.stopped) | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "runtime" | 7 | "runtime" | ||
| 8 | "strings" | 8 | "strings" | ||
| 9 | + | "sync/atomic" | |||
| 9 | "unsafe" | 10 | "unsafe" | ||
| 10 | 11 | ||||
| 11 | "golang.org/x/sys/unix" | 12 | "golang.org/x/sys/unix" | ||
| @@ -47,6 +48,7 @@ type Display struct { | |||||
| 47 | loop uintptr | 48 | loop uintptr | ||
| 48 | calls chan func() | 49 | calls chan func() | ||
| 49 | stopped chan struct{} | 50 | stopped chan struct{} | ||
| 51 | + | state atomic.Uint32 | |||
| 50 | } | 52 | } | ||
| 51 | 53 | ||||
| 52 | func NewDisplay() (*Display, error) { | 54 | func NewDisplay() (*Display, error) { | ||
| @@ -79,8 +81,22 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) | |||||
| 79 | return nil | 81 | return nil | ||
| 80 | } | 82 | } | ||
| 81 | 83 | ||||
| 84 | + | // Close destroys a display that has not started Run. It is a no-op once Run | |||
| 85 | + | // has started; Run owns destruction in that case. Call before Run starts. | |||
| 86 | + | func (d *Display) Close() { | |||
| 87 | + | if !d.state.CompareAndSwap(0, 1) { | |||
| 88 | + | return | |||
| 89 | + | } | |||
| 90 | + | wlDisplayDestroyClients(d.c) | |||
| 91 | + | wlDisplayDestroy(d.c) | |||
| 92 | + | close(d.stopped) | |||
| 93 | + | } | |||
| 94 | + | ||||
| 82 | // Run dispatches libwayland on one OS-locked goroutine until ctx is done. | 95 | // Run dispatches libwayland on one OS-locked goroutine until ctx is done. | ||
| 83 | func (d *Display) Run(ctx context.Context) error { | 96 | func (d *Display) Run(ctx context.Context) error { | ||
| 97 | + | if !d.state.CompareAndSwap(0, 2) { | |||
| 98 | + | return errors.New("purego-libwayland: display already started or closed") | |||
| 99 | + | } | |||
| 84 | runtime.LockOSThread() | 100 | runtime.LockOSThread() | ||
| 85 | defer runtime.UnlockOSThread() | 101 | defer runtime.UnlockOSThread() | ||
| 86 | defer close(d.stopped) | 102 | defer close(d.stopped) | ||
M server/display_test.go +23 -0
| @@ -111,3 +111,26 @@ func TestDoBlockedDuringCancellation(t *testing.T) { | |||
| 111 | 111 | t.Fatal("blocked Do leaked") | |
| 112 | 112 | } | |
| 113 | 113 | } | |
| 114 | + | ||
| 115 | + | func TestCloseBeforeRun(t *testing.T) { | |
| 116 | + | d, err := NewDisplay() | |
| 117 | + | if err != nil { | |
| 118 | + | t.Fatal(err) | |
| 119 | + | } | |
| 120 | + | fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0) | |
| 121 | + | if err != nil { | |
| 122 | + | t.Fatal(err) | |
| 123 | + | } | |
| 124 | + | defer unix.Close(fd) | |
| 125 | + | if err := unix.Bind(fd, &unix.SockaddrUnix{Name: "\x00purego-close-test"}); err != nil { | |
| 126 | + | t.Fatal(err) | |
| 127 | + | } | |
| 128 | + | if err := unix.Listen(fd, 1); err != nil { | |
| 129 | + | t.Fatal(err) | |
| 130 | + | } | |
| 131 | + | if err := d.AddSocketFD(fd); err != nil { | |
| 132 | + | t.Fatal(err) | |
| 133 | + | } | |
| 134 | + | d.Close() | |
| 135 | + | d.Close() | |
| 136 | + | } | |
| @@ -111,3 +111,26 @@ func TestDoBlockedDuringCancellation(t *testing.T) { | |||||
| 111 | t.Fatal("blocked Do leaked") | 111 | t.Fatal("blocked Do leaked") | ||
| 112 | } | 112 | } | ||
| 113 | } | 113 | } | ||
| 114 | + | ||||
| 115 | + | func TestCloseBeforeRun(t *testing.T) { | |||
| 116 | + | d, err := NewDisplay() | |||
| 117 | + | if err != nil { | |||
| 118 | + | t.Fatal(err) | |||
| 119 | + | } | |||
| 120 | + | fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0) | |||
| 121 | + | if err != nil { | |||
| 122 | + | t.Fatal(err) | |||
| 123 | + | } | |||
| 124 | + | defer unix.Close(fd) | |||
| 125 | + | if err := unix.Bind(fd, &unix.SockaddrUnix{Name: "\x00purego-close-test"}); err != nil { | |||
| 126 | + | t.Fatal(err) | |||
| 127 | + | } | |||
| 128 | + | if err := unix.Listen(fd, 1); err != nil { | |||
| 129 | + | t.Fatal(err) | |||
| 130 | + | } | |||
| 131 | + | if err := d.AddSocketFD(fd); err != nil { | |||
| 132 | + | t.Fatal(err) | |||
| 133 | + | } | |||
| 134 | + | d.Close() | |||
| 135 | + | d.Close() | |||
| 136 | + | } | |||